Add commands for searching and initiating private chats with users (ref #7)

This commit is contained in:
Tulir Asokan
2017-11-25 14:08:06 +02:00
parent 3b42b17bb7
commit ae71433743
7 changed files with 155 additions and 32 deletions
+8
View File
@@ -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",
+7 -6
View File
@@ -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",
+12 -6
View File
@@ -108,7 +108,7 @@ class MautrixTelegram {
* @param {TelegramPeer} peer The TelegramPeer object whose portal to get.
* @returns {Promise<Portal>} 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<TelegramUser>} 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<MatrixUser>} 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
+67 -2
View File
@@ -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] <query>")
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 <id>")
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 //
+44 -14
View File
@@ -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.")
+10 -1
View File
@@ -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,
+7 -3
View File
@@ -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) {