Add support for creating Telegram chats from Matrix

This commit is contained in:
Tulir Asokan
2017-12-03 22:23:06 +02:00
parent b203f3b182
commit 58ea45638e
4 changed files with 178 additions and 40 deletions
+2 -2
View File
@@ -95,8 +95,8 @@ does not do this automatically.
* [x] Private chat creation by inviting Telegram user to new room * [x] Private chat creation by inviting Telegram user to new room
* [ ] Joining public channels/supergroups using room aliases * [ ] Joining public channels/supergroups using room aliases
* [x] Searching for Telegram users using management commands * [x] Searching for Telegram users using management commands
* [ ] Creating new Telegram chats from Matrix * [x] Creating new Telegram chats from Matrix
* [ ] Creating Telegram chats for existing Matrix rooms * [x] Creating Telegram chats for existing Matrix rooms
* Misc * Misc
* [ ] Use optional bot to relay messages for unauthenticated Matrix users * [ ] Use optional bot to relay messages for unauthenticated Matrix users
* [x] Properly handle upgrading groups to supergroups * [x] Properly handle upgrading groups to supergroups
+25 -13
View File
@@ -484,6 +484,16 @@ class MautrixTelegram {
return members return members
} }
async getRoomTitle(roomID, intent = this.botIntent) {
const roomState = await intent.roomState(roomID)
for (const event of roomState) {
if (event.type === "m.room.name") {
return event.content.name
}
}
return undefined
}
/** /**
* Handle an invite to a Matrix room. * Handle an invite to a Matrix room.
* *
@@ -556,15 +566,9 @@ class MautrixTelegram {
await intent.leave(evt.room_id) await intent.leave(evt.room_id)
} else { } else {
const portal = await this.getPortalByRoomID(evt.room_id) const portal = await this.getPortalByRoomID(evt.room_id)
if (!portal) { if (portal) {
await intent.sendMessage(evt.room_id, { await portal.inviteTelegram(sender.telegramPuppet, user)
msgtype: "m.notice",
body: "Inviting additional Telegram users to private chats or non-portal rooms is not supported.",
})
await intent.leave(evt.room_id)
return
} }
await portal.inviteTelegram(sender.telegramPuppet, user)
} }
} catch (err) { } catch (err) {
console.error(`Failed to process invite to room ${evt.room_id} for Telegram user ${telegramID}: ${err}`) console.error(`Failed to process invite to room ${evt.room_id} for Telegram user ${telegramID}: ${err}`)
@@ -596,13 +600,16 @@ class MautrixTelegram {
return return
} }
const cmdprefix = this.config.bridge.commands.prefix
const hasCommandPrefix = cmdprefix && evt.content.body.startsWith(`${cmdprefix} `)
const portal = await this.getPortalByRoomID(evt.room_id) const portal = await this.getPortalByRoomID(evt.room_id)
if (portal) { if (portal && !hasCommandPrefix) {
portal.handleMatrixEvent(user, evt) portal.handleMatrixEvent(user, evt)
return return
} }
let isManagement = this.managementRooms.includes(evt.room_id) let isManagement = this.managementRooms.includes(evt.room_id) || hasCommandPrefix
if (!isManagement) { if (!isManagement) {
const roomMembers = await this.getRoomMembers(evt.room_id) const roomMembers = await this.getRoomMembers(evt.room_id)
if (roomMembers.length === 2 && roomMembers.includes(asBotID)) { if (roomMembers.length === 2 && roomMembers.includes(asBotID)) {
@@ -610,8 +617,7 @@ class MautrixTelegram {
isManagement = true isManagement = true
} }
} }
const cmdprefix = this.config.bridge.commands.prefix if (isManagement) {
if (isManagement || (cmdprefix && evt.content.body.startsWith(`${cmdprefix} `))) {
const prefixLength = cmdprefix.length + 1 const prefixLength = cmdprefix.length + 1
if (cmdprefix && evt.content.body.startsWith(`${cmdprefix} `)) { if (cmdprefix && evt.content.body.startsWith(`${cmdprefix} `)) {
evt.content.body = evt.content.body.substr(prefixLength) evt.content.body = evt.content.body.substr(prefixLength)
@@ -636,7 +642,13 @@ class MautrixTelegram {
format: "org.matrix.custom.html", format: "org.matrix.custom.html",
}) })
} }
commands.run(user, command, args, replyFunc, this, evt) commands.run(user, command, args, replyFunc, {
app: this,
evt,
roomID: evt.room_id,
isManagement,
isPortal: !!portal,
})
} }
} }
+109 -25
View File
@@ -14,6 +14,7 @@
// 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 makePasswordHash = require("telegram-mtproto").plugins.makePasswordHash const makePasswordHash = require("telegram-mtproto").plugins.makePasswordHash
const Portal = require("./portal")
const commands = {} const commands = {}
@@ -30,10 +31,14 @@ const commands = {}
* @param {string} command The command itself. * @param {string} command The command itself.
* @param {Array<string>} args A list of arguments. * @param {Array<string>} args A list of arguments.
* @param {function} reply A function that is called to reply to the command. * @param {function} reply A function that is called to reply to the command.
* @param {MautrixTelegram} app The app main class instance. * @param {object} extra Extra information that the handlers may find useful.
* @param {MatrixEvent} evt The event that caused this call. * @param {MautrixTelegram} extra.app The app main class instance.
* @param {MatrixEvent} extra.evt The event that caused this call.
* @param {string} extra.roomID The ID of the Matrix room the command was sent to.
* @param {boolean} extra.isManagement Whether or not the Matrix room is a management room.
* @param {boolean} extra.isPortal Whether or not the Matrix room is a portal to a Telegram chat.
*/ */
function run(sender, command, args, reply, app, evt) { function run(sender, command, args, reply, extra) {
const commandFunc = this.commands[command] const commandFunc = this.commands[command]
if (!commandFunc) { if (!commandFunc) {
if (sender.commandStatus) { if (sender.commandStatus) {
@@ -43,13 +48,13 @@ function run(sender, command, args, reply, app, evt) {
return undefined return undefined
} }
args.unshift(command) args.unshift(command)
return sender.commandStatus.next(sender, args, reply, app, evt) return sender.commandStatus.next(sender, args, reply, extra)
} }
reply("Unknown command. Try `$cmdprefix help` for help.") reply("Unknown command. Try `$cmdprefix help` for help.")
return undefined return undefined
} }
try { try {
return commandFunc(sender, args, reply, app, evt) return commandFunc(sender, args, reply, extra)
} catch (err) { } catch (err) {
reply(`Error running command: ${err}.`) reply(`Error running command: ${err}.`)
if (err instanceof Error) { if (err instanceof Error) {
@@ -62,10 +67,13 @@ function run(sender, command, args, reply, app, evt) {
commands.cancel = () => "Nothing to cancel." commands.cancel = () => "Nothing to cancel."
commands.help = (sender, args, reply, app, evt) => { commands.help = (sender, args, reply, { isManagement, isPortal }) => {
let replyMsg = "" let replyMsg = ""
if (app.managementRooms.includes(evt.room_id)) { if (isManagement) {
replyMsg += "This is a management room: prefixing commands with `$cmdprefix` is not required.\n" replyMsg += "This is a management room: prefixing commands with `$cmdprefix` is not required.\n"
} else if (isPortal) {
replyMsg += "**This is a portal room**: you must always prefix commands with `$cmdprefix`.\n" +
"Management commands will not be sent to Telegram.\n"
} else { } else {
replyMsg += "**This is not a management room**: you must prefix commands with `$cmdprefix`.\n" replyMsg += "**This is not a management room**: you must prefix commands with `$cmdprefix`.\n"
} }
@@ -79,7 +87,9 @@ _**Generic bridge commands**: commands for using the bridge that aren't related
_**Telegram actions**: commands for using the bridge to interact with Telegram._<br/> _**Telegram actions**: commands for using the bridge to interact with Telegram._<br/>
**login** &lt;_phone_&gt; - Request an authentication code.<br/> **login** &lt;_phone_&gt; - Request an authentication code.<br/>
**logout** - Log out from Telegram.<br/> **logout** - Log out from Telegram.<br/>
**search** [_-r|--remote_] &lt;_query_&gt; - Search your contacts or the Telegram servers for users. **search** [_-r|--remote_] &lt;_query_&gt; - Search your contacts or the Telegram servers for users.<br/>
**create** &lt;_group/channel_&gt; [_room ID_] - Create a Telegram chat of the given type for a Matrix room.
If the room ID is not specified, a chat for the current room is created.
_**Temporary commands**: commands that will be replaced with more Matrix-y actions later._<br/> _**Temporary commands**: commands that will be replaced with more Matrix-y actions later._<br/>
**pm** &lt;_id_&gt; - Open a private chat with the given Telegram user ID. **pm** &lt;_id_&gt; - Open a private chat with the given Telegram user ID.
@@ -90,13 +100,17 @@ _**Debug commands**: commands to help in debugging the bridge. Disabled by defau
reply(replyMsg, { allowHTML: true }) reply(replyMsg, { allowHTML: true })
} }
commands.setManagement = (sender, _, reply, app, evt) => { commands.setManagement = (sender, _, reply, { app, roomID, isPortal }) => {
app.managementRooms.push(evt.room_id) if (isPortal) {
reply("You may not mark portal rooms as management rooms.")
return
}
app.managementRooms.push(roomID)
reply("Room marked as management room. You can now run commands without the `$cmdprefix` prefix.") reply("Room marked as management room. You can now run commands without the `$cmdprefix` prefix.")
} }
commands.unsetManagement = (sender, _, reply, app, evt) => { commands.unsetManagement = (sender, _, reply, { app, roomID }) => {
app.managementRooms.splice(app.managementRooms.indexOf(evt.room_id), 1) app.managementRooms.splice(app.managementRooms.indexOf(roomID), 1)
reply("Room unmarked as management room. You must now include the `$cmdprefix` prefix when running commands.") reply("Room unmarked as management room. You must now include the `$cmdprefix` prefix when running commands.")
} }
@@ -108,13 +122,29 @@ commands.unsetManagement = (sender, _, reply, app, evt) => {
/** /**
* Two-factor authentication handler. * Two-factor authentication handler.
*/ */
commands.enterPassword = async (sender, args, reply) => { commands.enterPassword = async (sender, args, reply, { isManagement }) => {
if (args.length === 0) { if (!isManagement) {
reply("**Usage:** `$cmdprefix <password>`") reply("Logging in is considered a confidential action, and thus is only allowed in management rooms.")
return
} else if (args.length === 0) {
reply("**Usage:** `$cmdprefix <password> [salt]`")
return return
} }
const hash = makePasswordHash(sender.commandStatus.salt, args[0]) let salt
if (!sender.commandStatus || !sender.commandStatus.salt) {
if (args.length > 1) {
salt = args[1]
} else {
reply("No password salt found. Did you enter your phone code already?")
return
}
} else {
salt = sender.commandStatus.salt
}
const hash = makePasswordHash(salt, args[0])
try { try {
await sender.checkPassword(hash) await sender.checkPassword(hash)
reply(`Logged in successfully as ${sender.telegramPuppet.getDisplayName()}.`) reply(`Logged in successfully as ${sender.telegramPuppet.getDisplayName()}.`)
@@ -131,8 +161,11 @@ commands.enterPassword = async (sender, args, reply) => {
/* /*
* Login code send handler. * Login code send handler.
*/ */
commands.enterCode = async (sender, args, reply) => { commands.enterCode = async (sender, args, reply, { isManagement }) => {
if (args.length === 0) { if (!isManagement) {
reply("Logging in is considered a confidential action, and thus is only allowed in management rooms.")
return
} else if (args.length === 0) {
reply("**Usage:** `$cmdprefix <authentication code>`") reply("**Usage:** `$cmdprefix <authentication code>`")
return return
} }
@@ -165,8 +198,11 @@ Enter your password using \`$cmdprefix <password>\``)
/* /*
* Login code request handler. * Login code request handler.
*/ */
commands.login = async (sender, args, reply) => { commands.login = async (sender, args, reply, { isManagement }) => {
if (args.length === 0) { if (!isManagement) {
reply("Logging in is considered a confidential action, and thus is only allowed in management rooms.")
return
} else if (args.length === 0) {
reply("**Usage:** `$cmdprefix login <phone number>`") reply("**Usage:** `$cmdprefix login <phone number>`")
return return
} }
@@ -209,9 +245,54 @@ commands.logout = async (sender, args, reply) => {
// General command handlers // // General command handlers //
////////////////////////////// //////////////////////////////
commands.search = async (sender, args, reply, app) => { commands.create = async (sender, args, reply, { app, roomID }) => {
if (args.length < 1 || (args[0] !== "group" && args[0] !== "channel")) {
reply("**Usage:** `$cmdprefix create <group/channel>`")
return
} else if (!sender._telegramPuppet) {
reply("This command requires you to be logged in.")
return
} else if (args[0] === "channel") {
reply("Creating channels is not yet supported.")
return
}
if (args.length > 1) {
roomID = args[1]
}
// TODO make sure that the AS bot is in the room.
const title = await app.getRoomTitle(roomID)
if (!title) {
reply("Please set a room name before creating a Telegram chat.")
return
}
let portal = await app.getPortalByRoomID(roomID)
if (portal) {
reply("This is already a portal room.")
return
}
portal = new Portal(app, roomID)
try {
await portal.createTelegramChat(sender.telegramPuppet, title)
reply(`Telegram chat created. ID: ${portal.id}`)
if (app.managementRooms.includes(roomID)) {
app.managementRooms.splice(app.managementRooms.indexOf(roomID), 1)
}
} catch (err) {
reply(`Failed to create Telegram chat: ${err}`)
}
}
commands.search = async (sender, args, reply, { app }) => {
if (args.length < 1) { if (args.length < 1) {
reply("Usage: $cmdprefix search [-r|--remote] <query>") reply("**Usage:** `$cmdprefix search [-r|--remote] <query>`")
return
} else if (!sender._telegramPuppet) {
reply("This command requires you to be logged in.")
return return
} }
const msg = [] const msg = []
@@ -252,9 +333,12 @@ commands.search = async (sender, args, reply, app) => {
reply(msg.join("\n"), { allowHTML: true }) reply(msg.join("\n"), { allowHTML: true })
} }
commands.pm = async (sender, args, reply, app) => { commands.pm = async (sender, args, reply, { app }) => {
if (args.length < 1) { if (args.length < 1) {
reply("Usage: $cmdprefix pm <id>") reply("**Usage:** `$cmdprefix pm <id>`")
return
} else if (!sender._telegramPuppet) {
reply("This command requires you to be logged in.")
return return
} }
const user = await app.getTelegramUser(+args[0], { createIfNotFound: false }) const user = await app.getTelegramUser(+args[0], { createIfNotFound: false })
@@ -277,7 +361,7 @@ commands.pm = async (sender, args, reply, app) => {
// Debug command handlers // // Debug command handlers //
//////////////////////////// ////////////////////////////
commands.api = async (sender, args, reply, app) => { commands.api = async (sender, args, reply, { app }) => {
if (!app.config.bridge.commands.allow_direct_api_calls) { if (!app.config.bridge.commands.allow_direct_api_calls) {
reply("Direct API calls are forbidden on this mautrix-telegram instance.") reply("Direct API calls are forbidden on this mautrix-telegram instance.")
return return
+42
View File
@@ -499,6 +499,48 @@ class Portal {
} }
} }
async createTelegramChat(telegramPOV, title) {
const members = await this.app.getRoomMembers(this.roomID)
const telegramInviteIDs = []
const asBotID = this.app.bot.getUserId()
for (const member of members) {
if (member === asBotID) {
continue
}
const user = await this.app.getMatrixUser(member)
if (user._telegramPuppet) {
telegramInviteIDs.push(user.telegramPuppet.userID)
}
const match = this.app.usernameRegex.exec(member)
if (!match || match.length < 2) {
continue
}
telegramInviteIDs.push(+match[1])
}
if (telegramInviteIDs.length < 2) {
// TODO once we have the option for a bot, this error will need to be changed.
throw new Error("Not enough users")
}
const telegramInvites = []
for (const userID of telegramInviteIDs) {
const user = await this.app.getTelegramUser(userID, { createIfNotFound: false })
if (!user) {
continue
}
telegramInvites.push(user.toPeer(telegramPOV).toInputObject())
}
const createUpdates = await telegramPOV.client("messages.createChat", {
title,
users: telegramInvites,
})
const chat = createUpdates.chats[0]
this.peer = new TelegramPeer("chat", chat.id, { title })
await this.save()
}
/** /**
* Create a Matrix room for this portal. * Create a Matrix room for this portal.
* *