UNPKG

adaptorex

Version:

Connect all your live interactive storytelling devices and software

1,454 lines (1,346 loc) 42.6 kB
/** * @requires plugin * @requires client * @requires logic * * @requires file * * @module telegram/telegram * @copyright Lasse Marburg 2023 * @license MIT */ const file = require("../../file.js") const logic = require("../logic/logic.js") const messenger = require("../messenger.js") var schema = require("./schema.json") const path = require("path") const client = require("./client.js") /** @typedef {import('./client.js').Account} Account */ /** @typedef {import('../../game.js').Game} Game */ /** @typedef {import('../../session.js').Session} Session */ /** * interface to telegram API using telethon python library through child process * * @class Telegram * @extends {messenger.Messenger} */ class Telegram extends messenger.Messenger { constructor() { super(schema, "telegram.id") } /** * if not exists, create chats collection * adjust account and bot add functions * run init * * @returns {Promise} - resolves with current schema when ready */ setup(config, game) { return new Promise((resolve, reject) => { file.mkdir(game.data + "/telegram_sessions") super .setup(config, game, { accounts: client.Account, bots: client.Bot }) .then((result) => { return this.game.addDataCollection("chats") }) .then((result) => { this.addDialogTemplate("onMessage") this.addTemplate("editChat", (payload, action) => { const title = `telegram ${action.name}` const regex = new RegExp(`${action.plugin}\.[a-zA-Z]*\.`) let subtitle = `Edit ${payload.chat} with ${payload.agent?.replace(regex, "")}` const body = [] if (payload.add_members) { body.push({ text: `add members: ${payload.add_members.join(", ")}` }) } if (payload.remove_members) { body.push({ text: `remove members: ${payload.remove_members.join(", ")}` }) } if (payload.title) { body.push({ text: `title: ${payload.title}` }) } if (payload.photo) { body.push({ text: `photo: ${payload.photo}` }) } if (payload.permissions) { body.push({ text: `change permissions` }) } return { title, subtitle, body } }) this.addTemplate("buttons", (payload, action) => { const title = `${action.plugin} ${action.name}` const regex = new RegExp(`${action.plugin}\.[a-zA-Z]*\.`) let subtitle = `${payload.type} buttons from ${payload.agent?.replace(regex, "")} to ${payload.to}` const body = [{ text: payload.message }] if (payload.buttons && payload.buttons.length) { for (const [i, row] of payload.buttons.entries()) { if (row && row.length) { for (const [b, button] of row.entries()) { text = `${i + 1}.${b + 1}. ${button.label}` if (button.respond) { text += ` (${button.respond.length} ${button.respond.length == 1 ? "reply" : "replies"})` } body.push({ text: text, next: button.next }) } } } } return { title, subtitle, body } }) return this.load({}) }) .then((result) => { return resolve(this.schema) }) .catch((error) => { return reject(error) }) }) } /** * quit and rerun init if settings changed * * @param {Object} settings - api id and api hash for telegram developer account * */ async update(settings) { if ( settings.api_hash != this.settings.api_hash || settings.api_id != this.settings.api_id ) { this.settings.api_id = settings.api_id // assign separate values to not overwrite settings reference. Only this way values will also change for item. this.settings.api_hash = settings.api_hash for (let account in this.accounts.items) { this.log.info( "Reconnecting account " + this.accounts.items[account].name ) await this.accounts.items[account].setup() await this.accounts.items[account].connect() } for (let bot in this.bots.items) { this.log.info("Reconnecting bot " + this.bots.items[bot].name) await this.bots.items[bot].setup() await this.bots.items[bot].connect() } } } async sendMessage(data, session) { let msg = new Message(data, session, this.game) await msg.setup([this.accounts, this.bots]) await msg.sendMessage() } async sendFile(data, session) { let msg = new Message(data, session, this.game) await msg.setup([this.accounts, this.bots]) await msg.sendFile() } async sendGeo(data, session) { let msg = new Message(data, session, this.game) await msg.setup([this.accounts, this.bots]) await msg.sendGeo() } async sendPoll(data, session) { let msg = new Message(data, session, this.game) await msg.setup([this.accounts, this.bots]) await msg.sendPoll() } async castVote(data, session) { let msg = new Message(data, session, this.game) await msg.setup([this.accounts, this.bots]) await msg.sendVote() } async sendQuiz(data, session) { let msg = new Message(data, session, this.game) await msg.setup([this.accounts, this.bots]) await msg.sendQuiz() } async buttons(data, session) { let route = "button" if (data.type == "keyboard") { route = "message" data.if = [] for (let row of data.buttons) { for (let button of row) { if (button.respond || button.next) { if (!data.if) { data.if = [] } data.if.push({ equals: button.label, case_sensitive: true, respond: button.respond, next: button.next }) } } } } let dialog = new ButtonDialog(data, session, this.game, route) await dialog.setup([this.bots]) // Do not (re-)send buttons on restart if action is muted if (session.action.status == "active") { await dialog.sendButtons(data) } return { cancel: dialog.cancel.bind(dialog), mute: dialog.mute.bind(dialog), unmute: dialog.unmute.bind(dialog) } } async onMessage(data, session) { let dialog = new Dialog(data, session, this.game) await dialog.setup([this.accounts, this.bots]) return { cancel: dialog.cancel.bind(dialog), mute: dialog.mute.bind(dialog), unmute: dialog.unmute.bind(dialog) } } async createChat(data, session) { let chat = new Chat(data, session, this.game) await chat.setup([this.accounts, this.bots]) await chat.create() } async editChat(data, session) { let chat = new Chat(data, session, this.game) await chat.setup([this.accounts, this.bots]) await chat.edit() } command(input) { switch (input[0]) { default: super.command(input) break } } } /** * temporary session of two members sending 1-n messages/responses back and forth. * * requires an agent that is an existing account and a reference to a chat document. * Referencing a player is possible. The player needs a telegram.id or at least a phone_number property. * * after resolving the references in setup: * - this.chat is the chat document object. * - this.chat_id is the chat or player telegram id or phone_number * * * @class Conversation */ class Conversation extends messenger.Conversation { constructor(data, session, game, route) { super(data, session, game, route) /** * The telegram client account API * @type {Account} */ this.agent if (this.hasOwnProperty("chat")) { this.chat_reference = this.chat delete this.chat } else if (this.hasOwnProperty("to")) { this.chat_reference = this.to } if (this.hasOwnProperty("agent")) { this.agent_reference = this.agent delete this.agent } this.game = game } /** * get telegram chat compatible identifier. * * if the identified document has a `telegram_contacts` property it is treated as a telegram user. * getId will extract as many information as possible from the user and try to add it as a contact as long as that didn't happen before. * * Different accounts will all access the same telegram property. Adding the accounts name to `telegram_contacts` will make sure, * that only accounts that did not have contact with the user before will try to add the user to their contacts. * * @param {Object} document - player or chat item document * @param {string} [reference] - variable reference to player or chat item. Defaults to this.chat_reference * @returns identifier for telethon chat. undefined if no id could be found */ async getId(document, reference) { if (!reference) { reference = this.chat_reference } let agent_is_bot = this.agent instanceof client.Bot if (document.hasOwnProperty("telegram")) { if (document.hasOwnProperty("telegram_contacts")) { if (!document.telegram_contacts.includes(this.agent.name)) { let user = {} if (!document.telegram.phone) { try { user = await this.agent.getUser(document.telegram.id) } catch (error) { if ( (!agent_is_bot && error.message.startsWith("Cannot cast")) || error.message.startsWith("Could not find") ) { this.session.log.warn( `Could not get user\n${error.message}\nRetry after loading conversations with getDialogs.` ) let dialogs = await this.agent.getDialogs() // Get Open conversations to make sure the new user is recognizable this.session.log( `Fetched ${dialogs.length} conversations to find unknown user ${document.telegram.id} after getUser failed.` ) user = await this.agent.getUser(document.telegram.id) } else { throw error } } let user_update = { telegram: user } if (user.hasOwnProperty("id")) { user_update.telegram.id = this.longID(user.id) } if (user.first_name && !document.hasOwnProperty("name")) { user_update["name"] = user.first_name } await this.session.variables.set(reference, user_update) } else { user = document.telegram } if (!user.last_name) { user["last_name"] = "" } if (!agent_is_bot && user.phone) { let contact try { contact = await this.agent.addContact(user) } catch (error) { if ( error.message.startsWith("Cannot cast") || error.message.startsWith("Could not find") ) { this.session.log.warn( `Could not add contact \n${error.message}\nWill not skip followup actions!` ) let dialogs = await this.agent.getDialogs() // Get Open conversations to make sure the new user is recognizable this.session.log( `Fetched ${dialogs.length} conversations to find recently added contact ${document.telegram.id} after addContact failed.` ) } else { throw error } } if (contact && contact.hasOwnProperty("id")) { let contact_update = { telegram: contact } if (contact.first_name && !document.hasOwnProperty("name")) { contact_update["name"] = contact.first_name } if (!document.hasOwnProperty("phone_number")) { contact_update["phone_number"] = "+" + contact.phone } contact_update.telegram.id = this.longID(contact.id) await this.session.variables.set(reference, contact_update) await this.session.variables.push(reference, { telegram_contacts: this.agent.name }) this.session.log.info("add telegram contact for " + contact.phone) } else { this.session.log.error( "Failed to add telegram contact " + document._id + ". Maybe due to user privacy settings." ) this.session.log.error(user) } } else if (agent_is_bot && user.first_name) { await this.session.variables.push(reference, { telegram_contacts: this.agent.name }) } else { this.session.log( `Could not add ${document.telegram.id} (document ${document._id} ${document.name || ""}) to telegram contacts. Maybe due to user privacy settings.` ) // this.session.log(user) } } } if (document.telegram.hasOwnProperty("id")) { return this.agent.getPeerTypeId(document.telegram) } else { throw ( "Document with telegram property is missing telegram id. " + document._id ) } } else if ( !agent_is_bot && document.hasOwnProperty("phone_number") && document.hasOwnProperty("name") ) { let data = { phone: document.phone_number } if (document.hasOwnProperty("name")) { data["first_name"] = document.name } else { data["first_name"] = document._id } if (document.hasOwnProperty("last_name")) { data["last_name"] = document.last_name } else { data["last_name"] = "" } let result = await this.agent.addContact(data) let result_update = { telegram: result } if (result.hasOwnProperty("id")) { result_update.telegram.id = this.longID(result.id) } await this.session.variables.set(reference, result_update) this.session.log.info("add telegram contact for " + document.phone_number) this.session.log.info(result) return this.agent.getPeerTypeId(result) } else { throw new adaptor.NotFoundError( "could not resolve chat id for document " + document._id ) } } /** * Convert IDs so they are stored properly to database * * mongodb requires to specifically set the data type for 64bit integers. * * @param {number|array<number>} id - telegram user or chat id * @returns {Object|array|number} mongodb Long int64 Object or number (nedb) */ longID(id) { if (Array.isArray(id)) { let id_list = [] for (let i of id) { id_list.push(this.game.db.toLong(i)) } return id_list } let long_id = this.game.db.toLong(id) return long_id } sendMessage(content) { return new Promise((resolve, reject) => { this.session.variables .review(content.text) .then((result) => { delete content.text content["message"] = result + "" if (content.message == "") { throw new adaptor.InvalidError( `${this.agent.name} can not send message to ${this.chat_id}. Text can not be empty` ) } let properties = Object.assign( { entity: this.chat_id }, content ) return this.agent.sendMessage(properties) }) .then((result) => { this.session.log.info( this.agent.name + " sent message to " + this.chat_id + ": " + content.message ) return resolve() }) .catch((err) => { return reject(err) }) }) } sendFile(content) { return new Promise((resolve, reject) => { this.session.variables .review(content.file) .then((result) => { content.file = this.game.getFilePath(result) if (content.caption) { return this.session.variables.review(content.caption) } else { return } }) .then((result) => { content["caption"] = result let properties = Object.assign( { entity: this.chat_id }, content ) this.session.log( "Send File " + content.file + " from " + this.agent.name + " to " + this.chat_id ) return this.agent.sendFile(properties) }) .then((result) => { return resolve() }) .catch((err) => { return reject(err) }) }) } async sendGeo(content) { let lat = await this.session.variables.review(content.latitude) let lon = await this.session.variables.review(content.longitude) if (typeof lat === "string") { lat = parseFloat(lat) } if (typeof lon === "string") { lon = parseFloat(lon) } await this.agent.sendGeo({ entity: this.chat_id, latitude: lat, longitude: lon }) this.session.log( this.agent.name + " sent Geo Location " + lon + " " + lat + " to " + this.chat_id ) } async sendPoll(content) { let question = await this.session.variables.review(content.question) let answers = await this.session.variables.findAndReplace(content.answers) if (content.correct_answer) { content.correct_answer = await this.session.variables.review( content.correct_answer ) } let poll_id = Math.floor(Math.random() * (99999 - 10000) + 10000) let public_voters = !content.private let result = await this.agent.sendPoll({ entity: this.chat_id, question: question, answers: answers, correct_answer: content.correct_answer, id: poll_id, public_voters: public_voters, multiple_choice: content.multiple_choice, quiz: content.quiz }) if (this.agent.telegram && this.agent.telegram.id) { content["from_id"] = this.agent.telegram.id content["from_voted"] = false } content["id"] = this.longID(result.media.poll.id) content["msg_id"] = result.id content["voters_count"] = 0 content["ambiguous"] = true let update = {} update["polls." + content.name] = content await this.session.variables.set(this.chat_reference, update) this.session.log( this.agent.name + " sent Poll " + result.id + " to " + this.chat_reference + ": " + content.question ) } /** * Cast a vote in one of the polls that where stored in a chat document * * Figures out what poll to cast vote in and maps selected option to option index * * @param {object} data - sendVote message parameters */ async sendVote(data) { let poll = await this.session.variables.review(data.poll) if (!this.chat.polls) { throw new adaptor.InvalidError( "Can not cast vote for poll " + poll + ". There is no poll in chat Item " + this.chat_reference ) } if (!this.chat.polls[poll]) { throw new adaptor.InvalidError( "Can not cast vote for poll " + poll + ". Poll could not be found in chat Item " + this.chat_reference ) } let poll_data = this.chat.polls[poll] let options_index = [] for (let option of this.options) { if (isNaN(option)) { option = await this.session.variables.review(option) for (let i = 0; i < poll_data.answers.length; i++) { if (poll_data.answers[i].includes(option)) { options_index.push(i) break } } } else { options_index.push(option) } } this.session.log.info( this.agent.name + " cast vote for option(s) " + options_index + " in poll " + poll + " msg id " + poll_data.msg_id + " chat id " + this.chat_id ) let result = await this.agent.sendVote({ peer: this.chat_id, options: options_index, msg_id: poll_data.msg_id }) if (result.updates) { if (this.agent.telegram && poll_data.from_id == this.agent.telegram.id) { result.updates[0].account_voted = true } await this.agent.pollUpdate(result.updates[0]) } } async sendButtons(content) { let message = await this.session.variables.review(content.message) /** * alternative with row indicator: * * let buttons = [[]] * for(let b of content.buttons) { * if(typeof b.row === "number") { * while(b.row >= buttons.length) { * buttons.push([]) * } * buttons[b.row].push(await this.session.variables.review(b.label)) * } else { * buttons[0].push(await this.session.variables.review(b.label)) * } * } * */ let buttons = [] for (const [i, row] of content.buttons.entries()) { buttons.push([]) for (const button of row) { let label = await this.session.variables.review(button.label) button.label = label buttons[i].push(label) } } await this.agent.sendButtons({ entity: this.chat_id, message: message, buttons: buttons, type: content.type, typing: content.typing }) this.session.log( `${this.agent.name} sent ${content.type} Buttons to ${this.chat_id}: ${buttons}` ) } } /** * respond to incoming messages. Dialog is between 1 adaptor:ex account and 1 group chat or player. * * Each incoming message is queued to avoid conflicts and race conditions. */ class Dialog extends Conversation { constructor(data, session, game, route = "message") { super(data, session, game, route) /** stacks all incoming message data during this conversation */ this.history = [] } /** * * @param {Array<Account>} agents - telegram account items that where added to the plugin */ async setup(agents) { await super.setup(agents) } /** * compare incoming message with conditions. * * goto else response if there is no match. * * ignore if incoming groupchat message is not from the user specified in "from" * * cancel the "no answer" timeout responses if there is any type of response in if or else. * * @param {Object} properties - incoming message properties like text, date, from, to, media etc. * @returns {Promise<boolean>} - resolves true if there was some kind of match. false otherwise. */ async incomingMessage(properties) { this.history.push(properties) if (this.hasOwnProperty("from") && properties.peer_type == "chat") { let is_from = this.from_ids.some( (is_from_id) => is_from_id == properties.from_id ) if (!is_from) { this.session.log.trace( "ignore message from " + properties.from_id + " in chat " + this.chat_id + " expect message from " + properties.from_id + " instead" ) return false } } if (this.hasOwnProperty("if")) { for (let if_condition of this["if"]) { let condition = new DialogCondition( if_condition, properties, this.session, this.game ) let reaction = await condition.match() if (reaction) { await this.respond(reaction, properties) return true } } } if (this.hasOwnProperty("else")) { let reaction = DialogCondition.getReaction(this["else"], this.session) await this.respond(reaction, properties) return true } else { let content = properties.text if ("media" in properties) { content = properties.media } this.session.log( 'no match on message "' + content + '" from ' + this.chat_id + " and no else defined" ) return false } } /** * Respond according to reaction object. Maybe: * - run "next" callback * - download media * - respond to message * - reply to message * * message and response details are stored to dialog level variable: * **match** store what part of the message triggered next cue * **download** store download path and media type * * @param {Object} reaction - object that stores how to react on incoming message * @param {Object} message - message details * @returns {Promise} - resolves undefined once all is done */ async respond(reaction, message) { this.cancelDelayedMessages() if ("download" in reaction) { if ( ![ "photo", "image", "document", "audio", "voice", "video", "sticker" ].includes(message.media) ) { this.session.log.warn( `Can not download media. ${message.media} media type does not provide a file.` ) } else { let directory = await this.session.variables.review( reaction.download.dir ) directory = this.game.getFilePath(directory) file.mkdir(directory) let file_path = await this.agent.downloadMedia({ message: message.id, chat: this.chat_id, dir: directory }) if (reaction.download.file) { reaction.download.file = await this.session.variables.review( reaction.download.file ) let new_file_path = path.resolve( directory, reaction.download.file + path.parse(file_path).ext ) await file.mv(file_path, new_file_path) file_path = new_file_path } if (reaction.download.variable) { reaction.download.variable = await this.session.variables.review( reaction.download.variable ) this.session.variables.set(reaction.download.variable, file_path) } message["download"] = file_path } } if (message.peer) { message.peer = this.game.db.toLong(message.peer) } if (message.from_id) { message.from_id = this.game.db.toLong(message.from_id) } if ("match" in reaction) { message["match"] = reaction.match } await this.session.variables.store(message) if ("respond" in reaction) { await this.sendMessage({ text: reaction.respond }) } if ("reply" in reaction) { await this.sendMessage({ text: reaction.reply, reply_to: message.id }) } if ("next" in reaction) { this.session.next(reaction.next) } } } class ButtonDialog extends Dialog { constructor(data, session, game, route) { super(data, session, game, route) } async setup(agents) { await super.setup(agents) if (this.type == "inline") { this.listenerCallback = this.session.getCallback((properties) => { return new Promise((resolve, reject) => { this.queue.put(properties, (response) => { if (response && response.error) { return reject(response.error) } return resolve(response) }) }) }) /** queue of unresolved incoming message responses */ this.queue = new adaptor.Queue(this.buttonClick.bind(this)) this.routing = this.agent.on( this.route, { id: this.chat_id, priority: this.priority }, this.listenerCallback ) } } /** * compare incoming button with buttons list. * * ignore if incoming groupchat button click is not from the user specified in "from" * * cancel the "no answer" timeout responses if there is any type of response in if or else. * * @param {Object} properties - incoming message properties like text, date, from, to, media etc. * @returns {Promise<boolean>} - resolves true if there was some kind of match. false otherwise. */ async buttonClick(properties) { this.history.push(properties) if (this.hasOwnProperty("from") && properties.peer_type == "chat") { let is_from = this.from_ids.some( (is_from_id) => is_from_id == properties.from_id ) if (!is_from) { this.session.log.trace( "ignore message from " + properties.from_id + " in chat " + this.chat_id + " expect message from " + properties.from_id + " instead" ) return false } } if (this.hasOwnProperty("buttons")) { for (let row of this["buttons"]) { for (let button of row) { if (button.label == properties.button) { let reaction = DialogCondition.getReaction(button, this.session) reaction.match = button.label await this.respond(reaction, properties) } } } } } async cancel() { if (this.hasOwnProperty("buttons") && this.type == "inline") { this.agent.cancel(this.route, this.chat_id, this.routing) await this.queue.cancel() } else { this.agent.clearButtons({ entity: this.chat_id }) } await super.cancel() } } /** * allows to find media in message * * @param {Object} properties - condition properties * @class DialogCondition * @extends {logic.Condition} */ class DialogCondition extends logic.Condition { constructor(properties, value_properties, session, game) { super(properties, session, game) this.value = value_properties.text if (properties.hasOwnProperty("media")) { if (properties.media == "downloadable") { this.condition = [ "photo", "image", "document", "audio", "voice", "video", "sticker" ] } else if (properties.media == "any") { this.condition = [ "photo", "image", "document", "audio", "voice", "video", "sticker", "geo", "geolive", "contact", "poll", "webpage", "venue", "game", "dice", "invoice" ] } else { this.condition = properties.media } this.survey = this.equals this.name = "contains media" this.value = value_properties.media } this.properties = properties } feedback(result) { if (result) { let react = DialogCondition.getReaction(this.properties, this.session) react["match"] = result return react } return undefined } /** * Find out how to react to the current event. * * returns one of these options: * - what message text to send (as response or reply) * - to ignore * - to dispatch next cue (if there is no respond or if the last respond is called based on count * * @param {Object} react - stores information about if/how to react that are altered with each call of this function * * @returns {Object} - instructions on how to handle current response status. */ static getReaction(react, session) { let return_value = {} if (react.hasOwnProperty("download")) { return_value["download"] = react.download } if (react.respond && react.respond.length) { if (!react.hasOwnProperty("count")) { react["count"] = 0 } else { react.count++ } if ( react.count >= react.respond.length - 1 && react.hasOwnProperty("next") ) { if (react.next) { return_value["next"] = react.next } } if (react.count < react.respond.length) { if (typeof react.respond[react.count] === "string") { return_value["respond"] = react.respond[react.count] } else if (react.respond[react.count].mode) { let mode = react.respond[react.count].mode return_value[mode] = react.respond[react.count].text } else { session.log.error("failed to create message response.") return { type: "ignore" } } return return_value } session.log("No response left. Ignoring no. " + react.count) return { type: "ignore" } } else { react["count"] = 0 if (react.hasOwnProperty("next")) { return_value["next"] = react.next } return_value["type"] = "ignore" return return_value } } } /** * Create and Edit Telegram Chatgroups * * @class Chat * @extends {Conversation} */ class Chat extends Conversation { constructor(data, session, game) { super(data, session, game) } /** * Remove "-" that might have been prepended to chat_id property * * @param {Array<object>} agents */ async setup(agents) { await super.setup(agents) if (this.chat_id < 0) { this.chat_id = -this.chat_id } } /** * get list of telegram ids off chat members * * @param {array} members - list of variable references to chat members * @returns {Promise<array>} - list of telegram ids */ async getMembers(members) { let member_ids = [] for (let member of members) { let ref = await this.session.variables.getVariableReference(member) let member_docs = await this.game.db[ref.collection].find(ref.query) for (let member_doc of member_docs) { if (member_doc) { let member_id = await this.getId( member_doc, `${ref.collection}.{'_id':'${member_doc._id}'}` ) if (!member_ids.includes(member_id)) { member_ids.push(member_id) } } } } return member_ids } updateMembers(members, chat_id, operation) { let promises = [] for (let member of members) { promises.push( this.session.variables[operation](member, { chats: chat_id }) ) } return Promise.all(promises) } /** * create a group chat or channel via the account and when done edit its properties. * * get members from variables. Once created the chat will be referenced by `_id` in each member and the agent that created the chat. * * If given, create reference to chat in session it originates from. * * @param {Object} properties - members, name, etc... */ async create() { let member_ids = await this.getMembers(this.members) this.name = await this.session.variables.review(this.name) if (this.title) { this.title = await this.session.variables.review(this.title) } let chat = await this.agent.createChat({ title: this.title || this.name, users: member_ids }) this.chat_id = chat.id chat.id = this.longID(chat.id) let result = await this.session.variables.create( "chats", { name: this.name, title: this.title || this.name, members: this.longID(member_ids), member_count: chat.participants_count - 1, admin: this.agent.name, telegram: chat }, this.reference ) this.chat_reference = `chats.{_id:'${result.created_id}'}` await this.session.variables.add(this.agent_reference, { chats: result.created_id }) await this.updateMembers(this.members, result.created_id, "add") await this.edit(false) } /** * edit properties of existing chat. * * add and remove members and configure user rights. * * please note: most of the changes to the chat document is updated through chat updates using the gramjs update event. * The document updates in this function are redundant except for "remove_members" which does not update properly with the respective event. * * @returns {Promise<undefined>} */ async edit(change_title = true) { if (this.hasOwnProperty("add_members")) { let member_ids = await this.getMembers(this.add_members) await this.addMembers(member_ids) await this.session.variables.add(this.chat_reference, { members: { $each: this.longID(member_ids) } }) await this.updateMembers(this.add_members, this.chat._id, "add") this.session.log.info( member_ids.length + " members added to group chat " + this.chat.name ) } if (this.hasOwnProperty("remove_members")) { let member_ids = await this.getMembers(this.remove_members) let response = await this.removeMembers(member_ids) await this.session.variables.set(this.chat_reference, { member_count: response.participants_count - 1, "telegram.participants_count": response.participants_count }) await this.session.variables.pull(this.chat_reference, { members: { $in: member_ids } }) await this.updateMembers(this.remove_members, this.chat._id, "pull") this.session.log.info( member_ids.length + " members removed from group chat " + this.chat.name ) } if (this.photo) { this.photo = await this.session.variables.review(this.photo) this.photo = this.game.getFilePath(this.photo) await this.agent.editChat("photo", { chat_id: this.chat_id, photo: this.photo }) } if (this.about) { this.about = await this.session.variables.review(this.about) await this.agent.editChat("about", { peer: this.chat_id, about: this.about }) } if (change_title && this.title) { this.title = await this.session.variables.review(this.title) await this.agent.editChat("title", { chat_id: this.chat_id, title: this.title }) await this.session.variables.set(this.chat_reference, { title: this.title }) } if (this.create_link) { let result = await this.agent.editChat("create_link", { peer: this.chat_id }) await this.session.variables.set(this.chat_reference, { link: result.link }) } if (this.permissions) { let restrictions = {} for (let permission in this.permissions) { restrictions[permission] = !this.permissions[permission] } await this.agent.editChat( "restrictions", Object.assign({ peer: this.chat_id }, restrictions) ) } if (this.admin) { if (this.admin.hasOwnProperty("members")) { let admin_members = await this.getMembers(this.admin.members) delete this.admin.members for (let user of admin_members) { let admin = Object.assign( { entity: this.chat_id, user: user }, this.admin ) await this.agent.editChat("admin", admin) } } } return } /** * remove list of users from existing chat. * call removeUser function successively * * @param {Array} members - list of telegram compatible user ids * @returns {Object} infos about chat after latest remove user update */ async removeMembers(members) { let response for (const u of members) { response = await this.agent.removeUser({ chat_id: this.chat_id, user_id: u, revoke_history: false }) } return response } /** * add list of new users to existing chat. * call addUser function successively * * @param {Array} members - list of telegram compatible user ids * @returns {Object} infos about chat after latest add user update */ async addMembers(members) { let response for (const u of members) { response = await this.agent.addUser({ chat_id: this.chat_id, user_id: u, fwd_limit: 42 }) } return response } } /** * sends one message from/to conversation members. * * @class Message * @extends {Conversation} */ class Message extends Conversation { constructor(data, session, game) { super(data, session, game) } /** * Send a text message * * @return {Promise} - resolves undefined once message is sent. */ async sendMessage() { if (Array.isArray(this.player)) { throw new adaptor.InvalidError( "multiple players in one conversation not allowed" ) } else { await super.sendMessage({ text: this.text, pin: this.pin, typing: this.typing }) } } /** * Send a file in voice, video or document format. * * @return {Promise} - resolves undefined once file is sent. */ async sendFile() { if (Array.isArray(this.player)) { throw new adaptor.InvalidError( "multiple players in one conversation not allowed" ) } else { let content = { file: this.file, caption: this.caption } if (this.format) { if (this.format == "document") { content["force_document"] = true } else if (this.format == "voice") { content["voice_note"] = true } else if (this.format == "video") { content["video_note"] = true } } if (this.pin) { content["pin"] = this.pin } await super.sendFile(content) } } /** * Send a geo location widget. * * @return {Promise} - resolves undefined once location is sent. */ async sendGeo() { await super.sendGeo({ longitude: this.longitude, latitude: this.latitude }) } async sendPoll() { await super.sendPoll({ name: this.name, question: this.question, answers: this.answers, private: this.private, multiple_choice: this.multiple_choice }) } async sendQuiz() { await super.sendPoll({ name: this.name, question: this.question, answers: this.answers, correct_answer: this.correct_answer, private: this.private, multiple_choice: false, quiz: true }) } async sendVote() { await super.sendVote({ name: this.name, poll: this.poll, options: this.options }) } async sendTextButtons() { await super.sendTextButtons({ texts: this.buttons }) } } /** telegram connector class to adaptor */ module.exports.Plugin = Telegram