Add support for creating Telegram chats from Matrix
This commit is contained in:
@@ -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
@@ -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
@@ -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** <_phone_> - Request an authentication code.<br/>
|
**login** <_phone_> - Request an authentication code.<br/>
|
||||||
**logout** - Log out from Telegram.<br/>
|
**logout** - Log out from Telegram.<br/>
|
||||||
**search** [_-r|--remote_] <_query_> - Search your contacts or the Telegram servers for users.
|
**search** [_-r|--remote_] <_query_> - Search your contacts or the Telegram servers for users.<br/>
|
||||||
|
**create** <_group/channel_> [_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** <_id_> - Open a private chat with the given Telegram user ID.
|
**pm** <_id_> - 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
|
||||||
|
|||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user