x1ongzhu 1 vuosi sitten
vanhempi
commit
070c4a9aa8

+ 1 - 0
package.json

@@ -44,6 +44,7 @@
     "@nestjs/throttler": "^4.0.0",
     "@nestjs/typeorm": "^9.0.1",
     "@nestjs/websockets": "^10.3.8",
+    "@types/ws": "^8.5.10",
     "ali-oss": "^6.17.1",
     "axios": "^1.3.6",
     "bcrypt": "^5.1.0",

+ 19 - 3
src/events/events.gateway.ts

@@ -22,18 +22,34 @@ export class EventsGateway implements OnGatewayInit, OnGatewayConnection, OnGate
     @WebSocketServer()
     server: Server
 
-    constructor(@Inject(forwardRef(() => RcsService)) rcsService: RcsService) {}
+    constructor(
+        @Inject(forwardRef(() => RcsService))
+        private readonly rcsService: RcsService
+    ) {}
 
     afterInit(server: any) {
         Logger.log('Initialized!', 'EventsGateway')
     }
 
     handleConnection(client: Socket, ...args: any[]) {
-        Logger.log('Client connected: ' + client.id + ', ' + client.handshake.query.device, 'EventsGateway')
+        const id = client.handshake.query.id
+        const model = client.handshake.query.model
+        const name = client.handshake.query.name
+        Logger.log(`Client connected: id=${id}, model=${model}, name=${name}`, 'EventsGateway')
+        this.rcsService.deviceConnect(
+            client.handshake.query.id as string,
+            client.handshake.query.device as string,
+            client.handshake.query.name as string,
+            (client.handshake.query.canSend as string) === 'true'
+        )
     }
 
     handleDisconnect(client: any) {
-        Logger.log('Client disconnected: ' + client.id, 'EventsGateway')
+        const id = client.handshake.query.id
+        const model = client.handshake.query.model
+        const name = client.handshake.query.name
+        Logger.log(`Client disconnected: id=${id}, model=${model}, name=${name}`, 'EventsGateway')
+        this.rcsService.deviceDisconnect(client.handshake.query.id as string)
     }
 
     @SubscribeMessage('message')

+ 24 - 0
src/rcs/entities/device.entity.ts

@@ -0,0 +1,24 @@
+import { Column, CreateDateColumn, Entity, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm'
+
+@Entity()
+export class Device {
+    @PrimaryColumn({ length: 50 })
+    id: string
+
+    @CreateDateColumn()
+    createdAt: Date
+
+    @Column()
+    model: string
+
+    @Column()
+    name: string
+
+    @Column()
+    online: boolean
+
+    @Column()
+    canSend: boolean
+
+    
+}

+ 12 - 5
src/rcs/entities/sms-receive.entity.ts → src/rcs/entities/number-request.entity.ts

@@ -1,15 +1,19 @@
 import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
 import { JsonTransformer } from '../../transformers/json.transformer'
 
-export enum SmsReceiveStatus {
+export enum NumberRequestStatus {
     PENDING = 'pending',
     RECEIVED = 'received',
     EXPIRED = 'expired',
     ERROR = 'error'
 }
 
+export enum NumberRequestFrom {
+    USACODE = 'usacode'
+}
+
 @Entity()
-export class SmsReceive {
+export class NumberRequest {
     @PrimaryGeneratedColumn()
     id: number
 
@@ -19,7 +23,7 @@ export class SmsReceive {
     @Column()
     expiryTime: Date
 
-    @Column({ nullable: false })
+    @Column({ type: 'enum', enum: NumberRequestFrom, nullable: false })
     from: string
 
     @Column({ nullable: false })
@@ -28,9 +32,12 @@ export class SmsReceive {
     @Column()
     message: string
 
-    @Column({ type: 'enum', enum: SmsReceiveStatus, nullable: false, default: SmsReceiveStatus.PENDING })
-    status: SmsReceiveStatus
+    @Column({ type: 'enum', enum: NumberRequestStatus, nullable: false, default: NumberRequestStatus.PENDING })
+    status: NumberRequestStatus
 
     @Column({ type: 'text', transformer: new JsonTransformer() })
     extra: any
+
+    @Column()
+    device: string
 }

+ 9 - 2
src/rcs/entities/task-item.entity.ts

@@ -1,5 +1,12 @@
 import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
 
+export enum TaskItemStatus {
+    IDLE = 'idle',
+    PENDING = 'pending',
+    COMPLETED = 'completed',
+    ERROR = 'error'
+}
+
 @Entity()
 export class TaskItem {
     @PrimaryGeneratedColumn()
@@ -7,8 +14,8 @@ export class TaskItem {
 
     @CreateDateColumn()
     createdAt: Date
-    
-    @Column()
+
+    @Column({ type: 'enum', enum: TaskItemStatus, nullable: false, default: TaskItemStatus.IDLE })
     taskId: number
 
     @Column()

+ 1 - 1
src/rcs/entities/task.entity.ts

@@ -28,5 +28,5 @@ export class Task {
     message: string
 
     @Column({ type: 'enum', enum: TaskStatus, nullable: false, default: TaskStatus.IDLE })
-    status: string
+    status: TaskStatus
 }

+ 30 - 1
src/rcs/rcs.controller.ts

@@ -1,10 +1,12 @@
-import { Body, Controller, Delete, Get, Param, Post, Put, Req } from '@nestjs/common'
+import { Body, Controller, Delete, Get, Param, Post, Put, Req, UploadedFile, UseInterceptors } from '@nestjs/common'
 import { RcsService } from './rcs.service'
 import { PageRequest } from '../common/dto/page-request'
 import { TaskItem } from './entities/task-item.entity'
 import { Task } from './entities/task.entity'
 import { Phone } from './entities/phone.entity'
 import { PhoneList } from './entities/phone-list.entity'
+import { Device } from './entities/device.entity'
+import { FileInterceptor } from '@nestjs/platform-express'
 
 @Controller('rcs')
 export class RcsController {
@@ -50,6 +52,18 @@ export class RcsController {
         return await this.rcsService.delPhoneList(parseInt(id))
     }
 
+    @Post('/phone-list/:id/import')
+    @UseInterceptors(FileInterceptor('file'))
+    async importPhoneList(@Param('id') id: string, @UploadedFile() file: Express.Multer.File) {
+        const { buffer } = file
+        const phones = buffer
+            .toString()
+            .replaceAll('\r\n', '\n')
+            .split('\n')
+            .filter((phone) => phone.length > 0)
+        await this.rcsService.importList(parseInt(id), phones)
+    }
+
     @Delete('/phone/:id')
     async delPhone(@Param('id') id: string) {
         return await this.rcsService.delPhone(parseInt(id))
@@ -59,4 +73,19 @@ export class RcsController {
     async delTask(@Param('id') id: string) {
         return await this.rcsService.delTask(parseInt(id))
     }
+
+    @Post('/task/:id/start')
+    async startTask(@Param('id') id: string) {
+        return await this.rcsService.startTask(parseInt(id))
+    }
+
+    @Post('/device')
+    async findAllDevice(@Body() page: PageRequest<Device>) {
+        return await this.rcsService.findAllDevice(page)
+    }
+
+    @Post('/updateDevice/:id')
+    async deviceConnect(@Param('id') id: string, @Req() req) {
+        return await this.rcsService.deviceConnect(id, req.body)
+    }
 }

+ 8 - 2
src/rcs/rcs.module.ts

@@ -7,10 +7,16 @@ import { Phone } from './entities/phone.entity'
 import { Task } from './entities/task.entity'
 import { TaskItem } from './entities/task-item.entity'
 import { EventsModule } from '../events/events.module'
+import { USACodeApiService } from './usacode-api-service'
+import { Device } from './entities/device.entity'
+import { NumberRequest } from './entities/number-request.entity'
 
 @Module({
-    imports: [TypeOrmModule.forFeature([PhoneList, Phone, Task, TaskItem]), forwardRef(() => EventsModule)],
-    providers: [RcsService],
+    imports: [
+        TypeOrmModule.forFeature([PhoneList, Phone, Task, TaskItem, Device, NumberRequest]),
+        forwardRef(() => EventsModule)
+    ],
+    providers: [RcsService, USACodeApiService],
     controllers: [RcsController],
     exports: [RcsService]
 })

+ 92 - 5
src/rcs/rcs.service.ts

@@ -1,16 +1,20 @@
-import { Inject, Injectable, forwardRef } from '@nestjs/common'
+import { Inject, Injectable, OnModuleInit, forwardRef } from '@nestjs/common'
 import { InjectRepository } from '@nestjs/typeorm'
 import { PhoneList } from './entities/phone-list.entity'
 import { Repository } from 'typeorm'
 import { Phone } from './entities/phone.entity'
-import { Task } from './entities/task.entity'
+import { Task, TaskStatus } from './entities/task.entity'
 import { TaskItem } from './entities/task-item.entity'
 import { PageRequest } from 'src/common/dto/page-request'
 import { Pagination, paginate } from 'nestjs-typeorm-paginate'
 import { EventsGateway } from 'src/events/events.gateway'
+import axios from 'axios'
+import { USACodeApiService } from './usacode-api-service'
+import { Device } from './entities/device.entity'
+import { NumberRequest } from './entities/number-request.entity'
 
 @Injectable()
-export class RcsService {
+export class RcsService implements OnModuleInit {
     constructor(
         @InjectRepository(PhoneList)
         private phoneListRepository: Repository<PhoneList>,
@@ -20,10 +24,20 @@ export class RcsService {
         private taskRepository: Repository<Task>,
         @InjectRepository(TaskItem)
         private taskItemRepository: Repository<TaskItem>,
+        @InjectRepository(Device)
+        private deviceRepository: Repository<Device>,
+        @InjectRepository(NumberRequest)
+        private numberRequestRepository: Repository<NumberRequest>,
         @Inject(forwardRef(() => EventsGateway))
         private readonly eventsGateway: EventsGateway,
+        @Inject(forwardRef(() => USACodeApiService))
+        private readonly USACodeApiService: USACodeApiService
     ) {}
 
+    onModuleInit() {
+        this.deviceRepository.update({}, { online: false })
+    }
+
     async findAllPhoneList(req: PageRequest<PhoneList>): Promise<Pagination<PhoneList>> {
         return await paginate<PhoneList>(this.phoneListRepository, req.page, req.search)
     }
@@ -44,12 +58,35 @@ export class RcsService {
         return await this.phoneListRepository.save(phoneList)
     }
 
+    async importList(listId: number, phones: string[]): Promise<Phone[]> {
+        return await this.phoneRepository.save(
+            phones.map((number) => {
+                const phone = new Phone()
+                phone.listId = listId
+                phone.number = number
+                return phone
+            })
+        )
+    }
+
     async createPhone(phone: Phone): Promise<Phone> {
         return await this.phoneRepository.save(phone)
     }
 
     async createTask(task: Task): Promise<Task> {
-        return await this.taskRepository.save(task)
+        task = await this.taskRepository.save(task)
+        const phones = await this.phoneRepository.findBy({ listId: task.listId })
+        await this.taskItemRepository.save(
+            phones.map((phone) => {
+                const taskItem = new TaskItem()
+                taskItem.taskId = task.id
+                taskItem.number = phone.number
+                taskItem.message = task.message
+                taskItem.status = TaskStatus.IDLE
+                return taskItem
+            })
+        )
+        return task
     }
 
     async delPhoneList(id: number): Promise<void> {
@@ -64,5 +101,55 @@ export class RcsService {
         await this.taskRepository.delete(id)
     }
 
-    
+    async startTask(id: number): Promise<void> {
+        const task = await this.taskRepository.findOneBy({ id })
+        if (task && task.status === TaskStatus.IDLE) {
+            task.status = TaskStatus.PENDING
+            await this.taskRepository.save(task)
+            this.runTask(task)
+        }
+    }
+
+    async runTask(task: Task) {
+        const taskItems = await this.taskItemRepository.findBy({ taskId: task.id, status: TaskStatus.IDLE })
+        for (const taskItem of taskItems) {
+            taskItem.status = TaskStatus.PENDING
+            await this.taskItemRepository.save(taskItem)
+        }
+    }
+
+    async findAllDevice(req: PageRequest<Device>): Promise<Pagination<Device>> {
+        return await paginate<Device>(this.deviceRepository, req.page, req.search)
+    }
+
+    async deviceConnect(id: string, model: string, name?: string, canSend: boolean = false) {
+        let device = await this.deviceRepository.findOneBy({ id })
+        if (!device) {
+            device = new Device()
+        }
+        device.id = id
+        device.model = model
+        device.name = name
+        device.online = true
+        device.canSend = canSend
+        await this.deviceRepository.save(device)
+    }
+
+    async deviceDisconnect(id: string) {
+        const device = await this.deviceRepository.findOneBy({ id })
+        if (device) {
+            device.online = false
+            await this.deviceRepository.save(device)
+        }
+    }
+
+    async updateDevice(id: string, data: any) {
+        const device = await this.deviceRepository.findOneBy({ id })
+        if (device) {
+            Object.assign(device, data)
+            await this.deviceRepository.save(device)
+        }
+    }
+
+    async requestNumber(device: string) {}
 }

+ 83 - 0
src/rcs/usacode-api-service.ts

@@ -0,0 +1,83 @@
+import { Inject, Logger, OnModuleInit, forwardRef } from '@nestjs/common'
+import axios from 'axios'
+import * as WebSocket from 'ws'
+import { RcsService } from './rcs.service'
+import { Subject, takeUntil } from 'rxjs'
+import { randomUUID } from 'crypto'
+
+const axiosInstance = axios.create({
+    baseURL: 'https://panel.hellomeetyou.com/',
+    headers: {
+        'X-API-KEY': 'Z0ik3OMblJznryPSWTCOmSOm'
+    }
+})
+
+export class USACodeApiService implements OnModuleInit {
+    private readonly messageSubject = new Subject<any>()
+
+    constructor(
+        @Inject(forwardRef(() => RcsService))
+        private readonly rcsService: RcsService
+    ) {}
+
+    onModuleInit() {
+        const ws = new WebSocket('wss://panel.hellomeetyou.com/api/ws')
+
+        ws.on('open', () => {
+            Logger.log('Connected WS Server', 'USACodeApiService')
+            ws.send(
+                JSON.stringify([
+                    {
+                        jsonrpc: '2.0',
+                        method: 'subscribe',
+                        params: {
+                            apiKey: 'Z0ik3OMblJznryPSWTCOmSOm'
+                        }
+                    },
+                    {
+                        jsonrpc: '2.0',
+                        method: 'getSubscribedUserIds',
+                        id: randomUUID()
+                    }
+                ])
+            )
+        })
+
+        ws.on('message', (data) => {
+            Logger.log('Received message ' + data, 'USACodeApiService')
+            const message = JSON.parse(data.toString())
+            if (message instanceof Array) {
+                message.forEach((msg) => {
+                    this.messageSubject.next(msg)
+                })
+            } else {
+                this.messageSubject.next(message)
+            }
+        })
+
+        setInterval(() => {
+            ws.send(
+                JSON.stringify({
+                    jsonrpc: '2.0',
+                    method: 'ping'
+                })
+            )
+        }, 50000)
+    }
+
+    public async requestNumber() {
+        const { data: service } = await axiosInstance.post('/api/checkService', {
+            services: ['SERVICE_NOT_LISTED']
+        })
+        if (service.available && service.creditsCost <= 1) {
+            const { data: response } = await axiosInstance.post(
+                'https://panel.hellomeetyou.com/api/line/changeService',
+                {
+                    services: ['SERVICE_NOT_LISTED']
+                }
+            )
+            return response.phoneNumber
+        }
+        throw new Error(service.available ? 'Price too high' : 'Service not available')
+    }
+}

+ 7 - 0
yarn.lock

@@ -1518,6 +1518,13 @@
   resolved "https://registry.npmmirror.com/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz#18b97a972f94f60a679fd5c796d96421b9abb9fd"
   integrity sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==
 
+"@types/ws@^8.5.10":
+  version "8.5.10"
+  resolved "https://registry.npmmirror.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787"
+  integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==
+  dependencies:
+    "@types/node" "*"
+
 "@types/xml2js@^0.4.5":
   version "0.4.14"
   resolved "https://registry.npmmirror.com/@types/xml2js/-/xml2js-0.4.14.tgz#5d462a2a7330345e2309c6b549a183a376de8f9a"