add message parsers

This commit is contained in:
Sorunome 2020-03-25 15:47:51 +01:00
parent 19020890e1
commit 416df718bd
No known key found for this signature in database
GPG Key ID: B19471D07FC9BE9C
6 changed files with 219 additions and 34 deletions

13
package-lock.json generated
View File

@ -2123,6 +2123,11 @@
"type-fest": "^0.8.0" "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": { "htmlencode": {
"version": "0.0.4", "version": "0.0.4",
"resolved": "https://registry.npmjs.org/htmlencode/-/htmlencode-0.0.4.tgz", "resolved": "https://registry.npmjs.org/htmlencode/-/htmlencode-0.0.4.tgz",
@ -2858,6 +2863,14 @@
"semver": "^5.4.1" "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": { "noop-logger": {
"version": "0.1.1", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz",

View File

@ -18,6 +18,7 @@
"events": "^3.0.0", "events": "^3.0.0",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"mx-puppet-bridge": "0.0.35-1", "mx-puppet-bridge": "0.0.35-1",
"node-html-parser": "^1.2.13",
"skype-http": "git://github.com/Sorunome/skype-http#10555125f46307bbff93a8c4779889f4100669d2", "skype-http": "git://github.com/Sorunome/skype-http#10555125f46307bbff93a8c4779889f4100669d2",
"tslint": "^5.17.0", "tslint": "^5.17.0",
"typescript": "^3.7.4" "typescript": "^3.7.4"

View File

@ -45,9 +45,11 @@ export class Client extends EventEmitter {
} }
public async connect() { public async connect() {
if (this.state) { let connectedWithAuth = false;
if (this.state && false) {
try { try {
this.api = await skypeHttp.connect({ state: this.state, verbose: true }); this.api = await skypeHttp.connect({ state: this.state, verbose: true });
connectedWithAuth = true;
} catch (err) { } catch (err) {
this.api = await skypeHttp.connect({ this.api = await skypeHttp.connect({
credentials: { credentials: {
@ -56,6 +58,7 @@ export class Client extends EventEmitter {
}, },
verbose: true, verbose: true,
}); });
connectedWithAuth = false;
} }
} else { } else {
this.api = await skypeHttp.connect({ this.api = await skypeHttp.connect({
@ -65,6 +68,7 @@ export class Client extends EventEmitter {
}, },
verbose: true, verbose: true,
}); });
connectedWithAuth = false;
} }
this.api.on("event", (evt: skypeHttp.events.EventMessage) => { this.api.on("event", (evt: skypeHttp.events.EventMessage) => {
@ -109,10 +113,6 @@ export class Client extends EventEmitter {
this.emit("error", err); this.emit("error", err);
}); });
await this.api.listen();
await this.api.setStatus("Online");
try {
const contacts = await this.api.getContacts(); const contacts = await this.api.getContacts();
for (const contact of contacts) { for (const contact of contacts) {
this.contacts.set(contact.mri, contact); this.contacts.set(contact.mri, contact);
@ -121,9 +121,9 @@ export class Client extends EventEmitter {
for (const conversation of conversations) { for (const conversation of conversations) {
this.conversations.set(conversation.id, conversation); this.conversations.set(conversation.id, conversation);
} }
} catch (err) {
log.error(err); await this.api.listen();
} await this.api.setStatus("Online");
} }
public async disconnect() { public async disconnect() {

View File

@ -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(`<wrap>${msg}</wrap>`, {
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 `<i raw_pre="_" raw_post="_">${this.walkChildNodes(nodeHtml)}</i>`;
case "strong":
case "b":
return `<b raw_pre="*" raw_post="*">${this.walkChildNodes(nodeHtml)}</b>`;
case "del":
return `<s raw_pre="~" raw_post="~">${this.walkChildNodes(nodeHtml)}</s>`;
case "code":
return `<pre raw_pre="{code}" raw_post="{code}">${this.walkChildNodes(nodeHtml)}</pre>`;
case "a": {
const href = nodeHtml.attributes.href;
const inner = this.walkChildNodes(nodeHtml);
return `<a href="${escapeHtml(href)}">${inner}</a>`;
}
case "wrap":
return this.walkChildNodes(nodeHtml);
default:
if (!nodeHtml.tagName) {
return this.walkChildNodes(nodeHtml);
}
return `<${nodeHtml.tagName}>${this.walkChildNodes(nodeHtml)}</${nodeHtml.tagName}>`;
}
}
return "";
}
}

View File

@ -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 See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { import {
PuppetBridge, IRemoteUser, IRemoteRoom, IReceiveParams, IMessageEvent, IFileEvent, Log, MessageDeduplicator, Util, PuppetBridge, IRemoteUser, IRemoteRoom, IReceiveParams, IMessageEvent, IFileEvent, Log, MessageDeduplicator, Util,
ExpireSet, IRetList, 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 { NewMediaMessage as SkypeNewMediaMessage } from "skype-http/dist/lib/interfaces/api/api";
import * as decodeHtml from "decode-html"; import * as decodeHtml from "decode-html";
import * as escapeHtml from "escape-html"; import * as escapeHtml from "escape-html";
import { MatrixMessageParser } from "./matrixmessageparser";
import { SkypeMessageParser } from "./skypemessageparser";
const log = new Log("SkypePuppet:skype"); const log = new Log("SkypePuppet:skype");
@ -38,10 +41,14 @@ interface ISkypePuppets {
export class Skype { export class Skype {
private puppets: ISkypePuppets = {}; private puppets: ISkypePuppets = {};
private messageDeduplicator: MessageDeduplicator; private messageDeduplicator: MessageDeduplicator;
private matrixMessageParser: MatrixMessageParser;
private skypeMessageParser: SkypeMessageParser;
constructor( constructor(
private puppet: PuppetBridge, private puppet: PuppetBridge,
) { ) {
this.messageDeduplicator = new MessageDeduplicator(); this.messageDeduplicator = new MessageDeduplicator();
this.matrixMessageParser = new MatrixMessageParser();
this.skypeMessageParser = new SkypeMessageParser();
} }
public getUserParams(puppetId: number, contact: SkypeContact): IRemoteUser { public getUserParams(puppetId: number, contact: SkypeContact): IRemoteUser {
@ -160,8 +167,8 @@ export class Skype {
}); });
const MINUTE = 60000; const MINUTE = 60000;
client.on("error", async (err: Error) => { client.on("error", async (err: Error) => {
await this.puppet.sendStatusMessage(puppetId, "Error:" + err); await this.puppet.sendStatusMessage(puppetId, "Error: " + err);
await this.puppet.sendStatusMessage(puppetId, "Reconnecting in a minute... " + err.message); await this.puppet.sendStatusMessage(puppetId, "Reconnecting in a minute... ");
setTimeout(async () => { setTimeout(async () => {
await this.stopClient(puppetId); await this.stopClient(puppetId);
await this.startClient(puppetId); await this.startClient(puppetId);
@ -175,7 +182,7 @@ export class Skype {
await this.puppet.sendStatusMessage(puppetId, "connected"); await this.puppet.sendStatusMessage(puppetId, "connected");
} catch (err) { } catch (err) {
log.error("Failed to connect", 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 () => { setTimeout(async () => {
await this.startClient(puppetId); await this.startClient(puppetId);
}, MINUTE); }, MINUTE);
@ -310,7 +317,7 @@ export class Skype {
} }
let msg: string; let msg: string;
if (data.formattedBody) { if (data.formattedBody) {
msg = data.formattedBody; msg = this.matrixMessageParser.parse(data.formattedBody);
} else { } else {
msg = escapeHtml(data.body); msg = escapeHtml(data.body);
} }
@ -338,7 +345,7 @@ export class Skype {
} }
let msg: string; let msg: string;
if (data.formattedBody) { if (data.formattedBody) {
msg = data.formattedBody; msg = this.matrixMessageParser.parse(data.formattedBody);
} else { } else {
msg = escapeHtml(data.body); msg = escapeHtml(data.body);
} }
@ -440,18 +447,20 @@ export class Skype {
log.silly("normal message dedupe"); log.silly("normal message dedupe");
return; return;
} }
if (!rich) { let sendMsg: IMessageEvent;
await this.puppet.sendMessage(params, { if (rich) {
sendMsg = this.skypeMessageParser.parse(msg);
} else {
sendMsg = {
body: msg, body: msg,
emote, };
}); }
} else if (resource.native && resource.native.skypeeditedid) { if (emote) {
sendMsg.emote = true;
}
if (resource.native && resource.native.skypeeditedid) {
if (resource.content) { if (resource.content) {
await this.puppet.sendEdit(params, resource.native.skypeeditedid, { await this.puppet.sendEdit(params, resource.native.skypeeditedid, sendMsg);
body: msg,
formattedBody: msg,
emote,
});
} else if (p.deletedMessages.has(resource.native.skypeeditedid)) { } else if (p.deletedMessages.has(resource.native.skypeeditedid)) {
log.silly("normal message redact dedupe"); log.silly("normal message redact dedupe");
return; return;
@ -459,11 +468,7 @@ export class Skype {
await this.puppet.sendRedact(params, resource.native.skypeeditedid); await this.puppet.sendRedact(params, resource.native.skypeeditedid);
} }
} else { } else {
await this.puppet.sendMessage(params, { await this.puppet.sendMessage(params, sendMsg);
body: msg,
formattedBody: msg,
emote,
});
} }
} }

100
src/skypemessageparser.ts Normal file
View File

@ -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(`<wrap>${msg}</wrap>`, {
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", "<br>"),
};
}
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: `<em>${child.formattedBody}</em>`,
};
}
case "b": {
const child = this.walkChildNodes(nodeHtml);
return {
body: `*${child.body}*`,
formattedBody: `<strong>${child.formattedBody}</strong>`,
};
}
case "s": {
const child = this.walkChildNodes(nodeHtml);
return {
body: `~${child.body}~`,
formattedBody: `<del>${child.formattedBody}</del>`,
};
}
case "pre": {
const child = this.walkChildNodes(nodeHtml);
return {
body: `{code}${child.body}{code}`,
formattedBody: `<code>${child.formattedBody}</code>`,
};
}
case "a": {
const href = nodeHtml.attributes.href;
const child = this.walkChildNodes(nodeHtml);
return {
body: child.body === href ? href : `[${child.body}](${href})`,
formattedBody: `<a href="${escapeHtml(href)}">${child.formattedBody}</a>`,
};
}
case "e_m":
return {
body: "",
formattedBody: "",
};
default:
return this.walkChildNodes(nodeHtml);
}
}
return {
body: "",
formattedBody: "",
};
}
}