diff --git a/src/app.js b/src/app.js index b83128b7..143f54c9 100644 --- a/src/app.js +++ b/src/app.js @@ -64,6 +64,11 @@ class MautrixTelegram { return this.bridge.getIntent() } + getIntentForTelegramUser(id) { + return this.bridge.getIntentFromLocalpart( + this.config.bridge.username_template.replace("${ID}", id)) + } + getMatrixUser(id) { let user = this.matrixUsersByID.get(id) if (user) { @@ -155,7 +160,7 @@ class MautrixTelegram { return false } - encrypt(value) { + /*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"); @@ -172,7 +177,7 @@ class MautrixTelegram { ret += decipher.final("hex"); return ret; - }; + };*/ } module.exports = MautrixTelegram diff --git a/src/commands.js b/src/commands.js index c6bf1700..fc4bafce 100644 --- a/src/commands.js +++ b/src/commands.js @@ -25,85 +25,110 @@ function run(sender, command, args, reply, app) { return } args.unshift(command) - sender.commandStatus.next(sender, args, reply, app) - return + return sender.commandStatus.next(sender, args, reply, app) } command = this.commands[command] if (!command) { reply("Unknown command. Try \"$cmdprefix help\" for help.") return } - command(sender, args, reply, app) + return command(sender, args, reply, app) } commands.cancel = () => "Nothing to cancel." -const enterPassword = (sender, args, reply) => { +commands.help = (sender, args, reply) => { + reply("Help not yet implemented 3:") +} + + + ///////////////////////////// + // Authentication handlers // +///////////////////////////// + +/** + * Two-factor authentication handler. + */ +const enterPassword = async (sender, args, reply) => { if (args.length === 0) { reply("Usage: $cmdprefix ") return } const hash = makePasswordHash(sender.commandStatus.salt, args[0]) - sender.checkPassword(hash) - .then(() => { - // TODO show who the user logged in as - reply(`Logged in successfully as @${sender.telegramPuppet.getDisplayName()}.`) - sender.commandStatus = undefined - }, err => { - reply(`Login failed: ${err}`) - console.log(err) - }) + try { + await sender.checkPassword(hash) + reply(`Logged in successfully as @${sender.telegramPuppet.getDisplayName()}.`) + sender.commandStatus = undefined + } catch (err) { + reply(`Login failed: ${err}`) + console.log(err) + } } -const enterCode = (sender, args, reply) => { +/* + * Login code send handler. + */ +const enterCode = async (sender, args, reply) => { if (args.length === 0) { reply("Usage: $cmdprefix ") return } - sender.signInToTelegram(args[0]) - .then(data => { - if (data.status === "ok") { - // TODO show who the user logged in as - 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 "`) - sender.commandStatus = { - action: "Two-factor authentication", - next: enterPassword, - salt: data.salt, - } - } else { - reply(`Unexpected sign in response, status=${data.status}`) + try { + const data = await sender.signInToTelegram(args[0]) + if (data.status === "ok") { + // TODO show who the user logged in as + 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 "`) + sender.commandStatus = { + action: "Two-factor authentication", + next: enterPassword, + salt: data.salt, } - }, err => { - reply(`Login failed: ${err}`) - console.log(err) - }) + } else { + reply(`Unexpected sign in response, status=${data.status}`) + } + } catch (err) { + reply(`Login failed: ${err}`) + console.log(err) + } } -commands.login = (sender, args, reply) => { +/* + * Login code request handler. + */ +commands.login = async (sender, args, reply) => { if (args.length === 0) { reply("Usage: $cmdprefix login ") return } - sender.sendTelegramCode(args[0]) - .then(data => { - reply(`Login code sent to ${args[0]}. \nEnter the code using "$cmdprefix "`) - sender.commandStatus = { - action: "Phone code authentication", - next: enterCode, - } - console.log(data) - }, err => { - reply(`Failed to send code: ${err}`) - console.log(err) - }) + try { + const data = await sender.sendTelegramCode(args[0]) + reply(`Login code sent to ${args[0]}.\nEnter the code using "$cmdprefix "`) + sender.commandStatus = { + action: "Phone code authentication", + next: enterCode, + } + console.log(data) + } catch (err) { + reply(`Failed to send code: ${err}`) + console.log(err) + } } + ////////////////////////////// + // General command handlers // +////////////////////////////// + + + //////////////////////////// + // Debug command handlers // +//////////////////////////// + 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.") @@ -126,10 +151,6 @@ commands.api = async (sender, args, reply, app) => { } } -commands.help = (sender, args, reply) => { - reply("Help not yet implemented 3:") -} - module.exports = { commands, run, diff --git a/src/matrix-user.js b/src/matrix-user.js index f03fde7b..5ad43993 100644 --- a/src/matrix-user.js +++ b/src/matrix-user.js @@ -15,6 +15,10 @@ // along with this program. If not, see . const TelegramPuppet = require("./telegram-puppet") +/** + * MatrixUser represents a Matrix user who probably wants to control their + * Telegram account from Matrix. + */ class MatrixUser { constructor(app, userID) { this.app = app @@ -32,8 +36,8 @@ class MatrixUser { } const user = new MatrixUser(app, entry.id) - user.phoneNumber = entry.data.phone_number - user.phoneCodeHash = entry.data.phone_code_hash + user.phoneNumber = entry.data.phoneNumber + user.phoneCodeHash = entry.data.phoneCodeHash if (entry.data.puppet) { user.puppetData = entry.data.puppet user.telegramPuppet @@ -49,8 +53,8 @@ class MatrixUser { type: "matrix", id: this.userID, data: { - phone_number: this.phoneNumber, - phone_code_hash: this.phoneCodeHash, + phoneNumber: this.phoneNumber, + phoneCodeHash: this.phoneCodeHash, puppet: this.puppetData, }, } diff --git a/src/portal.js b/src/portal.js new file mode 100644 index 00000000..43144a9c --- /dev/null +++ b/src/portal.js @@ -0,0 +1,33 @@ +// 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 TelegramPeer = require("./telegram-peer") + +class Portal { + constructor(app, roomID, peer) { + this.app = app + + this.roomID = roomID + this.peer = peer + } + + static fromEntry(app, entry) { + if (entry.type !== "portal") { + throw new Error("MatrixUser can only be created from entry type \"portal\"") + } + + return new Portal(app, entry.data.roomID, TelegramPeer.fromSubentry(entry.data.peer)) + } +} diff --git a/src/telegram-peer.js b/src/telegram-peer.js new file mode 100644 index 00000000..746454f8 --- /dev/null +++ b/src/telegram-peer.js @@ -0,0 +1,89 @@ +// 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 . + +class TelegramPeer { + constructor(type, id, accessHash) { + this.type = type + this.id = id + this.accessHash = accessHash + } + + static fromTelegramData(peer) { + switch(peer._) { + case "peerChat": + return new Peer("chat", peer.chat_id) + case "peerUser": + return new Peer("user", peer.user_id, peer.access_hash) + case "peerChannel": + return new Peer("channel", peer.channel_id, peer.access_hash) + default: + throw new Error(`Unrecognized peer type ${peer._}`) + } + } + + toInputPeer() { + switch(this.type) { + case "chat": + return { + _: "inputPeerChat", + chat_id: this.id, + } + case "user": + return { + _: "inputPeerUser", + user_id: this.id, + access_hash: this.accessHash, + } + case "channel": + return { + _: "inputPeerChannel", + channel_id: this.id, + access_hash: this.accessHash, + } + default: + throw new Error(`Unrecognized peer type ${this.type}`) + } + } + + toInputChannel() { + if (this.type !== "channel") { + throw new Error(`Cannot convert peer of type ${this.type} into an inputChannel`) + } + + return { + _: "inputChannel", + channel_id: this.id, + access_hash: this.accessHash, + } + } + + static fromSubentry(entry) { + const accessHash = entry.accessHash ? new Buffer(entry.accessHash) : undefined + return new Peer(entry.type, entry.id, accessHash) + } + + toSubentry() { + return { + type: this.type, + id: this.id, + accessHash: this.accessHash.toString(), + } + } + + get key() { + return `${this.type} ${this.id}` + } +} diff --git a/src/telegram-puppet.js b/src/telegram-puppet.js index eafade3c..6f904d1a 100644 --- a/src/telegram-puppet.js +++ b/src/telegram-puppet.js @@ -17,33 +17,29 @@ const pkg = require("../package.json") const os = require("os") const telegram = require("telegram-mtproto") +/** + * TelegramPuppet represents a Telegram account being controlled from Matrix. + */ class TelegramPuppet { - constructor(opts) { + constructor(app, {userID, matrixUser, data, api_hash, api_id, server_config, api_config}) { this._client = undefined - this.userID = opts.userID - this.matrixUser = opts.matrixUser - this.data = opts.data + this.userID = userID + this.matrixUser = matrixUser + this.data = data - this.app = opts.app + this.app = app - this.serverConfig = Object.assign({}, opts.server_config) + this.serverConfig = Object.assign({}, server_config) - this.api_hash = opts.api_hash - this.api_id = opts.api_id + this.apiHash = api_hash + this.apiID = 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 @@ -62,11 +58,11 @@ class TelegramPuppet { this.apiConfig = Object.assign({}, { app_version: pkg.version, lang_code: "en", - api_id: opts.api_id, + api_id: api_id, initConnection : 0x69796de9, layer: 57, invokeWithLayer: 0xda9b0d0d, - }, opts.api_config) + }, api_config) if (this.data.dc && this.data[`dc${this.data.dc}_auth_key`]) { this.listen() @@ -74,19 +70,18 @@ class TelegramPuppet { } static fromSubentry(app, matrixUser, data) { - const userID = data.user_id - delete data.user_id - return new TelegramPuppet(Object.assign({ + const userID = data.userID + delete data.userID + return new TelegramPuppet(app, Object.assign({ userID, matrixUser, data, - app, }, app.config.telegram)) } toSubentry() { return Object.assign({ - user_id: this.userID, + userID: this.userID, }, this.data) } @@ -106,8 +101,8 @@ class TelegramPuppet { return this.client("auth.sendCode", { phone_number, current_number: true, - api_id: this.api_id, - api_hash: this.api_hash, + api_id: this.apiID, + api_hash: this.apiHash, }) } @@ -138,8 +133,8 @@ class TelegramPuppet { } getDisplayName() { - if (this.data.first_name || this.data.last_name) { - return `${this.data.first_name} ${this.data.last_name}` + if (this.data.firstName || this.data.lastName) { + return `${this.data.firstName} ${this.data.lastName}` } else if (this.data.username) { return this.data.username } @@ -149,9 +144,9 @@ class TelegramPuppet { 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.data.firstName = data.user.first_name + this.data.lastName = data.user.last_name + this.data.phoneNumber = data.user.phone_number this.matrixUser.saveChanges() this.listen() return { @@ -159,21 +154,39 @@ class TelegramPuppet { } } + onUpdate(update) { + console.log("Update received:", update) + } + handleUpdate(data) { - console.log(data) + switch(data._) { + case "updateShort": + this.onUpdate(data.update) + break + case "updates": + for (const update of data.updates) { + this.onUpdate(update) + } + break + case "updateShortChatMessage": + this.onUpdate(update) + break + default: + console.log("Unrecognized update type:", 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)) + client.bus.untypedMessage.observe(data => this.handleUpdate(data.message)) } try { console.log("Updating online status...") - //const statusUpdate = await client("account.updateStatus", { offline: false }) - //console.log(statusUpdate) + 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) diff --git a/src/telegram-user.js b/src/telegram-user.js new file mode 100644 index 00000000..ea5ffd37 --- /dev/null +++ b/src/telegram-user.js @@ -0,0 +1,155 @@ +// 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 . + +/** + * TelegramUser represents a Telegram user who probably has an + * appservice-managed Matrix account. + */ +class TelegramUser { + constructor(app, id, user) { + this.app = app + this.id = id + this._intent = undefined + if (user) { + this.updateInfo(user) + } + } + + static fromEntry(app, entry) { + if (entry.type !== "remote") { + throw new Error("TelegramUser can only be created from entry type \"remote\"") + } + + const user = new TelegramUser(app, entry.id) + const data = entry.data + user.firstName = data.firstName + user.lastName = data.lastName + user.username = data.username + user.phoneNumber = data.phoneNumber + user.photo = data.photo + user.avatarURL = data.avatarURL + } + + toEntry() { + return { + type: "remote", + id: this.id, + data: { + firstName: this.firstName, + lastName: this.lastName, + username: this.username, + phoneNumber: this.phoneNumber, + photo: this.photo, + avatarURL: this.avatarURL, + }, + } + } + + updateFrom(user) { + let changed = false + if (this.firstName !== user.first_name) { + this.firstName = user.first_name + changed = true + } + if (this.lastName !== user.last_name) { + this.lastName = user.last_name + changed = true + } + return changed + } + + get intent() { + if (!this._intent) { + this._intent = this.app.getIntentForTelegramID(this.id) + } + return this._intent + } + + get mxid() { + return this.intent.client.credentials.userId + } + + getDisplayName() { + if (this.firstName || this.lastName) { + return [this.firstName, this.lastName].filter(s => !!s) + .join(" ") + } else if (this.username) { + return this.username + } else if (this.phoneNumber) { + return this.phoneNumber + } + return this.id + } + + sendText(roomID, text) { + return this.intent.sendText(roomID, text) + } + + sendImage(roomID, opts) { + return this.intent.sendMessage(roomID, { + msgtype: "m.image", + url: opts.content_uri, + body: opts.name, + info: opts.info, + }) + } + + sendSelfStateEvent(roomID, type, content) { + return this.intent.sendStateEvent(roomID, type, this.getMxid(), content) + } + + uploadContent(opts) { + return this.intent.getClient() + .uploadContent({ + stream: opts.stream, + name: opts.name, + type: opts.type, + }, { + rawResponse: false, + }) + } + + async updateAvatarImageFrom(user, puppet) { + if (!user.photo) return Promise.resolve() + + const photo = user.photo.photo_big + 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 Promise.resolve(this.avatarURL) + } + + const file = await puppet.getFile(photo) + const name = `${photo.volume_id}_${photo.local_id}.${file.extension}` + + const uploaded = await this.uploadContent({ + stream: new Buffer(file.bytes), + name: name, + type: file.mimetype, + }) + + this.avatarURL = response.content_uri + this.photo = { + dc_id: photo.dc_id, + volume_id: photo.volume_id, + local_id: photo.local_id, + } + + await this.app.putUser(this) + return this.avatarURL + } +}