Logging in and auth persistence actually works now

This commit is contained in:
Tulir Asokan
2017-11-13 22:32:45 +02:00
parent daeaca8c4f
commit 0639f26a2c
6 changed files with 206 additions and 79 deletions
+9 -3
View File
@@ -25,6 +25,10 @@ bridge:
command_prefix: "!tg" 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. # 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. # You can enter a domain without the localpart to allow all users from that homeserver to use the bridge.
whitelist: whitelist:
@@ -33,8 +37,10 @@ bridge:
# Telegram app config. Generate your own keys at https://my.telegram.org/apps # Telegram app config. Generate your own keys at https://my.telegram.org/apps
telegram: 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: server_config:
dev: false dev: false
api_config: api_id: 12345
api_id: 12345 api_hash: tjyd5yge35lbodk1xwzw2jstp90k55qz
api_hash: tjyd5yge35lbodk1xwzw2jstp90k55qz
+1 -1
View File
@@ -10,7 +10,7 @@
"url": "https://github.com/tulir/mautrix-telegram.git" "url": "https://github.com/tulir/mautrix-telegram.git"
}, },
"dependencies": { "dependencies": {
"telegram-mtproto": "3.2.x", "telegram-mtproto": "3.x.x",
"matrix-js-sdk": "0.x.x", "matrix-js-sdk": "0.x.x",
"matrix-appservice-bridge": "1.x.x", "matrix-appservice-bridge": "1.x.x",
"commander": "2.11.x", "commander": "2.11.x",
+37 -10
View File
@@ -14,8 +14,10 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
const {Bridge} = require("matrix-appservice-bridge") const {Bridge} = require("matrix-appservice-bridge")
const crypto = require("crypto")
const commands = require("./commands") const commands = require("./commands")
const MatrixUser = require("./matrix-user") const MatrixUser = require("./matrix-user")
const YAML = require("yamljs")
class MautrixTelegram { class MautrixTelegram {
constructor(config) { constructor(config) {
@@ -40,10 +42,18 @@ class MautrixTelegram {
}) })
} }
run() { async run() {
console.log("Appservice listening on port %s", this.config.appservice.port) console.log("Appservice listening on port %s", this.config.appservice.port)
this.bridge.run(this.config.appservice.port, {}) await this.bridge.run(this.config.appservice.port, {})
//this.botIntent.setDisplayName(this.config.bridge.bot_displayname) 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() { get bot() {
@@ -57,7 +67,6 @@ class MautrixTelegram {
getMatrixUser(id) { getMatrixUser(id) {
let user = this.matrixUsersByID.get(id) let user = this.matrixUsersByID.get(id)
if (user) { if (user) {
console.log(id, "found in cache")
return Promise.resolve(user) return Promise.resolve(user)
} }
@@ -67,16 +76,13 @@ class MautrixTelegram {
}).then(entries => { }).then(entries => {
this.matrixUsersByID.get(id) this.matrixUsersByID.get(id)
if (user) { if (user) {
console.log(id, "found in cache (after race)")
return Promise.resolve(user) return Promise.resolve(user)
} }
if (entries.length) { if (entries.length) {
user = MatrixUser.fromEntry(this, entries[0]) user = MatrixUser.fromEntry(this, entries[0])
console.log(id, "loaded from database")
} else { } else {
user = new MatrixUser(this, id) user = new MatrixUser(this, id)
console.log(id, "created")
} }
this.matrixUsersByID.set(id, user) this.matrixUsersByID.set(id, user)
return user return user
@@ -85,10 +91,11 @@ class MautrixTelegram {
putUser(user) { putUser(user) {
const entry = user.toEntry() const entry = user.toEntry()
return this.bridge.getUserStore().upsert({ const query = {
type: entry.type, type: entry.type,
id: entry.id, id: entry.id,
}, entry) }
return this.bridge.getUserStore().upsert(query, entry)
} }
handleMatrixEvent(evt) { handleMatrixEvent(evt) {
@@ -124,7 +131,8 @@ class MautrixTelegram {
commands.run(user, command, args, reply => commands.run(user, command, args, reply =>
this.botIntent.sendText( this.botIntent.sendText(
evt.room_id, evt.room_id,
reply.replace("$cmdprefix", cmdprefix))) reply.replace("$cmdprefix", cmdprefix)),
this)
}) })
return return
} }
@@ -146,6 +154,25 @@ class MautrixTelegram {
} }
return false 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 module.exports = MautrixTelegram
+28 -18
View File
@@ -15,22 +15,9 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
const makePasswordHash = require("telegram-mtproto").plugins.makePasswordHash 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 = {} const commands = {}
function run(sender, command, args, reply) { function run(sender, command, args, reply, app) {
if (sender.commandStatus) { if (sender.commandStatus) {
if (command === "cancel") { if (command === "cancel") {
sender.commandStatus = undefined sender.commandStatus = undefined
@@ -38,14 +25,15 @@ function run(sender, command, args, reply) {
return return
} }
args.unshift(command) args.unshift(command)
sender.commandStatus.next(sender, args, reply) sender.commandStatus.next(sender, args, reply, app)
return return
} }
command = this.commands[command] command = this.commands[command]
if (!command) { if (!command) {
reply("Unknown command. Try \"$cmdprefix help\" for help.") reply("Unknown command. Try \"$cmdprefix help\" for help.")
return
} }
command(sender, args, reply) command(sender, args, reply, app)
} }
commands.cancel = () => "Nothing to cancel." commands.cancel = () => "Nothing to cancel."
@@ -60,7 +48,7 @@ const enterPassword = (sender, args, reply) => {
sender.checkPassword(hash) sender.checkPassword(hash)
.then(() => { .then(() => {
// TODO show who the user logged in as // TODO show who the user logged in as
reply(`Logged in successfully.`) reply(`Logged in successfully as @${sender.telegramPuppet.getDisplayName()}.`)
sender.commandStatus = undefined sender.commandStatus = undefined
}, err => { }, err => {
reply(`Login failed: ${err}`) reply(`Login failed: ${err}`)
@@ -78,7 +66,7 @@ const enterCode = (sender, args, reply) => {
.then(data => { .then(data => {
if (data.status === "ok") { if (data.status === "ok") {
// TODO show who the user logged in as // TODO show who the user logged in as
reply(`Logged in successfully.`) reply(`Logged in successfully as @${sender.telegramPuppet.getDisplayName()}.`)
sender.commandStatus = undefined sender.commandStatus = undefined
} else if (data.status === "need-password") { } else if (data.status === "need-password") {
reply(`You have two-factor authentication enabled. Password hint: ${data.hint} \nEnter your password using "$cmdprefix <password>"`) reply(`You have two-factor authentication enabled. Password hint: ${data.hint} \nEnter your password using "$cmdprefix <password>"`)
@@ -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 <method> <json data>")
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) => { commands.help = (sender, args, reply) => {
reply("Help not yet implemented 3:") reply("Help not yet implemented 3:")
} }
+25 -20
View File
@@ -42,8 +42,8 @@ class MatrixUser {
} }
toEntry() { toEntry() {
if (this.puppet) { if (this._telegramPuppet) {
this.puppetData = this.puppet.toSubentry() this.puppetData = this.telegramPuppet.toSubentry()
} }
return { return {
type: "matrix", type: "matrix",
@@ -72,32 +72,37 @@ class MatrixUser {
throw new Error(message) throw new Error(message)
} }
sendTelegramCode(phoneNumber) { async sendTelegramCode(phoneNumber) {
// TODO handle existing login? // TODO handle existing login?
try {
return this.telegramPuppet.sendCode(phoneNumber) const result = await this.telegramPuppet.sendCode(phoneNumber)
.then(result => { this.phoneNumber = phoneNumber
this.phoneNumber = phoneNumber this.phoneCodeHash = result.phone_code_hash
this.phoneCodeHash = result.phone_code_hash await this.saveChanges()
this.app.putUser(this) return result
return result } catch (err) {
}, err => this.parseTelegramError(err)) return this.parseTelegramError(err)
}
} }
signInToTelegram(phoneCode) { async signInToTelegram(phoneCode) {
if (!this.phoneNumber) throw new Error("Phone number not set") if (!this.phoneNumber) throw new Error("Phone number not set")
if (!this.phoneCodeHash) throw new Error("Phone code not sent") if (!this.phoneCodeHash) throw new Error("Phone code not sent")
return this.telegramPuppet.signIn(this.phoneNumber, this.phoneCodeHash, phoneCode) const result = await this.telegramPuppet.signIn(this.phoneNumber, this.phoneCodeHash, phoneCode)
.then(result => { this.phoneCodeHash = undefined
this.phoneCodeHash = undefined await this.saveChanges()
return this.app.putUser(this).then(() => result) return result
})
} }
checkPassword(password_hash) { async checkPassword(password_hash) {
return this.telegramPuppet.checkPassword(password_hash) const result = await this.telegramPuppet.checkPassword(password_hash)
.then(() => this.app.putUser(this)) await this.saveChanges()
return result
}
saveChanges() {
return this.app.putUser(this)
} }
} }
+106 -27
View File
@@ -31,11 +31,46 @@ class TelegramPuppet {
this.api_hash = opts.api_hash this.api_hash = opts.api_hash
this.api_id = opts.api_id 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({}, { this.apiConfig = Object.assign({}, {
app_version: pkg.version, app_version: pkg.version,
lang_code: "en", lang_code: "en",
api_id: opts.api_id, api_id: opts.api_id,
initConnection : 0x69796de9,
layer: 57,
invokeWithLayer: 0xda9b0d0d,
}, opts.api_config) }, opts.api_config)
if (this.data.dc && this.data[`dc${this.data.dc}_auth_key`]) {
this.listen()
}
} }
static fromSubentry(app, matrixUser, data) { static fromSubentry(app, matrixUser, data) {
@@ -51,19 +86,17 @@ class TelegramPuppet {
toSubentry() { toSubentry() {
return Object.assign({ return Object.assign({
user_id: this.userID user_id: this.userID,
}, this.data) }, this.data)
} }
get datacenter() {
return { dcID: 1 }
}
get client() { get client() {
if (!this._client) { if (!this._client) {
const self = this
this._client = telegram.MTProto({ this._client = telegram.MTProto({
api: this.apiConfig, api: this.apiConfig,
server: this.serverConfig, server: this.serverConfig,
app: { storage: this.puppetStorage },
}) })
} }
return this._client return this._client
@@ -76,40 +109,86 @@ class TelegramPuppet {
api_id: this.api_id, api_id: this.api_id,
api_hash: this.api_hash, api_hash: this.api_hash,
}) })
} }
signIn(phone_number, phone_code_hash, phone_code) { async signIn(phone_number, phone_code_hash, phone_code) {
return this.client("auth.signIn", { try {
phone_number, phone_code, phone_code_hash const result = await
}) this.client("auth.signIn", {
.then( phone_number, phone_code, phone_code_hash,
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
}
})
}) })
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) { async checkPassword(password_hash) {
return this.client("auth.checkPassword", {password_hash}) const result = await this.client("auth.checkPassword", { password_hash })
.then((result) => this.signInComplete(result)) 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) { signInComplete(data) {
this.userID = data.user.id 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 { 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 module.exports = TelegramPuppet