Skip to content
Snippets Groups Projects
telegram.service.ts 7.02 KiB
Newer Older
  • Learn to ignore specific revisions
  • import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common';
    
    Adrian Paschkowski's avatar
    V2  
    Adrian Paschkowski committed
    import { Markup, Telegraf } from 'telegraf';
    import { TelegrafContext } from 'telegraf/typings/context';
    import { TelegramEntry } from './telegram.entry';
    
    Pascal Kosak's avatar
    Pascal Kosak committed
    import { PrismaService } from '../prisma/prisma.service';
    
    Adrian Paschkowski's avatar
    V2  
    Adrian Paschkowski committed
    import { MinioService } from '../minio/minio.service';
    
    Pascal Kosak's avatar
    Pascal Kosak committed
    import { v4 as uuid } from 'uuid';
    
    import { PubSub } from 'graphql-subscriptions';
    import { EntryService } from '../utils/entry.service';
    
    Pascal Kosak's avatar
    Pascal Kosak committed
    
    @Injectable()
    
    Adrian Paschkowski's avatar
    V2  
    Adrian Paschkowski committed
    export class TelegramService implements OnModuleInit {
    	private readonly bot: Telegraf<TelegrafContext>;
    	private readonly logger = new Logger('TelegramService');
    	private readonly entries = new Map<number, TelegramEntry>();
    
    	constructor(
    
    		@Inject('PUBSUB')
    		private readonly pubSub: PubSub,
    
    Adrian Paschkowski's avatar
    V2  
    Adrian Paschkowski committed
    		private readonly minio: MinioService,
    
    		private readonly prisma: PrismaService,
    
    Adrian Paschkowski's avatar
    V2  
    Adrian Paschkowski committed
    	) {
    		this.bot = new Telegraf(process.env.TELEGRAM_TOKEN);
    
    		this.bot.start(this.start);
    
    
    xcf-t's avatar
    xcf-t committed
    		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),
    		);
    
    Adrian Paschkowski's avatar
    V2  
    Adrian Paschkowski committed
    
    
    xcf-t's avatar
    xcf-t committed
    		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),
    		);
    
    Adrian Paschkowski's avatar
    V2  
    Adrian Paschkowski committed
    
    
    xcf-t's avatar
    xcf-t committed
    		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),
    		);
    
    Adrian Paschkowski's avatar
    V2  
    Adrian Paschkowski committed
    	}
    
    	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>\n - Registers you as a new Agent. The password will be assigned to you beforehand.\n
    
    			/unregister\n - Removes the connection between your Telegram and Agent account.\n\n
    
    Adrian Paschkowski's avatar
    V2  
    Adrian Paschkowski committed
    			/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');
    
    
    			await this.pubSub.publish('onAgentUpdate', {
    				onAgentUpdate: agent,
    			});
    
    Adrian Paschkowski's avatar
    V2  
    Adrian Paschkowski committed
    		} 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 password = ctx.message.text.split(' ')[1];
    		if (!password) return await this.start(ctx);
    
    Adrian Paschkowski's avatar
    V2  
    Adrian Paschkowski committed
    
    
    xcf-t's avatar
    xcf-t committed
    		const exists = await this.prisma.agent.count({
    
    			where: { slug: password },
    
    xcf-t's avatar
    xcf-t committed
    		});
    		if (exists === 0) return await ctx.reply('Slug not found');
    
    
    Adrian Paschkowski's avatar
    V2  
    Adrian Paschkowski committed
    		const agent = await this.prisma.agent.update({
    
    			where: { slug: password },
    
    Adrian Paschkowski's avatar
    V2  
    Adrian Paschkowski committed
    			data: {
    				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) {
    
    		this.entries.delete(ctx.from.id);
    
    Adrian Paschkowski's avatar
    V2  
    Adrian Paschkowski committed
    		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: string | undefined;
    
    xcf-t's avatar
    xcf-t committed
    		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(
    				`${process.env.MINIO_ENTRY_IMAGE_PATH}/${image_id}.jpg`,
    				res,
    			);
    
    xcf-t's avatar
    xcf-t committed
    		}
    
    Adrian Paschkowski's avatar
    V2  
    Adrian Paschkowski committed
    
    
    		const entry = await this.prisma.entry.create({
    
    Adrian Paschkowski's avatar
    V2  
    Adrian Paschkowski committed
    			data: {
    
    xcf-t's avatar
    xcf-t committed
    				image_id,
    
    Adrian Paschkowski's avatar
    V2  
    Adrian Paschkowski committed
    				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 this.pubSub.publish('onEntryUpdate', {
    			onEntryUpdate: entry,
    		});
    
    
    Adrian Paschkowski's avatar
    V2  
    Adrian Paschkowski committed
    		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);
    	}
    
    Pascal Kosak's avatar
    Pascal Kosak committed
    }