Skip to content
Snippets Groups Projects
telegram.service.ts 7.41 KiB
import { Inject, 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';
import { PubSub } from 'graphql-subscriptions';
import { EntryService } from '../utils/entry.service';

@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(
		@Inject('PUBSUB')
		private readonly pubSub: PubSub,
		private readonly minio: MinioService,
		private readonly prisma: PrismaService,
	) {
		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 ?? message.caption;
		const location = message.location;
		const image = message?.photo?.reduce((prev, curr) => {
			if (prev.width > curr.width) return prev;
			else return curr;
		});

		const entry: TelegramEntry = this.entries.get(ctx.from.id) ?? {};

		if (content) entry.content = 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 hasEntities = !!message.entities || !!message.caption_entities;
		const entitiesWarning = hasEntities
			? '. Note that message formatting is currently unsupported and will appear as plain text.'
			: '';

		const msg = await ctx.reply('📌 Message queued' + entitiesWarning, {
			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
			/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,
			});
		} 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);

		const exists = await this.prisma.agent.count({
			where: { slug: password },
		});
		if (exists === 0) return await ctx.reply('Slug not found');

		const agent = await this.prisma.agent.update({
			where: { slug: password },
			data: {
				uid: String(ctx.from.id),
			},
		});

		if (agent) {
			await ctx.reply('Registered');

			await this.pubSub.publish('onAgentUpdate', {
				onAgentUpdate: agent,
			});
		} 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);
		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;

		let image_id: string | undefined;
		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,
			);
		}

		const entry = 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 this.pubSub.publish('onEntryUpdate', {
			onEntryUpdate: entry,
		});

		this.entries.delete(ctx.from.id);

		if (ctx.update?.callback_query?.message?.message_id)
			await ctx.telegram.editMessageReplyMarkup(
				ctx.from.id,
				ctx.update.callback_query.message.message_id,
				undefined,
				'',
			);

		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);
	}
}