Skip to content
Snippets Groups Projects
telegram.service.ts 10.9 KiB
Newer Older
  • Learn to ignore specific revisions
  • Pascal Kosak's avatar
    Pascal Kosak committed
    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';
    
    Pascal Kosak's avatar
    Pascal Kosak committed
    import * as seedWords from 'mnemonic-words';
    
    Pascal Kosak's avatar
    Pascal Kosak committed
    
    @Injectable()
    export class TelegramService {
    
    Pascal Kosak's avatar
    Pascal Kosak committed
        private telegram: TelegramBot;
    
    
        private messageCache = new Map<number, Partial<Entry>>();
    
    
    Pascal Kosak's avatar
    Pascal Kosak committed
        constructor(private prismaService: PrismaService) {
            this.telegram = new TelegramBot(process.env.BOT_TOKEN, {
                polling: true,
            });
    
    
            this.telegram.onText(
                /^\/register ([\w|-]+) (.+)/,
                this.register.bind(this),
            );
    
    Pascal Kosak's avatar
    Pascal Kosak committed
            this.telegram.onText(/^\/unregister/, this.unregister.bind(this));
    
    Pascal Kosak's avatar
    Pascal Kosak committed
            this.telegram.onText(/^\/profile/, this.profile.bind(this));
    
    Pascal Kosak's avatar
    Pascal Kosak committed
            this.telegram.onText(/^\/start/, this.start.bind(this));
    
    Adrian Paschkowski's avatar
    Adrian Paschkowski committed
            this.telegram.onText(/^\/entry( (\w+))?/, this.entry.bind(this));
    
    Pascal Kosak's avatar
    Pascal Kosak committed
            this.telegram.on('message', this.supplyValue.bind(this));
        }
    
    
    Pascal Kosak's avatar
    Pascal Kosak committed
        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' +
    
    Adrian Paschkowski's avatar
    Adrian Paschkowski committed
                    '/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.',
    
    Pascal Kosak's avatar
    Pascal Kosak committed
        private generateSlug() {
    
    Pascal Kosak's avatar
    Pascal Kosak committed
            function generateWord() {
                return seedWords[Math.round(Math.random() * seedWords.length)];
            }
    
            return [
                generateWord(),
                generateWord(),
                generateWord(),
                generateWord(),
            ].join('-');
    
    Pascal Kosak's avatar
    Pascal Kosak committed
        }
    
    
    Pascal Kosak's avatar
    Pascal Kosak committed
        async entry(msg: Message, match: RegExpMatchArray) {
    
    Adrian Paschkowski's avatar
    Adrian Paschkowski committed
            switch (match[2]?.toLowerCase()) {
    
    Pascal Kosak's avatar
    Pascal Kosak committed
                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);
    
    Adrian Paschkowski's avatar
    Adrian Paschkowski committed
                    return void (await this.telegram.sendMessage(
    
    Pascal Kosak's avatar
    Pascal Kosak committed
                        msg.chat.id,
                        '♻ Message Queue cleared',
    
    Adrian Paschkowski's avatar
    Adrian Paschkowski committed
                    ));
                default:
                    return this.sendError(
                        msg,
    
                        'Usage: /entry <private\\|public\\|clear>',
    
    Pascal Kosak's avatar
    Pascal Kosak committed
        }
    
    
        async commitMessage(id: number, isPrivate: boolean, msg: Message) {
            if (!(await this.isRegistered(msg.from.id)))
    
    Pascal Kosak's avatar
    Pascal Kosak committed
                return await this.sendError(msg, 'Not registered');
    
    Pascal Kosak's avatar
    Pascal Kosak committed
    
    
            if (!this.messageCache.has(id))
    
    Adrian Paschkowski's avatar
    Adrian Paschkowski committed
                return await this.sendError(msg, 'No queued Messages');
    
    Pascal Kosak's avatar
    Pascal Kosak committed
    
    
            let entry: Partial<Entry> = this.messageCache.get(msg.from.id);
    
    
            entry.private = isPrivate;
    
            this.messageCache.delete(msg.from.id);
    
    Pascal Kosak's avatar
    Pascal Kosak committed
    
    
            entry = await this.prismaService.entry.create({
    
    Pascal Kosak's avatar
    Pascal Kosak committed
            });
    
    
    Adrian Paschkowski's avatar
    Adrian Paschkowski committed
            await this.sendSuccess(
                msg,
                `Created new ${isPrivate ? 'private' : 'public'} Entry`,
    
            // Send a notification to subscriptions
    
    Pascal Kosak's avatar
    Pascal Kosak committed
            if (!entry.private) {
    
                await pubSub.publish('newEntry', {
                    newEntry: {
                        ...entry,
                        locked: isPrivate,
                    },
                });
    
    Pascal Kosak's avatar
    Pascal Kosak committed
            } else {
    
                await pubSub.publish('newEntry', {
                    newEntry: {
                        id: entry.id,
                        agentId: entry.agentId,
                        private: true,
                        createdAt: entry.createdAt,
                        locked: true,
                    },
                });
            }
    
    Pascal Kosak's avatar
    Pascal Kosak committed
        }
    
        async register(msg: Message, match: RegExpMatchArray) {
    
            if (!match[1] && !match[2]) return;
    
    Pascal Kosak's avatar
    Pascal Kosak committed
    
            if (await this.isRegistered(msg.from.id))
    
    Adrian Paschkowski's avatar
    Adrian Paschkowski committed
                return await this.sendError(msg, 'Already registered');
    
    Pascal Kosak's avatar
    Pascal Kosak committed
    
    
            const existsCheck = await this.prismaService.agent.count({
    
                where: { password: match[1], uid: null },
    
    Adrian Paschkowski's avatar
    Adrian Paschkowski committed
                return await this.sendError(
                    msg,
                    'agentId not found or already taken',
                );
    
    
            const agent = await this.prismaService.agent.update({
    
                    password: match[1],
    
    Pascal Kosak's avatar
    Pascal Kosak committed
                data: {
                    uid: String(msg.from.id),
    
                    name: match[2],
    
    Pascal Kosak's avatar
    Pascal Kosak committed
                    slug: this.generateSlug(),
    
                    tokenCode: {
                        create: { value: 1 },
                    },
    
    Pascal Kosak's avatar
    Pascal Kosak committed
                },
            });
    
    
    Adrian Paschkowski's avatar
    Adrian Paschkowski committed
            await this.sendSuccess(
                msg,
    
                `Successfully registered\\!\n` +
    
    Adrian Paschkowski's avatar
    Adrian Paschkowski committed
                    `\\- Id: \`${agent.id}\`\n` +
                    `\\- Name: \`${agent.name}\`\n` +
                    `\\- Code: \`${agent.slug}\``,
    
    Pascal Kosak's avatar
    Pascal Kosak committed
        async unregister(msg: Message) {
    
    Pascal Kosak's avatar
    Pascal Kosak committed
            if (!(await this.isRegistered(msg.from.id)))
    
    Pascal Kosak's avatar
    Pascal Kosak committed
                return await this.sendError(msg, 'Not registered');
    
    Pascal Kosak's avatar
    Pascal Kosak committed
    
            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({
    
    Pascal Kosak's avatar
    Pascal Kosak committed
                where: { agentId: agent.id },
            });
    
    
            await this.prismaService.tokenCode.delete({
                where: { id: agent.tokenCodeId },
            });
    
    
    Pascal Kosak's avatar
    Pascal Kosak committed
            await this.prismaService.agent.delete({
                where: { id: agent.id },
            });
    
    
    Adrian Paschkowski's avatar
    Adrian Paschkowski committed
            await this.sendSuccess(msg, 'Deleted all Entries');
    
    Pascal Kosak's avatar
    Pascal Kosak committed
        }
    
    
    Pascal Kosak's avatar
    Pascal Kosak committed
        async profile(msg: Message) {
    
    Pascal Kosak's avatar
    Pascal Kosak committed
            if (!(await this.isRegistered(msg.from.id)))
    
    Pascal Kosak's avatar
    Pascal Kosak committed
                return await this.sendError(msg, 'Not registered');
    
    Pascal Kosak's avatar
    Pascal Kosak committed
    
            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 },
    
    Pascal Kosak's avatar
    Pascal Kosak committed
            });
    
    
            const privateEntries = await this.prismaService.entry.count({
                where: { agentId: agent.id, private: true },
    
    Pascal Kosak's avatar
    Pascal Kosak committed
            });
    
            await this.telegram.sendMessage(
                msg.chat.id,
    
    Pascal Kosak's avatar
    Pascal Kosak committed
                `📂 Agent Information\n` +
    
    Adrian Paschkowski's avatar
    Adrian Paschkowski committed
                    `\\- 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>> {
    
    Pascal Kosak's avatar
    Pascal Kosak committed
            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`,
            };
        }
    
    
    Pascal Kosak's avatar
    Pascal Kosak committed
        async supplyValue(msg: Message, metadata: Metadata) {
            if (metadata.type === 'text' && msg.text.startsWith('/')) return;
    
            if (!(await this.isRegistered(msg.from.id))) return;
    
    
    Adrian Paschkowski's avatar
    Adrian Paschkowski committed
            if (metadata.type === 'document')
                return await this.sendError(
                    msg,
                    "Unsupported DataType\\. Please send pictures as a 'Photo', not as a 'File'\\.",
                );
    
    
    Pascal Kosak's avatar
    Pascal Kosak committed
            const agent = await this.prismaService.agent.findFirst({
                where: { uid: String(msg.from.id) },
            });
    
    
            let entry: Partial<Entry> = {};
    
    Pascal Kosak's avatar
    Pascal Kosak committed
            if (this.messageCache.has(msg.from.id)) {
    
                entry = this.messageCache.get(msg.from.id);
    
    Pascal Kosak's avatar
    Pascal Kosak committed
            } 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' },
                );
            }
    
    Pascal Kosak's avatar
    Pascal Kosak committed
    
    
    Pascal Kosak's avatar
    Pascal Kosak committed
    
    
    Adrian Paschkowski's avatar
    Adrian Paschkowski committed
            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',
                        );
    
                    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,
    
    Adrian Paschkowski's avatar
    Adrian Paschkowski committed
                        '➡ Queued Location',
    
    Adrian Paschkowski's avatar
    Adrian Paschkowski committed
                    break;
    
    Pascal Kosak's avatar
    Pascal Kosak committed
    
    
    Adrian Paschkowski's avatar
    Adrian Paschkowski committed
                default:
                    return await this.sendError(msg, 'Unsupported DataType');
    
    Pascal Kosak's avatar
    Pascal Kosak committed
            }
    
    
            this.messageCache.set(msg.from.id, entry);
    
    Pascal Kosak's avatar
    Pascal Kosak committed
        }
    
    Pascal Kosak's avatar
    Pascal Kosak committed
    
        private async isRegistered(uid: string | number): Promise<boolean> {
            return (
                (await this.prismaService.agent.count({
                    where: { uid: String(uid) },
                })) > 0
            );
        }
    
    Pascal Kosak's avatar
    Pascal Kosak committed
    
        private async sendError(msg: Message, message: string) {
    
    Adrian Paschkowski's avatar
    Adrian Paschkowski committed
            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',
            });
    
    Pascal Kosak's avatar
    Pascal Kosak committed
    }