From eff03b8c3034784d0b69228080d6ceb591a280be Mon Sep 17 00:00:00 2001
From: Pascal Kosak <pascal.kosak@ruhr-uni-bochum.de>
Date: Fri, 27 Aug 2021 20:47:20 +0200
Subject: [PATCH] Image update, Model changes, More Telegram commands, Message
 queue

---
 .gitignore                                    |   1 +
 photos/.gitkeep                               |   0
 prisma/schema.prisma                          |   2 -
 src/app.module.ts                             |   7 +-
 .../{auth.guard.ts => graphql-auth.guard.ts}  |   2 +-
 src/auth/web-auth.guard.ts                    |  30 +++
 src/images.controller.ts                      |  17 ++
 src/resolvers/agent.resolver.ts               |  35 +++-
 src/resolvers/entry.resolver.ts               |  20 +-
 src/resolvers/group.resolver.ts               |   9 +-
 src/resolvers/models/entry.model.ts           |   5 +-
 src/telegram/telegram.service.ts              | 174 ++++++++++--------
 12 files changed, 208 insertions(+), 94 deletions(-)
 create mode 100644 photos/.gitkeep
 rename src/auth/{auth.guard.ts => graphql-auth.guard.ts} (93%)
 create mode 100644 src/auth/web-auth.guard.ts
 create mode 100644 src/images.controller.ts

diff --git a/.gitignore b/.gitignore
index 1382ce4..3d5d1fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -235,3 +235,4 @@ modules.xml
 # End of https://www.toptal.com/developers/gitignore/api/webstorm+all,visualstudiocode,node,dotenv
 
 *.db
+/photos/*.jpg
\ No newline at end of file
diff --git a/photos/.gitkeep b/photos/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 71499ce..f1968e5 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -25,11 +25,9 @@ model Agent {
     entries Entry[]
 }
 
-// Types: 'picture', 'location', 'text'
 model Entry {
     id      String  @id @default(uuid())
 
-    type    String
     private Boolean
 
     content String?
diff --git a/src/app.module.ts b/src/app.module.ts
index 46d666f..7e2ad6b 100644
--- a/src/app.module.ts
+++ b/src/app.module.ts
@@ -3,7 +3,9 @@ import { PrismaModule } from './prisma/prisma.module';
 import { TelegramModule } from './telegram/telegram.module';
 import { GraphQLModule } from '@nestjs/graphql';
 import { ResolverModule } from './resolvers/resolver.module';
-import { AuthGuard } from './auth/auth.guard';
+import { GraphQLAuthGuard } from './auth/graphql-auth.guard';
+import { ImageController } from './images.controller';
+import { WebAuthGuard } from './auth/web-auth.guard';
 
 @Module({
     imports: [
@@ -18,6 +20,7 @@ import { AuthGuard } from './auth/auth.guard';
             }),
         }),
     ],
-    providers: [AuthGuard],
+    providers: [GraphQLAuthGuard, WebAuthGuard],
+    controllers: [ImageController],
 })
 export class AppModule {}
diff --git a/src/auth/auth.guard.ts b/src/auth/graphql-auth.guard.ts
similarity index 93%
rename from src/auth/auth.guard.ts
rename to src/auth/graphql-auth.guard.ts
index b1e3bb5..ef72b1b 100644
--- a/src/auth/auth.guard.ts
+++ b/src/auth/graphql-auth.guard.ts
@@ -5,7 +5,7 @@ import { Observable } from "rxjs";
 import { PrismaService } from "src/prisma/prisma.service";
 
 @Injectable()
-export class AuthGuard implements CanActivate {
+export class GraphQLAuthGuard implements CanActivate {
 
     constructor(private prismaService: PrismaService) {}
     
diff --git a/src/auth/web-auth.guard.ts b/src/auth/web-auth.guard.ts
new file mode 100644
index 0000000..9f63f6b
--- /dev/null
+++ b/src/auth/web-auth.guard.ts
@@ -0,0 +1,30 @@
+import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
+import { Request } from "express";
+import { PrismaService } from "src/prisma/prisma.service";
+
+@Injectable()
+export class WebAuthGuard implements CanActivate {
+
+    constructor(private prismaService: PrismaService) {}
+    
+    async canActivate(context: ExecutionContext): Promise<boolean> {
+        const req: Request = context.switchToHttp().getRequest();
+
+        const token = req.headers.authorization;
+
+        if (!token)
+            return false;
+
+        const group = await this.prismaService.group.findFirst({
+            where: { code: token },
+        });
+
+        if (!group)
+            return false;
+
+        req.group = group;
+
+        return true;
+    }
+
+}
\ No newline at end of file
diff --git a/src/images.controller.ts b/src/images.controller.ts
new file mode 100644
index 0000000..1523c0f
--- /dev/null
+++ b/src/images.controller.ts
@@ -0,0 +1,17 @@
+import { Controller, Get, Header, HttpCode, Param, Res, UseGuards } from '@nestjs/common';
+import { Response } from 'express';
+import { join } from 'path';
+import { WebAuthGuard } from './auth/web-auth.guard';
+
+@Controller('images')
+@UseGuards(WebAuthGuard)
+export class ImageController {
+
+    @Get(':id')
+    public image(
+      @Param('id') id: string,
+      @Res() res: Response,
+    ) {
+        res.sendFile(join(process.cwd(), 'photos', `${id}.jpg`));
+    }
+}
\ No newline at end of file
diff --git a/src/resolvers/agent.resolver.ts b/src/resolvers/agent.resolver.ts
index 3138bf7..dcbc342 100644
--- a/src/resolvers/agent.resolver.ts
+++ b/src/resolvers/agent.resolver.ts
@@ -3,17 +3,16 @@ import { Agent } from './models/agent.model';
 import { PrismaService } from '../prisma/prisma.service';
 import { GraphQLString } from 'graphql';
 import { NotFoundException, UseGuards } from '@nestjs/common';
-import { AuthGuard } from 'src/auth/auth.guard';
+import { GraphQLAuthGuard } from 'src/auth/graphql-auth.guard';
 import { Request } from 'express';
 
 @Resolver(() => Agent)
-@UseGuards(AuthGuard)
+@UseGuards(GraphQLAuthGuard)
 export class AgentResolver {
     constructor(private prismaService: PrismaService) {}
 
     @Query(() => [Agent])
     listAgents(@Context() { req }: { req: Request }) {
-        console.log(req.group)
         return this.prismaService.agent.findMany();
     }
 
@@ -31,7 +30,19 @@ export class AgentResolver {
     }
 
     @ResolveField('entries')
-    async entries(@Parent() entry: Agent) {
+    async entries(
+        @Parent() entry: Agent,
+        @Context() { req }: { req: Request },
+    ) {
+        const unlocks = await this.prismaService.group.findFirst({
+            where: { id: req.group.id },
+            select: {
+                unlocks: true,
+            },
+        })
+            .then(value => value.unlocks)
+            .then(unlocks => unlocks.map(value => value.id));
+
         const result = await this.prismaService.agent.findFirst({
             where: {
                 id: entry.id,
@@ -41,6 +52,22 @@ export class AgentResolver {
             },
         });
 
+        const entries = result.entries.map(entry => {
+            if (!entry.private || unlocks.includes(entry.id))
+                return {
+                    ...entry,
+                    locked: false,
+                };
+
+            return {
+                id: entry.id,
+                agentId: entry.agentId,
+                private: true,
+                createdAt: entry.createdAt,
+                locked: true,
+            };
+        });
 
+        return entries;
     }
 }
diff --git a/src/resolvers/entry.resolver.ts b/src/resolvers/entry.resolver.ts
index 70bc535..d914f24 100644
--- a/src/resolvers/entry.resolver.ts
+++ b/src/resolvers/entry.resolver.ts
@@ -4,10 +4,10 @@ import { PrismaService } from '../prisma/prisma.service';
 import { GraphQLString } from 'graphql';
 import { BadRequestException, NotFoundException, UseGuards } from '@nestjs/common';
 import { Request } from 'express';
-import { AuthGuard } from 'src/auth/auth.guard';
+import { GraphQLAuthGuard } from 'src/auth/graphql-auth.guard';
 
 @Resolver(() => Entry)
-@UseGuards(AuthGuard)
+@UseGuards(GraphQLAuthGuard)
 export class EntryResolver {
     constructor(private prismaService: PrismaService) {}
 
@@ -39,6 +39,17 @@ export class EntryResolver {
         if (req.group.tokens <= 0)
             throw new BadRequestException('No remaining tokens');
 
+        const count = await this.prismaService.group.count({
+            where: {
+                unlocks: {
+                    some: { id },
+                },
+            },
+        });
+
+        if (count > 0)
+            throw new BadRequestException("Already unlocked");
+
         await this.prismaService.group.update({
             where: { id: req.group.id },
             data: {
@@ -49,7 +60,10 @@ export class EntryResolver {
             },
         });
 
-        return unlock;
+        return {
+            ...unlock,
+            locked: false,
+        };
 
     }
 }
diff --git a/src/resolvers/group.resolver.ts b/src/resolvers/group.resolver.ts
index 781044b..92b360e 100644
--- a/src/resolvers/group.resolver.ts
+++ b/src/resolvers/group.resolver.ts
@@ -12,11 +12,11 @@ import { PrismaService } from '../prisma/prisma.service';
 import { GraphQLString } from 'graphql';
 import seedWords from 'mnemonic-words';
 import { NotFoundException, UseGuards } from '@nestjs/common';
-import { AuthGuard } from 'src/auth/auth.guard';
+import { GraphQLAuthGuard } from 'src/auth/graphql-auth.guard';
 import { Request } from 'express';
 
 @Resolver(() => Group)
-@UseGuards(AuthGuard)
+@UseGuards(GraphQLAuthGuard)
 export class GroupResolver {
     constructor(private prismaService: PrismaService) {}
 
@@ -57,6 +57,9 @@ export class GroupResolver {
             select: { unlocks: true },
         });
 
-        return result.unlocks;
+        return result.unlocks.map(unlock => ({
+            ...unlock,
+            locked: false,
+        }));
     }
 }
diff --git a/src/resolvers/models/entry.model.ts b/src/resolvers/models/entry.model.ts
index 1be72c1..e907a6f 100644
--- a/src/resolvers/models/entry.model.ts
+++ b/src/resolvers/models/entry.model.ts
@@ -12,13 +12,10 @@ export class Entry {
     @Field()
     locked: boolean;
 
-    @Field()
-    type: string;
-
     @Field({ nullable: true })
     content?: string;
     @Field({ nullable: true })
-    path?: string;
+    image?: string;
     @Field({ nullable: true })
     lat?: string;
     @Field({ nullable: true })
diff --git a/src/telegram/telegram.service.ts b/src/telegram/telegram.service.ts
index 16674c1..93e59f0 100644
--- a/src/telegram/telegram.service.ts
+++ b/src/telegram/telegram.service.ts
@@ -5,11 +5,14 @@ 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';
 
 @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,
@@ -19,7 +22,11 @@ export class TelegramService {
         this.telegram.onText(/^\/unregister/, this.unregister.bind(this));
         this.telegram.onText(/^\/state/, this.state.bind(this));
         this.telegram.onText(/^\/start/, this.start.bind(this));
-        this.telegram.onText(/^\/commit (.+)/, this.commit.bind(this));
+
+        this.telegram.onText(/^\/private/, this.private.bind(this));
+        this.telegram.onText(/^\/public/, this.public.bind(this));
+        this.telegram.onText(/^\/clear/, this.clear.bind(this));
+
         this.telegram.on('message', this.supplyValue.bind(this));
     }
 
@@ -32,40 +39,55 @@ export class TelegramService {
         ).toString(36);
     }
 
-    async start(msg: Message, match: RegExpMatchArray) {
-        await this.telegram.sendMessage(
+    async private(msg: Message, match: RegExpMatchArray) {
+        this.commitMessage(msg.from.id, true, msg);
+    }
+
+    async public(msg: Message, match: RegExpMatchArray) {
+        this.commitMessage(msg.from.id, false, msg);
+    }
+
+
+    async clear(msg: Message, match: RegExpMatchArray) {
+        this.messageCache.delete(msg.from.id);
+
+        return void (await this.telegram.sendMessage(
             msg.chat.id,
-            `/register [Name]\n/state\n/unregister`,
-        );
+            'Message Queue cleared!',
+        ));
     }
 
-    async commit(msg: Message, match: RegExpMatchArray) {
-        if (!match[1])
+    async commitMessage(id: number, isPrivate: boolean, msg: Message) {
+        if (!(await this.isRegistered(msg.from.id)))
             return void (await this.telegram.sendMessage(
                 msg.chat.id,
-                'No message',
+                'Not registered!',
             ));
 
-        if (!(await this.isRegistered(msg.from.id)))
+        if (!this.messageCache.has(id))
             return void (await this.telegram.sendMessage(
                 msg.chat.id,
-                'Not registered',
+                'No queued Messages!',
             ));
 
-        const agent = await this.prismaService.agent.findFirst({
-            where: { uid: String(msg.from.id) },
-        });
+        const entry = this.messageCache.get(msg.from.id);
+
+        entry.private = isPrivate;
+
+        this.messageCache.delete(msg.from.id);
 
         await this.prismaService.entry.create({
-            data: {
-                agentId: agent.id,
-                content: match[1],
-                private: false,
-                type: 'text',
-            },
+            data: entry as any,
         });
 
-        await this.telegram.sendMessage(msg.chat.id, 'Commit accepted');
+        // TODO: Send a notification to subscriptions
+    }
+
+    async start(msg: Message, match: RegExpMatchArray) {
+        await this.telegram.sendMessage(
+            msg.chat.id,
+            `/register [Name]\n/state\n/unregister`,
+        );
     }
 
     async register(msg: Message, match: RegExpMatchArray) {
@@ -135,20 +157,45 @@ export class TelegramService {
             where: { uid: String(msg.from.id) },
         });
 
-        const entries = await this.prismaService.entry.count({
-            where: { agentId: agent.id },
+        const publicEntries = await this.prismaService.entry.count({
+            where: { agentId: agent.id, private: false },
         });
 
-        const unlockEntries = await this.prismaService.entry.count({
-            where: { agentId: agent.id },
+        const privateEntries = await this.prismaService.entry.count({
+            where: { agentId: agent.id, private: true },
         });
 
         await this.telegram.sendMessage(
             msg.chat.id,
-            `Id: ${agent.id}\nName: ${agent.name}\nCode: ${agent.slug}\nPublic Entries: ${entries}\nUnlockable Entries: ${unlockEntries}`,
+            `Id: ${agent.id}\nName: ${agent.name}\nCode: ${agent.slug}\nPublic Entries: ${publicEntries}\nPrivate Entries: ${privateEntries}`,
         );
     }
 
+    async receiveImage(msg: Message): Promise<Partial<Entry>> {
+        let file;
+        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 supplyValue(msg: Message, metadata: Metadata) {
         if (metadata.type === 'text' && msg.text.startsWith('/')) return;
 
@@ -158,73 +205,50 @@ export class TelegramService {
             where: { uid: String(msg.from.id) },
         });
 
-        if (metadata.type === 'photo') {
-            let file,
-                size = -1;
-            for (const p of msg.photo) {
-                if (p.width > size) {
-                    size = p.width;
-                    file = p.file_id;
-                }
-            }
-
-            const id = uuid();
+        let entry: Partial<Entry> = {};
+        if (this.messageCache.has(msg.from.id))
+            entry = this.messageCache.get(msg.from.id);
 
-            const dest = fs.createWriteStream(
-                path.join(process.cwd(), 'static', 'photos', `${id}.jpg`),
-            );
-            const pipe = this.telegram.getFileStream(file).pipe(dest);
+        entry.agentId = agent.id;
 
-            await new Promise((resolve) => pipe.on('finish', resolve));
+        if (metadata.type === 'photo') {
+            const imageEntry = await this.receiveImage(msg);
+            
+            if (entry.image)
+                await this.telegram.sendMessage(
+                    msg.chat.id,
+                    'Overwriting previous image',
+                );
 
-            await this.prismaService.entry.create({
-                data: {
-                    type: 'picture',
-                    agentId: agent.id,
-                    image: `${id}.jpg`,
-                    private: true,
-                },
-            });
+            entry.image = imageEntry.image;
 
-            return void (await this.telegram.sendMessage(
+            await this.telegram.sendMessage(
                 msg.chat.id,
-                `Accepted Photo`,
-            ));
+                'Queued Image',
+            );
         } else if (metadata.type === 'text') {
-            await this.prismaService.entry.create({
-                data: {
-                    type: 'text',
-                    agentId: agent.id,
-                    content: msg.text,
-                    private: true,
-                },
-            });
+            entry.content = msg.text;
 
-            return void (await this.telegram.sendMessage(
+            await this.telegram.sendMessage(
                 msg.chat.id,
-                'Accepted Text',
-            ));
+                'Queued Text',
+            );
         } else if (metadata.type === 'location') {
-            await this.prismaService.entry.create({
-                data: {
-                    agentId: agent.id,
-                    type: 'location',
-                    lat: msg.location.latitude.toString(),
-                    lon: msg.location.longitude.toString(),
-                    private: true,
-                },
-            });
+            entry.lat = msg.location.latitude.toString();
+            entry.lon = msg.location.longitude.toString();
 
-            return void (await this.telegram.sendMessage(
+            await this.telegram.sendMessage(
                 msg.chat.id,
-                'Accepted Location',
-            ));
+                'Queued Location',
+            );
         } else {
             return void (await this.telegram.sendMessage(
                 msg.chat.id,
                 'Unsupported DataType',
             ));
         }
+
+        this.messageCache.set(msg.from.id, entry);
     }
 
     private async isRegistered(uid: string | number): Promise<boolean> {
-- 
GitLab