\``)
- sender.commandStatus = {
- action: "Phone code authentication",
- next: commands.enterCode,
- }
- } catch (err) {
- reply(`**Failed to send code:** ${err}`)
- if (err instanceof Error) {
- reply(["```", err.stack, "```"].join(""))
- console.error(err.stack)
- }
- }
-}
-
-commands.register = async (sender, args, reply) => {
- reply("Registration has not yet been implemented. Please use the official apps for now.")
-}
-
-commands.logout = async (sender, args, reply) => {
- try {
- sender.logOutFromTelegram()
- reply("Logged out successfully.")
- } catch (err) {
- reply(`**Failed to log out:** ${err}`)
- if (err instanceof Error) {
- reply(["```", err.stack, "```"].join(""))
- console.error(err.stack)
- }
- }
-}
-
-//////////////////////////////
-// General command handlers //
-//////////////////////////////
-
-commands.create = async (sender, args, reply, { app, roomID }) => {
- if (args.length < 1 || (args[0] !== "group" && args[0] !== "channel")) {
- reply("**Usage:** `$cmdprefix create `")
- return
- } else if (!sender._telegramPuppet) {
- reply("This command requires you to be logged in.")
- return
- } else if (args[0] === "channel") {
- reply("Creating channels is not yet supported.")
- return
- }
-
- if (args.length > 1) {
- roomID = args[1]
- }
-
- // TODO make sure that the AS bot is in the room.
-
- const title = await app.getRoomTitle(roomID)
- if (!title) {
- reply("Please set a room name before creating a Telegram chat.")
- return
- }
-
- let portal = await app.getPortalByRoomID(roomID)
- if (portal) {
- reply("This is already a portal room.")
- return
- }
-
- portal = new Portal(app, roomID)
- try {
- await portal.createTelegramChat(sender.telegramPuppet, title)
- reply(`Telegram chat created. ID: ${portal.id}`)
- if (app.managementRooms.includes(roomID)) {
- app.managementRooms.splice(app.managementRooms.indexOf(roomID), 1)
- }
- } catch (err) {
- reply(`Failed to create Telegram chat: ${err}`)
- }
-}
-
-commands.upgrade = async (sender, args, reply, { app, roomID }) => {
- if (!sender._telegramPuppet) {
- reply("This command requires you to be logged in.")
- return
- }
-
- const portal = await app.getPortalByRoomID(roomID)
- if (!portal) {
- reply("This is not a portal room.")
- return
- }
-
- await portal.upgradeTelegramChat(sender.telegramPuppet)
-}
-
-commands.search = async (sender, args, reply, { app }) => {
- if (args.length < 1) {
- reply("**Usage:** `$cmdprefix search [-r|--remote] `")
- return
- } else if (!sender._telegramPuppet) {
- reply("This command requires you to be logged in.")
- return
- }
- const 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"), { allowHTML: true })
- return
- }
- } else {
- args.shift()
- msg.push("-r flag found: forcing remote search")
- msg.push("")
- }
- const query = args.join(" ")
- if (query.length < 5) {
- reply("Failed to search server: Query is too short.")
- return
- }
- const telegramResults = await sender.searchTelegram(query)
- 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"), { allowHTML: true })
-}
-
-commands.pm = async (sender, args, reply, { app }) => {
- if (args.length < 1) {
- reply("**Usage:** `$cmdprefix pm `")
- return
- } else if (!sender._telegramPuppet) {
- reply("This command requires you to be logged in.")
- 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 //
-////////////////////////////
-
-commands.api = async (sender, args, reply, { app }) => {
- if (!app.config.bridge.commands.allow_direct_api_calls) {
- reply("Direct API calls are forbidden on this mautrix-telegram instance.")
- return
- }
- const apiMethod = args.shift()
- let apiArgs
- try {
- apiArgs = JSON.parse(args.join(" "))
- } catch (err) {
- reply("Invalid API method parameters. Usage: $cmdprefix api ")
- return
- }
- try {
- reply(`Calling ${apiMethod} with the following arguments:\n${JSON.stringify(apiArgs, "", " ")}`)
- const response = await sender.telegramPuppet.client(apiMethod, apiArgs)
- reply(`API call successful. Response:
-
-
- ${JSON.stringify(response, "", " ")}
-
`, { allowHTML: true })
- } catch (err) {
- reply(`API call errored. Response:\n${JSON.stringify(err, "", " ")}`)
- }
-}
-
-function timeout(promise, ms = 2500) {
- return new Promise((resolve, reject) => {
- promise.then(resolve, reject)
- setTimeout(() => reject(new Error("API call response not received")), ms)
- })
-}
-
-commands.ping = async (sender, args, reply) => {
- try {
- await timeout(sender.telegramPuppet.client("contacts.getContacts", {}))
- reply("Connection seems OK.")
- } catch (err) {
- reply(`Not connected: ${err}`)
- }
-}
-
-module.exports = {
- commands,
- run,
-}
diff --git a/src/formatter.js b/src/formatter.js
deleted file mode 100644
index 4a057f63..00000000
--- a/src/formatter.js
+++ /dev/null
@@ -1,360 +0,0 @@
-// mautrix-telegram - A Matrix-Telegram puppeting bridge
-// Copyright (C) 2017 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-/**
- * Utility functions to convert between Telegram and Matrix (HTML) formatting.
- *
- * WARNING: This module contains headache-causing regular expressions and other duct tape.
- *
- * @module formatter
- */
-
-String.prototype.insert = function(at, str) {
- return this.slice(0, at) + str + this.slice(at)
-}
-
-/**
- * Add a simple HTML tag to the given tag list.
- *
- * @param {Object[]} tags The tag list.
- * @param {Object} entity The Telegram format entity.
- * @param {number} entity.offset The index where the format entity starts.
- * @param {number} entity.length The length of the format entity.
- * @param {string} tag The HTML tag to add.
- * @param {number} priority The tag priority to use when sorting tags at the same index.
- * @private
- */
-function addSimpleTag(tags, entity, tag, priority = 0) {
- tags.push([entity.offset, `<${tag}>`, -priority])
- tags.push([entity.offset + entity.length, `${tag}>`, priority])
-}
-
-
-/**
- * Add a HTML tag to the given tag list.
- *
- * @param {Object[]} tags The tag list.
- * @param {Object} entity The Telegram format entity.
- * @param {number} entity.offset The index where the format entity starts.
- * @param {number} entity.length The length of the format entity.
- * @param {string} tag The HTML tag to add.
- * @param {string} attrs The HTML attributes to add to the tag.
- * @param {number} priority The tag priority to use when sorting tags at the same index.
- * @private
- */
-function addTag(tags, entity, tag, attrs, priority = 0) {
- tags.push([entity.offset, `<${tag} ${attrs}>`, -priority])
- tags.push([entity.offset + entity.length, `${tag}>`, priority])
-}
-
-/**
- * Convert a Telegram entity-formatted message to a Matrix HTML-formatted message.
- *
- * WARNING: I am not responsible for possible severe headaches caused by reading any part of this function.
- *
- * @param {string} message The plaintext message.
- * @param {Array} entities The Telegram formatting entities.
- * @param {MautrixTelegram} app The app main class instance to use when reformatting mentions.
- */
-function telegramToMatrix(message, entities, app) {
- const tags = []
- // Decreasing priority counter used to ensure that formattings right next to eachother don't flip like this:
- // *bold*_italic_ --> bolditalic
- let pc = 9001
-
- // Convert Telegram formatting entities into a weird custom indexed HTML tag format thingy.
- for (const entity of entities) {
- let url, tag, mxid
- switch (entity._) {
- case "messageEntityBold":
- tag = tag || "strong"
- case "messageEntityItalic":
- tag = tag || "em"
- case "messageEntityCode":
- tag = tag || "code"
- addSimpleTag(tags, entity, tag, --pc)
- break
- case "messageEntityPre":
- pc--
- addSimpleTag(tags, entity, "pre", pc)
- addTag(tags, entity, "code", `class="language-${entity.language}"`, pc + 1)
- break
- case "messageEntityBotCommand":
- // TODO bridge bot commands differently?
- message = `${message.substr(0, entity.offset)}!${message.substr(entity.offset + 1)}`
- case "messageEntityHashtag":
- addTag(tags, entity, "font", "color=\"blue\"", --pc)
- break
- case "messageEntityMentionName":
- let user = app.matrixUsersByTelegramID.get(entity.user_id)
- if (!user) {
- // TODO this loop step should be made useless
- for (const userByMXID of app.matrixUsersByID.values()) {
- if (userByMXID.telegramUserID === entity.user_id) {
- user = userByMXID
- app.matrixUsersByTelegramID.set(userByMXID.telegramUserID, userByMXID)
- break
- }
- }
- }
- mxid = user ?
- user.userID :
- app.getMXIDForTelegramUser(entity.user_id)
- case "messageEntityMention":
- if (!mxid) {
- const username = message.substr(entity.offset + 1, entity.length - 1)
- for (const userByMXID of app.matrixUsersByID.values()) {
- if (userByMXID._telegramPuppet && userByMXID._telegramPuppet.data.username === username) {
- mxid = userByMXID.userID
- break
- }
- }
- if (!mxid) {
- for (const userByID of app.telegramUsersByID.values()) {
- if (userByID.username === username) {
- mxid = userByID.mxid
- break
- }
- }
- }
- }
-
- if (!mxid) {
- continue
- }
- addTag(tags, entity, "a", `href="https://matrix.to/#/${mxid}"`)
- break
- case "messageEntityEmail":
- url = url || `mailto:${message.substr(entity.offset, entity.length)}`
- case "messageEntityUrl":
- url = url || message.substr(entity.offset, entity.length)
- case "messageEntityTextUrl":
- url = url || entity.url
- addTag(tags, entity, "a", `href="${url}"`, --pc)
- break
- }
- }
-
- // Sort tags in a mysterious way (it seems to work, don't touch it!).
- //
- // The important thing is that the tags are sorted last to first,
- // so when replacing by index, the index doesn't need to be adapted.
- tags.sort(([aIndex, , aPriority], [bIndex, , bPriority]) => bIndex - aIndex || aPriority - bPriority)
-
- // Insert tags into message
- for (const [index, replacement] of tags) {
- message = message.insert(index, replacement)
- }
- message = message.replace(/\n/g, "
\n")
- return message
-}
-
-// Formatting that is converted back to text
-const linebreaks = /
(\n)?/g
-const paragraphs = /([^]*?)<\/p>/g
-const headers = /([^]*?)<\/h[0-6]>/g
-const unorderedLists = /([^]*?)<\/ul>/g
-const orderedLists = /([^]*?)<\/ol>/g
-const listEntries = /- ([^]*?)<\/li>/g
-const blockquotes = /
([^]*?)<\/blockquote>/g
-
-// Formatting that is brutally murdered
-const strikedText = /([^]*?)<\/del>/g
-const underlinedText = /([^]*?)<\/u>/g
-
-// Formatting that is converted to Telegram entity formatting
-const boldText = /<(strong)>()([^]*?)<\/strong>/g
-const italicText = /<(em)>()([^]*?)<\/em>/g
-const codeblocks = /<(pre>()([^]*?)<\/code><\/pre>/g
-const codeblocksWithSyntaxHighlight = /<(pre>([^]*?)<\/code><\/pre>/g
-const inlineCode = /<(code)>()(.*?)<\/code>/g
-const emailAddresses = /([^]*?)<\/a>/g
-const mentions = /(.*?)<\/a>/g
-const hyperlinks = /<(a href)="(.*?)">([^]*?)<\/a>/g
-const commands = /(\s|^)!([^\s]+)/g
-const REGEX_CAPTURE_GROUP_COUNT = 3
-
-RegExp.any = function(...regexes) {
- let components = []
- for (const regex of regexes) {
- if (regex instanceof RegExp) {
- components = components.concat(regex._components || regex.source)
- }
- }
- return new RegExp(`(?:${components.join(")|(?:")})`)
-}
-
-const regexMonster = RegExp.any(boldText, italicText, codeblocks,
- codeblocksWithSyntaxHighlight, inlineCode, emailAddresses,
- mentions, hyperlinks)
-const NUMBER_OF_REGEXES_EATEN_BY_MONSTER = 8
-
-function regexMonsterMatchParser(match) {
- match.pop() // Remove full string
- const index = match.pop()
- let identifier, arg, text
- for (let i = 0; i < NUMBER_OF_REGEXES_EATEN_BY_MONSTER; i++) {
- if (match[i * REGEX_CAPTURE_GROUP_COUNT]) {
- identifier = match[i * REGEX_CAPTURE_GROUP_COUNT]
- arg = match[(i * REGEX_CAPTURE_GROUP_COUNT) + 1]
- text = match[(i * REGEX_CAPTURE_GROUP_COUNT) + 2]
- }
- }
- return { index, identifier, arg, text }
-}
-
-function regexMonsterHandler(identifier, arg, text, index, app) {
- let entity, entityClass, argField
- switch (identifier) {
- case "strong":
- entityClass = "Bold"
- break
- case "em":
- entityClass = "Italic"
- break
- case "pre> {
- entities.push({
- _: "messageEntityBotCommand",
- offset: index + prefix.length,
- length: command.length + 1,
- })
- return `${prefix}/${command}`
- })
-
- if (!isHTML) {
- return { message, entities }
- }
-
- // First replace all the things that don't get converted into Telegram entities
- message = message.replace(linebreaks, "\n")
- message = message.replace(paragraphs, "$1\n")
- message = message.replace(headers, (_, count, text) => `${"#".repeat(count)} ${text}`)
- message = message.replace(unorderedLists, (_, list) => list.replace(listEntries, "- $1"))
- message = message.replace(orderedLists, (_, list) => {
- let n = 0
- return list.replace(listEntries, (fullMatch, text) => `${++n}. ${text}`)
- })
- message = message.replace(blockquotes, (_, quote) => quote
- .split("\n")
- .map(line => line.trim())
- .filter(line => line.length > 0)
- .map(line => `> ${line}`)
- .join("\n"))
-
- // Just remove these, they have no textual or Telegramical representation.
- message = message.replace(strikedText, (_, text) => text)
- message = message.replace(underlinedText, (_, text) => text)
- message = message.trim()
-
- const regexMonsterReplacer = (match, ...args) => {
- const { index, identifier, arg, text } = regexMonsterMatchParser(args)
- if (!identifier) {
- // This shouldn't happen
- console.warn(`Warning: Match found but parsing failed for match "${match}"`)
- return match
- }
- const { replacement, entity } = regexMonsterHandler(identifier, arg, text, index, app)
- if (entity) {
- entities.push(entity)
- }
- return replacement || text
- }
-
- // We replace matches iteratively to make sure the indexes of matches are correct.
- let oldMessage = message
- message = message.replace(regexMonster, regexMonsterReplacer)
- while (oldMessage !== message) {
- oldMessage = message
- message = message.replace(regexMonster, regexMonsterReplacer)
- }
-
- return { message, entities }
-}
-
-module.exports = { telegramToMatrix, matrixToTelegram }
diff --git a/src/index.js b/src/index.js
deleted file mode 100755
index 60eb6209..00000000
--- a/src/index.js
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/usr/bin/env node
-// mautrix-telegram - A Matrix-Telegram puppeting bridge
-// Copyright (C) 2017 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-const { AppServiceRegistration } = require("matrix-appservice-bridge")
-const program = require("commander")
-const YAML = require("yamljs")
-const fs = require("fs")
-const MautrixTelegram = require("./app")
-const pkg = require("../package.json")
-
-program
- .version(pkg.version)
- .option("-c, --config ", "the file to load the config from. defaults to ./config.yaml")
- .option("-g, --generate-registration", "generate a registration based on the config")
- .option("-r, --registration ", "the file to save the registration to. defaults to ./registration.yaml")
- .parse(process.argv)
-
-// commander doesn't seem to set default values automatically.
-program.registration = program.registration || "./registration.yaml"
-program.config = program.config || "./config.yaml"
-
-const config = YAML.load(program.config)
-
-if (program.generateRegistration) {
- const registration = {
- id: config.appservice.id,
- hs_token: AppServiceRegistration.generateToken(),
- as_token: AppServiceRegistration.generateToken(),
- namespaces: {
- users: [{
- exclusive: true,
- regex: `@${config.bridge.username_template.replace("${ID}", ".+")}:${config.homeserver.domain}`,
- }],
- aliases: [{
- exclusive: true,
- regex: `#${config.bridge.alias_template.replace("${NAME}", ".+")}:${config.homeserver.domain}`,
- }],
- rooms: [],
- },
- url: `${config.appservice.protocol}://${config.appservice.hostname}:${config.appservice.port}`,
- sender_localpart: config.bridge.bot_username,
- rate_limited: false,
- }
-
- fs.writeFileSync(program.registration, YAML.stringify(registration, 10))
- config.appservice.registration = program.registration
- fs.writeFileSync(program.config, YAML.stringify(config, 10))
-
- console.log("Registration generated and saved to", program.registration)
- process.exit()
-}
-
-const app = new MautrixTelegram(config)
-app.run()
diff --git a/src/matrix-user.js b/src/matrix-user.js
deleted file mode 100644
index 54960cdf..00000000
--- a/src/matrix-user.js
+++ /dev/null
@@ -1,402 +0,0 @@
-// mautrix-telegram - A Matrix-Telegram puppeting bridge
-// Copyright (C) 2017 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-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
- * Telegram account from Matrix.
- */
-class MatrixUser {
- constructor(app, userID) {
- this.app = app
- this.userID = userID
- this.whitelisted = app.checkWhitelist(userID)
- this.phoneNumber = undefined
- this.phoneCodeHash = undefined
- this.commandStatus = undefined
- this.puppetData = undefined
- this.contacts = []
- this.chats = []
- this._telegramPuppet = undefined
- }
-
- /**
- * Get the user ID of the Telegram user this Matrix user controls.
- *
- * @returns {number|undefined} The Telegram user ID, or undefined if not logged in.
- */
- get telegramUserID() {
- return this._telegramPuppet
- ? this._telegramPuppet.userID || undefined
- : undefined
- }
-
- /**
- * Convert a database entry into a MatrixUser.
- *
- * @param {MautrixTelegram} app The app main class instance.
- * @param {Object} entry The database entry.
- * @returns {MatrixUser} The loaded MatrixUser.
- */
- static fromEntry(app, entry) {
- if (entry.type !== "matrix") {
- throw new Error("MatrixUser can only be created from entry type \"matrix\"")
- }
-
- const user = new MatrixUser(app, entry.id)
- user.phoneNumber = entry.data.phoneNumber
- user.phoneCodeHash = entry.data.phoneCodeHash
- user.setContactIDs(entry.data.contactIDs)
- user.setChatIDs(entry.data.chatIDs)
- if (entry.data.puppet) {
- user.puppetData = entry.data.puppet
- // Create the telegram puppet instance
- user.telegramPuppet
- }
- return user
- }
-
- /**
- * Convert this MatrixUser into a database entry.
- *
- * @returns {Object} A user store database entry.
- */
- toEntry() {
- if (this._telegramPuppet) {
- this.puppetData = this._telegramPuppet.toSubentry()
- }
- return {
- type: "matrix",
- id: this.userID,
- telegramID: this.telegramUserID,
- data: {
- phoneNumber: this.phoneNumber,
- phoneCodeHash: this.phoneCodeHash,
- contactIDs: this.contactIDs,
- chatIDs: this.chatIDs,
- puppet: this.puppetData,
- },
- }
- }
-
- /**
- * Get the telegram puppet this Matrix user controls.
- * If one doesn't exist, it'll be created based on the {@link #puppetData} field.
- *
- * @returns {TelegramPuppet} The Telegram account controller.
- */
- get telegramPuppet() {
- if (!this._telegramPuppet) {
- this._telegramPuppet = TelegramPuppet.fromSubentry(this.app, this, this.puppetData || {})
- }
- return this._telegramPuppet
- }
-
- /**
- * Get the IDs of all the Telegram contacts of this user.
- *
- * @returns {number[]} A list of Telegram user IDs.
- */
- get contactIDs() {
- return this.contacts.map(contact => contact.id)
- }
-
- /**
- * Get the IDs of all the Telegram chats this user is in.
- *
- * @returns {number[]} A list of Telegram chat IDs.
- */
- get chatIDs() {
- return this.chats.map(chat => chat.id)
- }
-
- /**
- * Update the contacts of this user based on a list of Telegram user IDs.
- *
- * @param {number[]} list The list of Telegram user IDs.
- */
- async setContactIDs(list) {
- if (!list) {
- return
- }
- this.contacts = await Promise.all(list.map(id => this.app.getTelegramUser(id)))
- }
-
- /**
- * Update the chats of this user based on a list of Telegram chat IDs.
- *
- * @param {number[]} list The list of Telegram chat IDs.
- */
- async setChatIDs(list) {
- if (!list) {
- return
- }
- this.chats = await Promise.all(list.map(id => this.app.getPortalByPeer(id)))
- }
-
- /**
- * Synchronize the contacts of this user.
- *
- * @returns {boolean} Whether or not anything changed.
- */
- async syncContacts() {
- const contacts = await this.telegramPuppet.client("contacts.getContacts", {
- hash: md5(this.contactIDs.join(",")),
- })
- if (contacts._ === "contacts.contactsNotModified") {
- return false
- }
- for (const [index, contact] of Object.entries(contacts.users)) {
- const telegramUser = await this.app.getTelegramUser(contact.id)
- await telegramUser.updateInfo(this.telegramPuppet, contact, true)
- contacts.users[index] = telegramUser
- }
- this.contacts = contacts.users
- await this.save()
- return true
- }
-
- /**
- * Synchronize the chats (groups, channels) of this user.
- *
- * @param {object} [opts] Additional options.
- * @param {boolean} opts.createRooms Whether or not portal rooms should be automatically created.
- * Defaults to {@code true}
- * @returns {boolean} Whether or not anything changed.
- */
- async syncChats({ createRooms = true } = {}) {
- const dialogs = await this.telegramPuppet.client("messages.getDialogs", {})
- let changed = false
-
- for (const user of dialogs.users) {
- this.app.debug("cyan", "Syncing data for", this.telegramPuppet.userID, JSON.stringify(user, "", " "))
- if (!user.self) {
- continue
- }
- // Automatically create Saved Messages room
- const peer = new TelegramPeer("user", user.id, {
- receiverID: user.id,
- accessHash: user.access_hash,
- })
- const portal = await this.app.getPortalByPeer(peer)
- if (createRooms) {
- try {
- await portal.createMatrixRoom(this.telegramPuppet, {
- invite: [this.userID],
- })
- } catch (err) {
- console.error(err)
- console.error(err.stack)
- }
- }
- }
-
- this.chats = []
- for (const dialog of dialogs.chats) {
- if (dialog._ === "chatForbidden" || dialog._ === "channelForbidden" || dialog.deactivated || dialog.left) {
- continue
- }
- this.app.debug("cyan", "Syncing data for ", this.telegramPuppet.userID, JSON.stringify(dialog, "", " "))
- const peer = new TelegramPeer(dialog._, dialog.id, {
- accessHash: dialog.access_hash,
- })
- const portal = await this.app.getPortalByPeer(peer)
- 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)
- continue
- }
- }
- if (await portal.updateInfo(this.telegramPuppet, dialog)) {
- changed = true
- }
- }
- await this.save()
- return changed
- }
-
- /**
- * Add a {@link Portal} to the chat list of this user.
- *
- * This should only be used for non-private chat portals.
- *
- * @param {Portal} portal The portal to add.
- */
- async join(portal) {
- if (!this.chats.includes(portal.id)) {
- this.chats.push(portal.id)
- await this.save()
- }
- }
-
- /**
- * Remove a {@link Portal} from the chat list of this user.
- *
- * This should only be used for non-private chat portals.
- *
- * @param {Portal} portal The portal to remove.
- */
- async leave(portal) {
- const chatIDIndex = this.chats.indexOf(portal.id)
- if (chatIDIndex > -1) {
- this.chats.splice(chatIDIndex, 1)
- await this.save()
- }
- }
-
- /**
- * Search for contacts of this user.
- *
- * @param {string} query The search query.
- * @param {object} [opts] Additional options.
- * @param {number} opts.maxResults The maximum number of results to show.
- * @param {number} opts.minSimilarity The minimum query similarity, below which results should be ignored.
- * @returns {Object[]} The search results.
- */
- async searchContacts(query, { maxResults = 5, minSimilarity = 0.45 } = {}) {
- const results = []
- for (const contact of this.contacts) {
- let displaynameSimilarity = 0
- let usernameSimilarity = 0
- let 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)
- 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)
- }
-
- /**
- * Search for non-contact Telegram users from the point of view of this user.
- * @param {string} query The search query.
- * @param {object} [opts] Additional options.
- * @param {number} opts.maxResults The maximum number of results to show.
- * @returns {Object[]} The search results.
- */
- 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
- }
-
- /**
- * Request a Telegarm phone code for logging in (or registering)
- *
- * @param {string} phoneNumber The phone number.
- * @returns {Object} The code send result as returned by {@link TelegramPuppet#sendCode()}.
- */
- async sendTelegramCode(phoneNumber) {
- if (this._telegramPuppet && this._telegramPuppet.userID) {
- throw new Error("You are already logged in. Please log out before logging in again.")
- }
- switch (this.telegramPuppet.checkPhone(phoneNumber)) {
- case "unregistered":
- throw new Error("That number has not been registered. Please register it first.")
- case "invalid":
- throw new Error("Invalid phone number.")
- }
- const result = await this.telegramPuppet.sendCode(phoneNumber)
- this.phoneNumber = phoneNumber
- this.phoneCodeHash = result.phone_code_hash
- await this.save()
- return result
- }
-
- /**
- * Log out from Telegram.
- */
- async logOutFromTelegram() {
- this.telegramPuppet.logOut()
- // TODO kick user from all portals
- this._telegramPuppet = undefined
- this.puppetData = undefined
- await this.save()
- }
-
- /**
- * Sign in to Telegram with a phone code sent using {@link #sendTelegramCode()}.
- *
- * @param {number} phoneCode The phone code.
- * @returns {Object} The sign in result as returned by {@link TelegramPuppet#signIn()}.
- */
- async signInToTelegram(phoneCode) {
- if (!this.phoneNumber) throw new Error("Phone number not set")
- if (!this.phoneCodeHash) throw new Error("Phone code not sent")
-
- const result = await this.telegramPuppet.signIn(this.phoneNumber, this.phoneCodeHash, phoneCode)
- this.phoneCodeHash = undefined
- await this.save()
- return result
- }
-
- /**
- * Finish signing in to Telegram using the two-factor auth password.
- *
- * @param {string} password_hash The salted hash of the password.
- * @returns {Object} The sign in result as returned by {@link TelegramPuppet#checkPassword()}
- */
- async checkPassword(password_hash) {
- const result = await this.telegramPuppet.checkPassword(password_hash)
- await this.save()
- return result
- }
-
- /**
- * Save this MatrixUser to the database.
- */
- save() {
- return this.app.putUser(this)
- }
-}
-
-module.exports = MatrixUser
diff --git a/src/portal.js b/src/portal.js
deleted file mode 100644
index 6f35665d..00000000
--- a/src/portal.js
+++ /dev/null
@@ -1,902 +0,0 @@
-// mautrix-telegram - A Matrix-Telegram puppeting bridge
-// Copyright (C) 2017 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-const md5 = require("md5")
-const TelegramPeer = require("./telegram-peer")
-const formatter = require("./formatter")
-
-/**
- * Portal represents a portal from a Matrix room to a Telegram chat.
- */
-class Portal {
- constructor(app, roomID, peer) {
- this.app = app
- this.type = "portal"
-
- this.roomID = roomID
- this.peer = peer
- this.accessHashes = new Map()
- // deduplicate duplication caused by telegram-mtproto bugs
- this.lastMessageIDs = new Map()
- // deduplicate duplication caused by multiple users
- this.messageHashes = []
- }
-
- /**
- * Get the peer ID of this portal.
- *
- * @returns {number} The ID of the peer of the Telegram side of this portal.
- */
- get id() {
- return this.peer.id
- }
-
- /**
- * Get the receiver ID of this portal. Only applicable for private chat portals.
- *
- * @returns {number} The ID of the receiving user of this portal.
- */
- get receiverID() {
- return this.peer.receiverID
- }
-
- /**
- * Convert a database entry into a Portal.
- *
- * @param {MautrixTelegram} app The app main class instance.
- * @param {Object} entry The database entry.
- * @returns {Portal} The loaded Portal.
- */
- static fromEntry(app, entry) {
- if (entry.type !== "portal") {
- throw new Error("MatrixUser can only be created from entry type \"portal\"")
- }
-
- const portal = new Portal(app, entry.roomID || entry.data.roomID, TelegramPeer.fromSubentry(entry.data.peer))
- portal.photo = entry.data.photo
- portal.avatarURL = entry.data.avatarURL
- if (portal.peer.type === "channel") {
- portal.accessHashes = new Map(entry.data.accessHashes)
- }
- return portal
- }
-
- /**
- * Synchronize the user list of this portal.
- *
- * @param {TelegramPuppet} telegramPOV The Telegram account whose point of view the data is/should be fetched from.
- * @param {UserFull[]} [users] The list of {@link https://tjhorner.com/tl-schema/type/UserFull user info}
- * objects.
- * @returns {boolean} Whether or not syncing was successful. It can only be unsuccessful if the
- * user list was not provided and an access hash was not found for the given
- * Telegram user.
- */
- async syncTelegramUsers(telegramPOV, users) {
- if (!users) {
- if (!await this.loadAccessHash(telegramPOV)) {
- return false
- }
- const data = await this.peer.getInfo(telegramPOV)
- users = data.users
- }
- for (const userData of users) {
- const user = await this.app.getTelegramUser(userData.id)
- // We don't want to update avatars here, as it would likely cause a flood error
- await user.updateInfo(telegramPOV, userData, { updateAvatar: false })
- await user.intent.join(this.roomID)
- }
- return true
- }
-
- /**
- * Copy a photo from Telegram to Matrix.
- *
- * @param {TelegramPuppet} telegramPOV The Telegram account whose point of view the image should be downloaded from.
- * @param {TelegramUser} sender The user who sent the photo.
- * @param {Photo} photo The Telegram {@link https://tjhorner.com/tl-schema/type/Photo Photo} object.
- * @returns {Object} The uploaded Matrix photo object.
- */
- async copyTelegramPhoto(telegramPOV, sender, photo) {
- const size = photo.sizes.slice(-1)[0]
- const uploaded = await this.copyTelegramFile(telegramPOV, sender, size.location, photo.id)
- uploaded.info.h = size.h
- uploaded.info.w = size.w
- uploaded.info.size = size.size
- uploaded.info.orientation = 0
- return uploaded
- }
-
-
- /**
- * Copy a file from Telegram to Matrix.
- *
- * @param {TelegramPuppet} telegramPOV The Telegram account whose point of view the file should be downloaded from.
- * @param {TelegramUser} sender The user who sent the file.
- * @param {FileLocation} location The Telegram {@link https://tjhorner.com/tl-schema/type/FileLocation
- * FileLocation}.
- * @returns {Object} The uploaded Matrix file object.
- */
- async copyTelegramFile(telegramPOV, sender, location, id) {
- id = id || location.id
- const file = await telegramPOV.getFile(location)
- const uploaded = await sender.intent.getClient().uploadContent({
- stream: file.buffer,
- name: `${id}.${file.extension}`,
- type: file.mimetype,
- }, { rawResponse: false })
- uploaded.matrixtype = file.matrixtype
- uploaded.info = {
- mimetype: file.mimetype,
- size: location.size,
- }
- return uploaded
- }
-
- /**
- * Update the avatar of this portal to the given photo.
- *
- * @param {TelegramPuppet} telegramPOV The Telegram account whose point of view the avatar should be downloaded
- * from, if necessary.
- * @param {ChatPhoto} photo The Telegram {@link https://tjhorner.com/tl-schema/type/ChatPhoto ChatPhoto}
- * object.
- * @returns {boolean} Whether or not the photo was updated.
- */
- async updateAvatar(telegramPOV, photo) {
- if (!photo || this.peer.type === "user") {
- return false
- }
-
- if (this.photo && this.avatarURL &&
- this.photo.dc_id === photo.dc_id &&
- this.photo.volume_id === photo.volume_id &&
- this.photo.local_id === photo.local_id) {
- return false
- }
-
- const file = await telegramPOV.getFile(photo)
- const name = `${photo.volume_id}_${photo.local_id}.${file.extension}`
-
- const uploaded = await this.app.botIntent.getClient().uploadContent({
- stream: file.buffer,
- name,
- type: file.mimetype,
- }, { rawResponse: false })
-
- this.avatarURL = uploaded.content_uri
- this.photo = {
- dc_id: photo.dc_id,
- volume_id: photo.volume_id,
- local_id: photo.local_id,
- }
-
- await this.app.botIntent.setRoomAvatar(this.roomID, this.avatarURL)
- return true
- }
-
- /**
- * Load the access hash for the given puppet.
- *
- * @param {TelegramPuppet} telegramPOV The puppet whose access hash to load.
- * @returns {boolean} As specified by {@link TelegramPeer#loadAccessHash(app, telegramPOV)}.
- */
- loadAccessHash(telegramPOV) {
- return this.peer.loadAccessHash(this.app, telegramPOV, { portal: this })
- }
-
- /**
- * Handle a Telegram typing event.
- *
- * @param {Object} evt The custom event object.
- * @param {number} evt.from The ID of the Telegram user who is typing.
- * @param {TelegramPeer} evt.to The peer where the user is typing.
- * @param {TelegramPuppet} evt.source The source where this event was captured.
- */
- async handleTelegramTyping(evt) {
- if (!this.isMatrixRoomCreated()) {
- return
- }
- const typer = await this.app.getTelegramUser(evt.from)
- // The Intent API currently doesn't allow you to set the
- // typing timeout. Once it does, we should set it to ~5.5s
- // as Telegram resends typing notifications every 5 seconds.
- typer.intent.sendTyping(this.roomID, true/*, 5500*/)
- }
-
- /**
- * Add a Telegram user to this room.
- *
- * This makes the Matrix puppet of that Telegram user join this room. If the Telegram user is also a puppet
- * controlled by a Matrix user, that Matrix user is invited as well.
- *
- * @param {number} userID The Telegram ID of the user to add.
- */
- async addUser(userID) {
- const matrixUser = await this.app.getMatrixUserByTelegramID(userID)
- if (matrixUser) {
- matrixUser.join(this)
- this.inviteMatrix(matrixUser.userID)
- }
- const telegramUser = await this.app.getTelegramUser(userID)
- await telegramUser.intent.join(this.roomID)
- }
-
- /**
- * Remove a Telegram user from this room.
- *
- * This makes the Matrix puppet of the given Telegram user leave this room. If the Telegram user is also a puppet
- * controlled by a Matrix user, that Matrix user is kicked with the message "Left Telegram chat".
- *
- * @param {number} userID The Telegram ID of the user to remove.
- */
- async deleteUser(userID) {
- const matrixUser = await this.app.getMatrixUserByTelegramID(userID)
- if (matrixUser) {
- matrixUser.leave(this)
- this.kickMatrix(matrixUser.userID, "Left Telegram chat")
- }
- const telegramUser = await this.app.getTelegramUser(userID)
- telegramUser.intent.leave(this.roomID)
- }
-
- /**
- * Handle a Telegram service message event.
- *
- * @param {Object} evt The custom event object.
- * @param {number} evt.from The ID of the Telegram user who caused the service message.
- * @param {TelegramPeer} evt.to The peer to which the message was sent.
- * @param {TelegramPuppet} evt.source The source where this event was captured.
- * @param {MessageAction} evt.action The Telegram {@link https://tjhorner.com/tl-schema/type/MessageAction
- * MessageAction} object.
- */
- async handleTelegramServiceMessage(evt) {
- if (!this.isMatrixRoomCreated()) {
- if (evt.action._ === "messageActionChatDeleteUser") {
- // We don't care about user deletions on chats without portals
- return
- }
- this.app.debug("magenta", "Service message received, creating room for", evt.to.id)
- await this.createMatrixRoom(evt.source, { invite: [evt.source.matrixUser.userID] })
- return
- }
- if (evt.id) {
- const last = this.lastMessageIDs.get(evt.source.userID)
- if (last && evt.id <= last) {
- this.app.debug(`Received old/duplicate message with ID ${evt.id} (latest ID: ${last})`)
- return
- }
- }
- this.lastMessageIDs.set(evt.source.userID, evt.id)
- switch (evt.action._) {
- case "messageActionChatCreate":
- // Portal gets created at beginning if it doesn't exist
- // Falls through to invite everyone in initial user list
- case "messageActionChatAddUser":
- for (const userID of evt.action.users) {
- await this.addUser(userID)
- }
- break
- case "messageActionChatJoinedByLink":
- await this.addUser(evt.from)
- break
- case "messageActionChannelCreate":
- // Portal gets created at beginning if it doesn't exist
- // Channels don't send initial user lists 3:<
- break
- case "messageActionChatMigrateTo":
- this.peer.id = evt.action.channel_id
- this.peer.type = "channel"
- const accessHash = await this.peer.fetchAccessHashFromServer(evt.source)
- if (!accessHash) {
- console.error("Failed to fetch access hash for mirgrated channel!")
- break
- }
- this.accessHashes.set(evt.source.userID, accessHash)
- await this.save()
- const sender = await this.app.getTelegramUser(evt.from)
- await sender.sendEmote(this.roomID, "upgraded this group to a supergroup.")
- break
- case "messageActionChatDeleteUser":
- await this.deleteUser(evt.action.user_id)
- break
- case "messageActionChatEditPhoto":
- const sizes = evt.action.photo.sizes
- let largestSize = sizes[0]
- let largestSizePixels = largestSize.w * largestSize.h
- for (const size of sizes) {
- const pixels = size.w * size.h
- if (pixels > largestSizePixels) {
- largestSizePixels = pixels
- largestSize = size
- }
- }
- // TODO once permissions are synced, make the avatar change event come from the user who changed the avatar
- await this.updateAvatar(evt.source, largestSize.location)
- break
- case "messageActionChatEditTitle":
- this.peer.title = evt.action.title
- await this.save()
- const intent = await this.getMainIntent()
- await intent.setRoomName(this.roomID, this.peer.title)
- break
- default:
- this.app.warn("Unhandled service message of type", evt.action._, "from", evt.from)
- 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
- }
-
- /**
- * @typedef PortalMessage A portal message event.
- *
- * @property {string} [text]
- * @property {string} [caption]
- *
- * @property {number} from
- * @property {number} [fwdFrom]
- *
- * @property {Object} to
- * @property {number} to.id
- *
- * @property {Object} [geo]
- * @property {number} geo.lat
- * @property {number} geo.long
- *
- * @property {Object} [document]
- * @property {number} document.id
- * @property {Object} [photo]
- * @property {number} photo.id
- */
-
- /**
- * Get a deduplication hash of the given event. The hash is formed of the text or caption, source, forward source
- * and target. For documents and photos, the file ID is included and for locations the longitude and latitude are
- * included.
- *
- * @param {PortalMessage} evt The event.
- * @returns {string} An md5 hash of the data.
- */
- hash(evt) {
- let base = (evt.text || evt.caption) + evt.from + evt.fwdFrom + evt.to.id
- if (evt.geo) {
- base += evt.geo.lat
- base += evt.geo.long
- } else if (evt.document) {
- base += evt.document.id
- } else if (evt.photo) {
- base += evt.photo.id
- }
- return md5(base)
- }
-
- /**
- * Hash the given event and check if it has been recently handled.
- *
- * @param {PortalMessage} evt The event.
- * @returns {boolean} Whether or not the event has been recently handled.
- */
- deduplicate(evt) {
- const hashed = this.hash(evt)
- if (this.messageHashes.includes(hashed)) {
- return true
- }
- this.messageHashes.unshift(hashed)
- if (this.messageHashes.length > 20) {
- this.messageHashes.length = 20
- }
- return false
- }
-
- /**
- * Handle a Telegram service message event.
- *
- * @param {Object} evt The custom event object.
- * @param {number} evt.from The ID of the Telegram user who sent the message.
- * @param {number} evt.fwdFrom The ID of the Telegram user who originally sent the message.
- * @param {TelegramPeer} evt.to The peer to which the message was sent.
- * @param {TelegramPuppet} evt.source The source where this event was captured.
- * @param {string} evt.text The text in the message.
- * @param {string} [evt.caption] The image/file caption.
- * @param {MessageEntity[]} [evt.entities] The Telegram {@link https://tjhorner.com/tl-schema/type/MessageEntity
- * formatting entities} in the message.
- * @param {messageMediaPhoto} [evt.photo] The Telegram {@link https://tjhorner.com/tl-schema/constructor/messageMediaPhoto Photo} attached to the message.
- * @param {messageMediaDocument} [evt.document] The Telegram {@link https://tjhorner.com/tl-schema/constructor/messageMediaDocument Document} attached to the message.
- * @param {messageMediaGeo} [evt.geo] The Telegram {@link https://tjhorner.com/tl-schema/constructor/messageMediaGeo Location} attached to the message.
- */
- async handleTelegramMessage(evt) {
- if (!this.isMatrixRoomCreated()) {
- try {
- const result = await this.createMatrixRoom(evt.source, { invite: [evt.source.matrixUser.userID] })
- if (!result.roomID) {
- return
- }
- } catch (err) {
- console.error("Error creating room:", err)
- console.error(err.stack)
- return
- }
- }
-
- if (this.deduplicate(evt)) {
- return
- }
-
- const sender = await this.app.getTelegramUser(evt.from)
- 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
- }
- }
-
- // TODO display forwards (evt.fwdFrom)
-
- if (evt.text && evt.text.length > 0) {
- if (evt.entities) {
- evt.html = formatter.telegramToMatrix(evt.text, evt.entities, this.app)
- sender.sendHTML(this.roomID, evt.html)
- } else {
- sender.sendText(this.roomID, evt.text)
- }
- }
-
- if (evt.photo) {
- const photo = await this.copyTelegramPhoto(evt.source, sender, evt.photo)
- photo.name = evt.caption || "Uploaded photo"
- sender.sendFile(this.roomID, photo)
- } else if (evt.document) {
- // TODO handle stickers better
- const file = await this.copyTelegramFile(evt.source, sender, evt.document)
- if (evt.caption) {
- file.name = evt.caption
- } else if (file.matrixtype === "m.audio") {
- file.name = "Uploaded audio"
- } else if (file.matrixtype === "m.video") {
- file.name = "Uploaded video"
- } else {
- file.name = "Uploaded document"
- }
- sender.sendFile(this.roomID, file)
- } else if (evt.geo) {
- sender.sendLocation(this.roomID, evt.geo)
- }
- }
-
- /**
- * Handle a Matrix event.
- *
- * @param {MatrixUser} sender The user who sent the message.
- * @param {Object} evt The {@link https://matrix.org/docs/spec/client_server/r0.3.0.html#event-structure
- * Matrix event}.
- */
- async handleMatrixEvent(sender, evt) {
- await this.loadAccessHash(sender.telegramPuppet)
- switch (evt.content.msgtype) {
- case "m.text":
- const { message, entities } = formatter.matrixToTelegram(
- evt.content.formatted_body || evt.content.body,
- evt.content.format === "org.matrix.custom.html",
- this.app)
- this.deduplicate({
- text: message,
- date: Math.round(Date.now() / 1000),
- from: sender.telegramPuppet.userID,
- fwdFrom: 0,
- to: {
- id: this.peer.id,
- },
- })
- await sender.telegramPuppet.sendMessage(this.peer, message, entities)
- break
- case "m.video":
- case "m.audio":
- case "m.file":
- // TODO upload document
- //break
- case "m.image":
- const intent = await this.getMainIntent()
- await intent.sendMessage(this.roomID, {
- msgtype: "m.notice",
- body: "Sending files is not yet supported.",
- })
- break
- case "m.location":
- const [, lat, long] = /geo:([-]?[0-9]+\.[0-9]+)+,([-]?[0-9]+\.[0-9]+)/.exec()
- this.deduplicate({
- text: message,
- date: Math.round(Date.now() / 1000),
- from: sender.telegramPuppet.userID,
- fwdFrom: 0,
- to: {
- id: this.peer.id,
- },
- geo: { lat, long },
- })
- await sender.telegramPuppet.sendMedia(this.peer, {
- _: "inputMediaGeoPoint",
- geo_point: {
- _: "inputGeoPoint",
- lat: +lat,
- long: +long,
- },
- })
- break
- default:
- this.app.warn("Unhandled event:", JSON.stringify(evt, "", " "))
- }
- }
-
- /**
- * @returns {boolean} Whether or not a Matrix room has been created for this Portal.
- */
- isMatrixRoomCreated() {
- return !!this.roomID
- }
-
- /**
- * Get the primary intent object for this Portal.
- *
- * For groups and channels, this is always the AS bot intent.
- * For private chats, it is the intent of the other user.
- *
- * @returns {Intent} The primary intent.
- */
- async getMainIntent() {
- return this.peer.type === "user"
- ? (await this.app.getTelegramUser(this.peer.id)).intent
- : this.app.botIntent
- }
-
- async inviteTelegram(telegramPOV, user) {
- if (this.peer.type === "chat") {
- const updates = await telegramPOV.client("messages.addChatUser", {
- chat_id: this.peer.id,
- user_id: user.toPeer(telegramPOV).toInputObject(),
- fwd_limit: 50,
- })
- 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()],
- })
- this.app.debug("green", "Channel invite result:", JSON.stringify(updates, "", " "))
- } else {
- throw new Error(`Can't invite user to peer type ${this.peer.type}`)
- }
- }
-
- async kickTelegram(telegramPOV, user) {
- let updates
- if (this.peer.type === "chat") {
- updates = await telegramPOV.client("messages.deleteChatUser", {
- chat_id: this.peer.id,
- user_id: user.toPeer(telegramPOV).toInputObject(),
- })
- } else if (this.peer.type === "channel") {
- this.loadAccessHash(telegramPOV)
- updates = await telegramPOV.client("channels.kickFromChannel", {
- channel: this.peer.toInputObject(),
- user_id: user.toPeer(telegramPOV).toInputObject(),
- kicked: true,
- })
- } else {
- throw new Error(`Can't invite user to peer type ${this.peer.type}`)
- }
- await telegramPOV.handleUpdate(updates)
- }
-
- /**
- * Invite one or more Matrix users to this Portal.
- *
- * @param {string[]|string} users The MXID or list of MXIDs to invite.
- */
- async inviteMatrix(users) {
- const intent = await this.getMainIntent()
- // TODO check membership before inviting?
- if (Array.isArray(users)) {
- for (const userID of users) {
- if (typeof userID === "string") {
- 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") {
- 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)
- }
- }
- }
- }
-
- /**
- * Kick one or more Matrix users from this Portal.
- *
- * @param {string[]|string} users The MXID or list of MXIDs to kick.
- * @param {string} reason The reason for kicking the user(s).
- */
- async kickMatrix(users, reason) {
- const intent = await this.getMainIntent()
- if (Array.isArray(users)) {
- for (const userID of users) {
- if (typeof userID === "string") {
- intent.kick(this.roomID, users, reason)
- }
- }
- } else if (typeof users === "string") {
- intent.kick(this.roomID, users, reason)
- }
- }
-
- async createTelegramChat(telegramPOV, title) {
- const members = await this.app.getRoomMembers(this.roomID)
- const telegramInviteIDs = []
- const asBotID = this.app.bot.getUserId()
- for (const member of members) {
- if (member === asBotID) {
- continue
- }
- const user = await this.app.getMatrixUser(member)
- if (user._telegramPuppet) {
- telegramInviteIDs.push(user.telegramPuppet.userID)
- }
-
- const match = this.app.usernameRegex.exec(member)
- if (!match || match.length < 2) {
- continue
- }
- telegramInviteIDs.push(+match[1])
- }
- if (telegramInviteIDs.length < 2) {
- // TODO once we have the option for a bot, this error will need to be changed.
- throw new Error("Not enough users")
- }
-
- const telegramInvites = []
- for (const userID of telegramInviteIDs) {
- const user = await this.app.getTelegramUser(userID, { createIfNotFound: false })
- if (!user) {
- continue
- }
- telegramInvites.push(user.toPeer(telegramPOV).toInputObject())
- }
-
- const createUpdates = await telegramPOV.client("messages.createChat", {
- title,
- users: telegramInvites,
- })
- const chat = createUpdates.chats[0]
- this.peer = new TelegramPeer("chat", chat.id, { title })
- await this.save()
- }
-
- async upgradeTelegramChat(telegramPOV) {
- if (this.peer.type !== "chat") {
- throw new Error("Can't upgrade non-chat portal.")
- }
- const updates = await telegramPOV.client("messages.migrateChat", {
- chat_id: this.id,
- })
- await telegramPOV.handleUpdate(updates)
- }
-
- /**
- * Create a Matrix room for this portal.
- *
- * @param {TelegramPuppet} telegramPOV
- * @param {string|string[] invite
- * @param {boolean} inviteEvenIfNotCreated
- * @returns {{created: boolean, roomID: string}}
- */
- async createMatrixRoom(telegramPOV, { invite = [], inviteEvenIfNotCreated = true } = {}) {
- if (this.roomID) {
- if (invite && inviteEvenIfNotCreated) {
- await this.inviteMatrix(invite)
- }
- return {
- created: false,
- roomID: this.roomID,
- }
- }
- if (this.creatingMatrixRoom) {
- await new Promise(resolve => setTimeout(resolve, 1000))
- return {
- created: false,
- roomID: this.roomID,
- }
- }
- this.creatingMatrixRoom = true
-
- if (!await this.loadAccessHash(telegramPOV)) {
- this.creatingMatrixRoom = false
- throw new Error(`Failed to load access hash for ${this.peer.type} ${this.peer.username || this.peer.id}.`)
- }
-
- let room, info, users
- try {
- ({ info, users } = await this.peer.getInfo(telegramPOV))
- if (this.peer.type === "chat") {
- room = await this.app.botIntent.createRoom({
- options: {
- 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)
- : undefined,
- 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: this.peer.id === this.peer.receiverID
- ? "Saved Messages (Telegram)"
- : undefined, //user.getDisplayName(),
- topic: "Telegram private chat",
- visibility: "private",
- invite,
- },
- })
- } else {
- this.creatingMatrixRoom = false
- throw new Error(`Unrecognized peer type: ${this.peer.type}`)
- }
- } catch (err) {
- this.creatingMatrixRoom = false
- throw err instanceof Error ? err : new Error(err)
- }
-
- this.roomID = room.room_id
- this.creatingMatrixRoom = false
- this.app.portalsByRoomID.set(this.roomID, this)
- await this.save()
- if (this.peer.type !== "user") {
- try {
- await this.syncTelegramUsers(telegramPOV, users)
- if (info.photo && info.photo.photo_big) {
- await this.updateAvatar(telegramPOV, info.photo.photo_big)
- }
- } catch (err) {
- console.error(err)
- if (err instanceof Error) {
- console.error(err.stack)
- }
- }
- }
- return {
- created: true,
- roomID: this.roomID,
- }
- }
-
- async updateInfo(telegramPOV, dialog) {
- if (!dialog) {
- 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")
- }
- dialog = user
- }
- 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
- }
- }
- if (this.peer.type === "user") {
- const user = await this.app.getTelegramUser(this.peer.id)
- await user.updateInfo(telegramPOV, dialog)
- } else if (dialog.photo && dialog.photo.photo_big) {
- changed = await this.updateAvatar(telegramPOV, dialog.photo.photo_big) || changed
- }
- changed = this.peer.updateInfo(dialog) || changed
- if (changed) {
- this.save()
- }
- return changed
- }
-
- /**
- * Convert this Portal into a database entry.
- *
- * @returns {Object} A room store database entry.
- */
- toEntry() {
- return {
- type: this.type,
- id: this.id,
- receiverID: this.receiverID,
- roomID: this.roomID,
- data: {
- peer: this.peer.toSubentry(),
- photo: this.photo,
- avatarURL: this.avatarURL,
- accessHashes: this.peer.type === "channel"
- ? Array.from(this.accessHashes)
- : undefined,
- },
- }
- }
-
- /**
- * Save this Portal to the database.
- */
- save() {
- return this.app.putRoom(this)
- }
-}
-
-module.exports = Portal
diff --git a/src/telegram-peer.js b/src/telegram-peer.js
deleted file mode 100644
index 2379670b..00000000
--- a/src/telegram-peer.js
+++ /dev/null
@@ -1,275 +0,0 @@
-// mautrix-telegram - A Matrix-Telegram puppeting bridge
-// Copyright (C) 2017 Tulir Asokan
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-/**
- * TelegramPeer represents some Telegram entity that can be messaged.
- *
- * The possible peer types are chat (groups), channel (includes supergroups) and user.
- */
-class TelegramPeer {
- constructor(type, id, { accessHash, receiverID, username, title } = {}) {
- this.type = type
- this.id = id
- this.accessHash = accessHash
- this.receiverID = receiverID
- this.username = username
- this.title = title
- }
-
- /**
- * Create a TelegramPeer based on peer data received from Telegram.
- *
- * @param {Object} peer The data received from Telegram.
- * @param {number} sender The user ID of the other person, in case the peer is an user referring to the receiver.
- * @param {number} receiverID The user ID of the receiver (in case peer type is {@code user})
- * @returns {TelegramPeer}
- */
- static fromTelegramData(peer, sender, receiverID) {
- switch (peer._) {
- case "peerChat":
- return new TelegramPeer("chat", peer.chat_id)
- case "peerUser":
- const args = {
- accessHash: peer.access_hash,
- receiverID,
- }
- if (sender === receiverID && peer.user_id !== receiverID) {
- return new TelegramPeer("user", peer.user_id, args)
- }
- return new TelegramPeer("user", sender, args)
- case "peerChannel":
- return new TelegramPeer("channel", peer.channel_id, {
- accessHash: peer.access_hash,
- })
- default:
- throw new Error(`Unrecognized peer type ${peer._}`)
- }
- }
-
- /**
- * Load the access hash for a specific puppeted Telegram user from the channel portal or TelegramUser info.
- *
- * @param {MautrixTelegram} app The app main class instance.
- * @param {TelegramPuppet} telegramPOV The puppeted Telegram user for whom the access hash is needed.
- * @param {Portal} [portal] Optional channel {@link Portal} instance to avoid calling {@link app#getPortalByPeer(peer)}.
- * Only used if {@link #type} is {@code user}.
- * @param {TelegramUser} [user] Optional {@link TelegramUser} instance to avoid calling {@link app#getTelegramUser(id)}.
- * Only used if {@link #type} is {@code channel}.
- * @returns {boolean} Whether or not the access hash was found and loaded.
- */
- async loadAccessHash(app, telegramPOV, { portal, user } = {}) {
- if (this.type === "chat") {
- return true
- } else if (this.type === "user") {
- user = user || await app.getTelegramUser(this.id)
- if (user.accessHashes.has(telegramPOV.userID)) {
- this.accessHash = user.accessHashes.get(telegramPOV.userID)
- return true
- }
- return false
- } else if (this.type === "channel") {
- portal = portal || await app.getPortalByPeer(this)
- if (portal.accessHashes.has(telegramPOV.userID)) {
- this.accessHash = portal.accessHashes.get(telegramPOV.userID)
- return true
- }
- return false
- }
- return false
- }
-
- /**
- * Update info based on a Telegram dialog.
- *
- * @param dialog The dialog data sent by Telegram.
- * @returns {boolean} Whether or not something was changed.
- */
- async updateInfo(dialog) {
- let changed = false
- if (dialog.username && (this.type === "channel" || this.type === "user")) {
- if (this.username !== dialog.username) {
- this.username = dialog.username
- changed = true
- }
- }
- if (dialog.title && this.title !== dialog.title) {
- this.title = dialog.title
- changed = true
- }
- return changed
- }
-
- async fetchAccessHashFromServer(telegramPOV) {
- const data = await this.getInfoFromDialogs(telegramPOV)
- if (!data) {
- return undefined
- }
- this.accessHash = data.access_hash
- return this.accessHash
- }
-
- async getInfoFromDialogs(telegramPOV) {
- const dialogs = await telegramPOV.client("messages.getDialogs", {})
- if (this.type === "user") {
- for (const user of dialogs.users) {
- if (user.id === this.id) {
- return user
- }
- }
- } else {
- for (const chat of dialogs.chats) {
- if (chat.id === this.id) {
- return chat
- }
- }
- }
- return undefined
- }
-
- /**
- * Get info about this peer from the Telegram servers.
- *
- * @param {TelegramPuppet} telegramPOV The Telegram user whose point of view the data should be fetched from.
- * @returns {{info: Object, users: Array