Authentication may or may not now work
This commit is contained in:
+151
@@ -0,0 +1,151 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
const {Bridge} = require("matrix-appservice-bridge")
|
||||
const commands = require("./commands")
|
||||
const MatrixUser = require("./matrix-user")
|
||||
|
||||
class MautrixTelegram {
|
||||
constructor(config) {
|
||||
this.config = config
|
||||
|
||||
this.matrixUsersByID = new Map()
|
||||
this.telegramUsersByID = new Map()
|
||||
|
||||
const self = this
|
||||
this.bridge = new Bridge({
|
||||
homeserverUrl: config.homeserver.address,
|
||||
domain: config.homeserver.domain,
|
||||
registration: config.appservice.registration,
|
||||
controller: {
|
||||
onUserQuery(user) {
|
||||
return {}
|
||||
},
|
||||
onEvent(request, context) {
|
||||
self.handleMatrixEvent(request.getData())
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
get bot() {
|
||||
return this.bridge.getBot()
|
||||
}
|
||||
|
||||
get botIntent() {
|
||||
return this.bridge.getIntent()
|
||||
}
|
||||
|
||||
getMatrixUser(id) {
|
||||
let user = this.matrixUsersByID.get(id)
|
||||
if (user) {
|
||||
console.log(id, "found in cache")
|
||||
return Promise.resolve(user)
|
||||
}
|
||||
|
||||
return this.bridge.getUserStore().select({
|
||||
type: "matrix",
|
||||
id,
|
||||
}).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
|
||||
})
|
||||
}
|
||||
|
||||
putUser(user) {
|
||||
const entry = user.toEntry()
|
||||
return this.bridge.getUserStore().upsert({
|
||||
type: entry.type,
|
||||
id: entry.id,
|
||||
}, entry)
|
||||
}
|
||||
|
||||
handleMatrixEvent(evt) {
|
||||
const asBotID = this.bridge.getBot().getUserId()
|
||||
if (evt.type === "m.room.member" && evt.state_key === asBotID) {
|
||||
if (evt.content.membership === "invite") {
|
||||
// Accept all invites
|
||||
this.botIntent.join(evt.room_id)
|
||||
.catch(err => {
|
||||
console.warn(`Failed to join room ${evt.room_id}:`, err)
|
||||
if (e instanceof Error) {
|
||||
console.warn(e.stack)
|
||||
}
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
if (evt.sender === asBotID || evt.type !== "m.room.message" || !evt.content) {
|
||||
// Ignore own messages and non-message events.
|
||||
return;
|
||||
}
|
||||
const cmdprefix = this.config.bridge.command_prefix
|
||||
if (evt.content.body.startsWith(cmdprefix + " ")) {
|
||||
this.getMatrixUser(evt.sender).then(user => {
|
||||
if (!user.whitelisted) {
|
||||
this.botIntent.sendText(evt.room_id, "You are not authorized to use this bridge.")
|
||||
return
|
||||
}
|
||||
|
||||
const prefixLength = cmdprefix.length + 1
|
||||
const args = evt.content.body.substr(prefixLength).split(" ")
|
||||
const command = args.shift()
|
||||
commands.run(user, command, args, reply =>
|
||||
this.botIntent.sendText(
|
||||
evt.room_id,
|
||||
reply.replace("$cmdprefix", cmdprefix)))
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
checkWhitelist(userID) {
|
||||
if (!this.config.bridge.whitelist || this.config.bridge.whitelist.length === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
userID = userID.toLowerCase()
|
||||
const userIDCapture = /\@.+\:(.+)/.exec(userID)
|
||||
const homeserver = userIDCapture && userIDCapture.length > 1 ? userIDCapture[1] : undefined
|
||||
for (let whitelisted of this.config.bridge.whitelist) {
|
||||
whitelisted = whitelisted.toLowerCase()
|
||||
if (whitelisted === userID || (homeserver && whitelisted === homeserver)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MautrixTelegram
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
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) {
|
||||
if (sender.commandStatus) {
|
||||
if (command === "cancel") {
|
||||
sender.commandStatus = undefined
|
||||
reply(`${sender.commandStatus.action} cancelled.`)
|
||||
return
|
||||
}
|
||||
args.unshift(command)
|
||||
sender.commandStatus.next(sender, args, reply)
|
||||
return
|
||||
}
|
||||
command = this.commands[command]
|
||||
if (!command) {
|
||||
reply("Unknown command. Try \"$cmdprefix help\" for help.")
|
||||
}
|
||||
command(sender, args, reply)
|
||||
}
|
||||
|
||||
commands.cancel = () => "Nothing to cancel."
|
||||
|
||||
const enterPassword = (sender, args, reply) => {
|
||||
if (args.length === 0) {
|
||||
reply("Usage: $cmdprefix <password>")
|
||||
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.`)
|
||||
sender.commandStatus = undefined
|
||||
}, err => {
|
||||
reply(`Login failed: ${err}`)
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
|
||||
const enterCode = (sender, args, reply) => {
|
||||
if (args.length === 0) {
|
||||
reply("Usage: $cmdprefix <authentication code>")
|
||||
return
|
||||
}
|
||||
|
||||
sender.signInToTelegram(args[0])
|
||||
.then(data => {
|
||||
if (data.status === "ok") {
|
||||
// TODO show who the user logged in as
|
||||
reply(`Logged in successfully.`)
|
||||
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 <password>"`)
|
||||
sender.commandStatus = {
|
||||
action: "Two-factor authentication",
|
||||
next: enterPassword,
|
||||
salt: data.salt,
|
||||
}
|
||||
} else {
|
||||
reply(`Unexpected sign in response, status=${data.status}`)
|
||||
}
|
||||
}, err => {
|
||||
reply(`Login failed: ${err}`)
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
|
||||
commands.login = (sender, args, reply) => {
|
||||
if (args.length === 0) {
|
||||
reply("Usage: $cmdprefix login <phone number>")
|
||||
return
|
||||
}
|
||||
|
||||
sender.sendTelegramCode(args[0])
|
||||
.then(data => {
|
||||
reply(`Login code sent to ${args[0]}. \nEnter the code using "$cmdprefix <code>"`)
|
||||
sender.commandStatus = {
|
||||
action: "Phone code authentication",
|
||||
next: enterCode,
|
||||
}
|
||||
console.log(data)
|
||||
}, err => {
|
||||
reply(`Failed to send code: ${err}`)
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
|
||||
commands.help = (sender, args, reply) => {
|
||||
reply("Help not yet implemented 3:")
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
commands,
|
||||
run,
|
||||
}
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/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 <http://www.gnu.org/licenses/>.
|
||||
const {AppServiceRegistration} = require("matrix-appservice-bridge")
|
||||
const commander = require("commander")
|
||||
const YAML = require("yamljs")
|
||||
const fs = require("fs")
|
||||
const MautrixTelegram = require("./app")
|
||||
const pkg = require("../package.json")
|
||||
|
||||
commander
|
||||
.version(pkg.version)
|
||||
.option("-c, --config <path>", "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 <path>", "the file to save the registration to. defaults to ./registration.yaml")
|
||||
.parse(process.argv)
|
||||
|
||||
commander.registration = commander.registration || "./registration.yaml"
|
||||
commander.config = commander.config || "./config.yaml"
|
||||
|
||||
const config = YAML.load(commander.config)
|
||||
|
||||
if (commander.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: [],
|
||||
rooms: [],
|
||||
},
|
||||
url: `${config.appservice.protocol}://${config.appservice.hostname}:${config.appservice.port}`,
|
||||
sender_localpart: config.bridge.bot_username,
|
||||
rate_limited: false,
|
||||
}
|
||||
fs.writeFileSync(commander.registration, YAML.stringify(registration, 10))
|
||||
config.appservice.registration = commander.registration
|
||||
fs.writeFileSync(commander.config, YAML.stringify(config, 10))
|
||||
return
|
||||
}
|
||||
|
||||
const app = new MautrixTelegram(config)
|
||||
app.run()
|
||||
@@ -0,0 +1,104 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
const TelegramPuppet = require("./telegram-puppet")
|
||||
|
||||
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._telegramPuppet = undefined
|
||||
}
|
||||
|
||||
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.phone_number
|
||||
user.phoneCodeHash = entry.data.phone_code_hash
|
||||
if (entry.data.puppet) {
|
||||
user.puppetData = entry.data.puppet
|
||||
user.telegramPuppet
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
toEntry() {
|
||||
if (this.puppet) {
|
||||
this.puppetData = this.puppet.toSubentry()
|
||||
}
|
||||
return {
|
||||
type: "matrix",
|
||||
id: this.userID,
|
||||
data: {
|
||||
phone_number: this.phoneNumber,
|
||||
phone_code_hash: this.phoneCodeHash,
|
||||
puppet: this.puppetData,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
get telegramPuppet() {
|
||||
if (!this._telegramPuppet) {
|
||||
this._telegramPuppet = TelegramPuppet.fromSubentry(this.app, this, this.puppetData || {})
|
||||
}
|
||||
return this._telegramPuppet
|
||||
}
|
||||
|
||||
parseTelegramError(err) {
|
||||
const message = err.toPrintable ? err.toPrintable() : err.toString()
|
||||
|
||||
if (err instanceof Error) {
|
||||
throw err
|
||||
}
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
checkPassword(password_hash) {
|
||||
return this.telegramPuppet.checkPassword(password_hash)
|
||||
.then(() => this.app.putUser(this))
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MatrixUser
|
||||
@@ -0,0 +1,115 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
const pkg = require("../package.json")
|
||||
const os = require("os")
|
||||
const telegram = require("telegram-mtproto")
|
||||
|
||||
class TelegramPuppet {
|
||||
constructor(opts) {
|
||||
this._client = undefined
|
||||
this.userID = opts.userID
|
||||
this.matrixUser = opts.matrixUser
|
||||
this.data = opts.data
|
||||
|
||||
this.app = opts.app
|
||||
|
||||
this.serverConfig = Object.assign({}, opts.server_config)
|
||||
|
||||
this.api_hash = opts.api_hash
|
||||
this.api_id = opts.api_id
|
||||
|
||||
this.apiConfig = Object.assign({}, {
|
||||
app_version: pkg.version,
|
||||
lang_code: "en",
|
||||
api_id: opts.api_id,
|
||||
}, opts.api_config)
|
||||
}
|
||||
|
||||
static fromSubentry(app, matrixUser, data) {
|
||||
const userID = data.user_id
|
||||
delete data.user_id
|
||||
return new TelegramPuppet(Object.assign({
|
||||
userID,
|
||||
matrixUser,
|
||||
data,
|
||||
app,
|
||||
}, app.config.telegram))
|
||||
}
|
||||
|
||||
toSubentry() {
|
||||
return Object.assign({
|
||||
user_id: this.userID
|
||||
}, this.data)
|
||||
}
|
||||
|
||||
get datacenter() {
|
||||
return { dcID: 1 }
|
||||
}
|
||||
|
||||
get client() {
|
||||
if (!this._client) {
|
||||
this._client = telegram.MTProto({
|
||||
api: this.apiConfig,
|
||||
server: this.serverConfig,
|
||||
})
|
||||
}
|
||||
return this._client
|
||||
}
|
||||
|
||||
sendCode(phone_number) {
|
||||
return this.client("auth.sendCode", {
|
||||
phone_number,
|
||||
current_number: true,
|
||||
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
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
checkPassword(password_hash) {
|
||||
return this.client("auth.checkPassword", {password_hash})
|
||||
.then((result) => this.signInComplete(result))
|
||||
}
|
||||
|
||||
signInComplete(data) {
|
||||
this.userID = data.user.id
|
||||
return {
|
||||
status: "ok"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TelegramPuppet
|
||||
Reference in New Issue
Block a user