import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { Markup, Telegraf } from 'telegraf'; import { TelegrafContext } from 'telegraf/typings/context'; import { TelegramEntry } from './telegram.entry'; import { PrismaService } from '../prisma/prisma.service'; import { MinioService } from '../minio/minio.service'; import { v4 as uuid } from 'uuid'; @Injectable() export class TelegramService implements OnModuleInit { private readonly bot: Telegraf<TelegrafContext>; private readonly logger = new Logger('TelegramService'); private readonly entries = new Map<number, TelegramEntry>(); constructor( private readonly prisma: PrismaService, private readonly minio: MinioService, ) { this.bot = new Telegraf(process.env.TELEGRAM_TOKEN); this.bot.start(this.start); this.bot.command('unregister', (ctx) => this.unregister.call(this, ctx).catch(console.error), ); this.bot.command('register', (ctx) => this.register.call(this, ctx).catch(console.error), ); this.bot.command('profile', (ctx) => this.profile.call(this, ctx).catch(console.error), ); this.bot.on('message', (ctx) => this.onMessage.call(this, ctx).catch(console.error), ); this.bot.on('edited_message', (ctx) => this.onMessage.call(this, ctx).catch(console.error), ); this.bot.action('delete', (ctx) => this.deleteButton.call(this, ctx).catch(console.error), ); this.bot.action('public', (ctx) => this.publicButton.call(this, ctx).catch(console.error), ); this.bot.action('private', (ctx) => this.privateButton.call(this, ctx).catch(console.error), ); } async onModuleInit() { await this.bot .launch() .then(() => this.bot.telegram.getMe()) .then((user) => this.logger.log(`Logged in as ${user.username}`)); } async onMessage(ctx: TelegrafContext) { if (!(await this.isRegistered(ctx.from.id))) { await ctx.reply('User not registered'); return; } const message = ctx.message ?? ctx.editedMessage; const content = message.text; const location = message.location; const image = message?.photo?.reduce((prev, curr) => { if (prev.width > curr.width) return prev; else return curr; }); const entry: TelegramEntry = { content }; if (image) entry.photo = { id: image.file_id, width: image.width, height: image.height, }; if (location) entry.location = { lat: location.latitude, lon: location.longitude, }; if (!entry.content && !entry.photo && !entry.location) return; this.entries.set(ctx.from.id, entry); const msg = await ctx.reply('📌 Message queued', { reply_markup: { inline_keyboard: [ [ Markup.callbackButton('Save as public', 'public'), Markup.callbackButton('Save as private', 'private'), Markup.callbackButton('Delete', 'delete'), ], ], one_time_keyboard: true, resize_keyboard: true, }, }); try { for (let i = 1; i <= 2; i++) await ctx.telegram.editMessageReplyMarkup( ctx.from.id, msg.message_id - i, undefined, '', ); } catch (e) {} } async start(ctx: TelegrafContext) { await ctx.reply(` /register <password> <name>\n - Registers you as a new Agent. The password 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 /profile\n - Shows your current profile. `); } async unregister(ctx: TelegrafContext) { const agent = await this.prisma.agent.update({ where: { uid: String(ctx.from.id) }, data: { uid: null }, }); if (agent) { await ctx.reply('Unregistered'); } else { await ctx.reply('User not registered'); } } async register(ctx: TelegrafContext) { if (!ctx.message.text) return; if (await this.isRegistered(ctx.from.id)) { await ctx.reply('User already registered'); return; } const args = ctx.message.text.split(' '); if (args.length !== 3) return await this.start(ctx); const exists = await this.prisma.agent.count({ where: { slug: args[1] }, }); if (exists === 0) return await ctx.reply('Slug not found'); const agent = await this.prisma.agent.update({ where: { slug: args[1] }, data: { name: args[2], uid: String(ctx.from.id), }, }); if (agent) { await ctx.reply('Registered'); } else { await ctx.reply('Password not found'); } } async profile(ctx: TelegrafContext) { const agent = await this.prisma.agent.findFirst({ where: { uid: String(ctx.from.id) }, }); if (!agent) { await ctx.reply('User not registered'); return; } const [publicEntries, privateEntries] = await this.prisma.$transaction([ this.prisma.entry.count({ where: { agentId: agent.id, private: false }, }), this.prisma.entry.count({ where: { agentId: agent.id, private: true }, }), ]); await ctx.reply( `📂 Agent Information\n` + `- Id: ${agent.id}\n` + `- Name: ${agent.name}\n` + `- Code: ${agent.slug}\n` + `- Public Entries: ${publicEntries}\n` + `- Private Entries: ${privateEntries}`, ); } async deleteButton(ctx: TelegrafContext) { await ctx.deleteMessage(); } async privateButton(ctx: TelegrafContext) { if (!(await this.isRegistered(ctx.from.id))) { await ctx.reply('User not registered'); return; } await this.createEntry(ctx, true); } async publicButton(ctx: TelegrafContext) { if (!(await this.isRegistered(ctx.from.id))) { await ctx.reply('User not registered'); return; } await this.createEntry(ctx, false); } async createEntry(ctx: TelegrafContext, isPrivate: boolean) { const data = this.entries.get(ctx.from.id); if (!data) return; if (ctx.update?.callback_query?.message?.message_id) await ctx.telegram.editMessageReplyMarkup( ctx.from.id, ctx.update.callback_query.message.message_id, undefined, '', ); this.entries.delete(ctx.from.id); let image_id; if (data.photo) { image_id = uuid(); const link = await ctx.telegram.getFileLink(data.photo.id); const res = await fetch(link) .then((res) => res.arrayBuffer()) .then((buf) => Buffer.from(buf)); await this.minio.saveFile(image_id, 'jpg', res); } await this.prisma.entry.create({ data: { image_id, image_width: data.photo?.width, image_height: data.photo?.height, content: data.content, lat: data.location?.lat, lon: data.location?.lon, private: isPrivate, agent: { connect: { uid: String(ctx.from.id) }, }, }, }); await ctx.reply(`✅ ${isPrivate ? 'Private' : 'Public'} Entry created`); } isRegistered(uid: string | number): Promise<boolean> { return this.prisma.agent .count({ where: { uid: String(uid) }, }) .then((count) => count > 0); } }