780 lines
25 KiB
JavaScript
780 lines
25 KiB
JavaScript
// 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 TelegramPeer = require("./telegram-peer")
|
|
const formatter = require("./formatter")
|
|
const chalk = require("chalk")
|
|
|
|
/**
|
|
* Portal represents a portal from a Matrix room to a Telegram chat.
|
|
*/
|
|
class Portal {
|
|
constructor(app, roomID, peer) {
|
|
this.app = app
|
|
this.type = "portal"
|
|
|
|
this.roomID = roomID
|
|
this.peer = peer
|
|
this.accessHashes = new Map()
|
|
}
|
|
|
|
/**
|
|
* Get the peer ID of this portal.
|
|
*
|
|
* @returns {number} The ID of the peer of the Telegram side of this portal.
|
|
*/
|
|
get id() {
|
|
return this.peer.id
|
|
}
|
|
|
|
/**
|
|
* Get the receiver ID of this portal. Only applicable for private chat portals.
|
|
*
|
|
* @returns {number} The ID of the receiving user of this portal.
|
|
*/
|
|
get receiverID() {
|
|
return this.peer.receiverID
|
|
}
|
|
|
|
/**
|
|
* Convert a database entry into a Portal.
|
|
*
|
|
* @param {MautrixTelegram} app The app main class instance.
|
|
* @param {Object} entry The database entry.
|
|
* @returns {Portal} The loaded Portal.
|
|
*/
|
|
static fromEntry(app, entry) {
|
|
if (entry.type !== "portal") {
|
|
throw new Error("MatrixUser can only be created from entry type \"portal\"")
|
|
}
|
|
|
|
const portal = new Portal(app, entry.roomID || entry.data.roomID, TelegramPeer.fromSubentry(entry.data.peer))
|
|
portal.photo = entry.data.photo
|
|
portal.avatarURL = entry.data.avatarURL
|
|
if (portal.peer.type === "channel") {
|
|
portal.accessHashes = new Map(entry.data.accessHashes)
|
|
}
|
|
return portal
|
|
}
|
|
|
|
/**
|
|
* Synchronize the user list of this portal.
|
|
*
|
|
* @param {TelegramPuppet} telegramPOV The Telegram account whose point of view the data is/should be fetched from.
|
|
* @param {UserFull[]} [users] The list of {@link https://tjhorner.com/tl-schema/type/UserFull user info}
|
|
* objects.
|
|
* @returns {boolean} Whether or not syncing was successful. It can only be unsuccessful if the
|
|
* user list was not provided and an access hash was not found for the given
|
|
* Telegram user.
|
|
*/
|
|
async syncTelegramUsers(telegramPOV, users) {
|
|
if (!users) {
|
|
if (!await this.loadAccessHash(telegramPOV)) {
|
|
return false
|
|
}
|
|
const data = await this.peer.getInfo(telegramPOV)
|
|
users = data.users
|
|
}
|
|
for (const userData of users) {
|
|
const user = await this.app.getTelegramUser(userData.id)
|
|
// We don't want to update avatars here, as it would likely cause a flood error
|
|
await user.updateInfo(telegramPOV, userData, { updateAvatar: false })
|
|
await user.intent.join(this.roomID)
|
|
}
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Copy a photo from Telegram to Matrix.
|
|
*
|
|
* @param {TelegramPuppet} telegramPOV The Telegram account whose point of view the image should be downloaded from.
|
|
* @param {TelegramUser} sender The user who sent the photo.
|
|
* @param {Photo} photo The Telegram {@link https://tjhorner.com/tl-schema/type/Photo Photo} object.
|
|
* @returns {Object} The uploaded Matrix photo object.
|
|
*/
|
|
async copyTelegramPhoto(telegramPOV, sender, photo) {
|
|
const size = photo.sizes.slice(-1)[0]
|
|
const uploaded = await this.copyTelegramFile(telegramPOV, sender, size.location, photo.id)
|
|
uploaded.info.h = size.h
|
|
uploaded.info.w = size.w
|
|
uploaded.info.size = size.size
|
|
uploaded.info.orientation = 0
|
|
return uploaded
|
|
}
|
|
|
|
|
|
/**
|
|
* Copy a file from Telegram to Matrix.
|
|
*
|
|
* @param {TelegramPuppet} telegramPOV The Telegram account whose point of view the file should be downloaded from.
|
|
* @param {TelegramUser} sender The user who sent the file.
|
|
* @param {FileLocation} location The Telegram {@link https://tjhorner.com/tl-schema/type/FileLocation
|
|
* FileLocation}.
|
|
* @returns {Object} The uploaded Matrix file object.
|
|
*/
|
|
async copyTelegramFile(telegramPOV, sender, location, id) {
|
|
id = id || location.id
|
|
const file = await telegramPOV.getFile(location)
|
|
const uploaded = await sender.intent.getClient().uploadContent({
|
|
stream: file.buffer,
|
|
name: `${id}.${file.extension}`,
|
|
type: file.mimetype,
|
|
}, { rawResponse: false })
|
|
uploaded.matrixtype = file.matrixtype
|
|
uploaded.info = {
|
|
mimetype: file.mimetype,
|
|
size: location.size,
|
|
}
|
|
return uploaded
|
|
}
|
|
|
|
/**
|
|
* Update the avatar of this portal to the given photo.
|
|
*
|
|
* @param {TelegramPuppet} telegramPOV The Telegram account whose point of view the avatar should be downloaded
|
|
* from, if necessary.
|
|
* @param {ChatPhoto} photo The Telegram {@link https://tjhorner.com/tl-schema/type/ChatPhoto ChatPhoto}
|
|
* object.
|
|
* @returns {boolean} Whether or not the photo was updated.
|
|
*/
|
|
async updateAvatar(telegramPOV, photo) {
|
|
if (!photo || this.peer.type === "user") {
|
|
return false
|
|
}
|
|
|
|
if (this.photo && this.avatarURL &&
|
|
this.photo.dc_id === photo.dc_id &&
|
|
this.photo.volume_id === photo.volume_id &&
|
|
this.photo.local_id === photo.local_id) {
|
|
return false
|
|
}
|
|
|
|
const file = await telegramPOV.getFile(photo)
|
|
const name = `${photo.volume_id}_${photo.local_id}.${file.extension}`
|
|
|
|
const uploaded = await this.app.botIntent.getClient().uploadContent({
|
|
stream: file.buffer,
|
|
name,
|
|
type: file.mimetype,
|
|
}, { rawResponse: false })
|
|
|
|
this.avatarURL = uploaded.content_uri
|
|
this.photo = {
|
|
dc_id: photo.dc_id,
|
|
volume_id: photo.volume_id,
|
|
local_id: photo.local_id,
|
|
}
|
|
|
|
await this.app.botIntent.setRoomAvatar(this.roomID, this.avatarURL)
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Load the access hash for the given puppet.
|
|
*
|
|
* @param {TelegramPuppet} telegramPOV The puppet whose access hash to load.
|
|
* @returns {boolean} As specified by {@link TelegramPeer#loadAccessHash(app, telegramPOV)}.
|
|
*/
|
|
loadAccessHash(telegramPOV) {
|
|
return this.peer.loadAccessHash(this.app, telegramPOV, { portal: this })
|
|
}
|
|
|
|
/**
|
|
* Handle a Telegram typing event.
|
|
*
|
|
* @param {Object} evt The custom event object.
|
|
* @param {number} evt.from The ID of the Telegram user who is typing.
|
|
* @param {TelegramPeer} evt.to The peer where the user is typing.
|
|
* @param {TelegramPuppet} evt.source The source where this event was captured.
|
|
*/
|
|
async handleTelegramTyping(evt) {
|
|
if (!this.isMatrixRoomCreated()) {
|
|
return
|
|
}
|
|
const typer = await this.app.getTelegramUser(evt.from)
|
|
// The Intent API currently doesn't allow you to set the
|
|
// typing timeout. Once it does, we should set it to ~5.5s
|
|
// as Telegram resends typing notifications every 5 seconds.
|
|
typer.intent.sendTyping(this.roomID, true/*, 5500*/)
|
|
}
|
|
|
|
/**
|
|
* Handle a Telegram service message event.
|
|
*
|
|
* @param {Object} evt The custom event object.
|
|
* @param {number} evt.from The ID of the Telegram user who caused the service message.
|
|
* @param {TelegramPeer} evt.to The peer to which the message was sent.
|
|
* @param {TelegramPuppet} evt.source The source where this event was captured.
|
|
* @param {MessageAction} evt.action The Telegram {@link https://tjhorner.com/tl-schema/type/MessageAction
|
|
* MessageAction} object.
|
|
*/
|
|
async handleTelegramServiceMessage(evt) {
|
|
if (!this.isMatrixRoomCreated()) {
|
|
if (evt.action._ === "messageActionChatDeleteUser") {
|
|
// We don't care about user deletions on chats without portals
|
|
return
|
|
}
|
|
this.app.debug("magenta", "Service message received, creating room for", evt.to.id)
|
|
await this.createMatrixRoom(evt.source, { invite: [evt.source.matrixUser.userID] })
|
|
return
|
|
}
|
|
let matrixUser, telegramUser
|
|
switch (evt.action._) {
|
|
case "messageActionChatCreate":
|
|
// Portal gets created at beginning if it doesn't exist
|
|
// Falls through to invite everyone in initial user list
|
|
case "messageActionChatAddUser":
|
|
for (const userID of evt.action.users) {
|
|
matrixUser = await this.app.getMatrixUserByTelegramID(userID)
|
|
if (matrixUser) {
|
|
matrixUser.join(this)
|
|
this.inviteMatrix(matrixUser.userID)
|
|
}
|
|
telegramUser = await this.app.getTelegramUser(userID)
|
|
telegramUser.intent.join(this.roomID)
|
|
}
|
|
break
|
|
case "messageActionChannelCreate":
|
|
// Portal gets created at beginning if it doesn't exist
|
|
// Channels don't send initial user lists 3:<
|
|
break
|
|
case "messageActionChatMigrateTo":
|
|
this.peer.id = evt.action.channel_id
|
|
this.peer.type = "channel"
|
|
const accessHash = await this.peer.fetchAccessHashFromServer(evt.source)
|
|
if (!accessHash) {
|
|
console.error("Failed to fetch access hash for mirgrated channel!")
|
|
break
|
|
}
|
|
this.accessHashes.set(evt.source.userID, accessHash)
|
|
await this.save()
|
|
const sender = await this.app.getTelegramUser(evt.from)
|
|
await sender.sendEmote(this.roomID, "upgraded this group to a supergroup.")
|
|
break
|
|
case "messageActionChatDeleteUser":
|
|
matrixUser = await this.app.getMatrixUserByTelegramID(evt.action.user_id)
|
|
if (matrixUser) {
|
|
matrixUser.leave(this)
|
|
this.kickMatrix(matrixUser.userID, "Left Telegram chat")
|
|
}
|
|
telegramUser = await this.app.getTelegramUser(evt.action.user_id)
|
|
telegramUser.intent.leave(this.roomID)
|
|
break
|
|
case "messageActionChatEditPhoto":
|
|
const sizes = evt.action.photo.sizes
|
|
let largestSize = sizes[0]
|
|
let largestSizePixels = largestSize.w * largestSize.h
|
|
for (const size of sizes) {
|
|
const pixels = size.w * size.h
|
|
if (pixels > largestSizePixels) {
|
|
largestSizePixels = pixels
|
|
largestSize = size
|
|
}
|
|
}
|
|
// TODO once permissions are synced, make the avatar change event come from the user who changed the avatar
|
|
await this.updateAvatar(evt.source, largestSize.location)
|
|
break
|
|
case "messageActionChatEditTitle":
|
|
this.peer.title = evt.action.title
|
|
await this.save()
|
|
const intent = await this.getMainIntent()
|
|
await intent.setRoomName(this.roomID, this.peer.title)
|
|
break
|
|
default:
|
|
this.app.warn("Unhandled service message of type", evt.action._)
|
|
this.app.warn(JSON.stringify(evt.action, "", " "))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Context: Matrix user X is logged into mautrix-telegram and has a private chat portal room with Telegram user Y.
|
|
* X sends message to Y from another Telegram client.
|
|
*
|
|
* Problem: We can't control X's Matrix account. We also can't make sure that X's Telegram account's Matrix puppet
|
|
* is always in private chat portal rooms, since X could create a private chat portal by inviting Y's
|
|
* puppet without giving it, the only AS-controllable user in the room, any power.
|
|
*
|
|
* Solution: When encountering an error caused by the above situation, this function is called.
|
|
* This function first tries to invite X's Matrix puppet to the room.
|
|
* If that fails, text messages are sent through the other user as notices and other messages are dropped.
|
|
*
|
|
* @param {Object} evt The custom event object (see #handleTelegramMessage(evt))
|
|
* @param {TelegramUser} sender The Telegram user object of the sender.
|
|
* @returns {boolean} Whether or not the puppet for the sender was successfully invited.
|
|
*/
|
|
async tryFixPrivateChatForOutgoingMessage(evt, sender) {
|
|
try {
|
|
const intent = await this.getMainIntent()
|
|
await intent.invite(this.roomID, sender.mxid)
|
|
return true
|
|
} catch (_) {
|
|
const receiver = await this.app.getTelegramUser(evt.to.id, { createIfNotFound: false })
|
|
if (receiver) {
|
|
if (evt.text) {
|
|
receiver.sendNotice(this.roomID, `[Your message from another client] ${evt.text}`)
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Handle a Telegram service message event.
|
|
*
|
|
* @param {Object} evt The custom event object.
|
|
* @param {number} evt.from The ID of the Telegram user who caused the service message.
|
|
* @param {TelegramPeer} evt.to The peer to which the message was sent.
|
|
* @param {TelegramPuppet} evt.source The source where this event was captured.
|
|
* @param {string} evt.text The text in the message.
|
|
* @param {string} [evt.caption] The image/file caption.
|
|
* @param {MessageEntity[]} [evt.entities] The Telegram {@link https://tjhorner.com/tl-schema/type/MessageEntity
|
|
* formatting entities} in the message.
|
|
* @param {messageMediaPhoto} [evt.photo] The Telegram {@link https://tjhorner.com/tl-schema/constructor/messageMediaPhoto Photo} attached to the message.
|
|
* @param {messageMediaDocument} [evt.document] The Telegram {@link https://tjhorner.com/tl-schema/constructor/messageMediaDocument Document} attached to the message.
|
|
* @param {messageMediaGeo} [evt.geo] The Telegram {@link https://tjhorner.com/tl-schema/constructor/messageMediaGeo Location} attached to the message.
|
|
*/
|
|
async handleTelegramMessage(evt) {
|
|
const a = Object.assign({}, evt)
|
|
delete a.source
|
|
if (!this.isMatrixRoomCreated()) {
|
|
try {
|
|
const result = await this.createMatrixRoom(evt.source, { invite: [evt.source.matrixUser.userID] })
|
|
if (!result.roomID) {
|
|
return
|
|
}
|
|
} catch (err) {
|
|
console.error("Error creating room:", err)
|
|
console.error(err.stack)
|
|
return
|
|
}
|
|
}
|
|
|
|
const sender = await this.app.getTelegramUser(evt.from)
|
|
try {
|
|
await sender.intent.sendTyping(this.roomID, false)
|
|
} catch (err) {
|
|
if (evt.to.type === "user") {
|
|
if (!await this.tryFixPrivateChatForOutgoingMessage(evt, sender)) {
|
|
return
|
|
}
|
|
await sender.intent.sendTyping(this.roomID, false)
|
|
} else {
|
|
throw err
|
|
}
|
|
}
|
|
|
|
if (evt.text && evt.text.length > 0) {
|
|
if (evt.entities) {
|
|
evt.html = formatter.telegramToMatrix(evt.text, evt.entities, this.app)
|
|
sender.sendHTML(this.roomID, evt.html)
|
|
} else {
|
|
sender.sendText(this.roomID, evt.text)
|
|
}
|
|
}
|
|
|
|
if (evt.photo) {
|
|
const photo = await this.copyTelegramPhoto(evt.source, sender, evt.photo)
|
|
photo.name = evt.caption || "Uploaded photo"
|
|
sender.sendFile(this.roomID, photo)
|
|
} else if (evt.document) {
|
|
// TODO handle stickers better
|
|
const file = await this.copyTelegramFile(evt.source, sender, evt.document)
|
|
if (evt.caption) {
|
|
file.name = evt.caption
|
|
} else if (file.matrixtype === "m.audio") {
|
|
file.name = "Uploaded audio"
|
|
} else if (file.matrixtype === "m.video") {
|
|
file.name = "Uploaded video"
|
|
} else {
|
|
file.name = "Uploaded document"
|
|
}
|
|
sender.sendFile(this.roomID, file)
|
|
} else if (evt.geo) {
|
|
sender.sendLocation(this.roomID, evt.geo)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle a Matrix event.
|
|
*
|
|
* @param {MatrixUser} sender The user who sent the message.
|
|
* @param {Object} evt The {@link https://matrix.org/docs/spec/client_server/r0.3.0.html#event-structure
|
|
* Matrix event}.
|
|
*/
|
|
async handleMatrixEvent(sender, evt) {
|
|
await this.loadAccessHash(sender.telegramPuppet)
|
|
switch (evt.content.msgtype) {
|
|
case "m.text":
|
|
const { message, entities } = formatter.matrixToTelegram(
|
|
evt.content.formatted_body || evt.content.body,
|
|
evt.content.format === "org.matrix.custom.html",
|
|
this.app)
|
|
await sender.telegramPuppet.sendMessage(this.peer, message, entities)
|
|
break
|
|
case "m.video":
|
|
case "m.audio":
|
|
case "m.file":
|
|
// TODO upload document
|
|
//break
|
|
case "m.image":
|
|
const intent = await this.getMainIntent()
|
|
await intent.sendMessage(this.roomID, {
|
|
msgtype: "m.notice",
|
|
body: "Sending files is not yet supported.",
|
|
})
|
|
break
|
|
case "m.location":
|
|
const [, lat, long] = /geo:([-]?[0-9]+\.[0-9]+)+,([-]?[0-9]+\.[0-9]+)/.exec()
|
|
await sender.telegramPuppet.sendMedia(this.peer, {
|
|
_: "inputMediaGeoPoint",
|
|
geo_point: {
|
|
_: "inputGeoPoint",
|
|
lat: +lat,
|
|
long: +long,
|
|
},
|
|
})
|
|
break
|
|
default:
|
|
this.app.warn("Unhandled event:", JSON.stringify(evt, "", " "))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @returns {boolean} Whether or not a Matrix room has been created for this Portal.
|
|
*/
|
|
isMatrixRoomCreated() {
|
|
return !!this.roomID
|
|
}
|
|
|
|
/**
|
|
* Get the primary intent object for this Portal.
|
|
*
|
|
* For groups and channels, this is always the AS bot intent.
|
|
* For private chats, it is the intent of the other user.
|
|
*
|
|
* @returns {Intent} The primary intent.
|
|
*/
|
|
async getMainIntent() {
|
|
return this.peer.type === "user"
|
|
? (await this.app.getTelegramUser(this.peer.id)).intent
|
|
: this.app.botIntent
|
|
}
|
|
|
|
async inviteTelegram(telegramPOV, user) {
|
|
if (this.peer.type === "chat") {
|
|
const updates = await telegramPOV.client("messages.addChatUser", {
|
|
chat_id: this.peer.id,
|
|
user_id: user.toPeer(telegramPOV).toInputObject(),
|
|
fwd_limit: 50,
|
|
})
|
|
this.app.debug("green", "Chat invite result:", JSON.stringify(updates, "", " "))
|
|
} else if (this.peer.type === "channel") {
|
|
const updates = await telegramPOV.client("channels.inviteToChannel", {
|
|
channel: this.peer.toInputObject(),
|
|
users: [user.toPeer(telegramPOV).toInputObject()],
|
|
})
|
|
this.app.debug("green", "Channel invite result:", JSON.stringify(updates, "", " "))
|
|
} else {
|
|
throw new Error(`Can't invite user to peer type ${this.peer.type}`)
|
|
}
|
|
}
|
|
|
|
async kickTelegram(telegramPOV, user) {
|
|
let updates
|
|
if (this.peer.type === "chat") {
|
|
updates = await telegramPOV.client("messages.deleteChatUser", {
|
|
chat_id: this.peer.id,
|
|
user_id: user.toPeer(telegramPOV).toInputObject(),
|
|
})
|
|
} else if (this.peer.type === "channel") {
|
|
this.loadAccessHash(telegramPOV)
|
|
updates = await telegramPOV.client("channels.kickFromChannel", {
|
|
channel: this.peer.toInputObject(),
|
|
user_id: user.toPeer(telegramPOV).toInputObject(),
|
|
kicked: true,
|
|
})
|
|
} else {
|
|
throw new Error(`Can't invite user to peer type ${this.peer.type}`)
|
|
}
|
|
await telegramPOV.handleUpdate(updates)
|
|
}
|
|
|
|
/**
|
|
* Invite one or more Matrix users to this Portal.
|
|
*
|
|
* @param {string[]|string} users The MXID or list of MXIDs to invite.
|
|
*/
|
|
async inviteMatrix(users) {
|
|
const intent = await this.getMainIntent()
|
|
// TODO check membership before inviting?
|
|
if (Array.isArray(users)) {
|
|
for (const userID of users) {
|
|
if (typeof userID === "string") {
|
|
try {
|
|
await intent.invite(this.roomID, userID)
|
|
} catch (err) {
|
|
if (err.httpStatus !== 403) {
|
|
console.error(`Failed to invite ${userID} to ${this.roomID}:`)
|
|
console.error(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (typeof users === "string") {
|
|
try {
|
|
await intent.invite(this.roomID, users)
|
|
} catch (err) {
|
|
if (err.httpStatus !== 403) {
|
|
console.error(`Failed to invite ${users} to ${this.roomID}:`)
|
|
console.error(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Kick one or more Matrix users from this Portal.
|
|
*
|
|
* @param {string[]|string} users The MXID or list of MXIDs to kick.
|
|
* @param {string} reason The reason for kicking the user(s).
|
|
*/
|
|
async kickMatrix(users, reason) {
|
|
const intent = await this.getMainIntent()
|
|
if (Array.isArray(users)) {
|
|
for (const userID of users) {
|
|
if (typeof userID === "string") {
|
|
intent.kick(this.roomID, users, reason)
|
|
}
|
|
}
|
|
} else if (typeof users === "string") {
|
|
intent.kick(this.roomID, users, reason)
|
|
}
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
async upgradeTelegramChat(telegramPOV) {
|
|
if (this.peer.type !== "chat") {
|
|
throw new Error("Can't upgrade non-chat portal.")
|
|
}
|
|
const updates = await telegramPOV.client("messages.migrateChat", {
|
|
chat_id: this.id,
|
|
})
|
|
await telegramPOV.handleUpdate(updates)
|
|
}
|
|
|
|
/**
|
|
* Create a Matrix room for this portal.
|
|
*
|
|
* @param {TelegramPuppet} telegramPOV
|
|
* @param {string|string[] invite
|
|
* @param {boolean} inviteEvenIfNotCreated
|
|
* @returns {{created: boolean, roomID: string}}
|
|
*/
|
|
async createMatrixRoom(telegramPOV, { invite = [], inviteEvenIfNotCreated = true } = {}) {
|
|
if (this.roomID) {
|
|
if (invite && inviteEvenIfNotCreated) {
|
|
await this.inviteMatrix(invite)
|
|
}
|
|
return {
|
|
created: false,
|
|
roomID: this.roomID,
|
|
}
|
|
}
|
|
if (this.creatingMatrixRoom) {
|
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
return {
|
|
created: false,
|
|
roomID: this.roomID,
|
|
}
|
|
}
|
|
this.creatingMatrixRoom = true
|
|
|
|
if (!await this.loadAccessHash(telegramPOV)) {
|
|
this.creatingMatrixRoom = false
|
|
throw new Error(`Failed to load access hash for ${this.peer.type} ${this.peer.username || this.peer.id}.`)
|
|
}
|
|
|
|
let room, info, users
|
|
try {
|
|
({ info, users } = await this.peer.getInfo(telegramPOV))
|
|
if (this.peer.type === "chat") {
|
|
room = await this.app.botIntent.createRoom({
|
|
options: {
|
|
name: info.title,
|
|
topic: info.about,
|
|
visibility: "private",
|
|
invite,
|
|
},
|
|
})
|
|
} else if (this.peer.type === "channel") {
|
|
room = await this.app.botIntent.createRoom({
|
|
options: {
|
|
name: info.title,
|
|
topic: info.about,
|
|
visibility: info.username ? "public" : "private",
|
|
room_alias_name: info.username
|
|
? this.app.config.bridge.alias_template.replace("${NAME}", info.username)
|
|
: undefined,
|
|
invite,
|
|
},
|
|
})
|
|
} else if (this.peer.type === "user") {
|
|
const user = await this.app.getTelegramUser(info.id)
|
|
await user.updateInfo(telegramPOV, info, { updateAvatar: true })
|
|
room = await user.intent.createRoom({
|
|
createAsClient: true,
|
|
options: {
|
|
name: this.peer.id === this.peer.receiverID
|
|
? "Saved Messages (Telegram)"
|
|
: undefined, //user.getDisplayName(),
|
|
topic: "Telegram private chat",
|
|
visibility: "private",
|
|
invite,
|
|
},
|
|
})
|
|
} else {
|
|
this.creatingMatrixRoom = false
|
|
throw new Error(`Unrecognized peer type: ${this.peer.type}`)
|
|
}
|
|
} catch (err) {
|
|
this.creatingMatrixRoom = false
|
|
throw err instanceof Error ? err : new Error(err)
|
|
}
|
|
|
|
this.roomID = room.room_id
|
|
this.creatingMatrixRoom = false
|
|
this.app.portalsByRoomID.set(this.roomID, this)
|
|
await this.save()
|
|
if (this.peer.type !== "user") {
|
|
try {
|
|
await this.syncTelegramUsers(telegramPOV, users)
|
|
if (info.photo && info.photo.photo_big) {
|
|
await this.updateAvatar(telegramPOV, info.photo.photo_big)
|
|
}
|
|
} catch (err) {
|
|
console.error(err)
|
|
if (err instanceof Error) {
|
|
console.error(err.stack)
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
created: true,
|
|
roomID: this.roomID,
|
|
}
|
|
}
|
|
|
|
async updateInfo(telegramPOV, dialog) {
|
|
if (!dialog) {
|
|
this.app.warn("updateInfo called without dialog data")
|
|
const { user } = this.peer.getInfo(telegramPOV)
|
|
if (!user) {
|
|
throw new Error("Dialog data not given and fetching data failed")
|
|
}
|
|
dialog = user
|
|
}
|
|
let changed = false
|
|
if (this.peer.type === "channel") {
|
|
if (telegramPOV && this.accessHashes.get(telegramPOV.userID) !== dialog.access_hash) {
|
|
this.accessHashes.set(telegramPOV.userID, dialog.access_hash)
|
|
changed = true
|
|
}
|
|
}
|
|
if (this.peer.type === "user") {
|
|
const user = await this.app.getTelegramUser(this.peer.id)
|
|
await user.updateInfo(telegramPOV, dialog)
|
|
} else if (dialog.photo && dialog.photo.photo_big) {
|
|
changed = await this.updateAvatar(telegramPOV, dialog.photo.photo_big) || changed
|
|
}
|
|
changed = this.peer.updateInfo(dialog) || changed
|
|
if (changed) {
|
|
this.save()
|
|
}
|
|
return changed
|
|
}
|
|
|
|
/**
|
|
* Convert this Portal into a database entry.
|
|
*
|
|
* @returns {Object} A room store database entry.
|
|
*/
|
|
toEntry() {
|
|
return {
|
|
type: this.type,
|
|
id: this.id,
|
|
receiverID: this.receiverID,
|
|
roomID: this.roomID,
|
|
data: {
|
|
peer: this.peer.toSubentry(),
|
|
photo: this.photo,
|
|
avatarURL: this.avatarURL,
|
|
accessHashes: this.peer.type === "channel"
|
|
? Array.from(this.accessHashes)
|
|
: undefined,
|
|
},
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save this Portal to the database.
|
|
*/
|
|
save() {
|
|
return this.app.putRoom(this)
|
|
}
|
|
}
|
|
|
|
module.exports = Portal
|