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
|
||||
* [ ] Joining public channels/supergroups using room aliases
|
||||
* [x] Searching for Telegram users using management commands
|
||||
* [ ] Creating new Telegram chats from Matrix
|
||||
* [ ] Creating Telegram chats for existing Matrix rooms
|
||||
* [x] Creating new Telegram chats from Matrix
|
||||
* [x] Creating Telegram chats for existing Matrix rooms
|
||||
* Misc
|
||||
* [ ] Use optional bot to relay messages for unauthenticated Matrix users
|
||||
* [x] Properly handle upgrading groups to supergroups
|
||||
|
||||
+25
-13
@@ -484,6 +484,16 @@ class MautrixTelegram {
|
||||
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.
|
||||
*
|
||||
@@ -556,15 +566,9 @@ class MautrixTelegram {
|
||||
await intent.leave(evt.room_id)
|
||||
} else {
|
||||
const portal = await this.getPortalByRoomID(evt.room_id)
|
||||
if (!portal) {
|
||||
await intent.sendMessage(evt.room_id, {
|
||||
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
|
||||
if (portal) {
|
||||
await portal.inviteTelegram(sender.telegramPuppet, user)
|
||||
}
|
||||
await portal.inviteTelegram(sender.telegramPuppet, user)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to process invite to room ${evt.room_id} for Telegram user ${telegramID}: ${err}`)
|
||||
@@ -596,13 +600,16 @@ class MautrixTelegram {
|
||||
return
|
||||
}
|
||||
|
||||
const cmdprefix = this.config.bridge.commands.prefix
|
||||
const hasCommandPrefix = cmdprefix && evt.content.body.startsWith(`${cmdprefix} `)
|
||||
|
||||
const portal = await this.getPortalByRoomID(evt.room_id)
|
||||
if (portal) {
|
||||
if (portal && !hasCommandPrefix) {
|
||||
portal.handleMatrixEvent(user, evt)
|
||||
return
|
||||
}
|
||||
|
||||
let isManagement = this.managementRooms.includes(evt.room_id)
|
||||
let isManagement = this.managementRooms.includes(evt.room_id) || hasCommandPrefix
|
||||
if (!isManagement) {
|
||||
const roomMembers = await this.getRoomMembers(evt.room_id)
|
||||
if (roomMembers.length === 2 && roomMembers.includes(asBotID)) {
|
||||
@@ -610,8 +617,7 @@ class MautrixTelegram {
|
||||
isManagement = true
|
||||
}
|
||||
}
|
||||
const cmdprefix = this.config.bridge.commands.prefix
|
||||
if (isManagement || (cmdprefix && evt.content.body.startsWith(`${cmdprefix} `))) {
|
||||
if (isManagement) {
|
||||
const prefixLength = cmdprefix.length + 1
|
||||
if (cmdprefix && evt.content.body.startsWith(`${cmdprefix} `)) {
|
||||
evt.content.body = evt.content.body.substr(prefixLength)
|
||||
@@ -636,7 +642,13 @@ class MautrixTelegram {
|
||||
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
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
const makePasswordHash = require("telegram-mtproto").plugins.makePasswordHash
|
||||
const Portal = require("./portal")
|
||||
|
||||
const commands = {}
|
||||
|
||||
@@ -30,10 +31,14 @@ const commands = {}
|
||||
* @param {string} command The command itself.
|
||||
* @param {Array<string>} args A list of arguments.
|
||||
* @param {function} reply A function that is called to reply to the command.
|
||||
* @param {MautrixTelegram} app The app main class instance.
|
||||
* @param {MatrixEvent} evt The event that caused this call.
|
||||
* @param {object} extra Extra information that the handlers may find useful.
|
||||
* @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]
|
||||
if (!commandFunc) {
|
||||
if (sender.commandStatus) {
|
||||
@@ -43,13 +48,13 @@ function run(sender, command, args, reply, app, evt) {
|
||||
return undefined
|
||||
}
|
||||
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.")
|
||||
return undefined
|
||||
}
|
||||
try {
|
||||
return commandFunc(sender, args, reply, app, evt)
|
||||
return commandFunc(sender, args, reply, extra)
|
||||
} catch (err) {
|
||||
reply(`Error running command: ${err}.`)
|
||||
if (err instanceof Error) {
|
||||
@@ -62,10 +67,13 @@ function run(sender, command, args, reply, app, evt) {
|
||||
|
||||
commands.cancel = () => "Nothing to cancel."
|
||||
|
||||
commands.help = (sender, args, reply, app, evt) => {
|
||||
commands.help = (sender, args, reply, { isManagement, isPortal }) => {
|
||||
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"
|
||||
} 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 {
|
||||
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/>
|
||||
**login** <_phone_> - Request an authentication code.<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/>
|
||||
**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 })
|
||||
}
|
||||
|
||||
commands.setManagement = (sender, _, reply, app, evt) => {
|
||||
app.managementRooms.push(evt.room_id)
|
||||
commands.setManagement = (sender, _, reply, { app, roomID, isPortal }) => {
|
||||
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.")
|
||||
}
|
||||
|
||||
commands.unsetManagement = (sender, _, reply, app, evt) => {
|
||||
app.managementRooms.splice(app.managementRooms.indexOf(evt.room_id), 1)
|
||||
commands.unsetManagement = (sender, _, reply, { app, roomID }) => {
|
||||
app.managementRooms.splice(app.managementRooms.indexOf(roomID), 1)
|
||||
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.
|
||||
*/
|
||||
commands.enterPassword = async (sender, args, reply) => {
|
||||
if (args.length === 0) {
|
||||
reply("**Usage:** `$cmdprefix <password>`")
|
||||
commands.enterPassword = async (sender, args, reply, { isManagement }) => {
|
||||
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 <password> [salt]`")
|
||||
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 {
|
||||
await sender.checkPassword(hash)
|
||||
reply(`Logged in successfully as ${sender.telegramPuppet.getDisplayName()}.`)
|
||||
@@ -131,8 +161,11 @@ commands.enterPassword = async (sender, args, reply) => {
|
||||
/*
|
||||
* Login code send handler.
|
||||
*/
|
||||
commands.enterCode = async (sender, args, reply) => {
|
||||
if (args.length === 0) {
|
||||
commands.enterCode = async (sender, args, reply, { isManagement }) => {
|
||||
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>`")
|
||||
return
|
||||
}
|
||||
@@ -165,8 +198,11 @@ Enter your password using \`$cmdprefix <password>\``)
|
||||
/*
|
||||
* Login code request handler.
|
||||
*/
|
||||
commands.login = async (sender, args, reply) => {
|
||||
if (args.length === 0) {
|
||||
commands.login = async (sender, args, reply, { isManagement }) => {
|
||||
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>`")
|
||||
return
|
||||
}
|
||||
@@ -209,9 +245,54 @@ commands.logout = async (sender, args, reply) => {
|
||||
// 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) {
|
||||
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
|
||||
}
|
||||
const msg = []
|
||||
@@ -252,9 +333,12 @@ commands.search = async (sender, args, reply, app) => {
|
||||
reply(msg.join("\n"), { allowHTML: true })
|
||||
}
|
||||
|
||||
commands.pm = async (sender, args, reply, app) => {
|
||||
commands.pm = async (sender, args, reply, { app }) => {
|
||||
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
|
||||
}
|
||||
const user = await app.getTelegramUser(+args[0], { createIfNotFound: false })
|
||||
@@ -277,7 +361,7 @@ commands.pm = async (sender, args, reply, app) => {
|
||||
// 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) {
|
||||
reply("Direct API calls are forbidden on this mautrix-telegram instance.")
|
||||
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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user