update stuffs, user profile updates should work now

This commit is contained in:
Sorunome 2020-03-25 23:48:12 +01:00
parent 416df718bd
commit dc333129da
No known key found for this signature in database
GPG Key ID: B19471D07FC9BE9C
6 changed files with 303 additions and 87 deletions

17
package-lock.json generated
View File

@ -2511,6 +2511,11 @@
"resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz",
"integrity": "sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4="
},
"lodash.toarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
"integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE="
},
"logform": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz",
@ -2863,6 +2868,14 @@
"semver": "^5.4.1"
}
},
"node-emoji": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz",
"integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==",
"requires": {
"lodash.toarray": "^4.4.0"
}
},
"node-html-parser": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-1.2.13.tgz",
@ -3606,8 +3619,8 @@
}
},
"skype-http": {
"version": "git://github.com/Sorunome/skype-http.git#10555125f46307bbff93a8c4779889f4100669d2",
"from": "git://github.com/Sorunome/skype-http.git#10555125f46307bbff93a8c4779889f4100669d2",
"version": "git://github.com/Sorunome/skype-http.git#e8a92f8bc4929443bbba9755b6dd1ce90026bf4e",
"from": "git://github.com/Sorunome/skype-http.git#e8a92f8bc4929443bbba9755b6dd1ce90026bf4e",
"requires": {
"@types/cheerio": "^0.22.12",
"@types/escape-html": "0.0.20",

View File

@ -11,6 +11,7 @@
},
"author": "Sorunome",
"dependencies": {
"cheerio": "^1.0.0-rc.3",
"command-line-args": "^5.1.1",
"command-line-usage": "^5.0.5",
"decode-html": "^2.0.0",
@ -18,8 +19,9 @@
"events": "^3.0.0",
"js-yaml": "^3.13.1",
"mx-puppet-bridge": "0.0.35-1",
"node-emoji": "^1.10.0",
"node-html-parser": "^1.2.13",
"skype-http": "git://github.com/Sorunome/skype-http#10555125f46307bbff93a8c4779889f4100669d2",
"skype-http": "git://github.com/Sorunome/skype-http#e8a92f8bc4929443bbba9755b6dd1ce90026bf4e",
"tslint": "^5.17.0",
"typescript": "^3.7.4"
},

View File

@ -20,13 +20,18 @@ import { Context as SkypeContext } from "skype-http/dist/lib/interfaces/api/cont
const log = new Log("SkypePuppet:client");
// tslint:disable no-magic-numbers
const ID_TIMEOUT = 60000;
const CONTACTS_DELTA_INTERVAL = 5 * 60 * 1000;
// tslint:enable no-magic-numbers
export class Client extends EventEmitter {
public contacts: Map<string, SkypeContact | null> = new Map();
public conversations: Map<string, skypeHttp.Conversation | null> = new Map();
private api: skypeHttp.Api;
private handledIds: ExpireSet<string>;
private lastContactsDate: Date = new Date();
private contactsInterval: NodeJS.Timeout | null = null;
constructor(
private loginUsername: string,
private password: string,
@ -46,7 +51,7 @@ export class Client extends EventEmitter {
public async connect() {
let connectedWithAuth = false;
if (this.state && false) {
if (this.state) {
try {
this.api = await skypeHttp.connect({ state: this.state, verbose: true });
connectedWithAuth = true;
@ -71,63 +76,52 @@ export class Client extends EventEmitter {
connectedWithAuth = false;
}
this.api.on("event", (evt: skypeHttp.events.EventMessage) => {
if (!evt || !evt.resource) {
return;
}
const resource = evt.resource;
if (this.handledIds.has(resource.id)) {
return;
}
this.handledIds.add(resource.id);
log.debug(`Got new event of type ${resource.type}`);
const [type, subtype] = resource.type.split("/");
switch (type) {
case "Text":
this.emit("text", resource);
break;
case "RichText":
if (subtype === "Location") {
this.emit("location", resource);
} else if (subtype) {
this.emit("file", resource);
} else {
this.emit("richText", resource);
}
break;
case "Control":
if (subtype === "Typing" || subtype === "ClearTyping") {
this.emit("typing", resource, subtype === "Typing");
}
break;
case "ThreadActivity":
if (subtype === "MemberConsumptionHorizonUpdate") {
this.emit("presence", resource);
}
break;
}
});
this.api.on("error", (err: Error) => {
log.error("An error occured", err);
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.startupApi();
await this.api.listen();
await this.api.setStatus("Online");
if (connectedWithAuth) {
let resolved = false;
return new Promise((resolve, reject) => {
const TIMEOUT_SUCCESS = 5000;
setTimeout(() => {
if (resolved) {
return;
}
resolved = true;
resolve();
}, TIMEOUT_SUCCESS);
this.api.once("error", async () => {
if (resolved) {
return;
}
resolved = true;
// alright, re-try as normal user
try {
await this.api.stopListening();
this.api = await skypeHttp.connect({
credentials: {
username: this.loginUsername,
password: this.password,
},
verbose: true,
});
await this.startupApi();
resolve();
} catch (err) {
reject(err);
}
});
});
}
}
public async disconnect() {
await this.api.stopListening();
if (this.contactsInterval) {
clearInterval(this.contactsInterval);
this.contactsInterval = null;
}
}
public async getContact(id: string): Promise<SkypeContact | null> {
@ -193,7 +187,7 @@ export class Client extends EventEmitter {
}
public async downloadFile(url: string): Promise<Buffer> {
if (!url.includes("/views/imgpsh_fullsize_anim")) {
if (!url.includes("/views/")) {
url = url + "/views/imgpsh_fullsize_anim";
}
return await Util.DownloadFile(url, {
@ -238,4 +232,75 @@ export class Client extends EventEmitter {
): Promise<skypeHttp.Api.SendMessageResult> {
return await this.api.sendImage(opts, conversationId);
}
private async startupApi() {
this.api.on("event", (evt: skypeHttp.events.EventMessage) => {
if (!evt || !evt.resource) {
return;
}
const resource = evt.resource;
log.debug(`Got new event of type ${resource.type}`);
log.silly(evt);
const [type, subtype] = resource.type.split("/");
switch (type) {
case "RichText":
if (evt.resourceType === "NewMessage") {
if (resource.native.skypeeditedid || this.handledIds.has(resource.id)) {
break;
}
this.handledIds.add(resource.id);
if (subtype === "Location") {
this.emit("location", resource);
} else if (subtype) {
this.emit("file", resource);
} else {
this.emit("text", resource);
}
} else if (evt.resourceType === "MessageUpdate") {
this.emit("edit", resource);
}
break;
case "Control":
if (subtype === "Typing" || subtype === "ClearTyping") {
this.emit("typing", resource, subtype === "Typing");
}
break;
case "ThreadActivity":
if (subtype === "MemberConsumptionHorizonUpdate") {
this.emit("presence", resource);
}
break;
}
});
this.api.on("error", (err: Error) => {
log.error("An error occured", err);
this.emit("error", err);
});
const contacts = await this.api.getContacts();
for (const contact of contacts) {
this.contacts.set(contact.mri, contact);
}
this.lastContactsDate = new Date();
const conversations = await this.api.getConversations();
for (const conversation of conversations) {
this.conversations.set(conversation.id, conversation);
}
if (this.contactsInterval) {
clearInterval(this.contactsInterval);
this.contactsInterval = null;
}
this.contactsInterval = setInterval(this.updateContacts.bind(this), CONTACTS_DELTA_INTERVAL);
}
private async updateContacts() {
const contacts = await this.api.getContacts(true);
for (const contact of contacts) {
this.contacts.set(contact.mri, contact);
this.emit("updateContact", contact);
}
this.lastContactsDate = new Date();
}
}

View File

@ -24,7 +24,7 @@ export class MatrixMessageParser {
}
private walkChildNodes(node: Parser.Node): string {
return node.childNodes.map((node) => this.walkNode(node)).join("");
return node.childNodes.map((n) => this.walkNode(n)).join("");
}
private escape(s: string): string {

View File

@ -23,6 +23,7 @@ import * as decodeHtml from "decode-html";
import * as escapeHtml from "escape-html";
import { MatrixMessageParser } from "./matrixmessageparser";
import { SkypeMessageParser } from "./skypemessageparser";
import * as cheerio from "cheerio";
const log = new Log("SkypePuppet:skype");
@ -104,7 +105,7 @@ export class Skype {
return {
user: this.getUserParams(puppetId, contact),
room: this.getRoomParams(puppetId, conversation),
eventId: (resource as any).clientId || resource.native.clientmessageid || resource.id, // tslint:disable-line no-any
eventId: resource.id, // tslint:disable-line no-any
};
}
@ -125,16 +126,16 @@ export class Skype {
const client = p.client;
client.on("text", async (resource: skypeHttp.resources.TextResource) => {
try {
await this.handleSkypeText(puppetId, resource, false);
await this.handleSkypeText(puppetId, resource);
} catch (err) {
log.error("Error while handling text event", err);
}
});
client.on("richText", async (resource: skypeHttp.resources.RichTextResource) => {
client.on("edit", async (resource: skypeHttp.resources.RichTextResource) => {
try {
await this.handleSkypeText(puppetId, resource, true);
await this.handleSkypeEdit(puppetId, resource);
} catch (err) {
log.error("Error while handling richText event", err);
log.error("Error while handling edit event", err);
}
});
client.on("location", async (resource: skypeHttp.resources.RichTextLocationResource) => {
@ -165,6 +166,14 @@ export class Skype {
log.error("Error while handling presence event", err);
}
});
client.on("updateContact", async (contact: SkypeContact) => {
try {
const remoteUser = this.getUserParams(puppetId, contact);
await this.puppet.updateUser(remoteUser);
} catch (err) {
log.error("Error while handling updateContact event", err);
}
});
const MINUTE = 60000;
client.on("error", async (err: Error) => {
await this.puppet.sendStatusMessage(puppetId, "Error: " + err);
@ -324,9 +333,8 @@ export class Skype {
const dedupeKey = `${room.puppetId};${room.roomId}`;
this.messageDeduplicator.lock(dedupeKey, p.client.username, msg);
const ret = await p.client.sendMessage(conversation.id, msg);
const dedupeId = ret && ret.clientMessageId;
const eventId = ret && ret.MessageId;
this.messageDeduplicator.unlock(dedupeKey, p.client.username, dedupeId);
this.messageDeduplicator.unlock(dedupeKey, p.client.username, eventId);
if (eventId) {
await this.puppet.eventSync.insert(room.puppetId, data.eventId!, eventId);
}
@ -412,9 +420,8 @@ export class Skype {
const dedupeKey = `${room.puppetId};${room.roomId}`;
this.messageDeduplicator.lock(dedupeKey, p.client.username, `file:${data.filename}`);
const ret = await p.client[method](conversation.id, opts);
const dedupeId = ret && ret.clientMessageId;
const eventId = ret && ret.MessageId;
this.messageDeduplicator.unlock(dedupeKey, p.client.username, dedupeId);
this.messageDeduplicator.unlock(dedupeKey, p.client.username, eventId);
if (eventId) {
await this.puppet.eventSync.insert(room.puppetId, data.eventId!, eventId);
}
@ -423,12 +430,12 @@ export class Skype {
private async handleSkypeText(
puppetId: number,
resource: skypeHttp.resources.TextResource | skypeHttp.resources.RichTextResource,
rich: boolean,
) {
const p = this.puppets[puppetId];
if (!p) {
return;
}
const rich = resource.native.messagetype.startsWith("RichText");
log.info("Got new skype message");
log.silly(resource);
const params = await this.getSendParams(puppetId, resource);
@ -438,7 +445,66 @@ export class Skype {
}
let msg = resource.content;
let emote = false;
if (resource.native && resource.native.skypeemoteoffset) {
if (resource.native.skypeemoteoffset) {
emote = true;
msg = msg.substr(Number(resource.native.skypeemoteoffset));
}
const dedupeKey = `${puppetId};${params.room.roomId}`;
if (rich && msg.trim().startsWith("<URIObject") && msg.trim().endsWith("</URIObject>")) {
// okay, we might have a sticker or something...
const $ = cheerio.load(msg);
const obj = $("URIObject");
let uri = obj.attr("uri");
const filename = $(obj.find("OriginalName")).attr("v");
if (uri) {
if (await this.messageDeduplicator.dedupe(dedupeKey, params.user.userId, params.eventId, `file:${filename}`)) {
log.silly("file message dedupe");
return;
}
uri += "/views/thumblarge";
uri = uri.replace("static.asm.skype.com", "static-asm.secure.skypeassets.com");
const buffer = await p.client.downloadFile(uri);
await this.puppet.sendFileDetect(params, buffer, filename);
return;
}
}
if (await this.messageDeduplicator.dedupe(dedupeKey, params.user.userId, params.eventId, msg)) {
log.silly("normal message dedupe");
return;
}
let sendMsg: IMessageEvent;
if (rich) {
sendMsg = this.skypeMessageParser.parse(msg);
} else {
sendMsg = {
body: msg,
};
}
if (emote) {
sendMsg.emote = true;
}
await this.puppet.sendMessage(params, sendMsg);
}
private async handleSkypeEdit(
puppetId: number,
resource: skypeHttp.resources.TextResource | skypeHttp.resources.RichTextResource,
) {
const p = this.puppets[puppetId];
if (!p) {
return;
}
const rich = resource.native.messagetype.startsWith("RichText");
log.info("Got new skype edit");
log.silly(resource);
const params = await this.getSendParams(puppetId, resource);
if (!params) {
log.warn("Couldn't generate params");
return;
}
let msg = resource.content;
let emote = false;
if (resource.native.skypeemoteoffset) {
emote = true;
msg = msg.substr(Number(resource.native.skypeemoteoffset));
}
@ -458,17 +524,13 @@ export class Skype {
if (emote) {
sendMsg.emote = true;
}
if (resource.native && resource.native.skypeeditedid) {
if (resource.content) {
await this.puppet.sendEdit(params, resource.native.skypeeditedid, sendMsg);
} else if (p.deletedMessages.has(resource.native.skypeeditedid)) {
log.silly("normal message redact dedupe");
return;
} else {
await this.puppet.sendRedact(params, resource.native.skypeeditedid);
}
if (resource.content) {
await this.puppet.sendEdit(params, resource.id, sendMsg);
} else if (p.deletedMessages.has(resource.id)) {
log.silly("normal message redact dedupe");
return;
} else {
await this.puppet.sendMessage(params, sendMsg);
await this.puppet.sendRedact(params, resource.id);
}
}
@ -490,14 +552,6 @@ export class Skype {
log.silly("file message dedupe");
return;
}
if (resource.native && resource.native.skypeeditedid && !resource.uri) {
if (p.deletedMessages.has(resource.native.skypeeditedid)) {
log.silly("file message redact dedupe");
return;
}
await this.puppet.sendRedact(params, resource.native.skypeeditedid);
return;
}
const buffer = await p.client.downloadFile(resource.uri);
await this.puppet.sendFileDetect(params, buffer, filename);
}
@ -519,7 +573,7 @@ export class Skype {
private async handleSkypePresence(puppetId: number, resource: skypeHttp.resources.Resource) {
const p = this.puppets[puppetId];
if (!p || !resource.native) {
if (!p) {
return;
}
log.info("Got new skype presence event");

View File

@ -15,6 +15,7 @@ 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";
import * as emoji from "node-emoji";
export class SkypeMessageParser {
public parse(msg: string): IMessageEvent {
@ -26,7 +27,13 @@ export class SkypeMessageParser {
}
private walkChildNodes(node: Parser.Node): IMessageEvent {
return node.childNodes.map((node) => this.walkNode(node)).reduce((acc, curr) => {
if (!node.childNodes.length) {
return {
body: "",
formattedBody: "",
};
}
return node.childNodes.map((n) => this.walkNode(n)).reduce((acc, curr) => {
return {
body: acc.body + curr.body,
formattedBody: acc.formattedBody! + curr.formattedBody!,
@ -83,7 +90,82 @@ export class SkypeMessageParser {
formattedBody: `<a href="${escapeHtml(href)}">${child.formattedBody}</a>`,
};
}
case "ss": {
// skype emoji
const type = nodeHtml.attributes.type;
let emojiType = {
smile: "slightly_smiling_face",
sad: "slightly_frowning_face",
laugh: "grin",
cool: "sunglasses",
hearteyes: "heart_eyes",
stareyes: "star-struck",
like: "thumbsup",
cwl: "rolling_on_the_floor_laughing",
xd: "laughing",
happyface: "smiley",
happyeyes: "smile",
// hysterical: "", TODO: find
sweatgrinning: "sweat_smile",
// smileeyes: "", TODO: find
blankface: "no_mouth",
surprised: "astonished",
upsidedownface: "upside_down_face",
loudlycrying: "sob",
shivering: "🥶",
speechless: "😐️",
tongueout: "stuck_out_tongue",
winktongueout: "stuck_out_tongue_winking_eye",
inlove: "🥰",
// wonder: "", TODO: find
// dull: "", TODO: find
yawn: "🥱",
puke: "face_vomiting",
// doh: "", TODO: find
angryface: "angry",
angry: "rage",
// wasntme: "", TODO: find
// worry: "", TODO: find
screamingfear: "scream",
// veryconfused: "", TODO: find
// mmm: "", TODO: find
nerdy: "nerd_face",
loveearth: "🌍️",
// rainbowsmile: "", TODO: find
// lipssealed: "", TODO: find
devil: "smiling_imp",
// envy: "", TODO: find
// makeup: "", TODO: find
think: "thinking_face",
rofl: "rolling_on_the_floor_laughing",
}[type];
const haveEmojiType = Boolean(emojiType);
if (!emojiType) {
emojiType = type;
}
let e = emoji.get(emojiType);
if (e === `:${emojiType}:`) {
e = emoji.get(emojiType + "_face");
}
if (!e.startsWith(":")) {
return {
body: e,
formattedBody: e,
};
}
if (haveEmojiType) {
return {
body: emojiType,
formattedBody: emojiType,
};
}
return {
body: `(${type})`,
formattedBody: `(${escapeHtml(type)})`,
};
}
case "e_m":
// empty edit tag
return {
body: "",
formattedBody: "",