diff --git a/example-config.yaml b/example-config.yaml index 18546df6..9b82f22e 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -25,6 +25,10 @@ bridge: command_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 + # 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. whitelist: @@ -33,8 +37,10 @@ bridge: # Telegram app config. Generate your own keys at https://my.telegram.org/apps 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 - api_config: - api_id: 12345 - api_hash: tjyd5yge35lbodk1xwzw2jstp90k55qz + api_id: 12345 + api_hash: tjyd5yge35lbodk1xwzw2jstp90k55qz diff --git a/package.json b/package.json index d76b2544..7e35e932 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "url": "https://github.com/tulir/mautrix-telegram.git" }, "dependencies": { - "telegram-mtproto": "3.2.x", + "telegram-mtproto": "3.x.x", "matrix-js-sdk": "0.x.x", "matrix-appservice-bridge": "1.x.x", "commander": "2.11.x", diff --git a/src/app.js b/src/app.js index 804a4ffe..b83128b7 100644 --- a/src/app.js +++ b/src/app.js @@ -14,8 +14,10 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . const {Bridge} = require("matrix-appservice-bridge") +const crypto = require("crypto") const commands = require("./commands") const MatrixUser = require("./matrix-user") +const YAML = require("yamljs") class MautrixTelegram { constructor(config) { @@ -40,10 +42,18 @@ class MautrixTelegram { }) } - run() { + async run() { console.log("Appservice listening on port %s", this.config.appservice.port) - this.bridge.run(this.config.appservice.port, {}) - //this.botIntent.setDisplayName(this.config.bridge.bot_displayname) + await this.bridge.run(this.config.appservice.port, {}) + const userEntries = await this.bridge.getUserStore().select({ + type: "matrix", + }) + for (const entry of userEntries) { + const user = MatrixUser.fromEntry(this, entry) + this.matrixUsersByID.set(entry.id, user) + } + // .then(() => + // this.botIntent.setDisplayName(this.config.bridge.bot_displayname)) } get bot() { @@ -57,7 +67,6 @@ class MautrixTelegram { getMatrixUser(id) { let user = this.matrixUsersByID.get(id) if (user) { - console.log(id, "found in cache") return Promise.resolve(user) } @@ -67,16 +76,13 @@ class MautrixTelegram { }).then(entries => { this.matrixUsersByID.get(id) if (user) { - console.log(id, "found in cache (after race)") return Promise.resolve(user) } if (entries.length) { user = MatrixUser.fromEntry(this, entries[0]) - console.log(id, "loaded from database") } else { user = new MatrixUser(this, id) - console.log(id, "created") } this.matrixUsersByID.set(id, user) return user @@ -85,10 +91,11 @@ class MautrixTelegram { putUser(user) { const entry = user.toEntry() - return this.bridge.getUserStore().upsert({ + const query = { type: entry.type, id: entry.id, - }, entry) + } + return this.bridge.getUserStore().upsert(query, entry) } handleMatrixEvent(evt) { @@ -124,7 +131,8 @@ class MautrixTelegram { commands.run(user, command, args, reply => this.botIntent.sendText( evt.room_id, - reply.replace("$cmdprefix", cmdprefix))) + reply.replace("$cmdprefix", cmdprefix)), + this) }) return } @@ -146,6 +154,25 @@ class MautrixTelegram { } return false } + + encrypt(value) { + var cipher = crypto.createCipher("aes-256-gcm", this.config.bridge.auth_key_password); + var ret = cipher.update(Buffer.from(value), "hex", "base64"); + ret += cipher.final("base64"); + + return [ret, cipher.getAuthTag().toString("base64")]; + } + + decrypt(value) { + if(!value) return value; + + var decipher = crypto.createDecipher("aes-256-gcm", this.config.bridge.auth_key_password); + decipher.setAuthTag(new Buffer(value[1], "base64")); + var ret = decipher.update(value[0], "base64", "hex"); + ret += decipher.final("hex"); + + return ret; + }; } module.exports = MautrixTelegram diff --git a/src/commands.js b/src/commands.js index 0042658c..c6bf1700 100644 --- a/src/commands.js +++ b/src/commands.js @@ -15,22 +15,9 @@ // along with this program. If not, see . const makePasswordHash = require("telegram-mtproto").plugins.makePasswordHash -class Command { - constructor(description, usage, func) { - this.description = description - this.usage = usage - this.func = func - } - - run(app, roomID, args) { - this.func(args, message => - app.botIntent.sendText(roomID, message)) - } -} - const commands = {} -function run(sender, command, args, reply) { +function run(sender, command, args, reply, app) { if (sender.commandStatus) { if (command === "cancel") { sender.commandStatus = undefined @@ -38,14 +25,15 @@ function run(sender, command, args, reply) { return } args.unshift(command) - sender.commandStatus.next(sender, args, reply) + sender.commandStatus.next(sender, args, reply, app) return } command = this.commands[command] if (!command) { reply("Unknown command. Try \"$cmdprefix help\" for help.") + return } - command(sender, args, reply) + command(sender, args, reply, app) } commands.cancel = () => "Nothing to cancel." @@ -60,7 +48,7 @@ const enterPassword = (sender, args, reply) => { sender.checkPassword(hash) .then(() => { // TODO show who the user logged in as - reply(`Logged in successfully.`) + reply(`Logged in successfully as @${sender.telegramPuppet.getDisplayName()}.`) sender.commandStatus = undefined }, err => { reply(`Login failed: ${err}`) @@ -78,7 +66,7 @@ const enterCode = (sender, args, reply) => { .then(data => { if (data.status === "ok") { // TODO show who the user logged in as - reply(`Logged in successfully.`) + reply(`Logged in successfully as @${sender.telegramPuppet.getDisplayName()}.`) sender.commandStatus = undefined } else if (data.status === "need-password") { reply(`You have two-factor authentication enabled. Password hint: ${data.hint} \nEnter your password using "$cmdprefix "`) @@ -116,6 +104,28 @@ commands.login = (sender, args, reply) => { }) } +commands.api = async (sender, args, reply, app) => { + if (!app.config.telegram.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:\n${JSON.stringify(response, "", " ")}`) + } catch (err) { + reply(`API call errored. Response:\n${JSON.stringify(err, "", " ")}`) + } +} + commands.help = (sender, args, reply) => { reply("Help not yet implemented 3:") } diff --git a/src/matrix-user.js b/src/matrix-user.js index d05addc7..f03fde7b 100644 --- a/src/matrix-user.js +++ b/src/matrix-user.js @@ -42,8 +42,8 @@ class MatrixUser { } toEntry() { - if (this.puppet) { - this.puppetData = this.puppet.toSubentry() + if (this._telegramPuppet) { + this.puppetData = this.telegramPuppet.toSubentry() } return { type: "matrix", @@ -72,32 +72,37 @@ class MatrixUser { throw new Error(message) } - sendTelegramCode(phoneNumber) { + async sendTelegramCode(phoneNumber) { // TODO handle existing login? - - return this.telegramPuppet.sendCode(phoneNumber) - .then(result => { - this.phoneNumber = phoneNumber - this.phoneCodeHash = result.phone_code_hash - this.app.putUser(this) - return result - }, err => this.parseTelegramError(err)) + try { + const result = await this.telegramPuppet.sendCode(phoneNumber) + this.phoneNumber = phoneNumber + this.phoneCodeHash = result.phone_code_hash + await this.saveChanges() + return result + } catch (err) { + return this.parseTelegramError(err) + } } - signInToTelegram(phoneCode) { + async signInToTelegram(phoneCode) { if (!this.phoneNumber) throw new Error("Phone number not set") if (!this.phoneCodeHash) throw new Error("Phone code not sent") - return this.telegramPuppet.signIn(this.phoneNumber, this.phoneCodeHash, phoneCode) - .then(result => { - this.phoneCodeHash = undefined - return this.app.putUser(this).then(() => result) - }) + const result = await this.telegramPuppet.signIn(this.phoneNumber, this.phoneCodeHash, phoneCode) + this.phoneCodeHash = undefined + await this.saveChanges() + return result } - checkPassword(password_hash) { - return this.telegramPuppet.checkPassword(password_hash) - .then(() => this.app.putUser(this)) + async checkPassword(password_hash) { + const result = await this.telegramPuppet.checkPassword(password_hash) + await this.saveChanges() + return result + } + + saveChanges() { + return this.app.putUser(this) } } diff --git a/src/telegram-puppet.js b/src/telegram-puppet.js index c56936ed..eafade3c 100644 --- a/src/telegram-puppet.js +++ b/src/telegram-puppet.js @@ -31,11 +31,46 @@ class TelegramPuppet { this.api_hash = opts.api_hash this.api_id = opts.api_id + this.puppetStorage = { + get: async (key) => { + let value = this.data[key] + /*if (value && key.match(/_auth_key$/)) { + value = this.app.decrypt(value) + }*/ + return value + }, + set: async (key, value) => { + /*if (value && key.match(/_auth_key$/)) { + value = this.app.encrypt(value) + }*/ + + if (this.data[key] === value) return Promise.resolve() + + this.data[key] = value + await this.matrixUser.saveChanges() + }, + remove: async (...keys) => { + keys.forEach((key) => delete this.data[key]) + await this.matrixUser.saveChanges() + }, + clear: async () => { + this.data = {} + await this.matrixUser.saveChanges() + }, + } + this.apiConfig = Object.assign({}, { app_version: pkg.version, lang_code: "en", api_id: opts.api_id, + initConnection : 0x69796de9, + layer: 57, + invokeWithLayer: 0xda9b0d0d, }, opts.api_config) + + if (this.data.dc && this.data[`dc${this.data.dc}_auth_key`]) { + this.listen() + } } static fromSubentry(app, matrixUser, data) { @@ -51,19 +86,17 @@ class TelegramPuppet { toSubentry() { return Object.assign({ - user_id: this.userID + user_id: this.userID, }, this.data) } - get datacenter() { - return { dcID: 1 } - } - get client() { if (!this._client) { + const self = this this._client = telegram.MTProto({ api: this.apiConfig, server: this.serverConfig, + app: { storage: this.puppetStorage }, }) } return this._client @@ -76,40 +109,86 @@ class TelegramPuppet { api_id: this.api_id, api_hash: this.api_hash, }) - } - signIn(phone_number, phone_code_hash, phone_code) { - return this.client("auth.signIn", { - phone_number, phone_code, phone_code_hash - }) - .then( - result => this.signInComplete(result), - err => { - if (err.type !== "SESSION_PASSWORD_NEEDED") { - throw err - } - this.client("account.getPassword", {}).then(data => { - return { - status: "need-password", - hint: data.hint, - salt: data.current_salt - } - }) + async signIn(phone_number, phone_code_hash, phone_code) { + try { + const result = await + this.client("auth.signIn", { + phone_number, phone_code, phone_code_hash, }) + this.signInComplete(result) + } catch (err) { + if (err.message !== "SESSION_PASSWORD_NEEDED") { + throw err + } + const password = await + this.client("account.getPassword", {}) + return { + status: "need-password", + hint: password.hint, + salt: password.current_salt, + } + } } - checkPassword(password_hash) { - return this.client("auth.checkPassword", {password_hash}) - .then((result) => this.signInComplete(result)) + async checkPassword(password_hash) { + const result = await this.client("auth.checkPassword", { password_hash }) + return this.signInComplete(result) + } + + getDisplayName() { + if (this.data.first_name || this.data.last_name) { + return `${this.data.first_name} ${this.data.last_name}` + } else if (this.data.username) { + return this.data.username + } + return this.data.phone_number } signInComplete(data) { this.userID = data.user.id + this.data.username = data.user.username + this.data.first_name = data.user.first_name + this.data.last_name = data.user.last_name + this.data.phone_number = data.user.phone_number + this.matrixUser.saveChanges() + this.listen() return { - status: "ok" + status: "ok", } } + + handleUpdate(data) { + console.log(data) + } + + async listen() { + const client = this.client + client.on("update", data => this.handleUpdate(data)) + if (client.bus) { + client.bus.untypedMessage.observe(data => this.handleUpdate(data)) + } + + try { + console.log("Updating online status...") + //const statusUpdate = await client("account.updateStatus", { offline: false }) + //console.log(statusUpdate) + console.log("Fetching initial state...") + const state = await client("updates.getState", {}) + console.log("Initial state:", state) + } catch (err) { + console.error("Error getting initial state:", err) + } + setInterval(async () => { + try { + const state = client("updates.getState", {}) + console.log("New state received") + } catch (err) { + console.error("Error updating state:", err) + } + }, 5000) + } } module.exports = TelegramPuppet