Merge pull request #5 from MrSedan/dev

Added some features
This commit is contained in:
2023-11-22 21:02:29 +03:00
committed by GitHub
27 changed files with 439 additions and 9 deletions

4
.gitmodules vendored Normal file
View File

@@ -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

View File

@@ -4,5 +4,6 @@
"semi": true, "semi": true,
"tabWidth": 4, "tabWidth": 4,
"printWidth": 150, "printWidth": 150,
"bracketSpacing": true "bracketSpacing": true,
"endOfLine": "lf"
} }

11
.vscode/settings.json vendored
View File

@@ -11,4 +11,15 @@
}, },
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode", "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"
}
} }

View File

@@ -1,6 +1,6 @@
import { config as configInit } from 'dotenv'; import { config as configInit } from 'dotenv';
configInit(); configInit({ path: '../.env' });
export const config = { export const config = {
database: { database: {

View File

@@ -0,0 +1,19 @@
import { Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
import { User } from './user.entity';
@Entity()
export class Admin {
constructor(props?: Partial<Admin>) {
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;
}

View File

@@ -0,0 +1,25 @@
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
import { Post } from './post.entity';
@Entity()
export class Image {
constructor(props?: Partial<Image>) {
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;
}

View File

@@ -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<Post>) {
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[];
}

View File

@@ -0,0 +1,5 @@
export enum EGetAll {
all = 'all',
will_post = 'will-post',
posted = 'posted',
}

View File

@@ -1,8 +1,11 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './database/user.entity'; 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({ @Module({
imports: [TypeOrmModule.forFeature([User])], imports: [TypeOrmModule.forFeature([User, Admin, Post, Image])],
exports: [TypeOrmModule], exports: [TypeOrmModule],
}) })
export class LibsModule {} export class LibsModule {}

View File

@@ -2,11 +2,14 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
import { config } from 'config'; import { config } from 'config';
import { LibsModule } from 'libs/libs.module'; 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 { AppInitService } from './modules/initialization/app.init.service';
import { PostModule } from './modules/post/post.module';
import { UserModule } from './modules/user/user.module'; import { UserModule } from './modules/user/user.module';
@Module({ @Module({
imports: [LibsModule, UserModule, TypeOrmModule.forRoot(<TypeOrmModuleOptions>config.database)], imports: [LibsModule, PostModule, AdminModule, UserModule, ImageModule, TypeOrmModule.forRoot(<TypeOrmModuleOptions>config.database)],
controllers: [], controllers: [],
providers: [AppInitService], providers: [AppInitService],
}) })

View File

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

View File

@@ -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 {}

View File

@@ -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<Admin>) {}
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;
}
}
}

View File

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

View File

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

View File

@@ -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 {}

View File

@@ -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<Image>,
@InjectRepository(Post) private postRepository: Repository<Post>,
) {}
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);
}
}
}

View File

@@ -1,11 +1,19 @@
import { Injectable, OnModuleInit } from '@nestjs/common'; import { Injectable, OnModuleInit } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; 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 { User } from 'libs/database/user.entity';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
@Injectable() @Injectable()
export class AppInitService implements OnModuleInit { export class AppInitService implements OnModuleInit {
constructor(@InjectRepository(User) private userRepository: Repository<User>) {} constructor(
@InjectRepository(User) private userRepository: Repository<User>,
@InjectRepository(Admin) private adminRepository: Repository<Admin>,
@InjectRepository(Post) private postRepository: Repository<Post>,
@InjectRepository(Image) private ImageRepository: Repository<Image>,
) {}
async onModuleInit() {} async onModuleInit() {}
} }

View File

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

View File

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

View File

@@ -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 {}

View File

@@ -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<Post>,
@InjectRepository(Admin) private adminRepository: Repository<Admin>,
) {}
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);
}
}
}

View File

@@ -6,13 +6,13 @@ import { UserService } from './user.service';
@ApiTags('User') @ApiTags('User')
@Controller('user') @Controller('user')
export class UserController { export class UserController {
constructor(private adminService: UserService) {} constructor(private userService: UserService) {}
@ApiOperation({ @ApiOperation({
description: 'Create or get user from db', description: 'Create or get user from db',
}) })
@Post('get') @Post('get')
async getUser(@Body() data: IGetUser) { async getUser(@Body() data: IGetUser) {
return await this.adminService.getUser(data); return await this.userService.getUser(data);
} }
} }

View File

@@ -11,7 +11,7 @@ export class UserService {
async getUser(data: IGetUser) { async getUser(data: IGetUser) {
try { 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({ let user = await this.userRepository.findOne({
where: { id: data.id }, where: { id: data.id },
}); });
@@ -21,7 +21,7 @@ export class UserService {
} }
return user; return user;
} catch (error) { } catch (error) {
this.logger.log(`[getUser] ${JSON.stringify({ error })}`); this.logger.log(`[user.getUser] ${JSON.stringify({ error })}`);
} }
} }
} }

21
docker-compose.yml Normal file
View File

@@ -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