mx-puppet-xmpp/src/skype.ts

416 lines
12 KiB
TypeScript
Raw Normal View History

2020-03-24 16:27:42 +00:00
/*
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 {
2020-03-24 20:10:49 +00:00
PuppetBridge, IRemoteUser, IRemoteRoom, IReceiveParams, IMessageEvent, IFileEvent, Log, MessageDeduplicator, Util,
ExpireSet,
2020-03-24 16:27:42 +00:00
} from "mx-puppet-bridge";
import { Client } from "./client";
import * as skypeHttp from "skype-http";
import { Contact as SkypeContact } from "skype-http/dist/lib/types/contact";
2020-03-24 20:10:49 +00:00
import { NewMediaMessage as SkypeNewMediaMessage } from "skype-http/dist/lib/interfaces/api/api";
2020-03-24 16:27:42 +00:00
import * as decodeHtml from "decode-html";
import * as escapeHtml from "escape-html";
const log = new Log("SkypePuppet:skype");
interface ISkypePuppet {
client: Client;
data: any;
2020-03-24 20:10:49 +00:00
deletedMessages: ExpireSet<string>;
2020-03-24 16:27:42 +00:00
}
interface ISkypePuppets {
[puppetId: number]: ISkypePuppet;
}
export class Skype {
private puppets: ISkypePuppets = {};
private messageDeduplicator: MessageDeduplicator;
constructor(
private puppet: PuppetBridge,
) {
this.messageDeduplicator = new MessageDeduplicator();
}
public getUserParams(puppetId: number, contact: SkypeContact): IRemoteUser {
return {
puppetId,
userId: contact.mri,
name: contact.displayName,
avatarUrl: contact.profile ? contact.profile.avatarUrl : null,
};
}
public getRoomParams(puppetId: number, conversation: skypeHttp.Conversation): IRemoteRoom {
const roomType = Number(conversation.id.split(":")[0]);
let roomId = conversation.id;
const isDirect = roomType === 8;
if (isDirect) {
return {
puppetId,
roomId: `dm-${puppetId}-${roomId}`,
isDirect: true,
};
}
let avatarUrl: 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);
}
}
return {
puppetId,
roomId,
name,
avatarUrl,
};
}
public async getSendParams(puppetId: number, resource: skypeHttp.resources.Resource): Promise<IReceiveParams | null> {
const roomType = Number(resource.conversation.split(":")[0]);
let roomId = resource.conversation;
const p = this.puppets[puppetId];
const contact = await p.client.getContact(resource.from.raw);
const conversation = await p.client.getConversation({
puppetId,
roomId: resource.conversation,
});
if (!contact || !conversation) {
return null;
}
return {
user: await this.getUserParams(puppetId, contact),
room: await this.getRoomParams(puppetId, conversation),
2020-03-24 20:10:49 +00:00
eventId: (resource as any).clientId || resource.native.clientmessageid || resource.id, // tslint:disable-line no-any
2020-03-24 16:27:42 +00:00
};
}
public async startClient(puppetId: number) {
const p = this.puppets[puppetId];
if (!p) {
return;
}
const client = p.client;
client.on("text", async (resource: skypeHttp.resources.TextResource) => {
try {
await this.handleSkypeText(puppetId, resource, false);
} catch (err) {
log.error("Error while handling text event", err);
}
});
client.on("richText", async (resource: skypeHttp.resources.RichTextResource) => {
try {
await this.handleSkypeText(puppetId, resource, true);
} catch (err) {
log.error("Error while handling richText event", err);
}
});
client.on("location", async (resource: skypeHttp.resources.RichTextLocationResource) => {
try {
} catch (err) {
log.error("Error while handling location event", err);
}
});
client.on("file", async (resource: skypeHttp.resources.FileResource) => {
try {
2020-03-24 20:10:49 +00:00
await this.handleSkypeFile(puppetId, resource);
2020-03-24 16:27:42 +00:00
} catch (err) {
log.error("Error while handling file event", err);
}
});
client.on("typing", async (resource: skypeHttp.resources.Resource, typing: boolean) => {
try {
} catch (err) {
log.error("Error while handling typing event", err);
}
});
await client.connect();
await this.puppet.setUserId(puppetId, client.username);
}
public async newPuppet(puppetId: number, data: any) {
if (this.puppets[puppetId]) {
await this.deletePuppet(puppetId);
}
const client = new Client(data.username, data.password);
2020-03-24 20:10:49 +00:00
const TWO_MIN = 120000;
2020-03-24 16:27:42 +00:00
this.puppets[puppetId] = {
client,
data,
2020-03-24 20:10:49 +00:00
deletedMessages: new ExpireSet(TWO_MIN),
2020-03-24 16:27:42 +00:00
};
await this.startClient(puppetId);
}
public async deletePuppet(puppetId: number) {
const p = this.puppets[puppetId];
if (!p) {
return;
}
await p.client.disconnect();
delete this.puppets[puppetId];
}
public async createUser(remoteUser: IRemoteUser): Promise<IRemoteUser | null> {
const p = this.puppets[remoteUser.puppetId];
if (!p) {
return null;
}
log.info(`Received create request for user update puppetId=${remoteUser.puppetId} userId=${remoteUser.userId}`);
const contact = await p.client.getContact(remoteUser.userId);
if (!contact) {
return null;
}
return this.getUserParams(remoteUser.puppetId, contact);
}
public async createRoom(room: IRemoteRoom): Promise<IRemoteRoom | null> {
const p = this.puppets[room.puppetId];
if (!p) {
return null;
}
log.info(`Received create request for channel update puppetId=${room.puppetId} roomId=${room.roomId}`);
const conversation = await p.client.getConversation(room);
if (!conversation) {
return null;
}
return this.getRoomParams(room.puppetId, conversation);
}
public async getUserIdsInRoom(room: IRemoteRoom): Promise<Set<string> | null> {
const p = this.puppets[room.puppetId];
if (!p) {
return null;
}
const conversation = await p.client.getConversation(room);
if (!conversation) {
return null;
}
const users = new Set<string>();
if (conversation.members) {
for (const member of conversation.members) {
users.add(member);
}
}
return users;
}
public async handleMatrixMessage(room: IRemoteRoom, data: IMessageEvent) {
const p = this.puppets[room.puppetId];
if (!p) {
return;
}
2020-03-24 20:10:49 +00:00
log.info("Received message from matrix");
2020-03-24 16:27:42 +00:00
const conversation = await p.client.getConversation(room);
if (!conversation) {
log.warn(`Room ${room.roomId} not found!`);
return;
}
let msg: string;
if (data.formattedBody) {
msg = data.formattedBody;
} else {
msg = escapeHtml(data.body);
}
const dedupeKey = `${room.puppetId};${room.roomId}`;
this.messageDeduplicator.lock(dedupeKey, p.client.username, msg);
const ret = await p.client.sendMessage(conversation.id, msg);
2020-03-24 20:10:49 +00:00
const dedupeId = ret && ret.clientMessageId;
const eventId = ret && ret.MessageId;
this.messageDeduplicator.unlock(dedupeKey, p.client.username, dedupeId);
if (eventId) {
await this.puppet.eventSync.insert(room.puppetId, data.eventId!, eventId);
}
}
public async handleMatrixEdit(room: IRemoteRoom, eventId: string, data: IMessageEvent) {
const p = this.puppets[room.puppetId];
if (!p) {
return;
}
log.info("Received edit from matrix");
const conversation = await p.client.getConversation(room);
if (!conversation) {
log.warn(`Room ${room.roomId} not found!`);
return;
}
let msg: string;
if (data.formattedBody) {
msg = data.formattedBody;
} else {
msg = escapeHtml(data.body);
}
const dedupeKey = `${room.puppetId};${room.roomId}`;
this.messageDeduplicator.lock(dedupeKey, p.client.username, msg);
await p.client.sendEdit(conversation.id, eventId, msg);
const newEventId = "";
this.messageDeduplicator.unlock(dedupeKey, p.client.username, newEventId);
if (newEventId) {
await this.puppet.eventSync.insert(room.puppetId, data.eventId!, newEventId);
}
}
public async handleMatrixRedact(room: IRemoteRoom, eventId: string) {
const p = this.puppets[room.puppetId];
if (!p) {
return;
}
log.info("Received edit from matrix");
const conversation = await p.client.getConversation(room);
if (!conversation) {
log.warn(`Room ${room.roomId} not found!`);
return;
}
p.deletedMessages.add(eventId);
await p.client.sendDelete(conversation.id, eventId);
}
public async handleMatrixImage(room: IRemoteRoom, data: IFileEvent) {
await this.handleMatrixFile(room, data, "sendImage");
}
public async handleMatrixAudio(room: IRemoteRoom, data: IFileEvent) {
await this.handleMatrixFile(room, data, "sendAudio");
}
public async handleMatrixFile(room: IRemoteRoom, data: IFileEvent, method?: string) {
if (!method) {
method = "sendDocument";
}
const p = this.puppets[room.puppetId];
if (!p) {
return;
}
log.info("Received file from matrix");
const conversation = await p.client.getConversation(room);
if (!conversation) {
log.warn(`Room ${room.roomId} not found!`);
return;
}
const buffer = await Util.DownloadFile(data.url);
const opts: SkypeNewMediaMessage = {
file: buffer,
name: data.filename,
};
if (data.info) {
if (data.info.w) {
opts.width = data.info.w;
}
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 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);
2020-03-24 16:27:42 +00:00
if (eventId) {
2020-03-24 20:10:49 +00:00
await this.puppet.eventSync.insert(room.puppetId, data.eventId!, eventId);
2020-03-24 16:27:42 +00:00
}
}
private async handleSkypeText(
puppetId: number,
resource: skypeHttp.resources.TextResource | skypeHttp.resources.RichTextResource,
rich: boolean,
) {
const p = this.puppets[puppetId];
if (!p) {
return;
}
log.info("Got new skype message");
log.silly(resource);
const params = await this.getSendParams(puppetId, resource);
if (!params) {
log.warn("Couldn't generate params");
return;
}
2020-03-24 20:10:49 +00:00
let msg = resource.content;
let emote = false;
if (resource.native && resource.native.skypeemoteoffset) {
emote = true;
msg = msg.substr(Number(resource.native.skypeemoteoffset));
}
2020-03-24 16:27:42 +00:00
const dedupeKey = `${puppetId};${params.room.roomId}`;
2020-03-24 20:10:49 +00:00
if (await this.messageDeduplicator.dedupe(dedupeKey, params.user.userId, params.eventId, msg)) {
log.silly("normal message dedupe");
2020-03-24 16:27:42 +00:00
return;
}
if (!rich) {
await this.puppet.sendMessage(params, {
2020-03-24 20:10:49 +00:00
body: msg,
emote,
2020-03-24 16:27:42 +00:00
});
} else if (resource.native && resource.native.skypeeditedid) {
if (resource.content) {
await this.puppet.sendEdit(params, resource.native.skypeeditedid, {
2020-03-24 20:10:49 +00:00
body: msg,
formattedBody: msg,
emote,
2020-03-24 16:27:42 +00:00
});
2020-03-24 20:10:49 +00:00
} else if (p.deletedMessages.has(resource.native.skypeeditedid)) {
log.silly("normal message redact dedupe");
return;
2020-03-24 16:27:42 +00:00
} else {
await this.puppet.sendRedact(params, resource.native.skypeeditedid);
}
} else {
await this.puppet.sendMessage(params, {
2020-03-24 20:10:49 +00:00
body: msg,
formattedBody: msg,
emote,
2020-03-24 16:27:42 +00:00
});
}
}
2020-03-24 20:10:49 +00:00
private async handleSkypeFile(puppetId: number, resource: skypeHttp.resources.FileResource) {
const p = this.puppets[puppetId];
if (!p) {
return;
}
log.info("Got new skype file");
log.silly(resource);
const params = await this.getSendParams(puppetId, resource);
if (!params) {
log.warn("Couldn't generate params");
return;
}
const filename = resource.original_file_name;
const dedupeKey = `${puppetId};${params.room.roomId}`;
if (await this.messageDeduplicator.dedupe(dedupeKey, params.user.userId, params.eventId, `file:${filename}`)) {
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);
}
2020-03-24 16:27:42 +00:00
}