diff --git a/example-config.yaml b/example-config.yaml index 2372fca4..75cff798 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -21,13 +21,19 @@ bridge: username_template: "telegram_${ID}" # ${DISPLAYNAME} is replaced with the display name of the Telegram user. displayname_template: "${DISPLAYNAME} (Telegram)" + # ${NAME} is replaced with the name part of the public channel/group invite link ( https://t.me/${NAME} ) + alias_template: "telegram_${NAME}" + # Username of the bot. The registration must be regenerated to change this. bot_username: telegrambot - command_prefix: "!tg" + # Bridge management command configuration + commands: + # The prefix for all management commands. + prefix: "!tg" - # The key used to encrypt Telegram authentication tokens - # You can generate a new key using `pwgen 32`. - auth_key_password: long_string_to_encrypt_telegram_auth_keys + # Enables the !tg api ... commands for debugging. + # Do not enable this in production, it allows all whitelisted users to call any Telegram API methods freely. + allow_direct_api_calls: false # Whitelist of user IDs that are allowed to use this bridge. Leave empty to disable. # You can enter a domain without the localpart to allow all users from that homeserver to use the bridge. @@ -35,12 +41,8 @@ bridge: - "internal-hs.example.com" - "@user:public.example.com" -# Telegram app config. Generate your own keys at https://my.telegram.org/apps +# Telegram config telegram: - # Enables the !tg api ... commands for debugging. - # Do not enable this in production, it allows all whitelisted users to call any Telegram API methods freely. - allow_direct_api_calls: false - server_config: - dev: false + # Get your own API keys at https://my.telegram.org/apps api_id: 12345 api_hash: tjyd5yge35lbodk1xwzw2jstp90k55qz diff --git a/src/app.js b/src/app.js index ff1173e7..cdf4111b 100644 --- a/src/app.js +++ b/src/app.js @@ -234,7 +234,7 @@ class MautrixTelegram { const user = await this.getMatrixUser(evt.sender) - const cmdprefix = this.config.bridge.command_prefix + const cmdprefix = this.config.bridge.commands.prefix if (evt.content.body.startsWith(cmdprefix + " ")) { if (!user.whitelisted) { this.botIntent.sendText(evt.room_id, "You are not authorized to use this bridge.") diff --git a/src/commands.js b/src/commands.js index ddcda921..22871188 100644 --- a/src/commands.js +++ b/src/commands.js @@ -45,11 +45,7 @@ cancel - Cancel an ongoing action (such as login). login - Request an authentication code. logout - Log out from Telegram. -api - Call a Telegram API method. Args is always a JSON object. Disabled by default. - -Temporary commands that will be replaced with better commands in the future: -createRoom - Create a portal room. is user, chat or channel and is the numeric ID of the Telegram chat. -syncUsers - Sync user info and join status in the given portal. Same arguments as createRoom.`) +api - Call a Telegram API method. Args is always a JSON object. Disabled by default.`) } @@ -143,34 +139,6 @@ commands.logout = async (sender, args, reply) => { } } -const TelegramPeer = require("./telegram-peer") -const Portal = require("./portal") - -commands.createRoom = async (sender, args, reply, app) => { - let peer = new TelegramPeer(args[0], +args[1]) - const portal = await app.getPortalByPeer(peer) - const roomID = await portal.createMatrixRoom(sender.telegramPuppet) - if (!roomID) { - reply("Failed to create room.") - return - } - await app.botIntent.invite(roomID, sender.userID) - reply(`Created room ${roomID} and invited ${sender.userID}`) -} - -commands.syncUsers = async (sender, args, reply, app) => { - let peer = new TelegramPeer(args[0], +args[1]) - const portal = await app.getPortalByPeer(peer) - try { - await portal.syncTelegramUsers(sender.telegramPuppet) - reply("Users synchronized successfully.") - } catch (err) { - reply(`Failed to sync users: ${err}`) - console.error(err) - console.error(err.stack) - } -} - ////////////////////////////// // General command handlers // ////////////////////////////// @@ -181,7 +149,7 @@ commands.syncUsers = async (sender, args, reply, app) => { //////////////////////////// commands.api = async (sender, args, reply, app) => { - if (!app.config.telegram.allow_direct_api_calls) { + if (!app.config.bridge.commands.allow_direct_api_calls) { reply("Direct API calls are forbidden on this mautrix-telegram instance.") return } diff --git a/src/index.js b/src/index.js index 305d328a..403c1968 100755 --- a/src/index.js +++ b/src/index.js @@ -43,7 +43,10 @@ if (commander.generateRegistration) { exclusive: true, regex: `@${config.bridge.username_template.replace("${ID}", ".+")}:${config.homeserver.domain}` }], - aliases: [], + aliases: [{ + exclusive: true, + regex: `#${config.bridge.alias_template.replace("${NAME}", ".+")}:${config.homeserver.domain}` + }], rooms: [], }, url: `${config.appservice.protocol}://${config.appservice.hostname}:${config.appservice.port}`, diff --git a/src/matrix-user.js b/src/matrix-user.js index eab8bf11..a3f9e44d 100644 --- a/src/matrix-user.js +++ b/src/matrix-user.js @@ -112,18 +112,45 @@ class MatrixUser { return true } - async syncDialogs() { + async syncDialogs({createRooms=true} = {}) { const dialogs = await this.telegramPuppet.client("messages.getDialogs", {}) let changed = false - for (const dialog of dialogs.chats) { + for (const dialog of dialogs.chats.concat(dialogs.users)) { if (dialog._ === "chatForbidden" || dialog.deactivated) { continue } - const peer = new TelegramPeer(dialog._, dialog.id) + const peer = new TelegramPeer(dialog._, dialog.id, { + receiverID: dialog._ === "user" + ? this.telegramPuppet.userID + : undefined + }) const portal = await this.app.getPortalByPeer(peer) if (await portal.updateInfo(this.telegramPuppet, dialog)) { changed = true } + if (createRooms) { + try { + const {roomID, created} = await portal.createMatrixRoom(this.telegramPuppet, { + invite: [this.userID], + }) + if (!created) { + // Make sure the user is invited, since the room already exists. + const intent = await (dialog._ === "user" + ? this.app.getTelegramUser(peer.id) + : 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) + } + } } return changed } diff --git a/src/portal.js b/src/portal.js index 2b51ebec..9c628412 100644 --- a/src/portal.js +++ b/src/portal.js @@ -120,60 +120,85 @@ class Portal { return !!this.roomID } - async createMatrixRoom(telegramPOV) { + async createMatrixRoom(telegramPOV, {invite = []} = {}) { if (this.roomID) { - return this.roomID + return { + created: false, + roomID: this.roomID, + } } - try { - if (!await this.loadAccessHash(telegramPOV)) { - return undefined - } + if (!await this.loadAccessHash(telegramPOV)) { + throw new Error("Failed to load access hash.") + } - let title, - info, - users - if (this.peer.type !== "user") { - ({ info, users } = await this.peer.getInfo(telegramPOV)) - title = info.title - } else { - ({ info } = await this.peer.getInfo(telegramPOV)) - users = await this.app.getTelegramUser(info.id) - await users.updateInfo(telegramPOV, info, { updateAvatar: true }) - title = users.getDisplayName() - } - - const room = await this.app.botIntent.createRoom({ + let room + const { info, users } = await this.peer.getInfo(telegramPOV) + if (this.peer.type === "chat") { + room = await this.app.botIntent.createRoom({ options: { - name: title, + name: info.title, + topic: info.about, visibility: "private", + invite, }, }) + } else if (this.peer.type === "channel") { + room = await this.app.botIntent.createRoom({ + options: { + name: info.title, + topic: info.about, + visibility: info.username ? "public" : "private", + room_alias_name: info.username + ? this.app.config.bridge.alias_template.replace("${NAME}", info.username) + : "", + invite, + }, + }) + } else if (this.peer.type === "user") { + const user = await this.app.getTelegramUser(info.id) + await user.updateInfo(telegramPOV, info, { updateAvatar: true }) + room = await user.intent.createRoom({ + createAsClient: true, + options: { + name: user.getDisplayName(), + topic: "Telegram private chat", + visibility: "private", + invite, + }, + }) + } else { + throw new Error(`Unrecognized peer type: ${this.peer.type}`) + } - this.roomID = room.room_id - this.app.portalsByRoomID.set(this.roomID, this) - await this.save() - if (this.peer.type !== "user") { - await this.updateAvatar(telegramPOV, info) + this.roomID = room.room_id + this.app.portalsByRoomID.set(this.roomID, this) + await this.save() + if (this.peer.type !== "user") { + try { await this.syncTelegramUsers(telegramPOV, users) - } else { - await users.intent.join(this.roomID) + await this.updateAvatar(telegramPOV, info) + } catch (err) { + console.error(err) + console.error(err.stack) } - return this.roomID - } catch (err) { - console.error(err) - console.error(err.stack) - return undefined + } + return { + created: true, + roomID: this.roomID, } } - updateInfo(telegramPOV, dialog) { + async updateInfo(telegramPOV, dialog) { let changed = false if (this.peer.type === "channel") { if (telegramPOV && this.accessHashes.get(telegramPOV.userID) !== dialog.access_hash) { this.accessHashes.set(telegramPOV.userID, dialog.access_hash) changed = true } + } else if (this.peer.type === "user") { + const user = await this.app.getTelegramUser(this.peer.id) + await user.updateInfo(telegramPOV, dialog) } changed = this.peer.updateInfo(dialog) || changed if (changed) { diff --git a/src/telegram-peer.js b/src/telegram-peer.js index cc9952c0..8f11fd14 100644 --- a/src/telegram-peer.js +++ b/src/telegram-peer.js @@ -15,23 +15,28 @@ // along with this program. If not, see . class TelegramPeer { - constructor(type, id, accessHash, receiverID) { + constructor(type, id, { accessHash, receiverID, username, title } = {}) { this.type = type this.id = id this.accessHash = accessHash this.receiverID = receiverID - this.username = undefined - this.title = undefined + this.username = username + this.title = title } - static fromTelegramData(peer, receiverID) { - switch(peer._) { + static fromTelegramData(peer, sender, receiverID) { + switch (peer._) { case "peerChat": return new TelegramPeer("chat", peer.chat_id) case "peerUser": - return new TelegramPeer("user", peer.user_id, peer.access_hash, receiverID) + return new TelegramPeer("user", sender, { + accessHash: peer.access_hash, + receiverID, + }) case "peerChannel": - return new TelegramPeer("channel", peer.channel_id, peer.access_hash) + return new TelegramPeer("channel", peer.channel_id, { + accessHash: peer.access_hash, + }) default: throw new Error(`Unrecognized peer type ${peer._}`) } @@ -48,7 +53,7 @@ class TelegramPeer { * Only used if {@link #type} is {@linkplain channel}. * @returns {Promise} Whether or not the access hash was found and loaded. */ - async loadAccessHash(app, telegramPOV, { portal, user }) { + async loadAccessHash(app, telegramPOV, { portal, user } = {}) { if (this.type === "chat") { return true } else if (this.type === "user") { @@ -70,7 +75,7 @@ class TelegramPeer { async updateInfo(dialog) { let changed = false - if (this.type === "channel") { + if (this.type === "channel" || this.type === "user") { if (this.username !== dialog.username) { this.username = dialog.username changed = true @@ -84,24 +89,28 @@ class TelegramPeer { } async getInfo(telegramPOV) { - let info, users - switch(this.type) { + let info, + users + switch (this.type) { case "user": info = await telegramPOV.client("users.getFullUser", { - id: this.toInputObject() + id: this.toInputObject(), }) users = [info.user] info = info.user + break case "chat": info = await telegramPOV.client("messages.getFullChat", { chat_id: this.id, }) users = info.users + info = info.chats[0] break case "channel": info = await telegramPOV.client("channels.getFullChannel", { channel: this.toInputObject(), }) + info = info.chats[0] const participants = await telegramPOV.client("channels.getParticipants", { channel: this.toInputObject(), filter: { _: "channelParticipantsRecent" }, @@ -114,13 +123,13 @@ class TelegramPeer { throw new Error(`Unknown peer type ${this.type}`) } return { - info: info.chats[0], - users + info: info, + users, } } toInputPeer() { - switch(this.type) { + switch (this.type) { case "chat": return { _: "inputPeerChat", @@ -144,7 +153,7 @@ class TelegramPeer { } toInputObject() { - switch(this.type) { + switch (this.type) { case "user": return { _: "inputUser", @@ -163,11 +172,7 @@ class TelegramPeer { } static fromSubentry(entry) { - const peer = new TelegramPeer(entry.type, entry.id) - peer.username = entry.username - peer.title = entry.title - peer.receiverID = entry.receiverID - return peer + return new TelegramPeer(entry.type, entry.id, entry) } toSubentry() { diff --git a/src/telegram-puppet.js b/src/telegram-puppet.js index cac12ea8..5faef4e4 100644 --- a/src/telegram-puppet.js +++ b/src/telegram-puppet.js @@ -257,20 +257,20 @@ class TelegramPuppet { await user.intent.getClient().setPresence({presence: status}) break case "updateUserTyping": - peer = new TelegramPeer("user", update.user_id, undefined, this.userID) + peer = new TelegramPeer("user", update.user_id, { receiverID: this.userID }) case "updateChatUserTyping": peer = peer || new TelegramPeer("chat", update.chat_id) portal = await this.app.getPortalByPeer(peer) if (portal.isMatrixRoomCreated()) { const sender = await this.app.getTelegramUser(update.user_id) // The Intent API currently doesn't allow you to set the - // typing timeout. If it does, we should set it to ~5.5s as - // Telegram resends typing notifications every 5 seconds. + // typing timeout. Once it does, we should set it to ~5.5s + // as Telegram resends typing notifications every 5 seconds. await sender.intent.sendTyping(portal.roomID, true/*, 5500*/) } break case "updateShortMessage": - peer = new TelegramPeer("user", update.user_id, undefined, this.userID) + peer = new TelegramPeer("user", update.user_id, { receiverID: this.userID }) case "updateShortChatMessage": peer = peer || new TelegramPeer("chat", update.chat_id) await this.handleMessage({ @@ -285,7 +285,7 @@ class TelegramPuppet { update = update.message // Message defined at message#90dddc11 in layer 71 await this.handleMessage({ from: update.from_id, - to: TelegramPeer.fromTelegramData(update.to_id, this.userID), + to: TelegramPeer.fromTelegramData(update.to_id, update.from_id, this.userID), text: update.message, }) break @@ -322,7 +322,7 @@ class TelegramPuppet { this.client.bus.untypedMessage.observe(data => this.handleUpdate(data.message)) try { - console.log("Updating online status...") + //console.log("Updating online status...") //const statusUpdate = await this.client("account.updateStatus", { offline: false }) //console.log(statusUpdate) console.log("Fetching initial state...") diff --git a/src/telegram-user.js b/src/telegram-user.js index b107f01a..3930e9e4 100644 --- a/src/telegram-user.js +++ b/src/telegram-user.js @@ -48,7 +48,10 @@ class TelegramUser { } toPeer(telegramPOV) { - return new TelegramPeer("user", this.id, this.accessHashes.get(telegramPOV.userID)) + return new TelegramPeer("user", this.id, { + accessHash: this.accessHashes.get(telegramPOV.userID), + receiverID: telegramPOV.userID, + }) } toEntry() { @@ -67,7 +70,7 @@ class TelegramUser { } } - async updateInfo(telegramPOV, user, { updateAvatar }) { + async updateInfo(telegramPOV, user, { updateAvatar } = {}) { let changed = false if (this.firstName !== user.first_name) { this.firstName = user.first_name @@ -141,10 +144,6 @@ class TelegramUser { }) } - sendSelfStateEvent(roomID, type, content) { - return this.intent.sendStateEvent(roomID, type, this.getMxid(), content) - } - uploadContent(opts) { return this.intent.getClient() .uploadContent({