diff --git a/src/client.ts b/src/client.ts index d9bdd2b..9559bef 100644 --- a/src/client.ts +++ b/src/client.ts @@ -16,19 +16,21 @@ import { EventEmitter } from "events"; import * as skypeHttp from "skype-http"; import { Contact as SkypeContact } from "skype-http/dist/lib/types/contact"; import { NewMediaMessage as SkypeNewMediaMessage } from "skype-http/dist/lib/interfaces/api/api"; +import { Context as SkypeContext } from "skype-http/dist/lib/interfaces/api/context"; const log = new Log("SkypePuppet:client"); -const ID_TIMEOUT = 60 * 1000; +const ID_TIMEOUT = 60000; export class Client extends EventEmitter { + public contacts: Map = new Map(); + public conversations: Map = new Map(); private api: skypeHttp.Api; private handledIds: ExpireSet; - private contacts: Map = new Map(); - private conversations: Map = new Map(); constructor( private loginUsername: string, private password: string, + private state?: SkypeContext.Json, ) { super(); this.handledIds = new ExpireSet(ID_TIMEOUT); @@ -38,14 +40,32 @@ export class Client extends EventEmitter { return "8:" + this.api.context.username; } + public get getState(): SkypeContext.Json { + return this.api.getState(); + } + public async connect() { - this.api = await skypeHttp.connect({ - credentials: { - username: this.loginUsername, - password: this.password, - }, - verbose: true, - }); + if (this.state) { + try { + this.api = await skypeHttp.connect({ state: this.state, verbose: true }); + } catch (err) { + this.api = await skypeHttp.connect({ + credentials: { + username: this.loginUsername, + password: this.password, + }, + verbose: true, + }); + } + } else { + this.api = await skypeHttp.connect({ + credentials: { + username: this.loginUsername, + password: this.password, + }, + verbose: true, + }); + } this.api.on("event", (evt: skypeHttp.events.EventMessage) => { if (!evt || !evt.resource) { @@ -86,6 +106,7 @@ export class Client extends EventEmitter { this.api.on("error", (err: Error) => { log.error("An error occured", err); + this.emit("error", err); }); await this.api.listen(); @@ -116,7 +137,7 @@ export class Client extends EventEmitter { return this.contacts.get(fullId) || null; } if (hasStart) { - id = id.substr(id.indexOf(":")+1); + id = id.substr(id.indexOf(":") + 1); } try { const rawContact = await this.api.getContact(id); @@ -177,7 +198,7 @@ export class Client extends EventEmitter { } return await Util.DownloadFile(url, { cookies: this.api.context.cookies, - headers: { Authorization: 'skype_token ' + this.api.context.skypeToken.value }, + headers: { Authorization: "skype_token " + this.api.context.skypeToken.value }, }); } @@ -206,14 +227,14 @@ export class Client extends EventEmitter { public async sendDocument( conversationId: string, - opts: SkypeNewMediaMessage + opts: SkypeNewMediaMessage, ): Promise { return await this.api.sendDocument(opts, conversationId); } public async sendImage( conversationId: string, - opts: SkypeNewMediaMessage + opts: SkypeNewMediaMessage, ): Promise { return await this.api.sendImage(opts, conversationId); } diff --git a/src/index.ts b/src/index.ts index ed5d5ca..6f5dd9c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -61,7 +61,6 @@ const protocol: IProtocolInformation = { image: true, audio: true, file: true, -// presence: true, // we want to be able to send presence edit: true, globalNamespace: true, }, @@ -101,7 +100,17 @@ async function run() { puppet.on("file", skype.handleMatrixFile.bind(skype)); puppet.setCreateUserHook(skype.createUser.bind(skype)); puppet.setCreateRoomHook(skype.createRoom.bind(skype)); + puppet.setGetDmRoomIdHook(skype.getDmRoom.bind(skype)); + puppet.setListUsersHook(skype.listUsers.bind(skype)); + puppet.setListRoomsHook(skype.listRooms.bind(skype)); puppet.setGetUserIdsInRoomHook(skype.getUserIdsInRoom.bind(skype)); + puppet.setGetDescHook(async (puppetId: number, data: any): Promise => { + let s = "Skype"; + if (data.username) { + s += ` as \`${data.username.substr("8:".length)}\``; + } + return s; + }); puppet.setGetDataFromStrHook(async (str: string): Promise => { const retData = { success: false, diff --git a/src/skype.ts b/src/skype.ts index fd2a7d5..aa5edd1 100644 --- a/src/skype.ts +++ b/src/skype.ts @@ -12,7 +12,7 @@ limitations under the License. */ import { PuppetBridge, IRemoteUser, IRemoteRoom, IReceiveParams, IMessageEvent, IFileEvent, Log, MessageDeduplicator, Util, - ExpireSet, + ExpireSet, IRetList, } from "mx-puppet-bridge"; import { Client } from "./client"; import * as skypeHttp from "skype-http"; @@ -23,6 +23,8 @@ import * as escapeHtml from "escape-html"; const log = new Log("SkypePuppet:skype"); +const ROOM_TYPE_DM = 8; + interface ISkypePuppet { client: Client; data: any; @@ -53,12 +55,11 @@ export class Skype { public getRoomParams(puppetId: number, conversation: skypeHttp.Conversation): IRemoteRoom { const roomType = Number(conversation.id.split(":")[0]); - let roomId = conversation.id; - const isDirect = roomType === 8; + const isDirect = roomType === ROOM_TYPE_DM; if (isDirect) { return { puppetId, - roomId: `dm-${puppetId}-${roomId}`, + roomId: `dm-${puppetId}-${conversation.id}`, isDirect: true, }; } @@ -76,7 +77,7 @@ export class Skype { } return { puppetId, - roomId, + roomId: conversation.id, name, avatarUrl, }; @@ -84,7 +85,6 @@ export class Skype { public async getSendParams(puppetId: number, resource: skypeHttp.resources.Resource): Promise { 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({ @@ -95,18 +95,26 @@ export class Skype { return null; } return { - user: await this.getUserParams(puppetId, contact), - room: await this.getRoomParams(puppetId, conversation), + 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 }; } + public async stopClient(puppetId: number) { + const p = this.puppets[puppetId]; + if (!p) { + return; + } + await p.client.disconnect(); + } + public async startClient(puppetId: number) { const p = this.puppets[puppetId]; if (!p) { return; } - p.client = new Client(p.data.username, p.data.password); + p.client = new Client(p.data.username, p.data.password, p.data.state); const client = p.client; client.on("text", async (resource: skypeHttp.resources.TextResource) => { try { @@ -124,7 +132,7 @@ export class Skype { }); client.on("location", async (resource: skypeHttp.resources.RichTextLocationResource) => { try { - + } catch (err) { log.error("Error while handling location event", err); } @@ -150,8 +158,28 @@ export class Skype { log.error("Error while handling presence event", err); } }); - await client.connect(); - await this.puppet.setUserId(puppetId, client.username); + const MINUTE = 60000; + client.on("error", async (err: Error) => { + await this.puppet.sendStatusMessage(puppetId, "Error:" + err); + await this.puppet.sendStatusMessage(puppetId, "Reconnecting in a minute... " + err.message); + setTimeout(async () => { + await this.stopClient(puppetId); + await this.startClient(puppetId); + }, MINUTE); + }); + try { + await client.connect(); + await this.puppet.setUserId(puppetId, client.username); + p.data.state = client.getState; + await this.puppet.setPuppetData(puppetId, p.data); + await this.puppet.sendStatusMessage(puppetId, "connected"); + } catch (err) { + log.error("Failed to connect", err); + await this.puppet.sendStatusMessage(puppetId, "Failed to connect, reconnecting in a minute... " + err.message); + setTimeout(async () => { + await this.startClient(puppetId); + }, MINUTE); + } } public async newPuppet(puppetId: number, data: any) { @@ -203,6 +231,54 @@ export class Skype { return this.getRoomParams(room.puppetId, conversation); } + public async getDmRoom(remoteUser: IRemoteUser): Promise { + const p = this.puppets[remoteUser.puppetId]; + if (!p) { + return null; + } + const contact = await p.client.getContact(remoteUser.userId); + if (!contact) { + return null; + } + return `dm-${remoteUser.puppetId}-${contact.mri}`; + } + + public async listUsers(puppetId: number): Promise { + const p = this.puppets[puppetId]; + if (!p) { + return []; + } + const reply: IRetList[] = []; + for (const [, contact] of p.client.contacts) { + if (!contact) { + continue; + } + reply.push({ + id: contact.mri, + name: contact.displayName, + }); + } + return reply; + } + + public async listRooms(puppetId: number): Promise { + const p = this.puppets[puppetId]; + if (!p) { + return []; + } + const reply: IRetList[] = []; + for (const [, conversation] of p.client.conversations) { + if (!conversation || conversation.id.startsWith("8:")) { + continue; + } + reply.push({ + id: conversation.id, + name: (conversation.threadProperties && conversation.threadProperties.topic) || "", + }); + } + return reply; + } + public async getUserIdsInRoom(room: IRemoteRoom): Promise | null> { const p = this.puppets[room.puppetId]; if (!p) {