diff --git a/.gitmodules b/.gitmodules index adf5066..3a5cc74 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "neuro-reply-bot-reworked"] path = neuro-reply-bot-reworked - url = git@github.com:MrSedan/neuro-reply-bot-reworked.git + url = https://github.com/MrSedan/neuro-reply-bot-reworked.git branch = dev \ No newline at end of file diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..e9c2afc --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,2 @@ +node_modules +dockerfile \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..3e9ce51 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,9 @@ +FROM node:lts-alpine +WORKDIR /app +RUN corepack enable && corepack prepare pnpm@latest --activate +COPY package.json pnpm-*.yaml ./ +RUN pnpm install --frozen-lockfile +COPY . . +RUN pnpm build +EXPOSE 3000 +CMD [ "pnpm", "start:prod" ] \ No newline at end of file diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev new file mode 100644 index 0000000..6d01c64 --- /dev/null +++ b/backend/Dockerfile.dev @@ -0,0 +1,8 @@ +FROM node:lts-alpine +WORKDIR /app +RUN corepack enable && corepack prepare pnpm@latest --activate +COPY package.json pnpm-*.yaml ./ +RUN pnpm install --frozen-lockfile +COPY . . +EXPOSE 3000 +CMD [ "pnpm", "start:dev" ] \ No newline at end of file diff --git a/backend/libs/database/payment.entity.ts b/backend/libs/database/payment.entity.ts new file mode 100644 index 0000000..71f68a0 --- /dev/null +++ b/backend/libs/database/payment.entity.ts @@ -0,0 +1,22 @@ +import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { ProxyUser } from './proxy_user.entity'; + +@Entity() +export class Payment { + constructor(props?: Partial) { + Object.assign(this, props); + } + + @PrimaryGeneratedColumn('increment') + public id!: number; + + @Column() + public user_uuid!: string; + + @Column({ type: 'timestamptz' }) + public payTime!: Date; + + @ManyToOne(() => ProxyUser, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_uuid' }) + user: ProxyUser; +} diff --git a/backend/libs/database/post.entity.ts b/backend/libs/database/post.entity.ts index f58dc2a..739d593 100644 --- a/backend/libs/database/post.entity.ts +++ b/backend/libs/database/post.entity.ts @@ -22,6 +22,9 @@ export class Post { @Column({ type: 'timestamptz' }) public timestamp!: Date; + @Column({ type: 'timestamptz', nullable: true }) + public edit_timestamp?: Date; + @Column({ nullable: false }) public from_user_id!: string; diff --git a/backend/libs/database/proxy_user.entity.ts b/backend/libs/database/proxy_user.entity.ts new file mode 100644 index 0000000..95f50ab --- /dev/null +++ b/backend/libs/database/proxy_user.entity.ts @@ -0,0 +1,35 @@ +import { Column, Entity, JoinColumn, OneToMany, OneToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { Payment } from './payment.entity'; +import { User } from './user.entity'; + +@Entity() +export class ProxyUser { + constructor(props?: Partial) { + Object.assign(this, props); + } + + @PrimaryGeneratedColumn('uuid') + public uuid!: string; + + @Column({ nullable: false, unique: true }) + public userName!: string; + + @Column({ nullable: true }) + public description?: string; + + @Column({ nullable: false }) + public link!: string; + + @Column({ nullable: false, type: 'timestamptz' }) + public connectDate!: Date; + + @Column({ nullable: true }) + public user_id!: string; + + @OneToOne(() => User, (user) => user.id, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true }) + @JoinColumn({ name: 'user_id' }) + public user?: User; + + @OneToMany(() => Payment, (payment) => payment.user) + public payments: Payment[]; +} diff --git a/backend/libs/libs.module.ts b/backend/libs/libs.module.ts index ef53738..f369309 100644 --- a/backend/libs/libs.module.ts +++ b/backend/libs/libs.module.ts @@ -4,8 +4,10 @@ import { User } from './database/user.entity'; import { Admin } from './database/admin.entity'; import { Post } from './database/post.entity'; import { Image } from './database/image.entity'; +import { Payment } from './database/payment.entity'; +import { ProxyUser } from './database/proxy_user.entity'; @Module({ - imports: [TypeOrmModule.forFeature([User, Admin, Post, Image])], + imports: [TypeOrmModule.forFeature([User, Admin, Post, Image, Payment, ProxyUser])], exports: [TypeOrmModule], }) export class LibsModule {} diff --git a/backend/package.json b/backend/package.json index c348abc..7e661ba 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,7 +11,7 @@ "start": "nest start", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", + "start:prod": "node dist/src/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", "test:watch": "jest --watch", diff --git a/backend/src/app.controller.ts b/backend/src/app.controller.ts new file mode 100644 index 0000000..ea73f2f --- /dev/null +++ b/backend/src/app.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; + +@ApiTags('App') +@Controller() +export class AppController { + @ApiOperation({ description: 'check site availability' }) + @Get('ping') + pingpong() { + return 'pong'; + } +} diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 65d3643..09548a3 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -2,15 +2,25 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; import { config } from 'config'; import { LibsModule } from 'libs/libs.module'; +import { AppController } from './app.controller'; 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 { ProxyModule } from './modules/proxy/proxy.module'; import { UserModule } from './modules/user/user.module'; @Module({ - imports: [LibsModule, PostModule, AdminModule, UserModule, ImageModule, TypeOrmModule.forRoot(config.database)], - controllers: [], + imports: [ + LibsModule, + PostModule, + AdminModule, + UserModule, + ImageModule, + ProxyModule, + TypeOrmModule.forRoot(config.database), + ], + controllers: [AppController], providers: [AppInitService], }) export class AppModule {} diff --git a/backend/src/modules/post/post.controller.ts b/backend/src/modules/post/post.controller.ts index 12953f9..08e0677 100644 --- a/backend/src/modules/post/post.controller.ts +++ b/backend/src/modules/post/post.controller.ts @@ -39,4 +39,10 @@ export class PostController { async editPost(@Param('postId') postId: string, @Body() data: IEditPost) { return await this.postService.editPost(postId, data); } + + @ApiOperation({ description: 'Get post to post' }) + @Get('post') + async post() { + return await this.postService.post(); + } } diff --git a/backend/src/modules/post/post.service.ts b/backend/src/modules/post/post.service.ts index 34045e1..08ba6f1 100644 --- a/backend/src/modules/post/post.service.ts +++ b/backend/src/modules/post/post.service.ts @@ -41,7 +41,7 @@ export class PostService { } if (post.text !== data.text) { post.text = data.text; - post.timestamp = new Date(); + post.edit_timestamp = new Date(); await this.postRepository.save(post); } return post; @@ -95,4 +95,23 @@ export class PostService { throw new HttpException("Can't find post with this media group id", HttpStatus.BAD_REQUEST); } } + + async post() { + try { + const posts = await this.postRepository.find({ order: { timestamp: 'ASC' }, where: { posted: false }, relations: { images: true } }); + if (!posts.length) throw new HttpException('Nothing to post', HttpStatus.NOT_FOUND); + const post = posts[0]; + post.posted = true; + this.logger.log(`[post.post] Post ${post.uuid} is posted`); + await this.postRepository.save(post); + return post; + } catch (error) { + if (error instanceof HttpException) { + this.logger.debug('[post.post] Not found'); + throw error; + } + this.logger.debug(`[post.post] error: ${JSON.stringify(error)}`); + throw new HttpException('Bad data', HttpStatus.BAD_REQUEST); + } + } } diff --git a/backend/src/modules/proxy/proxy.controller.ts b/backend/src/modules/proxy/proxy.controller.ts new file mode 100644 index 0000000..82a25ea --- /dev/null +++ b/backend/src/modules/proxy/proxy.controller.ts @@ -0,0 +1,46 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { IOperation, IProxyUser } from './proxy.dto'; +import { ProxyService } from './proxy.service'; + +@ApiTags('Proxy') +@Controller('proxy') +export class ProxyController { + constructor(private proxyService: ProxyService) {} + + @ApiOperation({ description: 'Method to create a new proxy user' }) + @Post('new-user') + async newUser(@Body() data: IProxyUser) { + return await this.proxyService.newUser(data); + } + + @ApiOperation({ description: 'get user by its username' }) + @Get('get-user/:userName') + async getUser(@Param('userName') userName: string) { + return await this.proxyService.getUser(userName); + } + + @ApiOperation({ description: 'get all users of proxy' }) + @Get('get-all-users') + async getAllUsers() { + return await this.proxyService.getAllUsers(); + } + + @ApiOperation({ description: 'adding an operation to user' }) + @Post('operation/add') + async addOperation(@Body() data: IOperation) { + return await this.proxyService.addOperation(data); + } + + @ApiOperation({ description: 'get user payments' }) + @Get('operation/get/:userName') + async getOperations(@Param('userName') userName: string) { + return this.proxyService.getOperations(userName); + } + + @ApiOperation({ description: 'get all payments' }) + @Get('operation/get-all') + async getAllOperations() { + return this.proxyService.getAllOperations(); + } +} diff --git a/backend/src/modules/proxy/proxy.dto.ts b/backend/src/modules/proxy/proxy.dto.ts new file mode 100644 index 0000000..0dbd923 --- /dev/null +++ b/backend/src/modules/proxy/proxy.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class IProxyUser { + @ApiProperty({ description: 'user name of user to identify them', example: 'username' }) readonly userName!: string; + @ApiProperty({ description: 'some user description if you want', example: 'Description of user' }) readonly description?: string; + @ApiProperty({ description: 'user link to connect to the proxy', example: 'vless://....' }) readonly link!: string; + @ApiProperty({ description: 'telegram user id to connect to user entity', example: '187564' }) readonly user_id?: string; +} + +export class IOperation { + @ApiProperty({ description: 'user name of user, that made new operation', example: 'username' }) readonly userName!: string; +} diff --git a/backend/src/modules/proxy/proxy.module.ts b/backend/src/modules/proxy/proxy.module.ts new file mode 100644 index 0000000..41835f0 --- /dev/null +++ b/backend/src/modules/proxy/proxy.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { LibsModule } from 'libs/libs.module'; +import { ProxyController } from './proxy.controller'; +import { ProxyService } from './proxy.service'; + +@Module({ + imports: [LibsModule], + controllers: [ProxyController], + providers: [ProxyService], +}) +export class ProxyModule {} diff --git a/backend/src/modules/proxy/proxy.service.ts b/backend/src/modules/proxy/proxy.service.ts new file mode 100644 index 0000000..25282cf --- /dev/null +++ b/backend/src/modules/proxy/proxy.service.ts @@ -0,0 +1,106 @@ +import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Payment } from 'libs/database/payment.entity'; +import { ProxyUser } from 'libs/database/proxy_user.entity'; +import { User } from 'libs/database/user.entity'; +import { Repository } from 'typeorm'; +import { IOperation, IProxyUser } from './proxy.dto'; + +@Injectable() +export class ProxyService { + private readonly logger: Logger = new Logger(ProxyService.name); + constructor( + @InjectRepository(ProxyUser) private proxyUserRepository: Repository, + @InjectRepository(Payment) private paymentRepository: Repository, + @InjectRepository(User) private userRepository: Repository, + ) {} + + async newUser(data: IProxyUser) { + try { + if ( + (await this.proxyUserRepository.findOne({ where: { userName: data.userName } })) || data.user_id + ? await this.proxyUserRepository.findOne({ where: { user_id: data.user_id } }) + : false + ) + throw new HttpException('User already exists', HttpStatus.FOUND); + const proxyUser = new ProxyUser(); + const user = data.user_id ? await this.userRepository.findOne({ where: { id: data.user_id } }) : null; + if (user) proxyUser.user = user; + proxyUser.description = data.description; + proxyUser.connectDate = new Date(); + proxyUser.userName = data.userName; + proxyUser.link = data.link; + return await this.proxyUserRepository.save(proxyUser); + } catch (error) { + if (error instanceof HttpException) { + this.logger.debug(`[proxy.newUser] error: user already created`); + throw error; + } + this.logger.debug(`[proxy.newUser] error: ${JSON.stringify(error)}`); + throw new HttpException('Bad data', HttpStatus.BAD_REQUEST); + } + } + + async getUser(userName: string) { + try { + const user = await this.proxyUserRepository.findOne({ where: { userName: userName } }); + if (!user) throw new HttpException('Not found', HttpStatus.NOT_FOUND); + return user; + } catch (error) { + if (error instanceof HttpException) { + this.logger.debug(`[proxy.getUser] error: not found`); + throw error; + } + this.logger.debug(`[proxy.getUser] error: ${JSON.stringify(error)}`); + throw new HttpException('Bad data', HttpStatus.BAD_REQUEST); + } + } + + async getAllUsers() { + try { + return await this.proxyUserRepository.find(); + } catch (error) { + this.logger.debug(`[proxy.getAllUsers] error: ${JSON.stringify(error)}`); + throw new HttpException('Bad data', HttpStatus.BAD_REQUEST); + } + } + + async addOperation(data: IOperation) { + try { + const user = await this.proxyUserRepository.findOne({ where: { userName: data.userName } }); + if (!user) throw new HttpException('Not found', HttpStatus.NOT_FOUND); + return await this.paymentRepository.save({ payTime: new Date(), user: user }); + } catch (error) { + if (error instanceof HttpException) { + this.logger.debug(`[proxy.addOperation] error: not found`); + throw error; + } + this.logger.debug(`[proxy.addOperation] error: ${JSON.stringify(error)}`); + throw new HttpException('Bad data', HttpStatus.BAD_REQUEST); + } + } + + async getOperations(userName: string) { + try { + const user = await this.proxyUserRepository.findOne({ where: { userName: userName }, relations: { payments: true } }); + if (!user) throw new HttpException('Not found', HttpStatus.NOT_FOUND); + return user.payments; + } catch (error) { + if (error instanceof HttpException) { + this.logger.debug(`[proxy.addOperation] error: not found`); + throw error; + } + this.logger.debug(`[proxy.addOperation] error: ${error}`); + throw new HttpException('Bad data', HttpStatus.BAD_REQUEST); + } + } + + async getAllOperations() { + try { + return await this.paymentRepository.find(); + } catch (error) { + this.logger.debug(`[proxy.addOperation] error: ${error}`); + throw new HttpException('Bad data', HttpStatus.BAD_REQUEST); + } + } +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..6aed970 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,74 @@ +version: '3.9' + +services: + db: + container_name: neuro_db_dev + 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 + networks: + - labnet + volumes: + - neuro_postgres_db:/var/lib/postgresql/data + restart: always + + backend: + container_name: neuro_backend_dev + build: + context: ./backend + dockerfile: Dockerfile.dev + develop: + watch: + - action: sync + path: ./backend/src + target: ./app/src + ignore: + - node_modules/ + - action: rebuild + path: package.json + - action: rebuild + path: ./libs + environment: + - DATABASE_PORT=5432 + - DATABASE_HOST=db + env_file: + - .env + networks: + - labnet + ports: + - 3000:3000 + depends_on: + - db + restart: always + + bot: + container_name: neuro_bot_dev + + build: + context: ./neuro-reply-bot-reworked + dockerfile: Dockerfile.dev + environment: + - API_URL=http://backend:3000 + networks: + - labnet + depends_on: + - backend + restart: always + develop: + watch: + - action: sync + path: ./neuro-reply-bot-reworked/ + target: ./app/ + +volumes: + neuro_postgres_db: + driver: local +networks: + labnet: diff --git a/docker-compose.yml b/docker-compose.yml index 6da560a..efea673 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,9 +13,41 @@ services: - "${DATABASE_PORT}:5432" env_file: - .env + networks: + - labnet volumes: - neuro_postgres_db:/var/lib/postgresql/data restart: always + + backend: + container_name: neuro_backend + build: ./backend + environment: + - DATABASE_PORT=5432 + - DATABASE_HOST=db + env_file: + - .env + networks: + - labnet + ports: + - 3000:3000 + depends_on: + - db + restart: always + + bot: + container_name: neuro_bot + build: ./neuro-reply-bot-reworked + environment: + - API_URL=http://backend:3000 + networks: + - labnet + depends_on: + - backend + restart: always + volumes: neuro_postgres_db: driver: local +networks: + labnet: diff --git a/neuro-reply-bot-reworked b/neuro-reply-bot-reworked index d286da6..142d18b 160000 --- a/neuro-reply-bot-reworked +++ b/neuro-reply-bot-reworked @@ -1 +1 @@ -Subproject commit d286da698e06a5bfa57131327db10d1fc716eea5 +Subproject commit 142d18bac35f49e7e26047fdf696eb7cb1c7f7b1