import { Injectable } from '@nestjs/common'; import { Message, Metadata } from 'node-telegram-bot-api'; import * as TelegramBot from 'node-telegram-bot-api'; import { PrismaService } from '../prisma/prisma.service'; import * as fs from 'fs'; import * as path from 'path'; import { v4 as uuid } from 'uuid'; import { Entry } from '@prisma/client'; import { pubSub } from '../pubSub.instance'; import * as seedWords from 'mnemonic-words'; @Injectable() export class TelegramService { private telegram: TelegramBot; private messageCache = new Map<number, Partial<Entry>>(); constructor(private prismaService: PrismaService) { this.telegram = new TelegramBot(process.env.BOT_TOKEN, { polling: true, }); this.telegram.onText( /^\/register ([\w|-]+) (.+)/, this.register.bind(this), ); this.telegram.onText(/^\/unregister/, this.unregister.bind(this)); this.telegram.onText(/^\/profile/, this.profile.bind(this)); this.telegram.onText(/^\/start/, this.start.bind(this)); this.telegram.onText(/^\/entry( (\w+))?/, this.entry.bind(this)); this.telegram.on('message', this.supplyValue.bind(this)); this.telegram.on('edited_message_text', this.editMessage.bind(this)); } async start(msg: Message) { await this.telegram.sendMessage( msg.chat.id, '/register <password> <name>\n - Registers you as a new Agent. The agentId will be assigned to you beforehand, while the name is what will be displayed to other users.\n' + '/unregister\n - Deletes all your Entries and Profile.\n\n' + '/entry <public|private>\n - Create new Public/Private Entry from your previous message cache.\n' + '/entry <clear>\n - Clears your current message cache.\n' + '/profile\n - Shows your current profile.', ); } private generateSlug() { function generateWord() { return seedWords[Math.round(Math.random() * seedWords.length)]; } return [ generateWord(), generateWord(), generateWord(), generateWord(), ].join('-'); } async entry(msg: Message, match: RegExpMatchArray) { switch (match[2]?.toLowerCase()) { case 'private': return await this.commitMessage(msg.from.id, true, msg); case 'public': return await this.commitMessage(msg.from.id, false, msg); case 'clear': this.messageCache.delete(msg.from.id); return void (await this.telegram.sendMessage( msg.chat.id, '♻ Message Queue cleared', )); default: return this.sendError( msg, 'Usage: /entry \\<private\\|public\\|clear\\>', ); } } async commitMessage(id: number, isPrivate: boolean, msg: Message) { if (!(await this.isRegistered(msg.from.id))) return await this.sendError(msg, 'Not registered'); if (!this.messageCache.has(id)) return await this.sendError(msg, 'No queued Messages'); let entry: Partial<Entry> = this.messageCache.get(msg.from.id); entry.private = isPrivate; this.messageCache.delete(msg.from.id); entry = await this.prismaService.entry.create({ data: entry as any, }); await this.sendSuccess( msg, `Created new ${isPrivate ? 'private' : 'public'} Entry`, ); // Send a notification to subscriptions if (!entry.private) { await pubSub.publish('newEntry', { newEntry: { ...entry, locked: isPrivate, }, }); } else { await pubSub.publish('newEntry', { newEntry: { id: entry.id, agentId: entry.agentId, private: true, createdAt: entry.createdAt, locked: true, }, }); } } async register(msg: Message, match: RegExpMatchArray) { if (!match[1] && !match[2]) return; if (await this.isRegistered(msg.from.id)) return await this.sendError(msg, 'Already registered'); const existsCheck = await this.prismaService.agent.count({ where: { password: match[1], uid: null }, }); if (existsCheck == 0) return await this.sendError( msg, 'agentId not found or already taken', ); const agent = await this.prismaService.agent.update({ where: { password: match[1], }, data: { uid: String(msg.from.id), name: match[2], slug: this.generateSlug(), tokenCode: { create: { value: 1 }, }, }, }); await this.sendSuccess( msg, `Successfully registered\\!\n` + `\\- Id: \`${agent.id}\`\n` + `\\- Name: \`${agent.name}\`\n` + `\\- Code: \`${agent.slug}\``, ); } async unregister(msg: Message) { if (!(await this.isRegistered(msg.from.id))) return await this.sendError(msg, 'Not registered'); const agent = await this.prismaService.agent.findFirst({ where: { uid: String(msg.from.id) }, }); await this.prismaService.entry.deleteMany({ where: { agentId: agent.id }, }); await this.prismaService.entry.deleteMany({ where: { agentId: agent.id }, }); await this.prismaService.tokenCode.delete({ where: { id: agent.tokenCodeId }, }); await this.prismaService.agent.delete({ where: { id: agent.id }, }); await this.sendSuccess(msg, 'Deleted all Entries'); } async profile(msg: Message) { if (!(await this.isRegistered(msg.from.id))) return await this.sendError(msg, 'Not registered'); const agent = await this.prismaService.agent.findFirst({ where: { uid: String(msg.from.id) }, }); const publicEntries = await this.prismaService.entry.count({ where: { agentId: agent.id, private: false }, }); const privateEntries = await this.prismaService.entry.count({ where: { agentId: agent.id, private: true }, }); await this.telegram.sendMessage( msg.chat.id, `📂 Agent Information\n` + `\\- Id: \`${agent.id}\`\n` + `\\- Name: \`${agent.name}\`\n` + `\\- Code: \`${agent.slug}\`\n` + `\\- Public Entries: \`${publicEntries}\`\n` + `\\- Private Entries: \`${privateEntries}\``, { parse_mode: 'MarkdownV2' }, ); } async receiveImage(msg: Message): Promise<Partial<Entry>> { let file: string; let size = -1; for (const p of msg.photo) { if (p.width > size) { size = p.width; file = p.file_id; } } const id = uuid(); const dest = fs.createWriteStream( path.join(process.cwd(), 'photos', `${id}.jpg`), ); const pipe = this.telegram.getFileStream(file).pipe(dest); await new Promise((resolve) => pipe.on('finish', resolve)); return { image: `${id}.jpg`, }; } async editMessage(msg: Message) { this.telegram.sendMessage( msg.chat.id, '❌ Message Edits are not supported. Please send new values as new messages.', { reply_to_message_id: msg.message_id, }, ); } async supplyValue(msg: Message, metadata: Metadata) { if (metadata.type === 'text' && msg.text.startsWith('/')) return; if (!(await this.isRegistered(msg.from.id))) return; if (metadata.type === 'document') return await this.sendError( msg, "Unsupported DataType\\. Please send pictures as a 'Photo', not as a 'File'\\.", ); const agent = await this.prismaService.agent.findFirst({ where: { uid: String(msg.from.id) }, }); let entry: Partial<Entry> = {}; if (this.messageCache.has(msg.from.id)) { entry = this.messageCache.get(msg.from.id); } else { await this.telegram.sendMessage( msg.chat.id, '📌 Message queue created\n' + '<pre>/entry private</pre> to submit as private element\n' + '<pre>/entry public</pre> to submit as public element\n' + '<pre>/entry clear</pre> to reset the queue', { parse_mode: 'HTML' }, ); } entry.agentId = agent.id; switch (metadata.type) { case 'photo': const imageEntry = await this.receiveImage(msg); if (entry.image) await this.telegram.sendMessage( msg.chat.id, '✂ Overwriting previous image', ); entry.image = imageEntry.image; await this.telegram.sendMessage(msg.chat.id, '➡ Queued Image'); if (!msg.caption) { // Allow captions to be handled like text messages break; } case 'text': if (entry.content) await this.telegram.sendMessage( msg.chat.id, '✂ Overwriting previous text', ); if (msg.entities) await this.telegram.sendMessage( msg.chat.id, '⚠️ Note that Message Entities such as formatting are not supported and text will show up without formatting', ); entry.content = msg.text || msg.caption; await this.telegram.sendMessage(msg.chat.id, '➡ Queued Text'); break; case 'location': if (entry.lat) await this.telegram.sendMessage( msg.chat.id, '✂ Overwriting previous location', ); entry.lat = msg.location.latitude.toString(); entry.lon = msg.location.longitude.toString(); await this.telegram.sendMessage( msg.chat.id, '➡ Queued Location', ); break; default: return await this.sendError(msg, 'Unsupported DataType'); } this.messageCache.set(msg.from.id, entry); } private async isRegistered(uid: string | number): Promise<boolean> { return ( (await this.prismaService.agent.count({ where: { uid: String(uid) }, })) > 0 ); } private async sendError(msg: Message, message: string) { await this.telegram.sendMessage(msg.chat.id, `❌ ${message}`, { parse_mode: 'MarkdownV2', }); } private async sendSuccess(msg: Message, message: string) { await this.telegram.sendMessage(msg.chat.id, `✅ ${message}`, { parse_mode: 'MarkdownV2', }); } }