mx-puppet-xmpp/src/client.ts

381 lines
11 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
2020-05-02 08:53:39 +00:00
http://www.apache.org/licenses/LICENSE-2.0
2020-03-24 16:27:42 +00:00
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.
*/
2020-04-01 16:35:57 +00:00
import { Log, IRemoteRoom, Util } from "mx-puppet-bridge";
2020-03-24 16:27:42 +00:00
import { EventEmitter } from "events";
2020-04-13 14:00:19 +00:00
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 { Context as SkypeContext } from "@sorunome/skype-http/dist/lib/interfaces/api/context";
2020-04-01 16:35:57 +00:00
import ExpireSet from "expire-set";
2020-05-02 08:53:39 +00:00
import * as toughCookie from "tough-cookie";
2020-03-24 16:27:42 +00:00
const log = new Log("SkypePuppet:client");
// tslint:disable no-magic-numbers
2020-03-24 21:18:38 +00:00
const ID_TIMEOUT = 60000;
const CONTACTS_DELTA_INTERVAL = 5 * 60 * 1000;
// tslint:enable no-magic-numbers
2020-03-24 16:27:42 +00:00
export class Client extends EventEmitter {
2020-04-25 08:00:23 +00:00
public contacts: Map<string, SkypeContact> = new Map();
public conversations: Map<string, skypeHttp.Conversation> = new Map();
2020-03-24 16:27:42 +00:00
private api: skypeHttp.Api;
private handledIds: ExpireSet<string>;
private contactsInterval: NodeJS.Timeout | null = null;
2020-03-24 16:27:42 +00:00
constructor(
private loginUsername: string,
private password: string,
2020-03-24 21:18:38 +00:00
private state?: SkypeContext.Json,
2020-03-24 16:27:42 +00:00
) {
super();
this.handledIds = new ExpireSet(ID_TIMEOUT);
}
public get username(): string {
return "8:" + this.api.context.username;
}
2020-03-24 21:18:38 +00:00
public get getState(): SkypeContext.Json {
return this.api.getState();
}
2020-03-24 16:27:42 +00:00
public async connect() {
2020-03-25 14:47:51 +00:00
let connectedWithAuth = false;
if (this.state) {
2020-03-24 21:18:38 +00:00
try {
this.api = await skypeHttp.connect({ state: this.state, verbose: true });
2020-03-25 14:47:51 +00:00
connectedWithAuth = true;
2020-03-24 21:18:38 +00:00
} catch (err) {
this.api = await skypeHttp.connect({
credentials: {
username: this.loginUsername,
password: this.password,
},
verbose: true,
});
2020-03-25 14:47:51 +00:00
connectedWithAuth = false;
2020-03-24 21:18:38 +00:00
}
} else {
this.api = await skypeHttp.connect({
credentials: {
username: this.loginUsername,
password: this.password,
},
verbose: true,
});
2020-03-25 14:47:51 +00:00
connectedWithAuth = false;
2020-03-24 21:18:38 +00:00
}
2020-03-24 16:27:42 +00:00
2020-03-26 18:05:37 +00:00
try {
await this.startupApi();
} catch (err) {
if (!connectedWithAuth) {
throw err;
}
this.api = await skypeHttp.connect({
credentials: {
username: this.loginUsername,
password: this.password,
},
verbose: true,
});
connectedWithAuth = false;
await this.startupApi();
}
const registerErrorHandler = () => {
this.api.on("error", (err: Error) => {
log.error("An error occured", err);
this.emit("error", err);
});
};
if (connectedWithAuth) {
let resolved = false;
2020-03-26 18:05:37 +00:00
return new Promise(async (resolve, reject) => {
const TIMEOUT_SUCCESS = 5000;
setTimeout(() => {
if (resolved) {
return;
2020-03-24 16:27:42 +00:00
}
resolved = true;
2020-03-26 18:05:37 +00:00
registerErrorHandler();
resolve();
}, TIMEOUT_SUCCESS);
this.api.once("error", async () => {
if (resolved) {
return;
2020-03-24 16:27:42 +00:00
}
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();
2020-03-26 18:05:37 +00:00
registerErrorHandler();
resolve();
} catch (err) {
reject(err);
2020-03-24 20:26:52 +00:00
}
});
2020-03-26 18:05:37 +00:00
await this.api.listen();
}).then(async () => {
await this.api.setStatus("Online");
});
2020-03-26 18:05:37 +00:00
} else {
registerErrorHandler();
await this.api.listen();
await this.api.setStatus("Online");
2020-03-25 14:47:51 +00:00
}
2020-03-24 16:27:42 +00:00
}
public async disconnect() {
2020-03-29 11:11:10 +00:00
if (this.api) {
await this.api.stopListening();
}
if (this.contactsInterval) {
clearInterval(this.contactsInterval);
this.contactsInterval = null;
}
2020-03-24 16:27:42 +00:00
}
public async getContact(id: string): Promise<SkypeContact | null> {
2020-04-22 17:15:15 +00:00
log.debug(`Fetching contact ${id}`);
2020-03-24 16:27:42 +00:00
const hasStart = Boolean(id.match(/^\d+:/));
const fullId = hasStart ? id : `8:${id}`;
if (this.contacts.has(fullId)) {
2020-04-22 17:15:15 +00:00
log.debug("Returning cached result");
const ret = this.contacts.get(fullId) || null;
log.silly(ret);
return ret;
2020-03-24 16:27:42 +00:00
}
if (hasStart) {
2020-03-24 21:18:38 +00:00
id = id.substr(id.indexOf(":") + 1);
2020-03-24 16:27:42 +00:00
}
try {
const rawContact = await this.api.getContact(id);
const contact: SkypeContact = {
personId: rawContact.id.raw,
workloads: null,
mri: rawContact.id.raw,
blocked: false,
authorized: true,
creationTime: new Date(),
displayName: (rawContact.name && rawContact.name.displayName) || rawContact.id.id,
displayNameSource: "profile" as any, // tslint:disable-line no-any
profile: {
avatarUrl: rawContact.avatarUrl || undefined,
name: {
first: (rawContact.name && rawContact.name).first || undefined,
surname: (rawContact.name && rawContact.name).surname || undefined,
nickname: (rawContact.name && rawContact.name).nickname || undefined,
},
},
};
this.contacts.set(contact.mri, contact || null);
2020-04-22 17:15:15 +00:00
log.debug("Returning new result");
log.silly(contact);
2020-03-24 16:27:42 +00:00
return contact || null;
} catch (err) {
// contact not found
2020-04-22 17:15:15 +00:00
log.debug("No such contact found");
2020-04-23 09:40:35 +00:00
log.debug(err.body || err);
2020-03-24 16:27:42 +00:00
return null;
}
}
public async getConversation(room: IRemoteRoom): Promise<skypeHttp.Conversation | null> {
2020-04-22 17:15:15 +00:00
log.debug(`Fetching conversation puppetId=${room.puppetId} roomId=${room.roomId}`);
2020-03-24 16:27:42 +00:00
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)) {
2020-04-22 17:15:15 +00:00
log.debug("Returning cached result");
const ret = this.conversations.get(id) || null;
log.silly(ret);
return ret;
2020-03-24 16:27:42 +00:00
}
try {
const conversation = await this.api.getConversation(id);
this.conversations.set(conversation.id, conversation || null);
2020-04-22 17:15:15 +00:00
log.debug("Returning new result");
log.silly(conversation);
2020-03-24 16:27:42 +00:00
return conversation || null;
} catch (err) {
// conversation not found
2020-04-22 17:15:15 +00:00
log.debug("No such conversation found");
2020-04-23 09:40:35 +00:00
log.debug(err.body || err);
2020-03-24 16:27:42 +00:00
return null;
}
}
2020-05-02 08:53:39 +00:00
public async downloadFile(url: string, type: string = "imgpsh_fullsize_anim"): Promise<Buffer> {
if (url.startsWith("https://api.asm.skype.com/") && !url.includes("/views/")) {
url = `${url}/views/${type}`;
2020-03-24 20:10:49 +00:00
}
2020-05-02 08:53:39 +00:00
const cookieJar = new toughCookie.CookieJar(this.api.context.cookies);
2020-03-24 20:10:49 +00:00
return await Util.DownloadFile(url, {
2020-04-01 16:09:21 +00:00
headers: {
Authorization: "skypetoken=" + this.api.context.skypeToken.value,
RegistrationToken: this.api.context.registrationToken.raw,
},
2020-05-02 08:53:39 +00:00
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);
}),
),
},
2020-03-24 20:10:49 +00:00
});
}
2020-03-24 16:27:42 +00:00
public async sendMessage(conversationId: string, msg: string): Promise<skypeHttp.Api.SendMessageResult> {
return await this.api.sendMessage({
textContent: msg,
}, conversationId);
}
2020-03-24 20:10:49 +00:00
public async sendEdit(conversationId: string, messageId: string, msg: string) {
return await this.api.sendEdit({
textContent: msg,
}, conversationId, messageId);
}
public async sendDelete(conversationId: string, messageId: string) {
return await this.api.sendDelete(conversationId, messageId);
}
public async sendAudio(
conversationId: string,
opts: SkypeNewMediaMessage,
): Promise<skypeHttp.Api.SendMessageResult> {
return await this.api.sendAudio(opts, conversationId);
}
public async sendDocument(
conversationId: string,
2020-03-24 21:18:38 +00:00
opts: SkypeNewMediaMessage,
2020-03-24 20:10:49 +00:00
): Promise<skypeHttp.Api.SendMessageResult> {
return await this.api.sendDocument(opts, conversationId);
}
public async sendImage(
conversationId: string,
2020-03-24 21:18:38 +00:00
opts: SkypeNewMediaMessage,
2020-03-24 20:10:49 +00:00
): 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;
}
});
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() {
2020-03-26 18:05:37 +00:00
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);
}
}
2020-03-24 16:27:42 +00:00
}