diff --git a/backend/.env.example b/.env.example similarity index 100% rename from backend/.env.example rename to .env.example diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..adf5066 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "neuro-reply-bot-reworked"] + path = neuro-reply-bot-reworked + url = git@github.com:MrSedan/neuro-reply-bot-reworked.git + branch = dev \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index ece7630..5368c51 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,5 +4,6 @@ "semi": true, "tabWidth": 4, "printWidth": 150, - "bracketSpacing": true + "bracketSpacing": true, + "endOfLine": "lf" } diff --git a/.vscode/settings.json b/.vscode/settings.json index 2b07ebc..c4f402b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,4 +11,15 @@ }, "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", + "[dockercompose]": { + "editor.insertSpaces": true, + "editor.tabSize": 2, + "editor.autoIndent": "advanced", + "editor.quickSuggestions": { + "other": true, + "comments": false, + "strings": true + }, + "editor.defaultFormatter": "ms-azuretools.vscode-docker" + } } \ No newline at end of file diff --git a/backend/config/index.ts b/backend/config/index.ts index 280614f..c359859 100644 --- a/backend/config/index.ts +++ b/backend/config/index.ts @@ -1,6 +1,6 @@ import { config as configInit } from 'dotenv'; -configInit(); +configInit({ path: '../.env' }); export const config = { database: { diff --git a/backend/libs/database/admin.entity.ts b/backend/libs/database/admin.entity.ts new file mode 100644 index 0000000..a8d2bc7 --- /dev/null +++ b/backend/libs/database/admin.entity.ts @@ -0,0 +1,19 @@ +import { Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { User } from './user.entity'; + +@Entity() +export class Admin { + constructor(props?: Partial) { + Object.assign(this, props); + } + + @PrimaryGeneratedColumn() + public id!: number; + + @Column({ nullable: false }) + public user_id!: string; + + @OneToOne(() => User, (user) => user.id, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + public user!: User; +} diff --git a/backend/libs/database/image.entity.ts b/backend/libs/database/image.entity.ts new file mode 100644 index 0000000..14ca27d --- /dev/null +++ b/backend/libs/database/image.entity.ts @@ -0,0 +1,25 @@ +import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; +import { Post } from './post.entity'; + +@Entity() +export class Image { + constructor(props?: Partial) { + Object.assign(this, props); + } + + @PrimaryColumn() + public message_id!: number; + + @Column({ nullable: false }) + public file_id!: string; + + @Column({ default: false }) + public has_spoiler!: boolean; + + @Column({ nullable: false }) + public post_uuid!: string; + + @ManyToOne(() => Post, (post) => post.uuid, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'post_uuid' }) + public post!: Post; +} diff --git a/backend/libs/database/post.entity.ts b/backend/libs/database/post.entity.ts new file mode 100644 index 0000000..f58dc2a --- /dev/null +++ b/backend/libs/database/post.entity.ts @@ -0,0 +1,34 @@ +import { Column, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; +import { Admin } from './admin.entity'; +import { Image } from './image.entity'; +@Entity() +export class Post { + constructor(props?: Partial) { + Object.assign(this, props); + } + + @PrimaryGeneratedColumn('uuid') + public uuid!: string; + + @Column({ default: false }) + public posted!: boolean; + + @Column() + public text: string; + + @Column({ nullable: true }) + public media_group_id: string; + + @Column({ type: 'timestamptz' }) + public timestamp!: Date; + + @Column({ nullable: false }) + public from_user_id!: string; + + @ManyToOne(() => Admin, (admin) => admin.user.id) + @JoinColumn({ name: 'from_user_id', referencedColumnName: 'user_id' }) + public from_user!: Admin; + + @OneToMany(() => Image, (image) => image.post) + public images: Image[]; +} diff --git a/backend/libs/enums/getAll.enum.ts b/backend/libs/enums/getAll.enum.ts new file mode 100644 index 0000000..814872b --- /dev/null +++ b/backend/libs/enums/getAll.enum.ts @@ -0,0 +1,5 @@ +export enum EGetAll { + all = 'all', + will_post = 'will-post', + posted = 'posted', +} diff --git a/backend/libs/libs.module.ts b/backend/libs/libs.module.ts index 6761b66..ef53738 100644 --- a/backend/libs/libs.module.ts +++ b/backend/libs/libs.module.ts @@ -1,8 +1,11 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from './database/user.entity'; +import { Admin } from './database/admin.entity'; +import { Post } from './database/post.entity'; +import { Image } from './database/image.entity'; @Module({ - imports: [TypeOrmModule.forFeature([User])], + imports: [TypeOrmModule.forFeature([User, Admin, Post, Image])], exports: [TypeOrmModule], }) export class LibsModule {} diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 52a881e..65d3643 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -2,11 +2,14 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; import { config } from 'config'; import { LibsModule } from 'libs/libs.module'; +import { AdminModule } from './modules/admin/admin.module'; +import { ImageModule } from './modules/image/image.module'; import { AppInitService } from './modules/initialization/app.init.service'; +import { PostModule } from './modules/post/post.module'; import { UserModule } from './modules/user/user.module'; @Module({ - imports: [LibsModule, UserModule, TypeOrmModule.forRoot(config.database)], + imports: [LibsModule, PostModule, AdminModule, UserModule, ImageModule, TypeOrmModule.forRoot(config.database)], controllers: [], providers: [AppInitService], }) diff --git a/backend/src/modules/admin/admin.controller.ts b/backend/src/modules/admin/admin.controller.ts new file mode 100644 index 0000000..411a9b5 --- /dev/null +++ b/backend/src/modules/admin/admin.controller.ts @@ -0,0 +1,22 @@ +import { Controller, Get, Param } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { AdminService } from './admin.service'; + +@ApiTags('Admin') +@Controller('admin') +export class AdminController { + constructor(private adminService: AdminService) {} + + @ApiOperation({ + description: 'Get admins from db', + }) + @Get('get') + async getAdmin() { + return await this.adminService.getAdmins(); + } + @ApiOperation({ description: 'Check admin is or not' }) + @Get('is-admin/:id') + async isAdmin(@Param('id') id: string) { + return await this.adminService.checkIsAdmin(id); + } +} diff --git a/backend/src/modules/admin/admin.module.ts b/backend/src/modules/admin/admin.module.ts new file mode 100644 index 0000000..44faa1e --- /dev/null +++ b/backend/src/modules/admin/admin.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { LibsModule } from 'libs/libs.module'; +import { AdminController } from './admin.controller'; +import { AdminService } from './admin.service'; + +@Module({ + imports: [LibsModule], + controllers: [AdminController], + providers: [AdminService], +}) +export class AdminModule {} diff --git a/backend/src/modules/admin/admin.service.ts b/backend/src/modules/admin/admin.service.ts new file mode 100644 index 0000000..cfd9557 --- /dev/null +++ b/backend/src/modules/admin/admin.service.ts @@ -0,0 +1,37 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Admin } from 'libs/database/admin.entity'; +import { Repository } from 'typeorm'; + +@Injectable() +export class AdminService { + private readonly logger: Logger = new Logger(AdminService.name); + constructor(@InjectRepository(Admin) private adminRepository: Repository) {} + + async getAdmins() { + try { + this.logger.debug(`[admin.getAdmins]`); + const admins = await this.adminRepository.find(); + return admins; + } catch (error) { + this.logger.log(`[getAdmin] ${JSON.stringify({ error })}`); + return []; + } + } + async checkIsAdmin(id: string) { + try { + this.logger.debug(`[admin.checkIsAdmin]`); + const admins = await this.adminRepository.findOne({ + relations: { user: true }, + where: { user: { id: id } }, + }); + if (!admins) { + return false; + } + return true; + } catch (error) { + this.logger.debug(`[checkIsAdmin] ${JSON.stringify({ error })}`); + return false; + } + } +} diff --git a/backend/src/modules/image/image.controller.ts b/backend/src/modules/image/image.controller.ts new file mode 100644 index 0000000..d83fbb4 --- /dev/null +++ b/backend/src/modules/image/image.controller.ts @@ -0,0 +1,16 @@ +import { Body, Controller, Post } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { IAddImage } from './image.dto'; +import { ImageService } from './image.service'; + +@ApiTags('Image') +@Controller('image') +export class ImageController { + constructor(private imageService: ImageService) {} + + @ApiOperation({ description: 'A method to add photo to post' }) + @Post('add') + async addImage(@Body() data: IAddImage) { + return await this.imageService.add(data); + } +} diff --git a/backend/src/modules/image/image.dto.ts b/backend/src/modules/image/image.dto.ts new file mode 100644 index 0000000..8a027b6 --- /dev/null +++ b/backend/src/modules/image/image.dto.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class IAddImage { + @ApiProperty({ description: 'A post that contains this photo', example: '1212-4324-asdf-23432' }) readonly post_id!: string; + @ApiProperty({ description: 'A telegram file id of photo', example: '1214244' }) readonly file_id!: string; + @ApiProperty({ description: 'Has image the spoiler?', example: false }) readonly has_spoiler!: boolean; + @ApiProperty({ description: 'A photo message id', example: '123124' }) readonly message_id!: number; +} diff --git a/backend/src/modules/image/image.module.ts b/backend/src/modules/image/image.module.ts new file mode 100644 index 0000000..2c8c6c1 --- /dev/null +++ b/backend/src/modules/image/image.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { LibsModule } from 'libs/libs.module'; +import { ImageController } from './image.controller'; +import { ImageService } from './image.service'; + +@Module({ + imports: [LibsModule], + controllers: [ImageController], + providers: [ImageService], +}) +export class ImageModule {} diff --git a/backend/src/modules/image/image.service.ts b/backend/src/modules/image/image.service.ts new file mode 100644 index 0000000..b2e4818 --- /dev/null +++ b/backend/src/modules/image/image.service.ts @@ -0,0 +1,26 @@ +import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Image } from 'libs/database/image.entity'; +import { Post } from 'libs/database/post.entity'; +import { Repository } from 'typeorm'; +import { IAddImage } from './image.dto'; +@Injectable() +export class ImageService { + private readonly logger: Logger = new Logger(ImageService.name); + constructor( + @InjectRepository(Image) private imageRepository: Repository, + @InjectRepository(Post) private postRepository: Repository, + ) {} + + async add(data: IAddImage) { + try { + this.logger.log(`[image.add] data: ${JSON.stringify(data)}`); + const post = await this.postRepository.findOne({ where: { uuid: data.post_id } }); + await this.imageRepository.save({ post: post, file_id: data.file_id, has_spoiler: data.has_spoiler, message_id: data.message_id }); + return { status: 'ok' }; + } catch (error) { + this.logger.debug(`[image.add] error: ${JSON.stringify(error)}`); + throw new HttpException('No posts', HttpStatus.BAD_REQUEST); + } + } +} diff --git a/backend/src/modules/initialization/app.init.service.ts b/backend/src/modules/initialization/app.init.service.ts index e22de72..9361f3e 100644 --- a/backend/src/modules/initialization/app.init.service.ts +++ b/backend/src/modules/initialization/app.init.service.ts @@ -1,11 +1,19 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { Admin } from 'libs/database/admin.entity'; +import { Image } from 'libs/database/image.entity'; +import { Post } from 'libs/database/post.entity'; import { User } from 'libs/database/user.entity'; import { Repository } from 'typeorm'; @Injectable() export class AppInitService implements OnModuleInit { - constructor(@InjectRepository(User) private userRepository: Repository) {} + constructor( + @InjectRepository(User) private userRepository: Repository, + @InjectRepository(Admin) private adminRepository: Repository, + @InjectRepository(Post) private postRepository: Repository, + @InjectRepository(Image) private ImageRepository: Repository, + ) {} async onModuleInit() {} } diff --git a/backend/src/modules/post/post.controller.ts b/backend/src/modules/post/post.controller.ts new file mode 100644 index 0000000..12953f9 --- /dev/null +++ b/backend/src/modules/post/post.controller.ts @@ -0,0 +1,42 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; +import { EGetAll } from 'libs/enums/getAll.enum'; +import { ICreatePost, IEditPost } from './post.dto'; +import { PostService } from './post.service'; + +@ApiTags('Post') +@Controller('post') +export class PostController { + constructor(private postService: PostService) {} + + @ApiOperation({ description: 'Creates a new post' }) + @Post('new') + async newPost(@Body() data: ICreatePost) { + return await this.postService.newPost(data); + } + + @ApiOperation({ description: 'Getting all posts. By default - all' }) + @Get('get-all/:status') + @ApiParam({ name: 'status', required: false, enum: EGetAll }) + async getAllPosts(@Param('status') status?: EGetAll) { + return await this.postService.getAllPosts(status || EGetAll.all); + } + + @ApiOperation({ description: 'Getting a post by uuid' }) + @Get('get/:postId') + async getPost(@Param('postId') postId: string) { + return await this.postService.getPost(postId); + } + + @ApiOperation({ description: 'Getting a post by its media group id' }) + @Get('get-by-media-group-id/:mediaGroupId') + async getByMediaGroup(@Param('mediaGroupId') mediaGroupId: string) { + return await this.postService.getByMediaGroup(mediaGroupId); + } + + @ApiOperation({ description: 'Editing a post by its uuid' }) + @Post('edit/:postId') + async editPost(@Param('postId') postId: string, @Body() data: IEditPost) { + return await this.postService.editPost(postId, data); + } +} diff --git a/backend/src/modules/post/post.dto.ts b/backend/src/modules/post/post.dto.ts new file mode 100644 index 0000000..9c94994 --- /dev/null +++ b/backend/src/modules/post/post.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class ICreatePost { + @ApiProperty({ description: 'Post text', example: 'Post text' }) readonly text!: string; + + @ApiProperty({ description: 'An id of user that creating post', example: '1234' }) readonly from_user_id!: string; + + @ApiProperty({ description: 'Post media group id', example: '123' }) readonly media_group_id?: string; +} + +export class IEditPost { + @ApiProperty({ description: 'Post text', example: 'Post text' }) readonly text!: string; +} diff --git a/backend/src/modules/post/post.module.ts b/backend/src/modules/post/post.module.ts new file mode 100644 index 0000000..9cabfe0 --- /dev/null +++ b/backend/src/modules/post/post.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { LibsModule } from 'libs/libs.module'; +import { PostController } from './post.controller'; +import { PostService } from './post.service'; + +@Module({ + imports: [LibsModule], + controllers: [PostController], + providers: [PostService], +}) +export class PostModule {} diff --git a/backend/src/modules/post/post.service.ts b/backend/src/modules/post/post.service.ts new file mode 100644 index 0000000..34045e1 --- /dev/null +++ b/backend/src/modules/post/post.service.ts @@ -0,0 +1,98 @@ +import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Admin } from 'libs/database/admin.entity'; +import { Post } from 'libs/database/post.entity'; +import { EGetAll } from 'libs/enums/getAll.enum'; +import { Repository } from 'typeorm'; +import { ICreatePost, IEditPost } from './post.dto'; + +@Injectable() +export class PostService { + private readonly logger: Logger = new Logger(PostService.name); + constructor( + @InjectRepository(Post) private postRepository: Repository, + @InjectRepository(Admin) private adminRepository: Repository, + ) {} + + async newPost(data: ICreatePost) { + try { + this.logger.log(`[post.newPost] data: ${JSON.stringify(data)}`); + const user = await this.adminRepository.findOne({ where: { user: { id: data.from_user_id } }, relations: { user: true } }); + const result = await this.postRepository.save({ + text: data.text, + media_group_id: data.media_group_id, + from_user: user, + timestamp: new Date(), + }); + this.logger.log(`Created new post: ${result.uuid}`); + return result; + } catch (error) { + this.logger.debug(`[post.newPost] error: ${JSON.stringify(error)}`); + throw new HttpException('No user with this id', HttpStatus.BAD_REQUEST); + } + } + + async editPost(postId: string, data: IEditPost) { + try { + this.logger.log(`[post.editPost] data: ${JSON.stringify(data)}`); + const post = await this.postRepository.findOne({ where: { uuid: postId } }); + if (!post) { + throw new HttpException('Post not found', HttpStatus.NOT_FOUND); + } + if (post.text !== data.text) { + post.text = data.text; + post.timestamp = new Date(); + await this.postRepository.save(post); + } + return post; + } catch (error) { + this.logger.debug(`[post.editPost] error: ${JSON.stringify(error)}`); + throw new HttpException('Post not found', HttpStatus.NOT_FOUND); + } + } + + async getAllPosts(status: EGetAll) { + try { + let obj: object; + switch (status) { + case EGetAll.will_post: + obj = { where: { posted: false } }; + break; + case EGetAll.all: + obj = {}; + break; + case EGetAll.posted: + obj = { where: { posted: true } }; + break; + } + return await this.postRepository.find(obj); + } catch (error) { + this.logger.log(`[post.getAllPosts] error: ${JSON.stringify(error)}`); + return []; + } + } + + async getPost(postId: string) { + try { + this.logger.log(`[post.getPost] data: ${postId}`); + const post = await this.postRepository.findOne({ where: { uuid: postId }, relations: { images: true } }); + if (!post) throw new Error("Can't find post"); + return post; + } catch (error) { + this.logger.log(`[post.getPost] error: ${JSON.stringify(error)}`); + throw new HttpException('No post with this id', HttpStatus.NOT_FOUND); + } + } + + async getByMediaGroup(mediaGroupId: string) { + try { + this.logger.log(`[post.getByMediaGroup] data: ${mediaGroupId}`); + const post = await this.postRepository.findOne({ where: { media_group_id: mediaGroupId } }); + if (!post) throw new Error("Can't find post"); + return post; + } catch (error) { + this.logger.debug(`[post.getByMediaGroup] error: ${JSON.stringify(error)}`); + throw new HttpException("Can't find post with this media group id", HttpStatus.BAD_REQUEST); + } + } +} diff --git a/backend/src/modules/user/user.controller.ts b/backend/src/modules/user/user.controller.ts index 8305e4a..7e8266f 100644 --- a/backend/src/modules/user/user.controller.ts +++ b/backend/src/modules/user/user.controller.ts @@ -6,13 +6,13 @@ import { UserService } from './user.service'; @ApiTags('User') @Controller('user') export class UserController { - constructor(private adminService: UserService) {} + constructor(private userService: UserService) {} @ApiOperation({ description: 'Create or get user from db', }) @Post('get') async getUser(@Body() data: IGetUser) { - return await this.adminService.getUser(data); + return await this.userService.getUser(data); } } diff --git a/backend/src/modules/user/user.service.ts b/backend/src/modules/user/user.service.ts index c334eb9..6854cbe 100644 --- a/backend/src/modules/user/user.service.ts +++ b/backend/src/modules/user/user.service.ts @@ -11,7 +11,7 @@ export class UserService { async getUser(data: IGetUser) { try { - this.logger.debug(`[admin.getUser] data: ${JSON.stringify(data)}`); + this.logger.debug(`[user.getUser] data: ${JSON.stringify(data)}`); let user = await this.userRepository.findOne({ where: { id: data.id }, }); @@ -21,7 +21,7 @@ export class UserService { } return user; } catch (error) { - this.logger.log(`[getUser] ${JSON.stringify({ error })}`); + this.logger.log(`[user.getUser] ${JSON.stringify({ error })}`); } } } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6da560a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +version: '3.9' + +services: + db: + container_name: neuro_db + image: postgres:alpine + environment: + - POSTGRES_USER=${DATABASE_USER} + - POSTGRES_PASSWORD=${DATABASE_PASSWORD} + - POSTGRES_DB=${DATABASE_NAME} + - PGDATA=/var/lib/postgresql/data/pgdata + ports: + - "${DATABASE_PORT}:5432" + env_file: + - .env + volumes: + - neuro_postgres_db:/var/lib/postgresql/data + restart: always +volumes: + neuro_postgres_db: + driver: local diff --git a/neuro-reply-bot-reworked b/neuro-reply-bot-reworked new file mode 160000 index 0000000..d286da6 --- /dev/null +++ b/neuro-reply-bot-reworked @@ -0,0 +1 @@ +Subproject commit d286da698e06a5bfa57131327db10d1fc716eea5