From 9525fa77760f6b242f7394bbeb935f4422a66dba Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 8 Jan 2018 01:26:20 +0200 Subject: [PATCH] Allow hiding debug messages and fix some portal create bugs --- example-config.yaml | 2 ++ package.json | 1 + src/app.js | 27 ++++++++++++-- src/matrix-user.js | 17 ++++++--- src/portal.js | 82 +++++++++++++++++++++++++++++++++++------- src/telegram-puppet.js | 42 ++++++++++++---------- src/telegram-user.js | 2 +- 7 files changed, 134 insertions(+), 39 deletions(-) diff --git a/example-config.yaml b/example-config.yaml index 290c8b45..99d74995 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -12,6 +12,8 @@ appservice: port: 8080 id: telegram + debug: false + # Path to the registration file. This is automatically updated when generating a registration. registration: ./registration.yaml diff --git a/package.json b/package.json index d0c1649b..139e12aa 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "url": "https://github.com/tulir/mautrix-telegram.git" }, "dependencies": { + "chalk": "^2.3.0", "colors": "1.1.x", "commander": "2.12.x", "escape-html": "1.0.x", diff --git a/src/app.js b/src/app.js index bb97a14e..37e78489 100644 --- a/src/app.js +++ b/src/app.js @@ -22,6 +22,7 @@ const MatrixUser = require("./matrix-user") const TelegramUser = require("./telegram-user") const TelegramPeer = require("./telegram-peer") const Portal = require("./portal") +const chalk = require("chalk") /** * The base class for the bridge. @@ -105,6 +106,7 @@ class MautrixTelegram { onUserQuery(/*user*/) { return {} }, + onLog: msg => self.debug("blue", msg), async onEvent(request/*, context*/) { try { await self.handleMatrixEvent(request.getData()) @@ -117,16 +119,35 @@ class MautrixTelegram { }) } + debug(color, ...message) { + if (this.config.appservice.debug) { + console.log(chalk[color](...message)) + } + } + + debugErr(color, ...message) { + if (this.config.appservice.debug) { + console.error(chalk[color](...message)) + } + } + + info(...message) { + console.log(...message) + } + + warn(...message) { + console.error(chalk.orange(...message)) + } + /** * Start the bridge. */ async run() { - console.log("Appservice listening on port %s", this.config.appservice.port) + this.info("Appservice listening on port %s", this.config.appservice.port) await this.bridge.run(this.config.appservice.port, {}) // Load all Matrix users to cache - const userEntries = await this.bridge.getUserStore() - .select({ type: "matrix" }) + const userEntries = await this.bridge.getUserStore().select({ type: "matrix" }) for (const entry of userEntries) { const user = MatrixUser.fromEntry(this, entry) diff --git a/src/matrix-user.js b/src/matrix-user.js index dac15ba5..33ba778f 100644 --- a/src/matrix-user.js +++ b/src/matrix-user.js @@ -17,6 +17,7 @@ const md5 = require("md5") const TelegramPuppet = require("./telegram-puppet") const TelegramPeer = require("./telegram-peer") const strSim = require("string-similarity") +const chalk = require("chalk") /** * MatrixUser represents a Matrix user who probably wants to control their @@ -211,22 +212,28 @@ class MatrixUser { if (dialog._ === "chatForbidden" || dialog._ === "channelForbidden" || dialog.deactivated) { continue } - const peer = new TelegramPeer(dialog._, dialog.id) + const peer = new TelegramPeer(dialog._, dialog.id, { + accessHash: dialog.access_hash, + }) const portal = await this.app.getPortalByPeer(peer) - if (await portal.updateInfo(this.telegramPuppet, dialog)) { - changed = true - } this.chats.push(portal) if (createRooms) { + if (peer.type === "channel") { + portal.accessHashes.set(this.telegramPuppet.userID, dialog.access_hash) + } try { await portal.createMatrixRoom(this.telegramPuppet, { invite: [this.userID], }) } catch (err) { + console.error(`Failed to create a room for ${dialog._} ${dialog.id}`) console.error(err) - console.error(err.stack) + continue } } + if (await portal.updateInfo(this.telegramPuppet, dialog)) { + changed = true + } } await this.save() return changed diff --git a/src/portal.js b/src/portal.js index 803e80ba..d403ab2c 100644 --- a/src/portal.js +++ b/src/portal.js @@ -15,6 +15,7 @@ // along with this program. If not, see . const TelegramPeer = require("./telegram-peer") const formatter = require("./formatter") +const chalk = require("chalk") /** * Portal represents a portal from a Matrix room to a Telegram chat. @@ -124,7 +125,6 @@ class Portal { * @returns {Object} The uploaded Matrix file object. */ async copyTelegramFile(telegramPOV, sender, location, id) { - console.log(JSON.stringify(location, "", " ")) id = id || location.id const file = await telegramPOV.getFile(location) const uploaded = await sender.intent.getClient().uploadContent({ @@ -226,7 +226,7 @@ class Portal { // We don't care about user deletions on chats without portals return } - console.log("Service message received, creating room for", evt.to.id) + this.app.debug("yellow", "Service message received, creating room for", evt.to.id) await this.createMatrixRoom(evt.source, { invite: [evt.source.matrixUser.userID] }) return } @@ -293,11 +293,42 @@ class Portal { await intent.setRoomName(this.roomID, this.peer.title) break default: - console.log("Unhandled service message of type", evt.action._) - console.log(evt.action) + this.app.warn("Unhandled service message of type", evt.action._) + this.app.warn(JSON.stringify(evt.action, "", " ")) } } + /** + * Context: Matrix user X is logged into mautrix-telegram and has a private chat portal room with Telegram user Y. + * X sends message to Y from another Telegram client. + * + * Problem: We can't control X's Matrix account. We also can't make sure that X's Telegram account's Matrix puppet + * is always in private chat portal rooms, since X could create a private chat portal by inviting Y's + * puppet without giving it, the only AS-controllable user in the room, any power. + * + * Solution: When encountering an error caused by the above situation, this function is called. + * This function first tries to invite X's Matrix puppet to the room. + * If that fails, text messages are sent through the other user as notices and other messages are dropped. + * + * @param {Object} evt The custom event object (see #handleTelegramMessage(evt)) + * @param {TelegramUser} sender The Telegram user object of the sender. + * @returns {boolean} Whether or not the puppet for the sender was successfully invited. + */ + async tryFixPrivateChatForOutgoingMessage(evt, sender) { + try { + const intent = await this.getMainIntent() + await intent.invite(this.roomID, sender.mxid) + return true + } catch (_) { + const receiver = await this.app.getTelegramUser(evt.to.id, { createIfNotFound: false }) + if (receiver) { + if (evt.text) { + receiver.sendNotice(this.roomID, `[Your message from another client] ${evt.text}`) + } + } + } + return false + } /** * Handle a Telegram service message event. @@ -315,6 +346,8 @@ class Portal { * @param {messageMediaGeo} [evt.geo] The Telegram {@link https://tjhorner.com/tl-schema/constructor/messageMediaGeo Location} attached to the message. */ async handleTelegramMessage(evt) { + const a = Object.assign({}, evt) + delete a.source if (!this.isMatrixRoomCreated()) { try { const result = await this.createMatrixRoom(evt.source, { invite: [evt.source.matrixUser.userID] }) @@ -329,7 +362,18 @@ class Portal { } const sender = await this.app.getTelegramUser(evt.from) - await sender.intent.sendTyping(this.roomID, false) + try { + await sender.intent.sendTyping(this.roomID, false) + } catch (err) { + if (evt.to.type === "user") { + if (!await this.tryFixPrivateChatForOutgoingMessage(evt, sender)) { + return + } + await sender.intent.sendTyping(this.roomID, false) + } else { + throw err + } + } if (evt.text && evt.text.length > 0) { if (evt.entities) { @@ -403,7 +447,7 @@ class Portal { }) break default: - console.log("Unhandled event:", evt) + this.app.warn("Unhandled event:", JSON.stringify(evt, "", " ")) } } @@ -435,13 +479,13 @@ class Portal { user_id: user.toPeer(telegramPOV).toInputObject(), fwd_limit: 50, }) - console.log("Chat invite result:", updates) + this.app.debug("green", "Chat invite result:", JSON.stringify(updates, "", " ")) } else if (this.peer.type === "channel") { const updates = await telegramPOV.client("channels.inviteToChannel", { channel: this.peer.toInputObject(), users: [user.toPeer(telegramPOV).toInputObject()], }) - console.log("Channel invite result:", updates) + this.app.debug("green", "Channel invite result:", JSON.stringify(updates, "", " ")) } else { throw new Error(`Can't invite user to peer type ${this.peer.type}`) } @@ -478,11 +522,25 @@ class Portal { if (Array.isArray(users)) { for (const userID of users) { if (typeof userID === "string") { - intent.invite(this.roomID, userID) + try { + await intent.invite(this.roomID, userID) + } catch (err) { + if (err.httpStatus !== 403) { + console.error(`Failed to invite ${userID} to ${this.roomID}:`) + console.error(err) + } + } } } } else if (typeof users === "string") { - intent.invite(this.roomID, users) + try { + await intent.invite(this.roomID, users) + } catch (err) { + if (err.httpStatus !== 403) { + console.error(`Failed to invite ${users} to ${this.roomID}:`) + console.error(err) + } + } } } @@ -586,7 +644,7 @@ class Portal { if (!await this.loadAccessHash(telegramPOV)) { this.creatingMatrixRoom = false - throw new Error("Failed to load access hash.") + throw new Error(`Failed to load access hash for ${this.peer.type} ${this.peer.username || this.peer.id}.`) } let room, info, users @@ -661,7 +719,7 @@ class Portal { async updateInfo(telegramPOV, dialog) { if (!dialog) { - console.log("updateInfo called without dialog data") + this.app.warn("updateInfo called without dialog data") const { user } = this.peer.getInfo(telegramPOV) if (!user) { throw new Error("Dialog data not given and fetching data failed") diff --git a/src/telegram-puppet.js b/src/telegram-puppet.js index b0b2ca42..f3304baf 100644 --- a/src/telegram-puppet.js +++ b/src/telegram-puppet.js @@ -18,6 +18,7 @@ const { nextRandomInt } = require("telegram-mtproto/lib/bin") const fileType = require("file-type") const pkg = require("../package.json") const TelegramPeer = require("./telegram-peer") +const chalk = require("chalk") /** * @module telegram-puppet @@ -279,7 +280,7 @@ class TelegramPuppet { async onUpdate(update) { if (!update) { - console.log("Oh noes! Empty update") + this.app.error("Oh noes! Empty update") return } let to, from, portal @@ -312,7 +313,7 @@ class TelegramPuppet { // case "updateShortMessage": to = new TelegramPeer("user", update.user_id, { receiverID: this.userID }) - from = update.user_id + from = update.out ? this.userID : update.user_id break case "updateShortChatMessage": to = new TelegramPeer("chat", update.chat_id) @@ -358,7 +359,6 @@ class TelegramPuppet { }) return } - console.log(update) await portal.handleTelegramMessage({ from, to, @@ -381,6 +381,9 @@ class TelegramPuppet { } async handleUpdate(data) { + if (!data.update || data.update._ !== "updateUserStatus") { + this.app.debug("green", "Raw event for", this.userID, JSON.stringify(data, "", " ")) + } try { switch (data._) { case "updateShort": @@ -389,8 +392,8 @@ class TelegramPuppet { break case "updates": // TODO use data.users and data.chats - console.log("Received updates users:", data.users) - console.log("Received updates chats:", data.chats) + this.app.debug("green", "Received updates users:", JSON.stringify(data.users, "", " ")) + this.app.debug("green", "Received updates chats:", JSON.stringify(data.chats, "", " ")) this.date = data.date const updateHandlers = [] for (const update of data.updates) { @@ -403,21 +406,24 @@ class TelegramPuppet { await this.onUpdate(data) break case "updatesTooLong": - console.log("Handling updatesTooLong", this.pts, this.date) + if (this.pts === 0) { + this.app.warn("updatesTooLong received, but we don't have a persistent timestamp :(") + break + } + this.app.debug("yellow", "Handling updatesTooLong", this.pts, this.date) const dat = await this.client("updates.getDifference", { pts: this.pts, date: this.date, qts: -1, }) - console.log("updatesTooLong data:", dat) + this.app.debug("yellow", `updatesTooLong data: ${JSON.stringify(dat, "", " ")}`) // TODO use updatesTooLong data? break default: - console.log("Unrecognized update type:", data._) + this.app.warn("Unrecognized update type:", data._) } } catch (err) { - console.error("Error handling update:", err) - console.error(err.stack) + this.app.warn("Error handling update:", err) } } @@ -429,30 +435,30 @@ class TelegramPuppet { //console.log("Updating online status...") //const statusUpdate = await this.client("account.updateStatus", { offline: false }) //console.log(statusUpdate) - console.log("Fetching initial state...") + this.app.info("Fetching initial state...") const state = await this.client("updates.getState", {}) - console.log("Initial state:", state) + this.app.debug("green", "Initial state:", JSON.stringify(state, "", " ")) } catch (err) { console.error("Error getting initial state:", err) } try { - console.log("Updating contact list...") + this.app.info("Updating contact list...") const changed = await this.matrixUser.syncContacts() if (!changed) { - console.log("Contacts were up-to-date") + this.app.info("Contacts were up-to-date") } else { - console.log("Contacts updated") + this.app.info("Contacts updated") } } catch (err) { console.error("Failed to update contacts:", err) } try { - console.log("Updating dialogs...") + this.app.info("Updating dialogs...") const changed = await this.matrixUser.syncChats() if (!changed) { - console.log("Dialogs were up-to-date") + this.app.info("Dialogs were up-to-date") } else { - console.log("Dialogs updated") + this.app.info("Dialogs updated") } } catch (err) { console.error("Failed to update dialogs:", err) diff --git a/src/telegram-user.js b/src/telegram-user.js index 6da83ba2..66626902 100644 --- a/src/telegram-user.js +++ b/src/telegram-user.js @@ -73,7 +73,7 @@ class TelegramUser { async updateInfo(telegramPOV, user, { updateAvatar = false } = {}) { if (!user) { - console.log("updateInfo called without user data") + this.app.warn("updateInfo called without user data") user = await telegramPOV.client("users.getFullUser", { id: this.toPeer(telegramPOV).toInputObject(), })