Explorar o código

chore: 重写渠道和运营商

x1ongzhu hai 1 ano
pai
achega
da78969144

+ 1 - 0
package.json

@@ -70,6 +70,7 @@
     "ioredis": "^5.3.2",
     "isomorphic-fetch": "^3.0.0",
     "keyv": "^4.5.2",
+    "mcc-mnc-list": "^1.1.11",
     "moment": "^2.30.1",
     "mysql2": "^3.1.2",
     "nestjs-typeorm-paginate": "^4.0.3",

+ 3 - 1
src/app.module.ts

@@ -19,6 +19,7 @@ import { PhoneListModule } from './phone-list/phone-list.module'
 import { DeviceModule } from './device/device.module'
 import { BalanceModule } from './balance/balance.module'
 import { ChannelModule } from './channel/channel.module'
+import { OperaterConfigModule } from './operator_config/operator_config.module';
 
 @Module({
     imports: [
@@ -78,7 +79,8 @@ import { ChannelModule } from './channel/channel.module'
         PhoneListModule,
         DeviceModule,
         BalanceModule,
-        ChannelModule
+        ChannelModule,
+        OperaterConfigModule
     ],
     controllers: [],
     providers: [

+ 2 - 9
src/channel/channel.controller.ts

@@ -7,8 +7,7 @@ import { Channel } from './entities/channel.entities'
 @Controller('channel')
 @Public()
 export class ChannelController {
-    constructor(private readonly channelService: ChannelService) {
-    }
+    constructor(private readonly channelService: ChannelService) {}
 
     @Post('/')
     async findAll(@Body() page: PageRequest<Channel>) {
@@ -24,10 +23,4 @@ export class ChannelController {
     async delete(@Param('id') id: number) {
         return await this.channelService.delete(id)
     }
-
-    @Get('/updateSwitch/:id')
-    async update(@Param('id') id: number) {
-        return await this.channelService.update(id)
-    }
-
-}
+}

+ 5 - 10
src/channel/channel.service.ts

@@ -7,11 +7,13 @@ import { paginate, Pagination } from 'nestjs-typeorm-paginate'
 
 @Injectable()
 export class ChannelService {
-
     constructor(
         @InjectRepository(Channel)
         private channelRepository: Repository<Channel>
-    ) {
+    ) {}
+
+    async all() {
+        return await this.channelRepository.find()
     }
 
     async findAll(req: PageRequest<Channel>): Promise<Pagination<Channel>> {
@@ -25,11 +27,4 @@ export class ChannelService {
     async delete(id: number): Promise<void> {
         await this.channelRepository.delete(id)
     }
-
-    async update(id: number) {
-        const channel = await this.channelRepository.findOneBy({ id })
-        channel.switch = !channel.switch
-        await this.channelRepository.save(channel)
-    }
-
-}
+}

+ 11 - 21
src/channel/entities/channel.entities.ts

@@ -1,31 +1,14 @@
 import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
 import { JsonTransformer } from '../../transformers/json.transformer'
+import { RcsNumberSource } from 'src/rcs-number/entities/rcs-number.entity'
 
 @Entity()
 export class Channel {
     @PrimaryGeneratedColumn()
     id: number
 
-    @Column()
-    mcc: string
-
-    @Column()
-    mnc: string
-
-    @Column()
-    country: string
-
-    @Column({ nullable: true })
-    operator: string
-
-    @Column({ type: 'text', nullable: true, transformer: new JsonTransformer() })
-    scope: number[]
-
-    @Column()
-    platform: string
-
-    @Column({ default: false })
-    switch: boolean
+    @Column({ type: 'enum', enum: RcsNumberSource, nullable: false, unique: true })
+    source: RcsNumberSource
 
     @CreateDateColumn()
     createdAt: Date
@@ -33,4 +16,11 @@ export class Channel {
     @Column({ nullable: true })
     remark: string
 
-}
+    @Column({ type: 'text', transformer: new JsonTransformer() })
+    countryConfig: [
+        {
+            countryCode: string
+            enabled: boolean
+        }
+    ]
+}

+ 7 - 1
src/device/device.service.ts

@@ -3,7 +3,7 @@ import { PageRequest } from '../common/dto/page-request'
 import { Device } from './entities/device.entity'
 import { Pagination, paginate } from 'nestjs-typeorm-paginate'
 import { InjectRepository } from '@nestjs/typeorm'
-import { Like, Repository } from 'typeorm'
+import { In, Like, Repository } from 'typeorm'
 
 @Injectable()
 export class DeviceService {
@@ -20,6 +20,12 @@ export class DeviceService {
         await this.deviceRepository.delete(id)
     }
 
+    async findByIds(ids: string[]) {
+        return await this.deviceRepository.findBy({
+            id: In(ids)
+        })
+    }
+
     async deviceConnect(id: string, socketId: string, model: string, name: string) {
         let device = await this.deviceRepository.findOneBy({ id })
         if (!device) {

+ 31 - 0
src/operator_config/entities/operator-config.entiy.ts

@@ -0,0 +1,31 @@
+import { JsonTransformer } from 'src/transformers/json.transformer'
+import { Column, CreateDateColumn, Entity, PrimaryColumn } from 'typeorm'
+
+interface OperatorMatcher {
+    match: string
+    matchType: 'equal' | 'regex' | 'prefix' | 'suffix' | 'in' | 'notIn'
+    mapTo: {
+        mcc: string
+        mnc: string
+    }
+    remark: string
+    enabled: boolean
+}
+
+@Entity()
+export class OperatorConfig {
+    @PrimaryColumn({ length: 50 })
+    country: string
+
+    @CreateDateColumn()
+    createdAt: Date
+
+    @Column({ nullable: true })
+    remark: string
+
+    @Column({ type: 'text', nullable: false, transformer: new JsonTransformer() })
+    matchers: OperatorMatcher[]
+
+    @Column({ default: false })
+    enabled: boolean
+}

+ 29 - 0
src/operator_config/operator_config.controller.ts

@@ -0,0 +1,29 @@
+import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common'
+import { OperaterConfigService } from './operator_config.service'
+import { PageRequest } from '../common/dto/page-request'
+import { OperatorConfig } from './entities/operator-config.entiy'
+
+@Controller('operator-config')
+export class OperaterConfigController {
+    constructor(private readonly operatorConfigService: OperaterConfigService) {}
+
+    @Post('/')
+    async findAll(@Body() page: PageRequest<OperatorConfig>) {
+        return await this.operatorConfigService.findAll(page)
+    }
+
+    @Put('/')
+    async create(@Body() body: any) {
+        return await this.operatorConfigService.create(body)
+    }
+
+    @Delete('/:id')
+    async delete(@Param('id') id: string) {
+        return await this.operatorConfigService.delete(id)
+    }
+
+    @Get('/updateSwitch/:id')
+    async update(@Param('id') id: string) {
+        return await this.operatorConfigService.update(id)
+    }
+}

+ 13 - 0
src/operator_config/operator_config.module.ts

@@ -0,0 +1,13 @@
+import { Module } from '@nestjs/common'
+import { OperaterConfigController } from './operator_config.controller'
+import { OperaterConfigService } from './operator_config.service'
+import { TypeOrmModule } from '@nestjs/typeorm'
+import { OperatorConfig } from './entities/operator-config.entiy'
+
+@Module({
+    imports: [TypeOrmModule.forFeature([OperatorConfig])],
+    controllers: [OperaterConfigController],
+    providers: [OperaterConfigService],
+    exports: [OperaterConfigService]
+})
+export class OperaterConfigModule {}

+ 39 - 0
src/operator_config/operator_config.service.ts

@@ -0,0 +1,39 @@
+import { Injectable } from '@nestjs/common'
+import { InjectRepository } from '@nestjs/typeorm'
+import { OperatorConfig } from './entities/operator-config.entiy'
+import { Repository } from 'typeorm'
+import { paginate, Pagination } from 'nestjs-typeorm-paginate'
+import { PageRequest } from 'src/common/dto/page-request'
+
+@Injectable()
+export class OperaterConfigService {
+    constructor(
+        @InjectRepository(OperatorConfig)
+        private operatorConfigRepository: Repository<OperatorConfig>
+    ) {}
+
+    async findByCountry(country: string) {
+        return await this.operatorConfigRepository.findOneBy({ country })
+    }
+
+    async random() {
+        return await this.operatorConfigRepository.createQueryBuilder().orderBy('RAND()').getOne()
+    }
+
+    async findAll(req: PageRequest<OperatorConfig>): Promise<Pagination<OperatorConfig>> {
+        return await paginate<OperatorConfig>(this.operatorConfigRepository, req.page, req.search)
+    }
+
+    async create(config: OperatorConfig): Promise<OperatorConfig> {
+        return await this.operatorConfigRepository.save(config)
+    }
+
+    async delete(country: string): Promise<void> {
+        await this.operatorConfigRepository.delete(country)
+    }
+
+    async update(country: string) {
+        const config = await this.operatorConfigRepository.findOneBy({ country })
+        await this.operatorConfigRepository.save(config)
+    }
+}

+ 6 - 4
src/rcs-number/entities/rcs-number.entity.ts

@@ -50,16 +50,18 @@ export class RcsNumber {
     @Column({ type: 'text', transformer: new JsonTransformer(), nullable: true })
     extra: any
 
+    @Column()
+    orderId: string
+
     @Column({ nullable: true })
     deviceId: string
 
     @Column({ nullable: true })
-    taskId: string
+    taskId: number
 
-    @Column({ nullable: true })
-    result:string
+    @Column({ type: 'text', nullable: true })
+    rawResponse: string
 
     @Exclude()
     deviceName: string
-
 }

+ 48 - 1
src/rcs-number/helpers.ts

@@ -1,8 +1,10 @@
+import { OperatorConfig } from 'src/operator_config/entities/operator-config.entiy'
+
 export function checkAndFormatNumber(country: string, number: string, carrierName?: string) {
     if (/no service/i.test(carrierName) || /emergency calls only/i.test(carrierName)) {
         throw new Error('Error carrierName')
     }
-    switch (country) {
+    switch (country.toLowerCase()) {
         case 'us':
             number = number.replace(/^\+1/, '').replace(/^1(\d{10})$/, '$1')
             if (!/^\d{10}$/.test(number)) {
@@ -40,3 +42,48 @@ export function checkAndFormatNumber(country: string, number: string, carrierNam
     }
     return number
 }
+
+export function matchOperator(
+    operatorCode: string,
+    operatorConfig: OperatorConfig
+): {
+    mcc: string
+    mnc: string
+} {
+    for (let matcher of operatorConfig.matchers) {
+        switch (matcher.matchType) {
+            case 'equal':
+                if (matcher.match === operatorCode) {
+                    return matcher.mapTo
+                }
+                break
+            case 'regex':
+                if (new RegExp(matcher.match).test(operatorCode)) {
+                    return matcher.mapTo
+                }
+                break
+            case 'prefix':
+                if (operatorCode.startsWith(matcher.match)) {
+                    return matcher.mapTo
+                }
+                break
+            case 'suffix':
+                if (operatorCode.endsWith(matcher.match)) {
+                    return matcher.mapTo
+                }
+                break
+            case 'in':
+                if (matcher.match.split(',').includes(operatorCode)) {
+                    return matcher.mapTo
+                }
+                break
+            case 'notIn':
+                if (!matcher.match.split(',').includes(operatorCode)) {
+                    return matcher.mapTo
+                }
+                break
+            default:
+        }
+    }
+    return null
+}

+ 54 - 125
src/rcs-number/impl/durian.service.ts

@@ -1,16 +1,12 @@
-import axios, { AxiosError } from 'axios'
-import { GetNumberService } from './get-number-service'
-import { RcsNumber, RcsNumberSource, RcsNumberStatus } from '../entities/rcs-number.entity'
+import axios from 'axios'
+import { GetNumberService, GetNumberResponse } from './get-number-service'
+import { RcsNumber, RcsNumberSource } from '../entities/rcs-number.entity'
 import { InjectRepository } from '@nestjs/typeorm'
-import { In, MoreThan, Not, Repository } from 'typeorm'
-import { Cron } from '@nestjs/schedule'
+import { Repository } from 'typeorm'
 import { Task } from '../../task/entities/task.entity'
 import { Channel } from '../../channel/entities/channel.entities'
-import { el } from 'date-fns/locale'
-import { addMinutes } from 'date-fns'
-import { channel } from 'diagnostics_channel'
-import { checkAndFormatNumber } from '../helpers'
-import { InternalServerErrorException, Logger } from '@nestjs/common'
+import { InternalServerErrorException } from '@nestjs/common'
+import * as mcc_mnc_list from 'mcc-mnc-list'
 
 const name = 'unsnap3094'
 const ApiKey = 'U3Jma1hkbUxXblEyL0ZYai9WWFVvdz09'
@@ -21,131 +17,64 @@ const durianInstance = axios.create({
 })
 
 export class durian extends GetNumberService {
-    constructor(
-        @InjectRepository(RcsNumber)
-        private rcsNumberRepository: Repository<RcsNumber>,
-        @InjectRepository(Task)
-        private taskRepository: Repository<Task>,
-        @InjectRepository(Channel)
-        private channelRepository: Repository<Channel>
-    ) {
-        super()
-    }
+    source: RcsNumberSource = RcsNumberSource.durian
 
-    async getNumber(deviceId?: string, taskId?: string, channelId?: string): Promise<RcsNumber> {
-        let channel: Channel = null
-        let fullNumber: string = null
-        let number: string = null
-        let imsi: string = null
-        try {
-            if (channelId) {
-                channel = await this.channelRepository.findOneBy({ id: parseInt(channelId) })
-            } else if (taskId) {
-                const task: Task = await this.taskRepository.findOneBy({ id: parseInt(taskId) })
-                if (task.channels?.length > 0) {
-                    const randomIndex = Math.floor(Math.random() * task.channels.length)
-                    channel = await this.channelRepository.findOneBy({ id: task.channels[randomIndex] })
-                }
+    async getNumber(country: string): Promise<GetNumberResponse> {
+        const res = await durianInstance.get('getMobile', {
+            params: {
+                name,
+                ApiKey,
+                cuy: country.toUpperCase(),
+                pid,
+                num: 1,
+                noblack: 1,
+                serial: 2
             }
-            if (!channel) {
-                // 从数据库中获取随机的channel
-                channel = await this.channelRepository.createQueryBuilder('channel').orderBy('RAND()').limit(1).getOne()
+        })
+        if (res.data.code !== 200) {
+            throw new InternalServerErrorException(res.data.msg)
+        }
+        const number = res.data.data
+        const imsiRes = await durianInstance.get('getMobileUserCode', {
+            params: {
+                name,
+                ApiKey,
+                pn: number
             }
-            Logger.log(
-                `channelId=${channel.id}, country=${channel.country}, mcc=${channel.mcc}, mnc=${channel.mnc}`,
-                'durian'
-            )
-            const res = await durianInstance.get('getMobile', {
-                params: {
-                    name,
-                    ApiKey,
-                    cuy: channel.country,
-                    pid,
-                    num: 1,
-                    noblack: 0,
-                    serial: 2,
-                    secret_key: null,
-                    vip: null
-                }
+        })
+        if (imsiRes.data.code !== 200) {
+            throw new InternalServerErrorException(imsiRes.data.msg)
+        }
+        const mcc_mnc = mcc_mnc_list
+            .filter({
+                countryCode: country.toUpperCase()
             })
-            fullNumber = res.data
-            number = res.data.slice(3)
-
-            // imsi
-            const codeData = await durianInstance.get('getMobileUserCode', {
-                params: {
-                    name,
-                    ApiKey,
-                    pn: number
-                }
+            .find((item) => {
+                return imsiRes.data.data.startsWith(item.mcc + item.mnc)
             })
-            imsi = codeData.data
-        } catch (e: any) {
-            if (e.response) {
-                Logger.error(e.response.data, 'durian')
-                throw new InternalServerErrorException(e.response.data.message)
-            } else {
-                throw new InternalServerErrorException(e.message)
-            }
-        }
-
-        if (!number) {
-            throw new InternalServerErrorException('No number available')
+        if (!mcc_mnc) {
+            throw new InternalServerErrorException('No MCC MNC found')
         }
-        if (imsi.includes((channel.mcc + channel.mnc))) {
-            throw new InternalServerErrorException('imsi not matches.')
+        return {
+            number,
+            orderId: number,
+            operatorCode: mcc_mnc.mcc + mcc_mnc.mnc,
+            rawResponse: [res.data, imsiRes.data]
         }
-        number = checkAndFormatNumber(channel.country, number)
-        const rcsNumber = new RcsNumber()
-        rcsNumber.mcc = channel.mcc
-        rcsNumber.mnc = channel.mnc
-        rcsNumber.country = channel.country
-        rcsNumber.number = number
-        rcsNumber.extra = { fullNumber }
-        rcsNumber.expiryTime = new Date(Date.now() + 5 * 60 * 1000)
-        rcsNumber.from = RcsNumberSource.durian
-        rcsNumber.status = RcsNumberStatus.PENDING
-        rcsNumber.deviceId = deviceId
-        rcsNumber.taskId = taskId
-        return await this.rcsNumberRepository.save(rcsNumber)
     }
 
-    @Cron('0/5 * * * * *')
-    async getMessage() {
-        const numbers = await this.rcsNumberRepository.findBy({
-            expiryTime: MoreThan(addMinutes(new Date(), -1)),
-            status: Not(RcsNumberStatus.EXPIRED),
-            from: RcsNumberSource.durian
-        })
-
-        numbers.forEach(async (number) => {
-            if (number.expiryTime < new Date()) {
-                if (number.status === RcsNumberStatus.PENDING) {
-                    number.status = RcsNumberStatus.EXPIRED
-                    await this.rcsNumberRepository.save(number)
-                }
-                return
-            }
-
-            try {
-                const { data } = await durianInstance.get('getMsg', {
-                    params: {
-                        name,
-                        ApiKey,
-                        pn: number.extra.fullNumber,
-                        pid,
-                        serial: 2
-                    }
-                })
-                if (data.code === 200) {
-                    number.status = RcsNumberStatus.SUCCESS
-                    number.message = data.data
-                    await this.rcsNumberRepository.save(number)
-                }
-            } catch (e) {
+    async reetriveMessage(orderId: string): Promise<string> {
+        const { data } = await durianInstance.get('getMsg', {
+            params: {
+                name,
+                ApiKey,
+                pn: orderId,
+                pid: pid,
+                serial: 2
             }
         })
-
+        if (data.code === 200) {
+            return `Your Messenger verification code is G-${data.data}`
+        }
     }
-
 }

+ 11 - 3
src/rcs-number/impl/get-number-service.ts

@@ -1,5 +1,13 @@
-import { RcsNumber } from '../entities/rcs-number.entity'
-
+import { RcsNumber, RcsNumberSource } from '../entities/rcs-number.entity'
+export interface GetNumberResponse {
+    number: string
+    operatorCode: string
+    orderId?: string
+    operatorName?: string
+    rawResponse: any
+}
 export abstract class GetNumberService {
-    abstract getNumber(deviceId?: string): Promise<RcsNumber>
+    abstract source: RcsNumberSource
+    abstract getNumber(conutry: string): Promise<GetNumberResponse>
+    abstract reetriveMessage(orderId: string): Promise<string | null>
 }

+ 38 - 258
src/rcs-number/impl/mwze167.service.ts

@@ -1,276 +1,56 @@
-import axios, { AxiosError } from 'axios'
-import { GetNumberService } from './get-number-service'
-import { RcsNumber, RcsNumberSource, RcsNumberStatus } from '../entities/rcs-number.entity'
-import { HttpException, InternalServerErrorException, Logger, OnModuleInit, forwardRef } from '@nestjs/common'
-import { InjectRepository } from '@nestjs/typeorm'
-import { In, MoreThan, Not, Repository } from 'typeorm'
-import { Cron } from '@nestjs/schedule'
-import { Task } from '../../task/entities/task.entity'
-import { Channel } from '../../channel/entities/channel.entities'
-import { el } from 'date-fns/locale'
-import { addMinutes } from 'date-fns'
-import { channel } from 'diagnostics_channel'
-import { checkAndFormatNumber } from '../helpers'
-import * as util from 'node:util'
+import axios from 'axios'
+import { GetNumberService, GetNumberResponse } from './get-number-service'
+import { RcsNumberSource } from '../entities/rcs-number.entity'
+import { Logger } from '@nestjs/common'
 
 const uid = '1715242407'
 const sign = 'da1a847ef8f0df331884ce45d97e7e42'
 const pid = '133'
 
-const name = 'unsnap3094'
-const ApiKey = 'U3Jma1hkbUxXblEyL0ZYai9WWFVvdz09'
-const dPid = '1580'
-
 const axiosInstance = axios.create({
     baseURL: 'http://api.mwze167.com/registerApi/'
 })
-const durianInstance = axios.create({
-    baseURL: 'http://104.194.87.44:1080/out/ext_api/'
-})
 
 export class mwze167 extends GetNumberService {
-    constructor(
-        @InjectRepository(RcsNumber)
-        private rcsNumberRepository: Repository<RcsNumber>,
-        @InjectRepository(Task)
-        private taskRepository: Repository<Task>,
-        @InjectRepository(Channel)
-        private channelRepository: Repository<Channel>
-    ) {
-        super()
-    }
-
-    async getNumber(deviceId?: string, taskId?: string, channelId?: string): Promise<RcsNumber> {
-        let channel: Channel = null
-        let number: string = null
-        let fullNumber: string = null
-        let orderId: string = null
-        let operatorName: string = null
-        let carrierName: string = null
-        let imsi: string = null
-        let mcc: string = null
-        let mnc: string = null
-        let platform: string = null
-        let result: string = null
-        try {
-            if (channelId) {
-                channel = await this.channelRepository.findOneBy({ id: parseInt(channelId) })
-            } else if (taskId) {
-                const task: Task = await this.taskRepository.findOneBy({ id: parseInt(taskId) })
-                if (task.channels?.length > 0) {
-                    const randomIndex = Math.floor(Math.random() * task.channels.length)
-                    channel = await this.channelRepository.findOneBy({ id: task.channels[randomIndex] })
-                }
-            }
-            if (!channel) {
-                // 从数据库中获取随机的channel
-                channel = await this.channelRepository.createQueryBuilder('channel').where('channel.switch = 1').orderBy('RAND()').limit(1).getOne()
-            }
-            Logger.log(
-                `channelId=${channel.id}, country=${channel.country}, mcc=${channel.mcc}, mnc=${channel.mnc},platform=${channel.platform}`,
-                `mwze167.service`
-            )
-
-            // 判断平台
-            platform = channel.platform
-            if (platform === 'durian') {
-                const res = await durianInstance.get('getMobile', {
-                    params: {
-                        name,
-                        ApiKey,
-                        cuy: channel.country,
-                        pid: dPid,
-                        num: 1,
-                        noblack: 0,
-                        serial: 2,
-                        secret_key: null,
-                        vip: null
-                    }
-                })
-                const resultData = res.data
-                Logger.log(resultData, 'durian')
-                if (resultData.code !== 200) {
-                    throw new Error(resultData.msg)
-                }
-                fullNumber = resultData.data
-                number = resultData.data.slice(3)
-
-                // imsi
-                const codeRes = await durianInstance.get('getMobileUserCode', {
-                    params: {
-                        name,
-                        ApiKey,
-                        pn: fullNumber
-                    }
-                })
-                const codeData = codeRes.data
-                result = JSON.stringify(codeData)
-                Logger.log(codeData, 'durian')
-                if (codeData.code !== 200) {
-                    throw new Error(codeData.msg)
-                }
-                mcc = codeData.data.slice(0, 3)
-                if (channel.mnc.length > 2) {
-                    imsi = codeData.data.slice(0, 6)
-                    mnc = codeData.data.slice(3, 6)
-                } else {
-                    imsi = codeData.data.slice(0, 5)
-                    mnc = codeData.data.slice(3, 5)
-                }
-
-            } else if (platform === 'mwze167') {
-                const res = await axiosInstance.get('getMobile', {
-                    params: {
-                        uid,
-                        sign,
-                        pid,
-                        cuy: channel.country.toUpperCase(),
-                        size: 1,
-                        ctype: 3
-                    }
-                })
-                const data = res.data
-                result = JSON.stringify(data)
-                Logger.log(data, 'mwze167')
-                if (data.code !== 0) {
-                    throw new Error(data.msg)
-                }
-                number = data.data[0]?.number
-                orderId = data.orderId
-
-                const jsonString: string = data.data[0]?.mcc
-                const parsedObject = JSON.parse(jsonString)
-
-                operatorName = parsedObject?.operatorName
-                carrierName = parsedObject?.carrierName
-
-                mcc = parsedObject?.mcc
-                mnc = parsedObject?.mnc
-                if (mnc.length === 1) {
-                    mnc = '0' + mnc
-                }
-                imsi = mcc + mnc
-            }
-        } catch (e: any) {
-            if (e.response) {
-                Logger.error(e.response.data, 'mwze167.service')
-                throw new InternalServerErrorException(e.response.data.message)
-            } else {
-                throw new InternalServerErrorException(e.message)
-            }
-        }
-
-        if (!number) {
-            throw new InternalServerErrorException('No number available')
-        }
-        // 比较operatorName,如果为空判断imsi
-        if (channel.operator !== null && platform === 'mwze167') {
-            if (operatorName !== channel.operator) {
-                throw new InternalServerErrorException('Operator name not matches.')
-            }
-        } else {
-            if (channel.scope && channel.scope.length > 0) {
-                // 判断是否在区间中
-                const scope = channel.scope
-                if (!scope.includes(Number(imsi))) {
-                    throw new InternalServerErrorException('IMSI not matches.')
-                }
-            } else if (mcc !== channel.mcc || mnc !== channel.mnc) {
-                // 直接比较
-                throw new InternalServerErrorException('MCC or MNC not matches.')
+    source: RcsNumberSource = RcsNumberSource.mwze167
+    async getNumber(country: string): Promise<GetNumberResponse> {
+        const res = await axiosInstance.get('getMobile', {
+            params: {
+                uid,
+                sign,
+                pid,
+                cuy: country.toUpperCase(),
+                size: 1,
+                ctype: 3
             }
+        })
+        let data = res.data
+        if (data.code !== 0 || !(data.data && data.data.length)) {
+            throw new Error(data.msg)
         }
-
-        number = checkAndFormatNumber(channel.country, number, carrierName)
-        const rcsNumber = new RcsNumber()
-        rcsNumber.mcc = channel.mcc
-        rcsNumber.mnc = channel.mnc
-        rcsNumber.country = channel.country
-        rcsNumber.number = number
-        if (channel.platform === 'mwze167') {
-            rcsNumber.extra = { orderId }
-            rcsNumber.from = RcsNumberSource.mwze167
-        } else if (channel.platform === 'durian') {
-            rcsNumber.extra = { fullNumber }
-            rcsNumber.from = RcsNumberSource.durian
+        Logger.log(data, 'mwze167')
+        const orderId = data.orderId
+        const operatorInfo = JSON.parse(data.data[0].mcc)
+        const operatorCode = operatorInfo.operator
+        const carrierName = operatorInfo.carrierName
+        const number = data.data[0].number
+
+        return {
+            orderId,
+            number,
+            operatorCode,
+            operatorName: carrierName,
+            rawResponse: data
         }
-        rcsNumber.expiryTime = new Date(Date.now() + 5 * 60 * 1000)
-        rcsNumber.status = RcsNumberStatus.PENDING
-        rcsNumber.deviceId = deviceId
-        rcsNumber.taskId = taskId
-        rcsNumber.result = result
-        return await this.rcsNumberRepository.save(rcsNumber)
     }
 
-    @Cron('0/5 * * * * *')
-    async getMessage() {
-        const numbers = await this.rcsNumberRepository.findBy({
-            expiryTime: MoreThan(addMinutes(new Date(), -1)),
-            status: Not(RcsNumberStatus.EXPIRED)
-        })
-        numbers.forEach(async (number) => {
-            if (number.expiryTime < new Date()) {
-                if (number.status === RcsNumberStatus.PENDING) {
-                    number.status = RcsNumberStatus.EXPIRED
-                    await this.rcsNumberRepository.save(number)
-                }
-                return
-            }
-
-            try {
-                if (number.from === RcsNumberSource.mwze167) {
-                    const { data } = await axiosInstance.get('getMsg', {
-                        params: {
-                            uid,
-                            sign,
-                            orderId: number.extra.orderId
-                        }
-                    })
-                    if (data.data) {
-                        const msg = data.data.map((i) => i.txt).join('\n')
-                        number.status = RcsNumberStatus.SUCCESS
-                        number.message = msg
-                        await this.rcsNumberRepository.save(number)
-                    }
-                } else if (number.from === RcsNumberSource.durian) {
-                    const { data } = await durianInstance.get('getMsg', {
-                        params: {
-                            name,
-                            ApiKey,
-                            pn: number.extra.fullNumber,
-                            pid: dPid,
-                            serial: 2
-                        }
-                    })
-                    if (data.code === 200) {
-                        number.status = RcsNumberStatus.SUCCESS
-                        number.message = `Your Messenger verification code is G-${data.data}`
-                        await this.rcsNumberRepository.save(number)
-                    }
-                }
-            } catch (e) {
-            }
+    async reetriveMessage(orderId: string): Promise<string> {
+        const { data } = await axiosInstance.get('getMsg', {
+            params: { uid, sign, orderId }
         })
+        if (data.data) {
+            const msg = data.data.map((i) => i.txt).join('\n')
+            return msg
+        }
     }
-
-
-    async getNumberOld(deviceId?: string): Promise<RcsNumber> {
-        const rcsNumber = new RcsNumber()
-        rcsNumber.mcc = '722'
-        rcsNumber.mnc = '299'
-        rcsNumber.country = 'ar'
-        rcsNumber.number = '92612446939'
-        let orderId = 'orderId": "1259926842729435136'
-        rcsNumber.extra = { orderId }
-        rcsNumber.expiryTime = new Date(Date.now() + 5 * 60 * 1000)
-        rcsNumber.from = RcsNumberSource.mwze167
-        rcsNumber.status = RcsNumberStatus.PENDING
-        rcsNumber.deviceId = deviceId
-        return await this.rcsNumberRepository.save(rcsNumber)
-    }
-
-    async sleep(ms: number): Promise<void> {
-        return new Promise(resolve => setTimeout(resolve, ms))
-    }
-
 }

+ 8 - 7
src/rcs-number/rcs-number.module.ts

@@ -4,15 +4,16 @@ import { RcsNumberController } from './rcs-number.controller'
 import { TypeOrmModule } from '@nestjs/typeorm'
 import { RcsNumber } from './entities/rcs-number.entity'
 import { mwze167 } from './impl/mwze167.service'
-import { Task } from '../task/entities/task.entity'
-import { Channel } from '../channel/entities/channel.entities'
-import { Device } from '../device/entities/device.entity'
+import { durian } from './impl/durian.service'
+import { DeviceModule } from 'src/device/device.module'
+import { OperaterConfigModule } from 'src/operator_config/operator_config.module'
+import { TaskModule } from 'src/task/task.module'
+import { ChannelModule } from 'src/channel/channel.module'
 
 @Module({
-    imports: [TypeOrmModule.forFeature([RcsNumber, Task, Channel, Device])],
-    providers: [RcsNumberService, mwze167],
+    imports: [TypeOrmModule.forFeature([RcsNumber]), DeviceModule, OperaterConfigModule, TaskModule, ChannelModule],
+    providers: [RcsNumberService, mwze167, durian],
     controllers: [RcsNumberController],
     exports: [RcsNumberService]
 })
-export class RcsNumberModule {
-}
+export class RcsNumberModule {}

+ 116 - 17
src/rcs-number/rcs-number.service.ts

@@ -1,35 +1,47 @@
 import { mwze167 } from './impl/mwze167.service'
 import { Injectable } from '@nestjs/common'
 import { InjectRepository } from '@nestjs/typeorm'
-import { RcsNumber } from './entities/rcs-number.entity'
+import { RcsNumber, RcsNumberSource, RcsNumberStatus } from './entities/rcs-number.entity'
 import { PageRequest } from '../common/dto/page-request'
 import { Pagination, paginate } from 'nestjs-typeorm-paginate'
-import { In, Repository } from 'typeorm'
+import { In, MoreThan, Not, Repository } from 'typeorm'
 import { USACodeApiService } from './usacode-api-service'
 import { Device } from '../device/entities/device.entity'
+import { DeviceService } from 'src/device/device.service'
+import { TaskService } from 'src/task/task.service'
+import { OperaterConfigService } from 'src/operator_config/operator_config.service'
+import { OperatorConfig } from 'src/operator_config/entities/operator-config.entiy'
+import { Cron } from '@nestjs/schedule'
+import { addMinutes } from 'date-fns'
+import { durian } from './impl/durian.service'
+import * as AsyncLock from 'async-lock'
+import { checkAndFormatNumber, matchOperator } from './helpers'
+import { GetNumberService } from './impl/get-number-service'
+import { ChannelService } from '../channel/channel.service'
 
 @Injectable()
 export class RcsNumberService {
+    private lock = new AsyncLock()
     constructor(
         @InjectRepository(RcsNumber)
         private rcsNumberRepository: Repository<RcsNumber>,
-        @InjectRepository(Device)
-        private deviceRepository: Repository<Device>,
-        private mwze167: mwze167
-    ) {
-    }
+        private deviceService: DeviceService,
+        private taskService: TaskService,
+        private operatorConfigService: OperaterConfigService,
+        private channelService: ChannelService,
+        private mwze167: mwze167,
+        private durian: durian
+    ) {}
 
     async findAll(req: PageRequest<RcsNumber>): Promise<Pagination<RcsNumber>> {
         let page = await paginate<RcsNumber>(this.rcsNumberRepository, req.page, req.search)
         let items = page.items
-        const deviceIds = items.map(item => item.deviceId)
-        const devices = await this.deviceRepository.findBy({
-            id: In(deviceIds)
-        })
+        const deviceIds = items.map((item) => item.deviceId)
+        const devices = await this.deviceService.findByIds(deviceIds)
         // 将devices中的name赋值到对应item的deviceId相同的对象的deviceName中
         for (let i = 0; i < items.length; i++) {
             const item = items[i]
-            const device = devices.find(device => device.id === item.deviceId)
+            const device = devices.find((device) => device.id === item.deviceId)
             if (device) {
                 item.deviceName = device.name
             }
@@ -37,11 +49,63 @@ export class RcsNumberService {
         return page
     }
 
-    async create(deviceId?: string, taskId?: string, channelId?: string): Promise<RcsNumber> {
-        console.log('taskId', taskId)
-        console.log('channelId', channelId)
-        //return await this.mwze167.getNumberOld(deviceId)
-        return await this.mwze167.getNumber(deviceId, taskId, channelId)
+    async create(country?: string, deviceId?: string, taskId?: number) {
+        let operatorConfig: OperatorConfig
+        if (country) {
+            operatorConfig = await this.operatorConfigService.findByCountry(country)
+        }
+        if (!operatorConfig) {
+            const task = await this.taskService.findById(taskId)
+            if (task && task.country?.length) {
+                country = task.country[Math.floor(Math.random() * task.country.length)]
+                operatorConfig = await this.operatorConfigService.findByCountry(country)
+            }
+        }
+        if (!operatorConfig) {
+            operatorConfig = await this.operatorConfigService.random()
+        }
+        if (!operatorConfig) {
+            throw new Error('No operator config found')
+        }
+
+        const channels = await this.channelService.all()
+        const availableChannels = channels.filter((channel) => {
+            return (
+                channel.countryConfig.find(
+                    (config) =>
+                        config.countryCode.toLowerCase() === operatorConfig.country ||
+                        config.countryCode.toUpperCase() === operatorConfig.country
+                )?.enabled || false
+            )
+        })
+        if (!availableChannels.length) {
+            throw new Error('No available channel found')
+        }
+        const channel = availableChannels[Math.floor(Math.random() * availableChannels.length)]
+        let numberService: GetNumberService
+        if (channel.source === RcsNumberSource.mwze167) {
+            numberService = this.mwze167
+        } else if (channel.source === RcsNumberSource.durian) {
+            numberService = this.durian
+        } else {
+            throw new Error('No number service found')
+        }
+        const res = await numberService.getNumber(operatorConfig.country)
+        res.number = checkAndFormatNumber(operatorConfig.country, res.number)
+        const mapTo = matchOperator(res.operatorCode, operatorConfig)
+        const number = new RcsNumber()
+        number.from = numberService.source
+        number.number = res.number
+        number.orderId = res.orderId
+        number.deviceId = deviceId
+        number.mcc = mapTo.mcc
+        number.mnc = mapTo.mnc
+        number.country = operatorConfig.country.toLowerCase()
+        number.status = RcsNumberStatus.PENDING
+        number.expiryTime = addMinutes(new Date(), 5)
+        number.taskId = taskId
+        number.rawResponse = JSON.stringify(res.rawResponse)
+        return await this.rcsNumberRepository.save(number)
     }
 
     async delete(id: number): Promise<void> {
@@ -52,4 +116,39 @@ export class RcsNumberService {
         return await this.rcsNumberRepository.findOneBy({ id })
     }
 
+    @Cron('0/5 * * * * *')
+    async getMessage() {
+        const numbers = await this.rcsNumberRepository.findBy({
+            expiryTime: MoreThan(addMinutes(new Date(), -1)),
+            status: Not(RcsNumberStatus.EXPIRED)
+        })
+
+        numbers.forEach(async (number) => {
+            if (number.expiryTime < new Date()) {
+                if (number.status === RcsNumberStatus.PENDING) {
+                    number.status = RcsNumberStatus.EXPIRED
+                    await this.rcsNumberRepository.save(number)
+                }
+                return
+            }
+
+            if (number.status === RcsNumberStatus.SUCCESS && number.from === RcsNumberSource.durian) {
+                return
+            }
+
+            try {
+                let msg: string
+                if (number.from === RcsNumberSource.mwze167) {
+                    msg = await this.mwze167.reetriveMessage(number.orderId)
+                } else if (number.from === RcsNumberSource.durian) {
+                    msg = await this.durian.reetriveMessage(number.orderId)
+                }
+                if (msg) {
+                    number.status = RcsNumberStatus.SUCCESS
+                    number.message = msg
+                    await this.rcsNumberRepository.save(number)
+                }
+            } catch (e) {}
+        })
+    }
 }

+ 2 - 1
src/task/entities/task-item.entity.ts

@@ -1,4 +1,4 @@
-import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
+import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
 
 export enum TaskItemStatus {
     IDLE = 'idle',
@@ -16,6 +16,7 @@ export class TaskItem {
     createdAt: Date
 
     @Column()
+    @Index()
     taskId: number
 
     @Column()

+ 2 - 2
src/task/entities/task.entity.ts

@@ -1,4 +1,4 @@
-import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
+import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
 import { Exclude } from 'class-transformer'
 import { JsonTransformer } from 'src/transformers/json.transformer'
 
@@ -59,7 +59,7 @@ export class Task {
     requestNumberInterval: number
 
     @Column({ type: 'text', nullable: true, transformer: new JsonTransformer() })
-    channels: number[]
+    country: string[]
 
     @Column({ type: 'text', transformer: new JsonTransformer(), nullable: true })
     dynamicMessage: { key: string; values: string[] }[]

+ 5 - 1
src/task/task.service.ts

@@ -63,6 +63,10 @@ export class TaskService implements OnModuleInit {
 
     private taskControllers: { [key: number]: AbortController } = {}
 
+    async findById(id: number): Promise<Task> {
+        return await this.taskRepository.findOneBy({ id })
+    }
+
     async findAllTask(req: PageRequest<Task>): Promise<Pagination<Task>> {
         const page = await paginate<Task>(this.taskRepository, req.page, req.search)
         if (page.items.length !== 0) {
@@ -139,7 +143,7 @@ export class TaskService implements OnModuleInit {
                 cleanCount: data.cleanCount,
                 requestNumberInterval: data.requestNumberInterval,
                 checkConnection: data.checkConnection,
-                channels: data.channels,
+                country: data.country,
                 matchDevice: data.matchDevice
             }
         )

+ 5 - 0
yarn.lock

@@ -5901,6 +5901,11 @@ map-visit@^1.0.0:
   dependencies:
     object-visit "^1.0.0"
 
+mcc-mnc-list@^1.1.11:
+  version "1.1.11"
+  resolved "https://registry.npmmirror.com/mcc-mnc-list/-/mcc-mnc-list-1.1.11.tgz#f1256aab7ce7422f1d827701d5ae5a11443be4b2"
+  integrity sha512-3kkW2fvLtsKch1ci+t/3VOggmhwAviHv8MzDp8Lq5u8u00QMflLSDiNRtXqAzSfCYoNslAs9Yy8deKreygHS/w==
+
 md5@^2.2.1, md5@^2.3.0:
   version "2.3.0"
   resolved "https://registry.npmmirror.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f"