diff --git a/README.md b/README.md
index 5b96a078..8d8db555 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/src/app.js b/src/app.js
index 57903f95..6566d60f 100644
--- a/src/app.js
+++ b/src/app.js
@@ -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,
+ })
}
}
diff --git a/src/commands.js b/src/commands.js
index ca9ee4bf..5f8f62e6 100644
--- a/src/commands.js
+++ b/src/commands.js
@@ -14,6 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
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} 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._
**login** <_phone_> - Request an authentication code.
**logout** - Log out from Telegram.
-**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.
+**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._
**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 `")
+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 [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 `")
return
}
@@ -165,8 +198,11 @@ Enter your password using \`$cmdprefix \``)
/*
* 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 `")
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 `")
+ 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] ")
+ reply("**Usage:** `$cmdprefix search [-r|--remote] `")
+ 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 ")
+ reply("**Usage:** `$cmdprefix pm `")
+ 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
diff --git a/src/portal.js b/src/portal.js
index b46d2e08..dd1384d1 100644
--- a/src/portal.js
+++ b/src/portal.js
@@ -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.
*