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