UNPKG

adaptorex

Version:

Connect all your live interactive storytelling devices and software

1,176 lines (1,068 loc) 39.9 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) { buttons[i].push(await this.session.variables.review(button.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) 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