From ae7143374317d779873ddaa95db2264f88d15f4b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 25 Nov 2017 14:08:06 +0200 Subject: [PATCH] Add commands for searching and initiating private chats with users (ref #7) --- package-lock.json | 8 +++++ package.json | 13 +++++---- src/app.js | 18 ++++++++---- src/commands.js | 69 ++++++++++++++++++++++++++++++++++++++++++-- src/matrix-user.js | 58 ++++++++++++++++++++++++++++--------- src/portal.js | 11 ++++++- src/telegram-user.js | 10 +++++-- 7 files changed, 155 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index a9a408a5..a7d36255 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3785,6 +3785,14 @@ } } }, + "string-similarity": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-1.2.0.tgz", + "integrity": "sha1-11FTyzg4RjGLejmo2SkrtNtOnDA=", + "requires": { + "lodash": "4.17.4" + } + }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", diff --git a/package.json b/package.json index 60fb912b..79ffa724 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,14 @@ "url": "https://github.com/tulir/mautrix-telegram.git" }, "dependencies": { - "telegram-mtproto": "3.x.x", - "matrix-js-sdk": "0.x.x", - "matrix-appservice-bridge": "1.x.x", - "commander": "2.11.x", - "yamljs": "0.3.x", "colors": "1.1.x", - "md5": "2.2.x" + "commander": "2.11.x", + "matrix-appservice-bridge": "1.x.x", + "matrix-js-sdk": "0.x.x", + "md5": "2.2.x", + "string-similarity": "^1.2.0", + "telegram-mtproto": "3.x.x", + "yamljs": "0.3.x" }, "devDependencies": { "eslint": "4.11.x", diff --git a/src/app.js b/src/app.js index 66466c06..1517e647 100644 --- a/src/app.js +++ b/src/app.js @@ -108,7 +108,7 @@ class MautrixTelegram { * @param {TelegramPeer} peer The TelegramPeer object whose portal to get. * @returns {Promise} The Portal object. */ - async getPortalByPeer(peer) { + async getPortalByPeer(peer, { createIfNotFound = true } = {}) { let portal = this.portalsByPeerID.get(peer.id) if (portal) { return portal @@ -132,8 +132,10 @@ class MautrixTelegram { if (entries.length) { portal = Portal.fromEntry(this, entries[0]) - } else { + } else if (createIfNotFound) { portal = new Portal(this, undefined, peer) + } else { + return undefined } this.portalsByPeerID.set(peer.id, portal) if (portal.roomID) { @@ -199,7 +201,7 @@ class MautrixTelegram { * @param {number} id The internal Telegram ID of the user to get. * @returns {Promise} The TelegramUser object. */ - async getTelegramUser(id) { + async getTelegramUser(id, { createIfNotFound = true } = {}) { let user = this.telegramUsersByID.get(id) if (user) { return user @@ -218,8 +220,10 @@ class MautrixTelegram { if (entries.length) { user = TelegramUser.fromEntry(this, entries[0]) - } else { + } else if (createIfNotFound) { user = new TelegramUser(this, id) + } else { + return undefined } this.telegramUsersByID.set(id, user) return user @@ -234,7 +238,7 @@ class MautrixTelegram { * @param {string} id The MXID of the Matrix user to get. * @returns {Promise} The MatrixUser object. */ - async getMatrixUser(id) { + async getMatrixUser(id, { createIfNotFound = true } = {}) { let user = this.matrixUsersByID.get(id) if (user) { return user @@ -253,8 +257,10 @@ class MautrixTelegram { if (entries.length) { user = MatrixUser.fromEntry(this, entries[0]) - } else { + } else if (createIfNotFound) { user = new MatrixUser(this, id) + } else { + return undefined } this.matrixUsersByID.set(id, user) return user diff --git a/src/commands.js b/src/commands.js index c3592fdc..d08fe4fd 100644 --- a/src/commands.js +++ b/src/commands.js @@ -32,7 +32,15 @@ function run(sender, command, args, reply, app) { reply("Unknown command. Try \"$cmdprefix help\" for help.") return undefined } - return commandFunc(sender, args, reply, app) + try { + return commandFunc(sender, args, reply, app) + } catch (err) { + reply(`Error running command: ${err}.`) + if (err instanceof Error) { + reply("Check bridge console for stack trace") + console.error(err.stack) + } + } } commands.cancel = () => "Nothing to cancel." @@ -129,7 +137,7 @@ commands.login = async (sender, args, reply) => { } commands.register = async (sender, args, reply) => { - reply("Registration has not yet been implemented. Please use the offical apps for now.") + reply("Registration has not yet been implemented. Please use the official apps for now.") } commands.logout = async (sender, args, reply) => { @@ -145,6 +153,63 @@ commands.logout = async (sender, args, reply) => { // General command handlers // ////////////////////////////// +commands.search = async (sender, args, reply, app) => { + if (args.length < 1) { + reply("Usage: $cmdprefix search [-r|--remote] ") + return + } + let msg = [] + if (args[0] !== "-r" && args[0] !== "--remote") { + const contactResults = await sender.searchContacts(args.join(" ")) + if (contactResults.length > 0) { + msg.push("Following results found from local contacts:") + msg.push("") + for (const {match, contact} of contactResults) { + msg.push(`- ${contact.getDisplayName()}: ${contact.id} (${match}% match)`) + } + msg.push("") + msg.push("To force searching from Telegram servers, add `-r` before the search query.") + reply(msg.join("\n")) + return + } + } else { + args.shift() + msg.push("-r flag found: forcing remote search") + msg.push("") + } + const telegramResults = await sender.searchTelegram(args.join(" ")) + if (telegramResults.length > 0) { + msg.push("Following results received from Telegram server:") + for (const user of telegramResults) { + msg.push(`- ${user.getDisplayName()}: ${user.id}`) + } + } else { + msg.push("No users found.") + } + reply(msg.join("\n")) +} + +commands.pm = async (sender, args, reply, app) => { + if (args.length < 1) { + reply("Usage: $cmdprefix pm ") + return + } + const user = await app.getTelegramUser(+args[0], { createIfNotFound: false }) + if (!user) { + reply("User info not saved. Try searching for the user first?") + return + } + const peer = user.toPeer(sender.telegramPuppet) + + const userInfo = await peer.getInfo(sender.telegramPuppet) + await user.updateInfo(sender.telegramPuppet, userInfo) + + const portal = await app.getPortalByPeer(peer) + await portal.createMatrixRoom(sender.telegramPuppet, { + invite: [sender.userID], + }) +} + //////////////////////////// // Debug command handlers // diff --git a/src/matrix-user.js b/src/matrix-user.js index 0bcccabf..0b0d3fd5 100644 --- a/src/matrix-user.js +++ b/src/matrix-user.js @@ -16,6 +16,7 @@ const md5 = require("md5") const TelegramPuppet = require("./telegram-puppet") const TelegramPeer = require("./telegram-peer") +const strSim = require("string-similarity"); /** * MatrixUser represents a Matrix user who probably wants to control their @@ -126,22 +127,9 @@ class MatrixUser { } if (createRooms) { try { - const { roomID, created } = await portal.createMatrixRoom(this.telegramPuppet, { + await portal.createMatrixRoom(this.telegramPuppet, { invite: [this.userID], }) - if (!created) { - // Make sure the user is invited, since the room already exists. - - const intent = this.app.botIntent - // FIXME check membership before re-inviting - //const membership = intent.getClient().getRoom(roomID).getMember(this.userID).membership - //if (membership !== "join") { - try { - await intent.invite(roomID, this.userID) - } catch (_) { - } - //} - } } catch (err) { console.error(err) console.error(err.stack) @@ -151,6 +139,48 @@ class MatrixUser { return changed } + async searchContacts(query, {maxResults=5, minSimilarity = 0.45} = {}) { + const results = [] + for (const contact of this.contacts) { + let displaynameSimilarity = 0, usernameSimilarity = 0, numberSimilarity = 0 + if (contact.firstName || contact.lastName) { + displaynameSimilarity = strSim.compareTwoStrings(query, contact.getFirstAndLastName()) + } + if (contact.username) { + usernameSimilarity = strSim.compareTwoStrings(query, contact.username) + } + if (contact.phoneNumber) { + numberSimilarity = strSim.compareTwoStrings(query, contact.phoneNumber) + } + const similarity = Math.max(displaynameSimilarity, usernameSimilarity, numberSimilarity) + console.log(contact.getDisplayName(), similarity, displaynameSimilarity, usernameSimilarity, numberSimilarity) + if (similarity >= minSimilarity) { + results.push({ + similarity, + match: Math.round(similarity * 1000) / 10, + contact, + }) + } + } + return results + .sort((a, b) => b.similarity - a.similarity) + .slice(0, maxResults) + } + + async searchTelegram(query, {maxResults=5} = {}) { + const results = await this.telegramPuppet.client("contacts.search", { + q: query, + limit: maxResults, + }) + const resultUsers = [] + for (const userInfo of results.users) { + const user = await this.app.getTelegramUser(userInfo.id) + user.updateInfo(this.telegramPuppet, userInfo) + resultUsers.push(user) + } + return resultUsers + } + async sendTelegramCode(phoneNumber) { if (this._telegramPuppet && this._telegramPuppet.userID) { throw new Error("You are already logged in. Please log out before logging in again.") diff --git a/src/portal.js b/src/portal.js index 95603029..bb67d14c 100644 --- a/src/portal.js +++ b/src/portal.js @@ -122,8 +122,17 @@ class Portal { return !!this.roomID } - async createMatrixRoom(telegramPOV, { invite = [] } = {}) { + async createMatrixRoom(telegramPOV, { invite = [], inviteEvenIfNotCreated = true } = {}) { if (this.roomID) { + if (invite && inviteEvenIfNotCreated) { + const intent = this.peer.type === "user" + ? (await this.app.getTelegramUser(this.peer.id)).intent + : this.app.botIntent + for (const userID of invite) { + // TODO check membership before inviting + intent.invite(this.roomID, userID) + } + } return { created: false, roomID: this.roomID, diff --git a/src/telegram-user.js b/src/telegram-user.js index f9c3702b..c2d97b87 100644 --- a/src/telegram-user.js +++ b/src/telegram-user.js @@ -70,7 +70,7 @@ class TelegramUser { } } - async updateInfo(telegramPOV, user, { updateAvatar } = {}) { + async updateInfo(telegramPOV, user, { updateAvatar = false } = {}) { let changed = false if (this.firstName !== user.first_name) { this.firstName = user.first_name @@ -115,10 +115,14 @@ class TelegramUser { return this.intent.client.credentials.userId } + getFirstAndLastName() { + return [this.firstName, this.lastName].filter(s => !!s) + .join(" ") + } + getDisplayName() { if (this.firstName || this.lastName) { - return [this.firstName, this.lastName].filter(s => !!s) - .join(" ") + return this.getFirstAndLastName() } else if (this.username) { return this.username } else if (this.phoneNumber) {