Basic XMPP

This commit is contained in:
Arcady Chumachenko 2021-11-12 13:07:57 +00:00
parent 282e4da983
commit db76a70295
11 changed files with 7867 additions and 788 deletions

5
.gitignore vendored
View File

@ -1,5 +1,8 @@
node_modules node_modules
config.yaml config.yaml
build build
skype-registration.yaml registration.yaml
xmpp-registration.yaml
*.db *.db
*-audit.json
*.log.*

View File

@ -1,11 +1,11 @@
FROM node:alpine AS builder FROM node:alpine AS builder
WORKDIR /opt/mx-puppet-skype WORKDIR /opt/mx-puppet-xmpp
# run build process as user in case of npm pre hooks # run build process as user in case of npm pre hooks
# pre hooks are not executed while running as root # pre hooks are not executed while running as root
RUN chown node:node /opt/mx-puppet-skype RUN chown node:node /opt/mx-puppet-xmpp
RUN apk --no-cache add git python make g++ pkgconfig \ RUN apk update && apk --no-cache add git python3 make g++ pkgconfig \
build-base \ build-base \
cairo-dev \ cairo-dev \
jpeg-dev \ jpeg-dev \
@ -15,7 +15,8 @@ RUN apk --no-cache add git python make g++ pkgconfig \
pixman-dev \ pixman-dev \
pangomm-dev \ pangomm-dev \
libjpeg-turbo-dev \ libjpeg-turbo-dev \
freetype-dev freetype-dev \
&& rm -rf /var/cache/apk/*
RUN wget -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub && \ RUN wget -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub && \
wget -O glibc-2.32-r0.apk https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.32-r0/glibc-2.32-r0.apk && \ wget -O glibc-2.32-r0.apk https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.32-r0/glibc-2.32-r0.apk && \
@ -38,10 +39,10 @@ FROM node:alpine
VOLUME /data VOLUME /data
ENV CONFIG_PATH=/data/config.yaml \ ENV CONFIG_PATH=/data/config.yaml \
REGISTRATION_PATH=/data/skype-registration.yaml REGISTRATION_PATH=/data/xmpp-registration.yaml
# su-exec is used by docker-run.sh to drop privileges # su-exec is used by docker-run.sh to drop privileges
RUN apk add --no-cache su-exec \ RUN apk update && apk add --no-cache su-exec \
cairo \ cairo \
jpeg \ jpeg \
pango \ pango \
@ -50,15 +51,16 @@ RUN apk add --no-cache su-exec \
pixman \ pixman \
pangomm \ pangomm \
libjpeg-turbo \ libjpeg-turbo \
freetype freetype \
&& rm -rf /var/cache/apk/*
WORKDIR /opt/mx-puppet-skype WORKDIR /opt/mx-puppet-xmpp
COPY docker-run.sh ./ COPY docker-run.sh ./
COPY --from=builder /opt/mx-puppet-skype/node_modules/ ./node_modules/ COPY --from=builder /opt/mx-puppet-xmpp/node_modules/ ./node_modules/
COPY --from=builder /opt/mx-puppet-skype/build/ ./build/ COPY --from=builder /opt/mx-puppet-xmpp/build/ ./build/
# change workdir to /data so relative paths in the config.yaml # change workdir to /data so relative paths in the config.yaml
# point to the persisten volume # point to the persisten volume
WORKDIR /data WORKDIR /data
ENTRYPOINT ["/opt/mx-puppet-skype/docker-run.sh"] ENTRYPOINT ["/opt/mx-puppet-xmpp/docker-run.sh"]

View File

@ -1,11 +1,17 @@
[![Support room on Matrix](https://img.shields.io/matrix/mx-puppet-bridge:sorunome.de.svg?label=%23mx-puppet-bridge%3Asorunome.de&logo=matrix&server_fqdn=sorunome.de)](https://matrix.to/#/#mx-puppet-bridge:sorunome.de)[![donate](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/Sorunome/donate) [![donate](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/rkd/donate)
# mx-puppet-skype # [WIP] mx-puppet-xmpp
This is a skype puppeting bridge for matrix. It is based on [mx-puppet-bridge](https://github.com/Sorunome/mx-puppet-bridge) and provide multi-user instances. This is a xmpp puppeting bridge for matrix. It is based on [mx-puppet-bridge](https://github.com/Sorunome/mx-puppet-bridge) and provide multi-user instances.
##
## Quick start using Docker ## Quick start using Docker
Docker image can be found at https://hub.docker.com/r/sorunome/mx-puppet-skype To build docker image:
```
docker build -t mx-puppet-xmpp:latest .
```
For docker you probably want the following changes in `config.yaml`: For docker you probably want the following changes in `config.yaml`:
@ -21,16 +27,16 @@ Also check the config for other values, like your homeserver domain.
* Clone and install: * Clone and install:
``` ```
git clone https://github.com/Sorunome/mx-puppet-skype.git git clone https://github.com/Sorunome/mx-puppet-xmpp.git
cd mx-puppet-skype cd mx-puppet-xmpp
npm install npm install
* Edit the configuration file and generate the registration file: * Edit the configuration file and generate the registration file:
``` ```
cp sample.config.yaml config.yaml cp sample.config.yaml config.yaml
# fill info about your homeserver and skype app credentials to config.yaml manually # fill info about your homeserver and xmpp app credentials to config.yaml manually
npm run start -- -r # generate registration file npm run start -- -r # generate registration file
or or
docker run -v </path/to/host>/data:/data -it sorunome/mx-puppet-skype -r docker run -v </path/to/host>/data:/data -it mx-puppet-xmpp -r
``` ```
* Copy the registration file to your synapse config directory. * Copy the registration file to your synapse config directory.
* Add the registration file to the list under `app_service_config_files:` in your synapse config. * Add the registration file to the list under `app_service_config_files:` in your synapse config.
@ -39,9 +45,9 @@ Also check the config for other values, like your homeserver domain.
``` ```
npm run start npm run start
``` ```
* Start a direct chat with the bot user (`@_skypepuppet_bot:domain.tld` unless you changed the config). * Start a direct chat with the bot user (`@_xmpppuppet_bot:domain.tld` unless you changed the config).
(Give it some time after the invite, it'll join after a minute maybe.) (Give it some time after the invite, it'll join after a minute maybe.)
* Get your Skype username and password as below, and tell the bot user to link your skype account: * Get your Xmpp username and password as below, and tell the bot user to link your xmpp account:
``` ```
link <username> <password> link <username> <password>
``` ```
@ -50,3 +56,17 @@ Also check the config for other values, like your homeserver domain.
list list
``` ```
Clicking rooms in the list will result in you receiving an invite to the bridged room. Clicking rooms in the list will result in you receiving an invite to the bridged room.
## Working
- link
- text messages (mx -> xmpp)
- text messages (xmpp -> mx)
## TODO
- replies
- edits
- deletes
- images
- files

View File

@ -32,7 +32,7 @@ else
fi fi
# $su_exec is used in case we have to drop the privileges # $su_exec is used in case we have to drop the privileges
exec $su_exec /usr/local/bin/node '/opt/mx-puppet-skype/build/index.js' \ exec $su_exec /usr/local/bin/node '/opt/mx-puppet-xmpp/build/index.js' \
-c "$CONFIG_PATH" \ -c "$CONFIG_PATH" \
-f "$REGISTRATION_PATH" \ -f "$REGISTRATION_PATH" \
$args $args

7617
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{ {
"name": "mx-puppet-skype", "name": "mx-puppet-xmpp",
"version": "0.0.0", "version": "0.0.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
@ -9,9 +9,9 @@
"start": "npm run-script build && node ./build/index.js", "start": "npm run-script build && node ./build/index.js",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "Sorunome", "author": "rkd",
"dependencies": { "dependencies": {
"@sorunome/skype-http": "^1.5.2", "@xmpp/client": "^0.13.0",
"cheerio": "^1.0.0-rc.3", "cheerio": "^1.0.0-rc.3",
"command-line-args": "^5.1.1", "command-line-args": "^5.1.1",
"command-line-usage": "^5.0.5", "command-line-usage": "^5.0.5",
@ -20,7 +20,7 @@
"events": "^3.0.0", "events": "^3.0.0",
"expire-set": "^1.0.0", "expire-set": "^1.0.0",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"mx-puppet-bridge": "0.1.4", "mx-puppet-bridge": "0.1.6",
"node-emoji": "^1.10.0", "node-emoji": "^1.10.0",
"node-html-parser": "^1.2.13", "node-html-parser": "^1.2.13",
"tough-cookie": "^4.0.0", "tough-cookie": "^4.0.0",

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2020 mx-puppet-skype Copyright 2020 mx-puppet-xmpp
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
@ -11,185 +11,108 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { Log, IRemoteRoom, Util } from "mx-puppet-bridge"; import { Log, IRemoteRoom } from "mx-puppet-bridge";
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import * as skypeHttp from "@sorunome/skype-http"; import { client, xml } from "@xmpp/client";
import { Contact as SkypeContact } from "@sorunome/skype-http/dist/lib/types/contact"; import { Client as XmppClient } from "@xmpp/client-core";
import { NewMediaMessage as SkypeNewMediaMessage } from "@sorunome/skype-http/dist/lib/interfaces/api/api";
import { Context as SkypeContext } from "@sorunome/skype-http/dist/lib/interfaces/api/context";
import ExpireSet from "expire-set";
import * as toughCookie from "tough-cookie";
const log = new Log("SkypePuppet:client"); const log = new Log("XmppPuppet:client");
// tslint:disable no-magic-numbers type Contact = {
const ID_TIMEOUT = 60000; personId: string,
const CONTACTS_DELTA_INTERVAL = 5 * 60 * 1000; workloads: any,
// tslint:enable no-magic-numbers mri: string,
blocked: boolean,
authorized: boolean,
creationTime: Date,
displayName: string,
displayNameSource: any, // tslint:disable-line no-any
profile: {
roomId: string,
avatarUrl: string | undefined,
name: {
first: string | undefined,
surname: string | undefined,
nickname: string | undefined,
},
},
}
export class Client extends EventEmitter { export class Client extends EventEmitter {
public contacts: Map<string, SkypeContact> = new Map(); public contacts: Map<string, Contact> = new Map();
public conversations: Map<string, skypeHttp.Conversation> = new Map(); public conversations: Map<string, any> = new Map();
private api: skypeHttp.Api; private api: XmppClient;
private handledIds: ExpireSet<string>;
private contactsInterval: NodeJS.Timeout | null = null;
constructor( constructor(
private loginUsername: string, private loginUsername: string,
private password: string, private password: string,
private state?: SkypeContext.Json, ) { super(); }
) {
super();
this.handledIds = new ExpireSet(ID_TIMEOUT);
}
public get username(): string { public get username(): string {
return "8:" + this.api.context.username; return this.loginUsername.split("@")[0].trim();
} }
public get getState(): SkypeContext.Json { public get host(): string {
return this.api.getState(); return this.loginUsername.split("@")[1].trim();
}
public get getState() {
return {};
} }
public async connect() { public async connect() {
let connectedWithAuth = false; log.info("Connecting to ", this.host);
if (this.state) { log.info(this.username);
try {
this.api = await skypeHttp.connect({ state: this.state, verbose: true });
connectedWithAuth = true;
} catch (err) {
this.api = await skypeHttp.connect({
credentials: {
username: this.loginUsername,
password: this.password,
},
verbose: true,
});
connectedWithAuth = false;
}
} else {
this.api = await skypeHttp.connect({
credentials: {
username: this.loginUsername,
password: this.password,
},
verbose: true,
});
connectedWithAuth = false;
}
try { this.api = client({
await this.startupApi(); service: "ws://"+this.host+":5280/xmpp-websocket",
} catch (err) { domain: this.host,
if (!connectedWithAuth) { resource: "mx_bridge",
throw err; username: this.username,
}
this.api = await skypeHttp.connect({
credentials: {
username: this.loginUsername,
password: this.password, password: this.password,
},
verbose: true,
}); });
connectedWithAuth = false;
await this.startupApi();
}
const registerErrorHandler = () => { await this.startupApi();
this.api.on("error", (err: Error) => { this.api.on("error", (err: Error) => {
log.error("An error occured", err); log.error("An error occured", err);
this.emit("error", err); this.emit("error", err);
}); });
}; this.api.start();
if (connectedWithAuth) {
let resolved = false;
return new Promise(async (resolve, reject) => {
const TIMEOUT_SUCCESS = 5000;
setTimeout(() => {
if (resolved) {
return;
}
resolved = true;
registerErrorHandler();
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();
registerErrorHandler();
resolve();
} catch (err) {
reject(err);
}
});
await this.api.listen();
}).then(async () => {
await this.api.setStatus("Online");
});
} else {
registerErrorHandler();
await this.api.listen();
await this.api.setStatus("Online");
}
} }
public async disconnect() { public async disconnect() {
if (this.api) { if (this.api) {
await this.api.stopListening(); await this.api.stop();
}
if (this.contactsInterval) {
clearInterval(this.contactsInterval);
this.contactsInterval = null;
} }
} }
public async getContact(id: string): Promise<SkypeContact | null> { public async getContact(username: string): Promise<any> {
log.debug(`Fetching contact ${id}`); log.debug(`Fetching contact from: ` + username);
const hasStart = Boolean(id.match(/^\d+:/)); if (this.contacts.has(username)) {
const fullId = hasStart ? id : `8:${id}`; const ret = this.contacts.get(username);
if (this.contacts.has(fullId)) {
log.debug("Returning cached result");
const ret = this.contacts.get(fullId) || null;
log.silly(ret);
return ret; return ret;
} }
if (hasStart) {
id = id.substr(id.indexOf(":") + 1);
}
try { try {
const rawContact = await this.api.getContact(id); const contact = {
const contact: SkypeContact = { personId: username,
personId: rawContact.id.raw,
workloads: null, workloads: null,
mri: rawContact.id.raw, mri: username,
blocked: false, blocked: false,
authorized: true, authorized: true,
creationTime: new Date(), creationTime: new Date(),
displayName: (rawContact.name && rawContact.name.displayName) || rawContact.id.id, displayName: username,
displayNameSource: "profile" as any, // tslint:disable-line no-any displayNameSource: "profile" as any, // tslint:disable-line no-any
profile: { profile: {
avatarUrl: rawContact.avatarUrl || undefined, roomId: username,
avatarUrl: undefined,
name: { name: {
first: (rawContact.name && rawContact.name).first || undefined, first: undefined,
surname: (rawContact.name && rawContact.name).surname || undefined, surname: undefined,
nickname: (rawContact.name && rawContact.name).nickname || undefined, nickname: username,
}, },
}, },
}; };
this.contacts.set(contact.mri, contact || null); this.contacts.set(contact.mri, contact);
log.debug("Returning new result"); log.debug("Returning new result");
log.silly(contact); log.silly(contact);
return contact || null; return contact || null;
@ -201,181 +124,93 @@ export class Client extends EventEmitter {
} }
} }
public async getConversation(room: IRemoteRoom): Promise<skypeHttp.Conversation | null> { public async getConversation(room: IRemoteRoom): Promise<any> {
log.debug(`Fetching conversation puppetId=${room.puppetId} roomId=${room.roomId}`); log.info(`Fetching conversation`, room);
log.info(`Fetching conversation puppetId=${room.puppetId} roomId=${room.roomId}`);
let id = room.roomId; let id = room.roomId;
const match = id.match(/^dm-\d+-/);
if (match) {
const [_, puppetId] = id.split("-");
if (Number(puppetId) !== room.puppetId) {
return null;
}
id = id.substr(match[0].length);
}
if (this.conversations.has(id)) { if (this.conversations.has(id)) {
log.debug("Returning cached result"); log.info("Returning cached result");
const ret = this.conversations.get(id) || null; const ret = this.conversations.get(id) || null;
log.silly(ret); log.silly(ret);
return ret; return ret;
} }
try { try {
const conversation = await this.api.getConversation(id); const conversation = {id: room.roomId, members: []};
this.conversations.set(conversation.id, conversation || null); this.conversations.set(room.roomId, conversation || null);
log.debug("Returning new result"); log.info("Returning new result");
log.silly(conversation); log.info(conversation);
return conversation || null; return conversation || null;
} catch (err) { } catch (err) {
// conversation not found // conversation not found
log.debug("No such conversation found"); log.error("No such conversation found");
log.debug(err.body || err); log.error(err.body || err);
return null; return null;
} }
} }
public async downloadFile(url: string, type: string = "imgpsh_fullsize_anim"): Promise<Buffer> { public async downloadFile(url: string, type: string = "imgpsh_fullsize_anim") {
if (url.startsWith("https://api.asm.skype.com/") && !url.includes("/views/")) { // TODO
url = `${url}/views/${type}`;
}
const cookieJar = new toughCookie.CookieJar(this.api.context.cookies);
return await Util.DownloadFile(url, {
responseType: "buffer",
headers: {
Authorization: "skypetoken=" + this.api.context.skypeToken.value,
RegistrationToken: this.api.context.registrationToken.raw,
},
cookieJar: {
setCookie: async (rawCookie: string, cookieUrl: string) =>
new Promise((resolve, reject) =>
cookieJar.setCookie(rawCookie, cookieUrl, (err, value) =>
err ? reject(err) : resolve(value),
),
),
getCookieString: async (cookieUrl: string) =>
new Promise((resolve, reject) =>
cookieJar.getCookieString(cookieUrl, (err, value) => {
if (err) {
reject(err);
return;
}
if (url.startsWith("https://api.asm.skype.com/")) {
if (value) {
value += "; ";
}
value += "skypetoken_asm=" + encodeURIComponent(this.api.context.skypeToken.value);
}
resolve(value);
}),
),
},
});
} }
public async sendMessage(conversationId: string, msg: string): Promise<skypeHttp.Api.SendMessageResult> { public async sendMessage(conversationId: string, msg: string) {
return await this.api.sendMessage({ return await this.api.send(xml(
textContent: msg, "message",
}, conversationId); { type: "chat", to: conversationId },
xml("body", {}, msg),
));
} }
public async sendEdit(conversationId: string, messageId: string, msg: string) { public async sendEdit(conversationId: string, messageId: string, msg: string) {
return await this.api.sendEdit({ // TODO
textContent: msg, // return await this.api.sendEdit({
}, conversationId, messageId); // textContent: msg,
// }, conversationId, messageId);
} }
public async sendDelete(conversationId: string, messageId: string) { public async sendDelete(conversationId: string, messageId: string) {
return await this.api.sendDelete(conversationId, messageId); // TODO
// return await this.api.sendDelete(conversationId, messageId);
} }
public async sendAudio( public async sendAudio(
conversationId: string, conversationId: string,
opts: SkypeNewMediaMessage, opts: any,
): Promise<skypeHttp.Api.SendMessageResult> { ) {
return await this.api.sendAudio(opts, conversationId); // TODO
// return await this.api.sendAudio(opts, conversationId);
} }
public async sendDocument( public async sendDocument(
conversationId: string, conversationId: string,
opts: SkypeNewMediaMessage, opts: any,
): Promise<skypeHttp.Api.SendMessageResult> { ) {
return await this.api.sendDocument(opts, conversationId); // TODO
// return await this.api.sendDocument(opts, conversationId);
} }
public async sendImage( public async sendImage(
conversationId: string, conversationId: string,
opts: SkypeNewMediaMessage, opts: any,
): Promise<skypeHttp.Api.SendMessageResult> { ) {
return await this.api.sendImage(opts, conversationId); // TODO
// return await this.api.sendImage(opts, conversationId);
} }
private async startupApi() { private async startupApi() {
this.api.on("event", (evt: skypeHttp.events.EventMessage) => { this.api.on("stanza", async (stanza) => {
if (!evt || !evt.resource) { if (stanza.is("message")) {
return; this.emit("text", stanza);
}
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;
} }
}); });
const contacts = await this.api.getContacts(); this.api.on("online", async (address) => {
for (const contact of contacts) { await this.api.send(xml("presence"));
this.contacts.set(contact.mri, contact); });
// 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);
} }
if (this.contactsInterval) {
clearInterval(this.contactsInterval);
this.contactsInterval = null;
}
this.contactsInterval = setInterval(this.updateContacts.bind(this), CONTACTS_DELTA_INTERVAL);
}
private async updateContacts() {
log.verbose("Getting contacts diff....");
try {
const contacts = await this.api.getContacts(true);
const MANY_CONTACTS = 5;
for (const contact of contacts) {
const oldContact = this.contacts.get(contact.mri) || null;
this.contacts.set(contact.mri, contact);
this.emit("updateContact", oldContact, contact);
}
} catch (err) {
log.error("Failed to get contacts diff", err);
this.emit("error", err);
}
}
}

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2020 mx-puppet-skype Copyright 2020 mx-puppet-xmpp
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
@ -23,10 +23,10 @@ import * as commandLineArgs from "command-line-args";
import * as commandLineUsage from "command-line-usage"; import * as commandLineUsage from "command-line-usage";
import * as fs from "fs"; import * as fs from "fs";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import { Skype } from "./skype"; import { Xmpp } from "./xmpp";
import { Client } from "./client"; import { Client } from "./client";
const log = new Log("SkypePuppet:index"); const log = new Log("XmppPuppet:index");
const commandOptions = [ const commandOptions = [
{ name: "register", alias: "r", type: Boolean }, { name: "register", alias: "r", type: Boolean },
@ -36,7 +36,7 @@ const commandOptions = [
]; ];
const options = Object.assign({ const options = Object.assign({
"register": false, "register": false,
"registration-file": "skype-registration.yaml", "registration-file": "xmpp-registration.yaml",
"config": "config.yaml", "config": "config.yaml",
"help": false, "help": false,
}, commandLineArgs(commandOptions)); }, commandLineArgs(commandOptions));
@ -45,8 +45,8 @@ if (options.help) {
// tslint:disable-next-line:no-console // tslint:disable-next-line:no-console
console.log(commandLineUsage([ console.log(commandLineUsage([
{ {
header: "Matrix Skype Puppet Bridge", header: "Matrix Xmpp Puppet Bridge",
content: "A matrix puppet bridge for Skype", content: "A matrix puppet bridge for Xmpp",
}, },
{ {
header: "Options", header: "Options",
@ -58,16 +58,16 @@ if (options.help) {
const protocol: IProtocolInformation = { const protocol: IProtocolInformation = {
features: { features: {
image: true, image: false,
audio: true, audio: false,
file: true, file: false,
edit: true, edit: false,
reply: true, reply: false,
globalNamespace: true, globalNamespace: true,
}, },
id: "skype", id: "xmpp",
displayname: "Skype", displayname: "Xmpp",
externalUrl: "https://skype.com/", externalUrl: "https://xmpp.com/",
}; };
const puppet = new PuppetBridge(options["registration-file"], options.config, protocol); const puppet = new PuppetBridge(options["registration-file"], options.config, protocol);
@ -77,8 +77,8 @@ if (options.register) {
puppet.readConfig(false); puppet.readConfig(false);
try { try {
puppet.generateRegistration({ puppet.generateRegistration({
prefix: "_skypepuppet_", prefix: "_xmpppuppet_",
id: "skype-puppet", id: "xmpp-puppet",
url: `http://${puppet.Config.bridge.bindAddress}:${puppet.Config.bridge.port}`, url: `http://${puppet.Config.bridge.bindAddress}:${puppet.Config.bridge.port}`,
}); });
} catch (err) { } catch (err) {
@ -90,24 +90,24 @@ if (options.register) {
async function run() { async function run() {
await puppet.init(); await puppet.init();
const skype = new Skype(puppet); const xmpp = new Xmpp(puppet);
puppet.on("puppetNew", skype.newPuppet.bind(skype)); puppet.on("puppetNew", xmpp.newPuppet.bind(xmpp));
puppet.on("puppetDelete", skype.deletePuppet.bind(skype)); puppet.on("puppetDelete", xmpp.deletePuppet.bind(xmpp));
puppet.on("message", skype.handleMatrixMessage.bind(skype)); puppet.on("message", xmpp.handleMatrixMessage.bind(xmpp));
puppet.on("edit", skype.handleMatrixEdit.bind(skype)); puppet.on("edit", xmpp.handleMatrixEdit.bind(xmpp));
puppet.on("reply", skype.handleMatrixReply.bind(skype)); puppet.on("reply", xmpp.handleMatrixReply.bind(xmpp));
puppet.on("redact", skype.handleMatrixRedact.bind(skype)); puppet.on("redact", xmpp.handleMatrixRedact.bind(xmpp));
puppet.on("image", skype.handleMatrixImage.bind(skype)); puppet.on("image", xmpp.handleMatrixImage.bind(xmpp));
puppet.on("audio", skype.handleMatrixAudio.bind(skype)); puppet.on("audio", xmpp.handleMatrixAudio.bind(xmpp));
puppet.on("file", skype.handleMatrixFile.bind(skype)); puppet.on("file", xmpp.handleMatrixFile.bind(xmpp));
puppet.setCreateUserHook(skype.createUser.bind(skype)); puppet.setCreateUserHook(xmpp.createUser.bind(xmpp));
puppet.setCreateRoomHook(skype.createRoom.bind(skype)); puppet.setCreateRoomHook(xmpp.createRoom.bind(xmpp));
puppet.setGetDmRoomIdHook(skype.getDmRoom.bind(skype)); puppet.setGetDmRoomIdHook(xmpp.getDmRoom.bind(xmpp));
puppet.setListUsersHook(skype.listUsers.bind(skype)); puppet.setListUsersHook(xmpp.listUsers.bind(xmpp));
puppet.setListRoomsHook(skype.listRooms.bind(skype)); puppet.setListRoomsHook(xmpp.listRooms.bind(xmpp));
puppet.setGetUserIdsInRoomHook(skype.getUserIdsInRoom.bind(skype)); puppet.setGetUserIdsInRoomHook(xmpp.getUserIdsInRoom.bind(xmpp));
puppet.setGetDescHook(async (puppetId: number, data: any): Promise<string> => { puppet.setGetDescHook(async (puppetId: number, data: any): Promise<string> => {
let s = "Skype"; let s = "Xmpp";
if (data.username) { if (data.username) {
s += ` as \`${data.username}\``; s += ` as \`${data.username}\``;
} }
@ -139,7 +139,7 @@ async function run() {
return retData; return retData;
}); });
puppet.setBotHeaderMsgHook((): string => { puppet.setBotHeaderMsgHook((): string => {
return "Skype Puppet Bridge"; return "Xmpp Puppet Bridge";
}); });
await puppet.start(); await puppet.start();
} }

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2020 mx-puppet-skype Copyright 2020 mx-puppet-xmpp
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2020 mx-puppet-skype Copyright 2020 mx-puppet-xmpp
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
@ -16,46 +16,46 @@ import {
IRetList, IReplyEvent, IRetList, IReplyEvent,
} from "mx-puppet-bridge"; } from "mx-puppet-bridge";
import { Client } from "./client"; import { Client } from "./client";
import * as skypeHttp from "@sorunome/skype-http";
import { Contact as SkypeContact } from "@sorunome/skype-http/dist/lib/types/contact";
import { NewMediaMessage as SkypeNewMediaMessage } from "@sorunome/skype-http/dist/lib/interfaces/api/api";
import { UnexpectedHttpStatusError } from "@sorunome/skype-http/dist/lib/errors";
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 { MatrixMessageParser } from "./matrixmessageparser";
import { SkypeMessageParser } from "./skypemessageparser"; import { XmppMessageParser } from "./xmppmessageparser";
import * as cheerio from "cheerio"; import * as cheerio from "cheerio";
import ExpireSet from "expire-set"; import ExpireSet from "expire-set";
const log = new Log("SkypePuppet:skype"); const log = new Log("XmppPuppet:xmpp");
const ROOM_TYPE_DM = 8; interface IXmppPuppet {
interface ISkypePuppet {
client: Client; client: Client;
data: any; data: any;
deletedMessages: ExpireSet<string>; deletedMessages: ExpireSet<string>;
restarting: boolean; restarting: boolean;
} }
interface ISkypePuppets { interface IXmppPuppets {
[puppetId: number]: ISkypePuppet; [puppetId: number]: IXmppPuppet;
} }
export class Skype { interface IStanza {
private puppets: ISkypePuppets = {}; attrs: {to: string, from:string, id: string};
getChild(path: string): {text: () => string}
}
export class Xmpp {
private puppets: IXmppPuppets = {};
private messageDeduplicator: MessageDeduplicator; private messageDeduplicator: MessageDeduplicator;
private matrixMessageParser: MatrixMessageParser; private matrixMessageParser: MatrixMessageParser;
private skypeMessageParser: SkypeMessageParser; private xmppMessageParser: XmppMessageParser;
constructor( constructor(
private puppet: PuppetBridge, private puppet: PuppetBridge,
) { ) {
this.messageDeduplicator = new MessageDeduplicator(); this.messageDeduplicator = new MessageDeduplicator();
this.matrixMessageParser = new MatrixMessageParser(); this.matrixMessageParser = new MatrixMessageParser();
this.skypeMessageParser = new SkypeMessageParser(); this.xmppMessageParser = new XmppMessageParser();
} }
public getUserParams(puppetId: number, contact: SkypeContact): IRemoteUser { public getUserParams(puppetId: number, contact: any): IRemoteUser {
return { return {
puppetId, puppetId,
userId: contact.mri, userId: contact.mri,
@ -64,55 +64,38 @@ export class Skype {
}; };
} }
public getRoomParams(puppetId: number, conversation: skypeHttp.Conversation): IRemoteRoom { public getRoomParams(puppetId: number, conversation: any): IRemoteRoom {
const roomType = Number(conversation.id.split(":")[0]);
const isDirect = roomType === ROOM_TYPE_DM;
if (isDirect) {
return {
puppetId,
roomId: `dm-${puppetId}-${conversation.id}`,
isDirect: true,
};
}
let avatarUrl: string | null = null; let avatarUrl: string | null = null;
let name: string | null = null; let name: string | null = null;
if (conversation.threadProperties) {
name = conversation.threadProperties.topic || null;
if (name) {
name = decodeHtml(name);
}
const picture = conversation.threadProperties.picture;
if (picture && picture.startsWith("URL@")) {
avatarUrl = picture.slice("URL@".length);
}
}
const p = this.puppets[puppetId]; const p = this.puppets[puppetId];
return { return {
puppetId, puppetId,
roomId: conversation.id, roomId: conversation.id,
name, name,
avatarUrl, avatarUrl,
downloadFile: async (url: string): Promise<Buffer> => { downloadFile: async (url: string): Promise<any> => {
return await p.client.downloadFile(url, "swx_avatar"); return await p.client.downloadFile(url, "swx_avatar");
}, },
}; };
} }
public async getSendParams(puppetId: number, resource: skypeHttp.resources.Resource): Promise<IReceiveParams | null> { public async getSendParams(puppetId: number, stanza: IStanza): Promise<IReceiveParams | null> {
const roomType = Number(resource.conversation.split(":")[0]);
const p = this.puppets[puppetId]; const p = this.puppets[puppetId];
const contact = await p.client.getContact(resource.from.raw); const contact = await p.client.getContact(stanza.attrs.from);
log.info("stanza.attrs", stanza.attrs);
const conversation = await p.client.getConversation({ const conversation = await p.client.getConversation({
puppetId, puppetId: puppetId,
roomId: resource.conversation, roomId: stanza.attrs.from.split("/")[0],
}); });
log.info("Received contact", contact);
log.info("Received conversation", conversation);
if (!contact || !conversation) { if (!contact || !conversation) {
return null; return null;
} }
return { return {
user: this.getUserParams(puppetId, contact), user: this.getUserParams(puppetId, contact),
room: this.getRoomParams(puppetId, conversation), room: this.getRoomParams(puppetId, conversation),
eventId: resource.id, // tslint:disable-line no-any eventId: stanza.attrs.id, // tslint:disable-line no-any
}; };
} }
@ -130,51 +113,58 @@ export class Skype {
return; return;
} }
await this.stopClient(puppetId); await this.stopClient(puppetId);
p.client = new Client(p.data.username, p.data.password, p.data.state); p.client = new Client(p.data.username, p.data.password);
const client = p.client; const client = p.client;
client.on("text", async (resource: skypeHttp.resources.TextResource) => { client.on("text", async (stanza: any) => {
try { try {
await this.handleSkypeText(puppetId, resource); await this.handleXmppText(puppetId, stanza);
} catch (err) { } catch (err) {
log.error("Error while handling text event", err); log.error("Error while handling text event", err);
} }
}); });
client.on("edit", async (resource: skypeHttp.resources.RichTextResource) => { client.on("edit", async (stanza: any) => {
try { try {
await this.handleSkypeEdit(puppetId, resource); await this.handleXmppEdit(puppetId, stanza);
} catch (err) { } catch (err) {
log.error("Error while handling edit event", err); log.error("Error while handling edit event", err);
} }
}); });
client.on("location", async (resource: skypeHttp.resources.RichTextLocationResource) => { client.on("location", async (stanza: any) => {
try { try {
} catch (err) { } catch (err) {
log.error("Error while handling location event", err); log.error("Error while handling location event", err);
} }
}); });
client.on("file", async (resource: skypeHttp.resources.FileResource) => { client.on("file", async (stanza: any) => {
try { try {
await this.handleSkypeFile(puppetId, resource); await this.handleXmppFile(puppetId, stanza);
} catch (err) { } catch (err) {
log.error("Error while handling file event", err); log.error("Error while handling file event", err);
} }
}); });
client.on("typing", async (resource: skypeHttp.resources.Resource, typing: boolean) => { client.on("typing", async (stanza: any, typing: boolean) => {
try { try {
await this.handleSkypeTyping(puppetId, resource, typing); await this.handleXmppTyping(puppetId, stanza, typing);
} catch (err) { } catch (err) {
log.error("Error while handling typing event", err); log.error("Error while handling typing event", err);
} }
}); });
client.on("presence", async (resource: skypeHttp.resources.Resource) => { client.on("presence", async (stanza: any) => {
try { try {
await this.handleSkypePresence(puppetId, resource); await this.handleXmppPresence(puppetId, stanza);
} catch (err) { } catch (err) {
log.error("Error while handling presence event", err); log.error("Error while handling presence event", err);
} }
}); });
client.on("updateContact", async (oldContact: SkypeContact | null, newContact: SkypeContact) => { client.on("receipt", async (stanza: any) => {
try {
await this.handleXmppPresence(puppetId, stanza);
} catch (err) {
log.error("Error while handling receipt event", err);
}
});
client.on("updateContact", async (oldContact: any | null, newContact: any) => {
try { try {
let update = oldContact === null; let update = oldContact === null;
const newUser = this.getUserParams(puppetId, newContact); const newUser = this.getUserParams(puppetId, newContact);
@ -197,16 +187,7 @@ export class Skype {
} }
p.restarting = true; p.restarting = true;
const causeName = (err as any).cause ? (err as any).cause.name : ""; const causeName = (err as any).cause ? (err as any).cause.name : "";
log.error("Error when polling"); log.error("Error when polling", err.message);
log.error("name: ", err.name);
const errr = err as any;
if (errr.cause) {
log.error("cause name: ", errr.cause.name);
}
log.error("code: ", errr.code);
log.error("body: ", errr.body);
log.error("cause: ", errr.cause);
log.error("data: ", errr.data);
log.error(err); log.error(err);
if (causeName === "UnexpectedHttpStatus") { if (causeName === "UnexpectedHttpStatus") {
await this.puppet.sendStatusMessage(puppetId, "Error: " + err); await this.puppet.sendStatusMessage(puppetId, "Error: " + err);
@ -346,6 +327,7 @@ export class Skype {
if (!p) { if (!p) {
return null; return null;
} }
log.info("getUserIdsInRoom", room);
const conversation = await p.client.getConversation(room); const conversation = await p.client.getConversation(room);
if (!conversation) { if (!conversation) {
return null; return null;
@ -356,10 +338,12 @@ export class Skype {
users.add(member); users.add(member);
} }
} }
log.info("getUserIdsInRoom users", users);
return users; return users;
} }
public async handleMatrixMessage(room: IRemoteRoom, data: IMessageEvent) { public async handleMatrixMessage(room: IRemoteRoom, data: IMessageEvent) {
log.info("handleMatrixMessage");
const p = this.puppets[room.puppetId]; const p = this.puppets[room.puppetId];
if (!p) { if (!p) {
return; return;
@ -414,6 +398,7 @@ export class Skype {
} }
public async handleMatrixReply(room: IRemoteRoom, eventId: string, data: IReplyEvent) { public async handleMatrixReply(room: IRemoteRoom, eventId: string, data: IReplyEvent) {
log.info("handleMatrixReply");
const p = this.puppets[room.puppetId]; const p = this.puppets[room.puppetId];
if (!p) { if (!p) {
return; return;
@ -481,230 +466,209 @@ export class Skype {
} }
public async handleMatrixImage(room: IRemoteRoom, data: IFileEvent) { public async handleMatrixImage(room: IRemoteRoom, data: IFileEvent) {
await this.handleMatrixFile(room, data, "sendImage"); // TODO
// await this.handleMatrixFile(room, data, "sendImage");
} }
public async handleMatrixAudio(room: IRemoteRoom, data: IFileEvent) { public async handleMatrixAudio(room: IRemoteRoom, data: IFileEvent) {
await this.handleMatrixFile(room, data, "sendAudio"); // TODO
// await this.handleMatrixFile(room, data, "sendAudio");
} }
public async handleMatrixFile(room: IRemoteRoom, data: IFileEvent, method?: string) { public async handleMatrixFile(room: IRemoteRoom, data: IFileEvent, method?: string) {
if (!method) { // TODO
method = "sendDocument"; // if (!method) {
} // method = "sendDocument";
const p = this.puppets[room.puppetId]; // }
if (!p) { // const p = this.puppets[room.puppetId];
return; // if (!p) {
} // return;
log.info("Received file from matrix"); // }
const conversation = await p.client.getConversation(room); // log.info("Received file from matrix");
if (!conversation) { // const conversation = await p.client.getConversation(room);
log.warn(`Room ${room.roomId} not found!`); // if (!conversation) {
return; // log.warn(`Room ${room.roomId} not found!`);
} // return;
const buffer = await Util.DownloadFile(data.url); // }
const opts: SkypeNewMediaMessage = { // const buffer = await Util.DownloadFile(data.url);
file: buffer, // const opts: XmppNewMediaMessage = {
name: data.filename, // file: buffer,
}; // name: data.filename,
if (data.info) { // };
if (data.info.w) { // if (data.info) {
opts.width = data.info.w; // if (data.info.w) {
} // opts.width = data.info.w;
if (data.info.h) { // }
opts.height = data.info.h; // if (data.info.h) {
} // opts.height = data.info.h;
} // }
const dedupeKey = `${room.puppetId};${room.roomId}`; // }
this.messageDeduplicator.lock(dedupeKey, p.client.username, `file:${data.filename}`); // const dedupeKey = `${room.puppetId};${room.roomId}`;
const ret = await p.client[method](conversation.id, opts); // this.messageDeduplicator.lock(dedupeKey, p.client.username, `file:${data.filename}`);
const eventId = ret && ret.MessageId; // const ret = await p.client[method](conversation.id, opts);
this.messageDeduplicator.unlock(dedupeKey, p.client.username, eventId); // const eventId = ret && ret.MessageId;
if (eventId) { // this.messageDeduplicator.unlock(dedupeKey, p.client.username, eventId);
await this.puppet.eventSync.insert(room, data.eventId!, eventId); // if (eventId) {
} // await this.puppet.eventSync.insert(room, data.eventId!, eventId);
// }
} }
private async handleSkypeText( private async handleXmppText(
puppetId: number, puppetId: number,
resource: skypeHttp.resources.TextResource | skypeHttp.resources.RichTextResource, stanza: IStanza,
) { ) {
const p = this.puppets[puppetId]; const p = this.puppets[puppetId];
if (!p) { if (!p) {
return; return;
} }
const rich = resource.native.messagetype.startsWith("RichText"); log.info("Got new xmpp message");
log.info("Got new skype message"); log.silly(stanza);
log.silly(resource); const params = await this.getSendParams(puppetId, stanza);
const params = await this.getSendParams(puppetId, resource);
if (!params) { if (!params) {
log.warn("Couldn't generate params"); log.warn("Couldn't generate params");
return; return;
} }
let msg = resource.content; let msg = stanza.getChild("body").text();
let emote = false; let emote = false;
if (resource.native.skypeemoteoffset) {
emote = true;
msg = msg.substr(Number(resource.native.skypeemoteoffset));
}
const dedupeKey = `${puppetId};${params.room.roomId}`; 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)) { if (await this.messageDeduplicator.dedupe(dedupeKey, params.user.userId, params.eventId, msg)) {
log.silly("normal message dedupe"); log.silly("normal message dedupe");
return; return;
} }
if (rich && msg.trim().startsWith("<quote")) { if (msg.trim().startsWith("<quote")) {
// TODO
// okay, we might have a reply... // okay, we might have a reply...
const $ = cheerio.load(msg); // const $ = cheerio.load(msg);
const quote = $("quote"); // const quote = $("quote");
const messageid = quote.attr("messageid"); // const messageid = quote.attr("messageid");
if (messageid) { // if (messageid) {
const sendQuoteMsg = this.skypeMessageParser.parse(msg, { noQuotes: true }); // const sendQuoteMsg = this.xmppMessageParser.parse(msg, { noQuotes: true });
await this.puppet.sendReply(params, messageid, sendQuoteMsg); // await this.puppet.sendReply(params, messageid, sendQuoteMsg);
return; // return;
} // }
} }
let sendMsg: IMessageEvent; let sendMsg: IMessageEvent;
if (rich) {
sendMsg = this.skypeMessageParser.parse(msg);
} else {
sendMsg = { sendMsg = {
body: msg, body: msg,
}; };
}
if (emote) {
sendMsg.emote = true;
}
await this.puppet.sendMessage(params, sendMsg); await this.puppet.sendMessage(params, sendMsg);
} }
private async handleSkypeEdit( private async handleXmppEdit(
puppetId: number, puppetId: number,
resource: skypeHttp.resources.TextResource | skypeHttp.resources.RichTextResource, stanza: IStanza,
) { ) {
const p = this.puppets[puppetId]; // TODO
if (!p) { // const p = this.puppets[puppetId];
return; // if (!p) {
} // return;
const rich = resource.native.messagetype.startsWith("RichText"); // }
log.info("Got new skype edit"); // const rich = resource.native.messagetype.startsWith("RichText");
log.silly(resource); // log.info("Got new xmpp edit");
const params = await this.getSendParams(puppetId, resource); // log.silly(resource);
if (!params) { // const params = await this.getSendParams(puppetId, resource);
log.warn("Couldn't generate params"); // if (!params) {
return; // log.warn("Couldn't generate params");
} // return;
let msg = resource.content; // }
let emote = false; // let msg = resource.content;
if (resource.native.skypeemoteoffset) { // let emote = false;
emote = true; // if (resource.native.xmppemoteoffset) {
msg = msg.substr(Number(resource.native.skypeemoteoffset)); // emote = true;
} // msg = msg.substr(Number(resource.native.xmppemoteoffset));
const dedupeKey = `${puppetId};${params.room.roomId}`; // }
if (await this.messageDeduplicator.dedupe(dedupeKey, params.user.userId, params.eventId, msg)) { // const dedupeKey = `${puppetId};${params.room.roomId}`;
log.silly("normal message dedupe"); // if (await this.messageDeduplicator.dedupe(dedupeKey, params.user.userId, params.eventId, msg)) {
return; // log.silly("normal message dedupe");
} // return;
let sendMsg: IMessageEvent; // }
if (rich) { // let sendMsg: IMessageEvent;
sendMsg = this.skypeMessageParser.parse(msg, { noQuotes: msg.trim().startsWith("<quote") }); // if (rich) {
} else { // sendMsg = this.xmppMessageParser.parse(msg, { noQuotes: msg.trim().startsWith("<quote") });
sendMsg = { // } else {
body: msg, // sendMsg = {
}; // body: msg,
} // };
if (emote) { // }
sendMsg.emote = true; // if (emote) {
} // sendMsg.emote = true;
if (resource.content) { // }
await this.puppet.sendEdit(params, resource.id, sendMsg); // if (resource.content) {
} else if (p.deletedMessages.has(resource.id)) { // await this.puppet.sendEdit(params, resource.id, sendMsg);
log.silly("normal message redact dedupe"); // } else if (p.deletedMessages.has(resource.id)) {
return; // log.silly("normal message redact dedupe");
} else { // return;
await this.puppet.sendRedact(params, resource.id); // } else {
} // await this.puppet.sendRedact(params, resource.id);
// }
} }
private async handleSkypeFile(puppetId: number, resource: skypeHttp.resources.FileResource) { private async handleXmppFile(puppetId: number, stanza: IStanza) {
const p = this.puppets[puppetId]; // TODO
if (!p) { // const p = this.puppets[puppetId];
return; // if (!p) {
} // return;
log.info("Got new skype file"); // }
log.silly(resource); // log.info("Got new xmpp file");
const params = await this.getSendParams(puppetId, resource); // log.silly(resource);
if (!params) { // const params = await this.getSendParams(puppetId, resource);
log.warn("Couldn't generate params"); // if (!params) {
return; // log.warn("Couldn't generate params");
} // return;
const filename = resource.original_file_name; // }
const dedupeKey = `${puppetId};${params.room.roomId}`; // const filename = resource.original_file_name;
if (await this.messageDeduplicator.dedupe(dedupeKey, params.user.userId, params.eventId, `file:${filename}`)) { // const dedupeKey = `${puppetId};${params.room.roomId}`;
log.silly("file message dedupe"); // if (await this.messageDeduplicator.dedupe(dedupeKey, params.user.userId, params.eventId, `file:${filename}`)) {
return; // log.silly("file message dedupe");
} // return;
const buffer = await p.client.downloadFile(resource.uri); // }
await this.puppet.sendFileDetect(params, buffer, filename); // const buffer = await p.client.downloadFile(resource.uri);
// await this.puppet.sendFileDetect(params, buffer, filename);
} }
private async handleSkypeTyping(puppetId: number, resource: skypeHttp.resources.Resource, typing: boolean) { private async handleXmppTyping(puppetId: number, stanza: IStanza, typing: boolean) {
const p = this.puppets[puppetId]; // TODO
if (!p) { // const p = this.puppets[puppetId];
return; // if (!p) {
} // return;
log.info("Got new skype typing event"); // }
log.silly(resource); // log.info("Got new xmpp typing event");
const params = await this.getSendParams(puppetId, resource); // log.silly(resource);
if (!params) { // const params = await this.getSendParams(puppetId, resource);
log.warn("Couldn't generate params"); // if (!params) {
return; // log.warn("Couldn't generate params");
} // return;
await this.puppet.setUserTyping(params, typing); // }
// await this.puppet.setUserTyping(params, typing);
} }
private async handleSkypePresence(puppetId: number, resource: skypeHttp.resources.Resource) { private async handleXmppPresence(puppetId: number, stanza: IStanza) {
const p = this.puppets[puppetId]; // TODO
if (!p) { // const p = this.puppets[puppetId];
return; // if (!p) {
} // return;
log.info("Got new skype presence event"); // }
log.silly(resource); // log.info("Got new xmpp presence event");
const content = JSON.parse(resource.native.content); // log.silly(resource);
const contact = await p.client.getContact(content.user); // const content = JSON.parse(resource.native.content);
const conversation = await p.client.getConversation({ // const contact = await p.client.getContact(content.user);
puppetId, // const conversation = await p.client.getConversation({
roomId: resource.conversation, // puppetId,
}); // roomId: resource.conversation,
if (!contact || !conversation) { // });
log.warn("Couldn't generate params"); // if (!contact || !conversation) {
return; // log.warn("Couldn't generate params");
} // return;
const params: IReceiveParams = { // }
user: this.getUserParams(puppetId, contact), // const params: IReceiveParams = {
room: this.getRoomParams(puppetId, conversation), // user: this.getUserParams(puppetId, contact),
}; // room: this.getRoomParams(puppetId, conversation),
const [id, _, clientId] = content.consumptionhorizon.split(";"); // };
params.eventId = id; // const [id, _, clientId] = content.consumptionhorizon.split(";");
await this.puppet.sendReadReceipt(params); // params.eventId = id;
params.eventId = clientId; // await this.puppet.sendReadReceipt(params);
await this.puppet.sendReadReceipt(params); // params.eventId = clientId;
// await this.puppet.sendReadReceipt(params);
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2020 mx-puppet-skype Copyright 2020 mx-puppet-xmpp
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
@ -17,12 +17,12 @@ import * as escapeHtml from "escape-html";
import { IMessageEvent } from "mx-puppet-bridge"; import { IMessageEvent } from "mx-puppet-bridge";
import * as emoji from "node-emoji"; import * as emoji from "node-emoji";
interface ISkypeMessageParserOpts { interface IXmppMessageParserOpts {
noQuotes?: boolean; noQuotes?: boolean;
} }
export class SkypeMessageParser { export class XmppMessageParser {
public parse(msg: string, opts: ISkypeMessageParserOpts = {}): IMessageEvent { public parse(msg: string, opts: IXmppMessageParserOpts = {}): IMessageEvent {
opts = Object.assign({ opts = Object.assign({
noQuotes: false, noQuotes: false,
}, opts); }, opts);
@ -33,7 +33,7 @@ export class SkypeMessageParser {
return this.walkNode(nodes, opts); return this.walkNode(nodes, opts);
} }
private walkChildNodes(node: Parser.Node, opts: ISkypeMessageParserOpts): IMessageEvent { private walkChildNodes(node: Parser.Node, opts: IXmppMessageParserOpts): IMessageEvent {
if (!node.childNodes.length) { if (!node.childNodes.length) {
return { return {
body: "", body: "",
@ -55,7 +55,7 @@ export class SkypeMessageParser {
}; };
} }
private walkNode(node: Parser.Node, opts: ISkypeMessageParserOpts): IMessageEvent { private walkNode(node: Parser.Node, opts: IXmppMessageParserOpts): IMessageEvent {
if (node.nodeType === Parser.NodeType.TEXT_NODE) { if (node.nodeType === Parser.NodeType.TEXT_NODE) {
return this.escape((node as Parser.TextNode).text); return this.escape((node as Parser.TextNode).text);
} else if (node.nodeType === Parser.NodeType.ELEMENT_NODE) { } else if (node.nodeType === Parser.NodeType.ELEMENT_NODE) {
@ -111,7 +111,7 @@ export class SkypeMessageParser {
}; };
} }
case "ss": { case "ss": {
// skype emoji // xmpp emoji
const type = nodeHtml.attributes.type; const type = nodeHtml.attributes.type;
let emojiType = { let emojiType = {
smile: "slightly_smiling_face", smile: "slightly_smiling_face",