import { IBotProviderMessageCtx } from '@lskjs/bots-base/types'; import BaseBotProvider from '@lskjs/bots-provider'; import get from 'lodash/get'; import isEmpty from 'lodash/isEmpty'; import set from 'lodash/set'; import { Context, session, Telegraf } from 'telegraf'; // export type TelegramBotContext = IBotProviderMessageCtx; type TelegramBotContext = Context; /** * Docs: https://telegraf.js.org/#/ */ type TelegramBotConfigType = { token: string; }; export default class TelegramBotProvider extends BaseBotProvider { provider = 'telegram'; Telegraf = Telegraf; eventTypes = [ 'message', 'edited_message', 'callback_query', 'inline_query', 'shipping_query', 'pre_checkout_query', 'chosen_inline_result', 'channel_post', 'edited_channel_post', ]; config: TelegramBotConfigType; async init(): Promise { await super.init(); if (!this.config.token) throw new Err('TelegramBotProvider !config.token'); this.client = new Telegraf(this.config.token); this.client.use(session()); // TODO: временный костыль, @volkovpishet починит // this.client.use(async (ctx, next) => { // if (!ctx.session) ctx.session = {}; // await next(); // }); } async run(): Promise { await super.run(); if (!this.client) return; await this.initEventEmitter(); await this.client.launch(); } async saveMessage(ctx: TelegramBotContext, meta: any): Promise { const BotsEventModel = await this.botsModule.module('models.BotsEventModel'); const BotsTelegramMessageModel = await this.botsModule.module('models.BotsTelegramMessageModel'); const BotsTelegramUserModel = await this.botsModule.module('models.BotsTelegramUserModel'); const BotsTelegramChatModel = await this.botsModule.module('models.BotsTelegramChatModel'); const botId = this.getBotId(); const type = this.getMessageType(ctx); const eventData = this.getMessage(ctx); const { from, chat } = eventData; let telegramUserId; let chatUserId; const flags = { new: true, upsert: true, }; if (from) { const { id } = from; ({ _id: telegramUserId } = await BotsTelegramUserModel.findOneAndUpdate({ id }, { ...from, id }, flags)); } if (chat) { const { id, title, username } = chat; ({ _id: chatUserId } = await BotsTelegramChatModel.findOneAndUpdate( { id }, { ...from, id, username: username || title }, flags, )); } const data = { botId, telegramUserId, chatUserId, type, meta, ...eventData, }; await BotsTelegramMessageModel.create(data); await BotsEventModel.create({ botId, provider: this.provider, type: 'message', data: eventData, }); this.log.trace('[message]: ', eventData); } /** * Docs: https://raw.githubusercontent.com/KnorpelSenf/typegram/master/types.d.ts * @param {object} ctx message or message context */ getMessageType(ctx: TelegramBotContext): string | null { const message = this.getMessage(ctx); if (message.audio) return 'audio'; if (message.document) return 'document'; if (message.animation) return 'animation'; if (message.photo) return 'photo'; if (message.sticker) return 'sticker'; if (message.video) return 'video'; if (message.video_note) return 'video_note'; if (message.voice) return 'voice'; if (message.contact) return 'contact'; if (message.dice) return 'dice'; if (message.game) return 'game'; // TODO: проверить if (message.poll) return 'poll'; if (message.location) return 'location'; if (message.venue) return 'venue'; // TODO: проверить if (message.text) return 'text'; // СПОРНО if (message.pinned_message) return 'pinned_message'; if (message.left_chat_member) return 'left_chat_member'; if (message.new_chat_members) return 'new_chat_members'; if (message.new_chat_title) return 'new_chat_title'; if (message.new_chat_photo) return 'new_chat_photo'; if (message.invoice) return 'invoice'; // TODO: проверить if (message.successful_payment) return 'successful_payment'; // TODO: проверить if (message.passport_data) return 'passport_data'; // TODO: проверить // if (message.reply_markup) return 'reply_markup'; // TODO: проверить return null; } getMessage(ctx: TelegramBotContext): TelegramBotContext { if (get(ctx, 'update.callback_query.message')) return get(ctx, 'update.callback_query.message'); if (get(ctx, 'update.channel_post')) return get(ctx, 'update.channel_post'); if (get(ctx, 'message')) return get(ctx, 'message'); return ctx; } getUser(ctx: TelegramBotContext): TelegramBotContext { if (get(ctx, 'update.callback_query.from')) return get(ctx, 'update.callback_query.from'); if (get(ctx, 'update.channel_post.from')) return get(ctx, 'update.channel_post.from'); if (get(ctx, 'update.message.from')) return get(ctx, 'update.message.from'); return ctx; } getUserId(ctx: TelegramBotContext): number | null { return this.getUser(ctx).id; } getMessageId(ctx: TelegramBotContext): number | null { if (['number', 'string'].includes(typeof ctx)) return ctx; const message = this.getMessage(ctx); return message.message_id || null; } getMessageUserId(ctx: TelegramBotContext): number | null { if (['number', 'string'].includes(typeof ctx)) return ctx; const message = this.getMessage(ctx); if (get(message, 'from.id')) return get(message, 'from.id'); return null; } getMessageChatId(ctx: TelegramBotContext): number | null { if (['number', 'string'].includes(typeof ctx)) return ctx; const message = this.getMessage(ctx); if (get(message, 'chat.id')) return get(message, 'chat.id'); return null; } getMessageTargetId(ctx: TelegramBotContext): number | null { if (['number', 'string'].includes(typeof ctx)) return ctx; const message = this.getMessage(ctx); return get(message, 'message_id', null); } getRepliedMessage(ctx: TelegramBotContext): IBotProviderMessageCtx { const message = this.getMessage(ctx); // console.log({message}) return message.reply_to_message || null; } getRepliedMessageId(ctx: TelegramBotContext): number | null { if (['number', 'string'].includes(typeof ctx)) return ctx; const message = this.getRepliedMessage(ctx); // console.log({getRepliedMessage: message}) if (!message) return null; return message.message_id || null; } getCallback(ctx: TelegramBotContext): any | null { return ctx.update && ctx.update.callback_query; } getCallbackMessage(ctx: TelegramBotContext): IBotProviderMessageCtx | null { const callback = this.getCallback(ctx); if (!callback) return null; return callback.message; } getCallbackMessageId(ctx: TelegramBotContext): number | null { if (['number', 'string'].includes(typeof ctx)) return ctx; const message = this.getRepliedMessage(ctx); if (!message) return null; return message.message_id || null; } getMessageCallbackData(ctx: TelegramBotContext): any | null { return get(ctx, 'update.callback_query.data', null); } getMessageText(ctx: TelegramBotContext): string | null { if (typeof ctx === 'string') return ctx; const message = this.getMessage(ctx); if (!message) return null; if (message.caption) return message.caption; if (message.text) return message.text; return null; } setMessageText(ctx: TelegramBotContext, text: string): string | null { if (typeof ctx === 'string') return null; // eslint-disable-next-line max-len if (get(ctx, 'update.callback_query.message.caption')) return set(ctx, 'update.callback_query.message.caption', text); if (get(ctx, 'update.callback_query.message.text')) return set(ctx, 'update.callback_query.message.text', text); if (get(ctx, 'update.channel_post.caption')) return set(ctx, 'update.channel_post.caption', text); if (get(ctx, 'update.channel_post.text')) return set(ctx, 'update.channel_post.text', text); if (get(ctx, 'message.caption')) return set(ctx, 'message.caption', text); if (get(ctx, 'message.text')) return set(ctx, 'message.text', text); return null; } async getChatMember(chatId: string | number, userId: string | number): any { try { const chatMember = await this.client.telegram.getChatMember(chatId, userId); return chatMember; } catch (error) { return {}; } } async userInChat(chatId: string | number, userId: string | number): boolean | undefined { const chatMember = await this.getChatMember(chatId, userId); if (isEmpty(chatMember)) return undefined; if (chatMember.status === 'left') return false; return true; } isMessageCallback(ctx: TelegramBotContext): boolean { return get(ctx, 'updateType') === 'callback_query'; } isMessageCommand(ctx: TelegramBotContext, command: RegExp | string): boolean { return this.isMessageStartsWith(ctx, `/${command || ''}`); } getMessageCommand(ctx: TelegramBotContext): string | null { const messageText = this.getMessageText(ctx); if (messageText[0] !== '/') return null; return messageText.split('@')[0].split(' ')[0]; } getMessageDate(ctx: TelegramBotContext): Date { const message = this.getMessage(ctx); return new Date(message.date * 1000); } getMessageChatType(ctx: TelegramBotContext): string | null { if (['number', 'string'].includes(typeof ctx)) return ctx; const message = this.getMessage(ctx); if (get(message, 'chat.type')) return get(message, 'chat.type'); return null; } /** * Function for resend content * Docs: https://raw.githubusercontent.com/KnorpelSenf/typegram/master/types.d.ts * * @param {int|string} chatId repost target * @param {object} ctx message or message context */ async repost( chatId: number | string, ctx: TelegramBotContext, initExtra?: Record, meta = {}, ): Promise { const type = this.getMessageType(ctx); const message = this.getMessage(ctx); // console.log({ ctx, type, message }); this.log.trace('repost', type); // if (ctx.group) { // ctx // } let method: string; let args: any[]; const extra: any = { ...(initExtra || {}), }; if (type === 'audio') { method = 'sendAudio'; args = [message.audio.file_id]; } else if (type === 'document') { method = 'sendDocument'; args = [message.document.file_id]; } else if (type === 'animation') { method = 'sendAnimation'; // TODO: проверить args = [message.document.file_id]; } else if (type === 'photo') { method = 'sendPhoto'; // text = message.caption || '';// TODO: проверить args = [message.photo[0].file_id]; } else if (type === 'sticker') { method = 'sendSticker'; args = [message.sticker.file_id]; } else if (type === 'video') { method = 'sendVideo'; args = [message.video.file_id]; } else if (type === 'video_note') { method = 'sendVideoNote'; args = [message.video_note.file_id]; } else if (type === 'voice') { method = 'sendVoice'; args = [message.voice.file_id]; } else if (type === 'contact') { method = 'sendContact'; args = [message.contact]; // TODO: xz } else if (type === 'dice') { method = 'sendDice'; args = [message.dice]; // TODO: xz } else if (type === 'game') { method = 'sendGame'; args = [message.game]; // TODO: xz } else if (type === 'poll') { if (message.poll.type === 'quiz') { method = 'sendQuiz'; } else { method = 'sendPoll'; } // console.log(message.poll.options); args = [message.poll.question, message.poll.options.map((option: any) => option.text)]; // TODO: xz } else if (type === 'location') { method = 'sendLocation'; args = [message.location.latitude, message.location.longitude]; // TODO: xz // args = [message.location.latitude; // opt = message.location.longitude; } else if (type === 'venue') { method = 'sendVenue'; args = [message.venue]; // TODO: xz // args = [message.location.latitude; // opt = message.location.longitude; } else if (type === 'text') { method = 'sendMessage'; args = [message.text]; } else { method = 'sendMessage'; args = [`НАТА РЕАЛИЗУЙ МЕНЯ!!! [${type}] ${message.message_id}`]; } /** Caption for the animation, audio, document, photo, video or voice, 0-1024 characters */ if (type && ['animation', 'audio', 'document', 'photo', 'video', 'voice'].includes(type)) { extra.caption = message.caption; } if (!this.client.telegram[method]) { this.log.error(`!telegram.${method}`); return null; } const telegramArgs = [chatId, ...args, extra]; this.log.trace(`telegram.${method}`, ...telegramArgs); const msg = await this.client.telegram[method](...telegramArgs); await this.saveMessage(msg, meta); return msg; } async sendContent(ctx: TelegramBotContext, content: any, extra = {}, meta = {}): Promise { this.log.trace('sendContent'); let type: string; let payload: any; if (typeof content === 'string') { type = 'text'; payload = content; } else { ({ type, ...payload } = content); if (type === 'text') { payload = payload.text; } } let method; if (type === 'document') { method = 'sendDocument'; } else if (type === 'photo') { method = 'sendPhoto'; } else { method = 'sendMessage'; } // this.log.trace('ctx', method) const msg = await this[method](ctx, payload, extra); await this.saveMessage(msg, meta); } async replyContent(ctx: TelegramBotContext, content: any, extra = {}, meta = {}): Promise { this.log.trace('replyContent'); // console.log({ content }); let type: string; let payload: any; if (typeof content === 'string') { type = 'text'; payload = content; } else { ({ type, ...payload } = content); } let method; if (type === 'document') { method = 'replyWithDocument'; } else if (type === 'photo') { method = 'replyWithText'; } else { method = 'reply'; } const msg = await ctx[method](payload, extra); await this.saveMessage(msg); } async reply(ctx: TelegramBotContext, payload: any, initExtra = {}) { this.log.trace('reply'); const extra = { reply_to_message_id: this.getRepliedMessageId(ctx) || this.getMessageId(ctx), //eslint-disable-line ...initExtra, }; // console.log({extra}) const msg = await this.client.telegram.sendMessage(this.getMessageChatId(ctx), payload, extra).catch((err: any) => { this.log.error(err); throw err; }); await this.saveMessage(msg, meta); return msg; } async editMessage(ctx: TelegramBotContext, payload: any, extra = {}, meta = {}) { this.log.trace('editMessage'); try { const msg = await this.client.telegram.editMessageText( this.getMessageChatId(ctx), this.getMessageId(ctx), null, payload, extra, ); await this.saveMessage(msg, meta); return msg; } catch (err: any) { this.log.error(err); return err; } } async editMessageReplyMarkup(ctx: TelegramBotContext, extra = {}, meta = {}) { this.log.trace('editMessage'); try { const msg = await this.client.telegram.editMessageReplyMarkup( this.getMessageChatId(ctx), this.getMessageId(ctx), null, extra.reply_markup, ); await this.saveMessage(msg, meta); return msg; } catch (err: any) { this.log.error(err); return err; } } async deleteMessage(ctx: TelegramBotContext): any { const BotsTelegramMessageModel = await this.botsModule.module('models.BotsTelegramMessageModel'); this.log.trace('deleteMessage'); const chatId = this.getMessageChatId(ctx); const messageId = this.getMessageTargetId(ctx); await BotsTelegramMessageModel.updateOne( { 'chat.id': chatId, message_id: messageId, }, { 'meta.status': 'deleted' }, ); return this.client.telegram.deleteMessage(chatId, messageId); } async sendMessage(ctx: any, text: string, extra = {}, meta = {}) { this.log.trace('sendMessage'); const msg = await this.client.telegram.sendMessage(this.getMessageChatId(ctx), ...args); await this.saveMessage(msg, meta); return msg; } async sendSticker(ctx: any, ...args: any[]) { this.log.trace('sendSticker'); const msg = await this.client.telegram.sendSticker(this.getMessageChatId(ctx), ...args); await this.saveMessage(msg); return msg; } async sendAnimation(ctx: any, ...args: any[]) { this.log.trace('sendAnimation'); const msg = await this.client.telegram.sendAnimation(this.getMessageChatId(ctx), ...args); await this.saveMessage(msg); return msg; } async sendDocument(ctx: any, ...args: any[]) { this.log.trace('sendDocument'); const msg = this.client.telegram.sendDocument(this.getMessageChatId(ctx), ...args); await this.saveMessage(msg); return msg; } async sendPhoto(ctx: any, ...args: any[]) { this.log.trace('sendPhoto'); const msg = await this.client.telegram.sendPhoto(this.getMessageChatId(ctx), ...args); await this.saveMessage(msg); return msg; } isMessageLike(ctx: any) { const message = this.getMessage(ctx); const likes = ['+', '👍', '➕'].map((a) => a.codePointAt(0)); let firstSign; if (message && message.text && message.text.codePointAt) { firstSign = message.text.codePointAt(0); } else if (message && message.sticker && message.sticker.emoji) { firstSign = message.sticker.emoji.codePointAt(0); } return firstSign && likes.includes(firstSign); } }