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