diff --git a/package-lock.json b/package-lock.json index 9ba76b7..035ef20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2123,6 +2123,11 @@ "type-fest": "^0.8.0" } }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=" + }, "htmlencode": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/htmlencode/-/htmlencode-0.0.4.tgz", @@ -2858,6 +2863,14 @@ "semver": "^5.4.1" } }, + "node-html-parser": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-1.2.13.tgz", + "integrity": "sha512-RTG5oDk3nh1oXQGYtQQa9w0v9LjltgzzO1tv3cpXzb9M/UkpFIZ5v4osx3OYAcnWx/ETBXjxs2cgtD9fhn6Bjw==", + "requires": { + "he": "1.1.1" + } + }, "noop-logger": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", diff --git a/package.json b/package.json index d4e8bc7..11041bb 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "events": "^3.0.0", "js-yaml": "^3.13.1", "mx-puppet-bridge": "0.0.35-1", + "node-html-parser": "^1.2.13", "skype-http": "git://github.com/Sorunome/skype-http#10555125f46307bbff93a8c4779889f4100669d2", "tslint": "^5.17.0", "typescript": "^3.7.4" diff --git a/src/client.ts b/src/client.ts index 9559bef..b11c28b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -45,9 +45,11 @@ export class Client extends EventEmitter { } public async connect() { - if (this.state) { + let connectedWithAuth = false; + if (this.state && false) { try { this.api = await skypeHttp.connect({ state: this.state, verbose: true }); + connectedWithAuth = true; } catch (err) { this.api = await skypeHttp.connect({ credentials: { @@ -56,6 +58,7 @@ export class Client extends EventEmitter { }, verbose: true, }); + connectedWithAuth = false; } } else { this.api = await skypeHttp.connect({ @@ -65,6 +68,7 @@ export class Client extends EventEmitter { }, verbose: true, }); + connectedWithAuth = false; } this.api.on("event", (evt: skypeHttp.events.EventMessage) => { @@ -109,21 +113,17 @@ export class Client extends EventEmitter { this.emit("error", err); }); + const contacts = await this.api.getContacts(); + for (const contact of contacts) { + this.contacts.set(contact.mri, contact); + } + const conversations = await this.api.getConversations(); + for (const conversation of conversations) { + this.conversations.set(conversation.id, conversation); + } + await this.api.listen(); await this.api.setStatus("Online"); - - try { - const contacts = await this.api.getContacts(); - for (const contact of contacts) { - this.contacts.set(contact.mri, contact); - } - const conversations = await this.api.getConversations(); - for (const conversation of conversations) { - this.conversations.set(conversation.id, conversation); - } - } catch (err) { - log.error(err); - } } public async disconnect() { diff --git a/src/matrixmessageparser.ts b/src/matrixmessageparser.ts new file mode 100644 index 0000000..d76c7ad --- /dev/null +++ b/src/matrixmessageparser.ts @@ -0,0 +1,66 @@ +/* +Copyright 2020 mx-puppet-skype +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as Parser from "node-html-parser"; +import * as escapeHtml from "escape-html"; + +export class MatrixMessageParser { + public parse(msg: string): string { + const nodes = Parser.parse(`${msg}`, { + lowerCaseTagName: true, + pre: true, + }); + return this.walkNode(nodes); + } + + private walkChildNodes(node: Parser.Node): string { + return node.childNodes.map((node) => this.walkNode(node)).join(""); + } + + private escape(s: string): string { + return s; + } + + private walkNode(node: Parser.Node): string { + if (node.nodeType === Parser.NodeType.TEXT_NODE) { + return this.escape((node as Parser.TextNode).text); + } else if (node.nodeType === Parser.NodeType.ELEMENT_NODE) { + const nodeHtml = node as Parser.HTMLElement; + switch (nodeHtml.tagName) { + case "em": + case "i": + return `${this.walkChildNodes(nodeHtml)}`; + case "strong": + case "b": + return `${this.walkChildNodes(nodeHtml)}`; + case "del": + return `${this.walkChildNodes(nodeHtml)}`; + case "code": + return `
${this.walkChildNodes(nodeHtml)}
`; + case "a": { + const href = nodeHtml.attributes.href; + const inner = this.walkChildNodes(nodeHtml); + return `${inner}`; + } + case "wrap": + return this.walkChildNodes(nodeHtml); + default: + if (!nodeHtml.tagName) { + return this.walkChildNodes(nodeHtml); + } + return `<${nodeHtml.tagName}>${this.walkChildNodes(nodeHtml)}`; + } + } + return ""; + } +} diff --git a/src/skype.ts b/src/skype.ts index aa5edd1..74f73ca 100644 --- a/src/skype.ts +++ b/src/skype.ts @@ -10,6 +10,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + import { PuppetBridge, IRemoteUser, IRemoteRoom, IReceiveParams, IMessageEvent, IFileEvent, Log, MessageDeduplicator, Util, ExpireSet, IRetList, @@ -20,6 +21,8 @@ import { Contact as SkypeContact } from "skype-http/dist/lib/types/contact"; import { NewMediaMessage as SkypeNewMediaMessage } from "skype-http/dist/lib/interfaces/api/api"; import * as decodeHtml from "decode-html"; import * as escapeHtml from "escape-html"; +import { MatrixMessageParser } from "./matrixmessageparser"; +import { SkypeMessageParser } from "./skypemessageparser"; const log = new Log("SkypePuppet:skype"); @@ -38,10 +41,14 @@ interface ISkypePuppets { export class Skype { private puppets: ISkypePuppets = {}; private messageDeduplicator: MessageDeduplicator; + private matrixMessageParser: MatrixMessageParser; + private skypeMessageParser: SkypeMessageParser; constructor( private puppet: PuppetBridge, ) { this.messageDeduplicator = new MessageDeduplicator(); + this.matrixMessageParser = new MatrixMessageParser(); + this.skypeMessageParser = new SkypeMessageParser(); } public getUserParams(puppetId: number, contact: SkypeContact): IRemoteUser { @@ -160,8 +167,8 @@ export class Skype { }); const MINUTE = 60000; client.on("error", async (err: Error) => { - await this.puppet.sendStatusMessage(puppetId, "Error:" + err); - await this.puppet.sendStatusMessage(puppetId, "Reconnecting in a minute... " + err.message); + await this.puppet.sendStatusMessage(puppetId, "Error: " + err); + await this.puppet.sendStatusMessage(puppetId, "Reconnecting in a minute... "); setTimeout(async () => { await this.stopClient(puppetId); await this.startClient(puppetId); @@ -175,7 +182,7 @@ export class Skype { await this.puppet.sendStatusMessage(puppetId, "connected"); } catch (err) { log.error("Failed to connect", err); - await this.puppet.sendStatusMessage(puppetId, "Failed to connect, reconnecting in a minute... " + err.message); + await this.puppet.sendStatusMessage(puppetId, "Failed to connect, reconnecting in a minute... " + err); setTimeout(async () => { await this.startClient(puppetId); }, MINUTE); @@ -310,7 +317,7 @@ export class Skype { } let msg: string; if (data.formattedBody) { - msg = data.formattedBody; + msg = this.matrixMessageParser.parse(data.formattedBody); } else { msg = escapeHtml(data.body); } @@ -338,7 +345,7 @@ export class Skype { } let msg: string; if (data.formattedBody) { - msg = data.formattedBody; + msg = this.matrixMessageParser.parse(data.formattedBody); } else { msg = escapeHtml(data.body); } @@ -440,18 +447,20 @@ export class Skype { log.silly("normal message dedupe"); return; } - if (!rich) { - await this.puppet.sendMessage(params, { + let sendMsg: IMessageEvent; + if (rich) { + sendMsg = this.skypeMessageParser.parse(msg); + } else { + sendMsg = { body: msg, - emote, - }); - } else if (resource.native && resource.native.skypeeditedid) { + }; + } + if (emote) { + sendMsg.emote = true; + } + if (resource.native && resource.native.skypeeditedid) { if (resource.content) { - await this.puppet.sendEdit(params, resource.native.skypeeditedid, { - body: msg, - formattedBody: msg, - emote, - }); + await this.puppet.sendEdit(params, resource.native.skypeeditedid, sendMsg); } else if (p.deletedMessages.has(resource.native.skypeeditedid)) { log.silly("normal message redact dedupe"); return; @@ -459,11 +468,7 @@ export class Skype { await this.puppet.sendRedact(params, resource.native.skypeeditedid); } } else { - await this.puppet.sendMessage(params, { - body: msg, - formattedBody: msg, - emote, - }); + await this.puppet.sendMessage(params, sendMsg); } } diff --git a/src/skypemessageparser.ts b/src/skypemessageparser.ts new file mode 100644 index 0000000..71fd7a7 --- /dev/null +++ b/src/skypemessageparser.ts @@ -0,0 +1,100 @@ +/* +Copyright 2020 mx-puppet-skype +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as Parser from "node-html-parser"; +import * as decodeHtml from "decode-html"; +import * as escapeHtml from "escape-html"; +import { IMessageEvent } from "mx-puppet-bridge"; + +export class SkypeMessageParser { + public parse(msg: string): IMessageEvent { + const nodes = Parser.parse(`${msg}`, { + lowerCaseTagName: true, + pre: true, + }); + return this.walkNode(nodes); + } + + private walkChildNodes(node: Parser.Node): IMessageEvent { + return node.childNodes.map((node) => this.walkNode(node)).reduce((acc, curr) => { + return { + body: acc.body + curr.body, + formattedBody: acc.formattedBody! + curr.formattedBody!, + }; + }); + } + + private escape(s: string): IMessageEvent { + return { + body: decodeHtml(s), + formattedBody: s.replace("\n", "
"), + }; + } + + private walkNode(node: Parser.Node): IMessageEvent { + if (node.nodeType === Parser.NodeType.TEXT_NODE) { + return this.escape((node as Parser.TextNode).text); + } else if (node.nodeType === Parser.NodeType.ELEMENT_NODE) { + const nodeHtml = node as Parser.HTMLElement; + switch (nodeHtml.tagName) { + case "i": { + const child = this.walkChildNodes(nodeHtml); + return { + body: `_${child.body}_`, + formattedBody: `${child.formattedBody}`, + }; + } + case "b": { + const child = this.walkChildNodes(nodeHtml); + return { + body: `*${child.body}*`, + formattedBody: `${child.formattedBody}`, + }; + } + case "s": { + const child = this.walkChildNodes(nodeHtml); + return { + body: `~${child.body}~`, + formattedBody: `${child.formattedBody}`, + }; + } + case "pre": { + const child = this.walkChildNodes(nodeHtml); + return { + body: `{code}${child.body}{code}`, + formattedBody: `${child.formattedBody}`, + }; + } + case "a": { + const href = nodeHtml.attributes.href; + const child = this.walkChildNodes(nodeHtml); + return { + body: child.body === href ? href : `[${child.body}](${href})`, + formattedBody: `${child.formattedBody}`, + }; + } + case "e_m": + return { + body: "", + formattedBody: "", + }; + default: + return this.walkChildNodes(nodeHtml); + } + } + return { + body: "", + formattedBody: "", + }; + } +}