Browse Source

fix: calls list

stage
Vitalik 1 month ago
parent
commit
af05c70a57
  1. 2
      src/app.module.ts
  2. 4
      src/config/entities.config.ts
  3. 1
      src/config/index.ts
  4. 2
      src/core/namespaces/ws.namespace.ts
  5. 54
      src/domain/calls/calls.module.ts
  6. 25
      src/domain/calls/controllers/calls.controller.ts
  7. 8
      src/domain/calls/core/answer-call.ts
  8. 13
      src/domain/calls/core/call-action.ts
  9. 0
      src/domain/calls/core/call-token.ts
  10. 0
      src/domain/calls/core/finish-call.ts
  11. 15
      src/domain/calls/core/on-connected.ts
  12. 8
      src/domain/calls/core/ready-to-connect.ts
  13. 17
      src/domain/calls/core/send-ice-candidates.ts
  14. 15
      src/domain/calls/core/send-rtc-session.ts
  15. 0
      src/domain/calls/core/start-call.ts
  16. 6
      src/domain/calls/core/update-media-settings.ts
  17. 27
      src/domain/calls/dto/call-status.dto.ts
  18. 37
      src/domain/calls/dto/call.dto.ts
  19. 0
      src/domain/calls/dto/end-call.dto.ts
  20. 1
      src/domain/calls/dto/index.ts
  21. 0
      src/domain/calls/dto/reject-call.dto.ts
  22. 0
      src/domain/calls/entities/call-log.entity.ts
  23. 0
      src/domain/calls/entities/call-member.entity.ts
  24. 47
      src/domain/calls/entities/call.entity.ts
  25. 6
      src/domain/calls/entities/index.ts
  26. 0
      src/domain/calls/exceptions/access-call-wrong.exception.ts
  27. 0
      src/domain/calls/exceptions/call-alerady-finished.exception.ts
  28. 0
      src/domain/calls/exceptions/call-already-ready-to-connect.exception.ts
  29. 0
      src/domain/calls/exceptions/call-answered.exception.ts
  30. 0
      src/domain/calls/exceptions/call-not-found.exception.ts
  31. 17
      src/domain/calls/exceptions/cant-update-status.exception.ts
  32. 9
      src/domain/calls/exceptions/index.ts
  33. 0
      src/domain/calls/exceptions/initiator-in-call.exception.ts
  34. 0
      src/domain/calls/exceptions/target-user-in-call.exception.ts
  35. 0
      src/domain/calls/exceptions/wrong-device.exception.ts
  36. 0
      src/domain/calls/services/calls-actions-factory.ts
  37. 63
      src/domain/calls/services/calls-ice-candidates.service.ts
  38. 59
      src/domain/calls/services/calls-read.service.ts
  39. 301
      src/domain/calls/services/calls.service.ts
  40. 2
      src/domain/calls/typing/consts/index.ts
  41. 7
      src/domain/calls/typing/enums/call-status.enum.ts
  42. 1
      src/domain/calls/typing/enums/index.ts
  43. 3
      src/domain/calls/typing/index.ts
  44. 4
      src/domain/calls/typing/interfaces/call-repository.interface.ts
  45. 19
      src/domain/calls/typing/interfaces/call.interface.ts
  46. 41
      src/domain/calls/typing/interfaces/calls-service.interface.ts
  47. 3
      src/domain/calls/typing/interfaces/index.ts
  48. 36
      src/domain/calls/v2/callsv2.module.ts
  49. 43
      src/domain/calls/v2/entities/call.entity.ts
  50. 7
      src/domain/calls/v2/entities/index.ts
  51. 8
      src/domain/calls/v2/exceptions/index.ts
  52. 92
      src/domain/calls/v2/services/calls.service.ts
  53. 13
      src/domain/real-time/gateways/main.gateway.ts
  54. 1
      src/domain/real-time/guards/jwt.guard.ts
  55. 13
      src/domain/real-time/services/ws-users.service.ts
  56. 4
      src/domain/real-time/services/ws.service.ts
  57. 10
      src/domain/sessions/guards/ip.guard.ts
  58. 1
      src/main.ts
  59. 49
      src/rest/common/calls/calls-test.controller.ts
  60. 113
      src/rest/common/calls/calls.controller.ts
  61. 29
      src/rest/common/calls/calls.module.ts
  62. 202
      src/rest/common/calls/calls.service.ts
  63. 9
      src/rest/common/calls/dto/answer-call.dto.ts
  64. 6
      src/rest/common/calls/dto/cancel-call.dto.ts
  65. 6
      src/rest/common/calls/dto/finish-call.dto.ts
  66. 6
      src/rest/common/calls/dto/get-income-data.dto.ts
  67. 9
      src/rest/common/calls/dto/ice-candidate.dto.ts
  68. 8
      src/rest/common/calls/dto/index.ts
  69. 9
      src/rest/common/calls/dto/negotiation.dto.ts
  70. 9
      src/rest/common/calls/dto/reject-call.dto.ts
  71. 9
      src/rest/common/calls/dto/start-call.dto.ts
  72. 2
      src/rest/common/index.ts

2
src/app.module.ts

@ -36,7 +36,6 @@ import { FirebaseApiModule } from './libs/firebase/firebase.module' @@ -36,7 +36,6 @@ import { FirebaseApiModule } from './libs/firebase/firebase.module'
import { LoggerModule } from './domain'
import { SecretModModule } from './domain/secret-mod/secret-mod.module'
import { CallsModule } from './domain/calls/calls.module'
import { Callsv2Module } from './domain/calls/v2/callsv2.module'
Cron.setup(getEnv('CRON_ENABLED') === 'true')
@ -77,7 +76,6 @@ const imports = [ @@ -77,7 +76,6 @@ const imports = [
LoggerModule,
CallsModule.forRoot(),
Callsv2Module.forRoot(),
SecretModModule.forFeature(),

4
src/config/entities.config.ts

@ -11,10 +11,9 @@ import { NOTIFICATION_ENTITIES } from 'src/domain/notifications/entities' @@ -11,10 +11,9 @@ import { NOTIFICATION_ENTITIES } from 'src/domain/notifications/entities'
import { ACTIVITY_ENTITIES } from 'src/domain/activities/entities'
import { CHAT_ENTITIES } from 'src/domain/chats/entities'
import { VERSIONS_ENTITIES } from 'src/domain/versions/entities'
import { CALLS_ENTITIES } from 'src/domain/calls/entities'
import { SETTINGS_ENTITIES } from 'src/domain/settings/entities'
import { SECRET_MOD_ENTITIES } from 'src/domain/secret-mod/entities'
import { CALLS_ENTITIES_V2 } from 'src/domain/calls/v2/entities'
import { CALLS_ENTITIES_V2 } from 'src/domain/calls/entities'
export const ENTITIES = [
...USERS_ENTITIES,
@ -30,7 +29,6 @@ export const ENTITIES = [ @@ -30,7 +29,6 @@ export const ENTITIES = [
...ACTIVITY_ENTITIES,
...CHAT_ENTITIES,
...VERSIONS_ENTITIES,
...CALLS_ENTITIES,
...SETTINGS_ENTITIES,
...SECRET_MOD_ENTITIES,
...CALLS_ENTITIES_V2,

1
src/config/index.ts

@ -22,6 +22,7 @@ const getDatabaseConfig = (): Parameters<typeof DatabaseModule['forRoot']> => { @@ -22,6 +22,7 @@ const getDatabaseConfig = (): Parameters<typeof DatabaseModule['forRoot']> => {
password: process.env.DATABASE_PASS,
database: process.env.DATABASE_DB,
synchronize: true,
connectTimeoutMS: 10000,
},
ENTITIES,
]

2
src/core/namespaces/ws.namespace.ts

@ -79,5 +79,7 @@ export namespace WebSockets { @@ -79,5 +79,7 @@ export namespace WebSockets {
* @param {any} data - додаткові дані
*/
emitToUser(userId: number, key: string, data?: any): void
emitToDevice(userId: number, deviceUuid: string, key: string, data?: any): void
}
}

54
src/domain/calls/calls.module.ts

@ -1,46 +1,38 @@ @@ -1,46 +1,38 @@
import { DynamicModule, Module } from '@nestjs/common'
import { provideEntity, RedisModule } from 'src/libs'
import { CALLS_REPOSITORY, CALLS_SERVICE } from './typing/consts'
import { Call } from './entities'
import { provideClass } from 'src/shared'
import { UsersModule } from 'src/domain/users/users.module'
import { JwtModule, provideEntity } from 'src/libs'
import { CALLS_LOGS_REPOSITORY, CALLS_MEMBERS_REPOSITORY, CALLS_REPOSITORY } from './typing'
import { Call, CallLog, CallMember } from './entities'
import { CallsActionsFactory } from './services/calls-actions-factory'
import { RealTimeModule } from 'src/domain/real-time/real-time.module'
import { NotificationsModule } from 'src/domain/notifications/notifications.module'
import { CallsService } from './services/calls.service'
import { RealTimeModule } from '../real-time/real-time.module'
import { UsersModule } from '../users/users.module'
import { NotificationsModule } from '../notifications/notifications.module'
import { APNsModule } from 'src/libs/apns/apns.module'
import { CallsIceCandidatesService } from './services/calls-ice-candidates.service'
import { V2CallsController } from './controllers/calls.controller'
import { SessionsModule } from 'src/domain/sessions/sessions.module'
import { CallsReadService } from './services/calls-read.service'
@Module({})
export class CallsModule {
static forFeature(): DynamicModule {
return {
module: CallsModule,
providers: [
provideEntity(CALLS_REPOSITORY, Call),
provideClass(CALLS_SERVICE, CallsService),
],
imports: [
RealTimeModule.forFeature(),
UsersModule.forFeature(),
NotificationsModule.forFeature(),
APNsModule.forFeature(),
RedisModule.forFeature(),
],
exports: [CALLS_REPOSITORY, CALLS_SERVICE],
}
}
static forRoot(): DynamicModule {
return {
module: CallsModule,
providers: [provideEntity(CALLS_REPOSITORY, Call), CallsIceCandidatesService],
imports: [
RealTimeModule.forFeature(),
SessionsModule.forFeature(),
JwtModule.forFeature(),
UsersModule.forFeature(),
RealTimeModule.forFeature(),
NotificationsModule.forFeature(),
APNsModule.forFeature(),
RedisModule.forFeature(),
],
providers: [
provideEntity(CALLS_REPOSITORY, Call),
provideEntity(CALLS_MEMBERS_REPOSITORY, CallMember),
provideEntity(CALLS_LOGS_REPOSITORY, CallLog),
CallsActionsFactory,
CallsService,
CallsReadService,
],
controllers: [V2CallsController],
}
}
}

25
src/domain/calls/v2/controllers/calls.controller.ts → src/domain/calls/controllers/calls.controller.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { Body, Controller, Get, Inject, Param, Post } from '@nestjs/common'
import { Body, Controller, Delete, Get, Inject, Param, Post } from '@nestjs/common'
import { ApiTags } from '@nestjs/swagger'
import { CallsService } from '../services/calls.service'
@ -14,22 +14,31 @@ import { @@ -14,22 +14,31 @@ import {
SendRTCSessionPayload,
UpdateMediaSettingsPayload,
} from '../typing'
import { ReqUser } from 'src/shared'
import { ReqPagination, ReqUser } from 'src/shared'
import { RejectCallByTokenDto, RejectCallDto } from '../dto/reject-call.dto'
import { AccessCallWrongException } from '../exceptions'
import { EndCallDto } from '../dto/end-call.dto'
import { AuthGuard } from 'src/domain/sessions/decorators'
import { CallsReadService } from '../services/calls-read.service'
import { IPagination } from 'src/core/interfaces'
@ApiTags('Calls v2')
@Controller('v2calls')
@ApiTags('Calls')
@Controller('calls')
export class V2CallsController {
constructor(
private readonly callsService: CallsService,
private readonly callsReadService: CallsReadService,
@Inject(CALLS_REPOSITORY)
private readonly callsRepository: CallsRepository,
) {}
@Get('/')
@AuthGuard()
public getCalls(@ReqUser() userId: number, @ReqPagination() pagination: IPagination) {
return this.callsReadService.getList(userId, pagination)
}
@Post('start')
@AuthGuard()
public start(@Body() payload: IStartCallPayload, @ReqUser() userId: number) {
@ -142,6 +151,12 @@ export class V2CallsController { @@ -142,6 +151,12 @@ export class V2CallsController {
@Get(':callId')
@AuthGuard()
public getCall(@Param('callId') callId: string, @ReqUser() userId: number) {
return this.callsService.getCall(callId, userId)
return this.callsReadService.getCall(callId, userId)
}
@Delete(':callId/history')
@AuthGuard()
public deleteCall(@Param('callId') callId: string, @ReqUser() userId: number) {
return this.callsService.deleteHistoryItem(userId, callId)
}
}

8
src/domain/calls/v2/core/answer-call.ts → src/domain/calls/core/answer-call.ts

@ -40,13 +40,11 @@ export class AnswerCall extends CallAction { @@ -40,13 +40,11 @@ export class AnswerCall extends CallAction {
protected async sendEvents() {
const members = await this.callsMembersRepository.find({
where: { callId: this.call.id },
select: ['userId'],
select: ['userId', 'deviceUuid'],
})
members.forEach(it => {
this.realTimeService.emitToUser(it.userId, CallSocketEvent.Answered, {
call: this.call,
})
this.emitToMembers(members, CallSocketEvent.Answered, {
call: this.call,
})
}

13
src/domain/calls/v2/core/call-action.ts → src/domain/calls/core/call-action.ts

@ -5,6 +5,7 @@ import { @@ -5,6 +5,7 @@ import {
CallsRepository,
CallStatus,
ICallEntity,
ICallMemberEntity,
} from '../typing'
import { Notifications, WebSockets } from 'src/core'
import { CallNotFoundException } from '../exceptions/call-not-found.exception'
@ -87,6 +88,16 @@ export class CallAction { @@ -87,6 +88,16 @@ export class CallAction {
}
protected async populateUsersToSend(call: ICallEntity, userId: number) {
return call.members.map(it => it.userId).filter(it => it !== userId)
return call.members.filter(it => it.userId !== userId)
}
protected emitSocketToMember(it: ICallMemberEntity, key: string, data?: any) {
this.realTimeService.emitToDevice(it.userId, it.deviceUuid, key, data)
}
protected emitToMembers(members: ICallMemberEntity[], key: string, data?: any) {
members.forEach(it => {
this.realTimeService.emitToDevice(it.userId, it.deviceUuid, key, data)
})
}
}

0
src/domain/calls/v2/core/call-token.ts → src/domain/calls/core/call-token.ts

0
src/domain/calls/v2/core/finish-call.ts → src/domain/calls/core/finish-call.ts

15
src/domain/calls/v2/core/on-connected.ts → src/domain/calls/core/on-connected.ts

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import {
CallMemberStatus,
CallSocketEvent,
CallStatus,
ICallEntity,
ICallMemberEntity,
@ -31,6 +32,8 @@ export class OnConnected extends CallAction { @@ -31,6 +32,8 @@ export class OnConnected extends CallAction {
await this.updateCall()
this.sendEvents()
await this.info(this.call.id, 'Call updated to status in progress')
}
@ -46,15 +49,25 @@ export class OnConnected extends CallAction { @@ -46,15 +49,25 @@ export class OnConnected extends CallAction {
})
const isAllConnecting = members.every(it => it.status === CallMemberStatus.Call)
console.log('isAllConnecting', isAllConnecting)
return isAllConnecting
}
protected async updateCall() {
console.log('UPDATE CALLL')
await this.callsRepository.update(
{ id: this.payload.callId },
{ status: CallStatus.InProgress },
)
this.call = await this.getCall(this.payload.callId)
}
protected async sendEvents() {
const members = await this.callsMembersRepository.find({
where: { callId: this.call.id },
select: ['userId', 'deviceUuid'],
})
this.emitToMembers(members, CallSocketEvent.CallUpdated, { call: this.call })
}
}

8
src/domain/calls/v2/core/ready-to-connect.ts → src/domain/calls/core/ready-to-connect.ts

@ -78,13 +78,9 @@ export class ReadyToConnect extends CallAction { @@ -78,13 +78,9 @@ export class ReadyToConnect extends CallAction {
protected async sendEvents() {
const members = await this.callsMembersRepository.find({
where: { callId: this.call.id },
select: ['userId'],
select: ['userId', 'deviceUuid'],
})
members.forEach(it => {
this.realTimeService.emitToUser(it.userId, CallSocketEvent.ReadyToConnect, {
call: this.call,
})
})
this.emitToMembers(members, CallSocketEvent.ReadyToConnect, { call: this.call })
}
}

17
src/domain/calls/v2/core/send-ice-candidates.ts → src/domain/calls/core/send-ice-candidates.ts

@ -1,10 +1,15 @@ @@ -1,10 +1,15 @@
import { CallSocketEvent, ICallEntity, SendIceCandidatesPayload } from '../typing'
import {
CallSocketEvent,
ICallEntity,
ICallMemberEntity,
SendIceCandidatesPayload,
} from '../typing'
import { CallAction } from './call-action'
export class SendIceCandidates extends CallAction {
private payload: SendIceCandidatesPayload
protected call: ICallEntity
protected usersIdsToSend: number[] = []
protected usersIdsToSend: ICallMemberEntity[] = []
public async send(payload: SendIceCandidatesPayload) {
this.payload = payload
@ -17,11 +22,9 @@ export class SendIceCandidates extends CallAction { @@ -17,11 +22,9 @@ export class SendIceCandidates extends CallAction {
}
protected async sendEvents() {
this.usersIdsToSend.map(userId => {
this.realTimeService.emitToUser(userId, CallSocketEvent.RTCIceCandidates, {
call: this.call,
iceCandidates: this.payload.iceCandidates,
})
this.emitToMembers(this.usersIdsToSend, CallSocketEvent.RTCIceCandidates, {
call: this.call,
iceCandidates: this.payload.iceCandidates,
})
}
}

15
src/domain/calls/v2/core/send-rtc-session.ts → src/domain/calls/core/send-rtc-session.ts

@ -1,10 +1,10 @@ @@ -1,10 +1,10 @@
import { CallSocketEvent, ICallEntity, SendRTCSessionPayload } from '../typing'
import { CallSocketEvent, ICallEntity, ICallMemberEntity, SendRTCSessionPayload } from '../typing'
import { CallAction } from './call-action'
export class SendRTCSession extends CallAction {
protected payload: SendRTCSessionPayload
protected call: ICallEntity
protected usersIdsToSend: number[] = []
protected usersIdsToSend: ICallMemberEntity[] = []
public async send(payload: SendRTCSessionPayload) {
this.payload = payload
@ -12,6 +12,7 @@ export class SendRTCSession extends CallAction { @@ -12,6 +12,7 @@ export class SendRTCSession extends CallAction {
this.call = await this.getCall(payload.callId)
this.usersIdsToSend = await this.populateUsersToSend(this.call, this.payload.userId)
this.sendEvents()
this.info(this.call.id, 'Send rtc session description', this.payload.userId)
@ -22,12 +23,10 @@ export class SendRTCSession extends CallAction { @@ -22,12 +23,10 @@ export class SendRTCSession extends CallAction {
}
protected async sendEvents() {
this.usersIdsToSend.map(userId => {
this.realTimeService.emitToUser(userId, CallSocketEvent.RTCSessionDescription, {
call: this.call,
mustAnswer: this.isNeedAnswer(),
sessionDescription: this.payload.sessionDescription,
})
this.emitToMembers(this.usersIdsToSend, CallSocketEvent.RTCSessionDescription, {
call: this.call,
mustAnswer: this.isNeedAnswer(),
sessionDescription: this.payload.sessionDescription,
})
}
}

0
src/domain/calls/v2/core/start-call.ts → src/domain/calls/core/start-call.ts

6
src/domain/calls/v2/core/update-media-settings.ts → src/domain/calls/core/update-media-settings.ts

@ -30,10 +30,6 @@ export class UpdateMediaSettings extends CallAction { @@ -30,10 +30,6 @@ export class UpdateMediaSettings extends CallAction {
}
protected async sendEvents() {
this.call.members.map(it => {
this.realTimeService.emitToUser(it.userId, CallSocketEvent.CallUpdated, {
call: this.call,
})
})
this.emitToMembers(this.call.members, CallSocketEvent.CallUpdated, { call: this.call })
}
}

27
src/domain/calls/dto/call-status.dto.ts

@ -1,27 +0,0 @@ @@ -1,27 +0,0 @@
import { CallStatus } from '../typing/enums'
export class CallStatusDto {
constructor(public readonly status: CallStatus) {}
public canUpdateStatus(targetStatus: CallStatus) {
const allowed = this.statusesValidators[`${this.status}-${targetStatus}`]
return allowed ? allowed : false
}
protected statusesValidators = {
[this.getValidatorKey(CallStatus.New, CallStatus.InProccess)]: true,
[this.getValidatorKey(CallStatus.New, CallStatus.Canceled)]: true,
[this.getValidatorKey(CallStatus.New, CallStatus.Rejected)]: true,
[this.getValidatorKey(CallStatus.InProccess, CallStatus.Canceled)]: true,
[this.getValidatorKey(CallStatus.InProccess, CallStatus.Finished)]: true,
}
protected getCurrentValidatorKey(targetStatus: CallStatus) {
return this.getValidatorKey(this.status, targetStatus)
}
protected getValidatorKey(from: CallStatus, to: CallStatus) {
return `${from}-${to}`
}
}

37
src/domain/calls/dto/call.dto.ts

@ -1,37 +0,0 @@ @@ -1,37 +0,0 @@
import { CallStatus } from '../typing/enums'
import { ICall } from '../typing/interfaces'
import { CallStatusDto } from './call-status.dto'
export class CallDto {
readonly id: string
readonly usersIds: number[]
readonly hideForUsersIds: number[]
readonly initiatorUserId: number
readonly finishedAt: string
readonly startAt: string
readonly title: string
readonly status: CallStatus
readonly statusDto: CallStatusDto
readonly createdAt: string
readonly updatedAt: string
constructor(callEntity: ICall) {
this.id = callEntity.id
this.usersIds = callEntity.usersIds
this.hideForUsersIds = callEntity.hideForUsersIds
this.initiatorUserId = callEntity.initiatorUserId
this.finishedAt = callEntity.finishedAt
this.startAt = callEntity.startAt
this.title = callEntity.title
this.status = callEntity.status
this.statusDto = new CallStatusDto(callEntity.status)
this.createdAt = callEntity.createdAt
this.updatedAt = callEntity.updatedAt
}
public getRecipients() {
return this.usersIds.filter(it => it !== this.initiatorUserId)
}
}

0
src/domain/calls/v2/dto/end-call.dto.ts → src/domain/calls/dto/end-call.dto.ts

1
src/domain/calls/dto/index.ts

@ -1 +0,0 @@ @@ -1 +0,0 @@
export * from './call.dto'

0
src/domain/calls/v2/dto/reject-call.dto.ts → src/domain/calls/dto/reject-call.dto.ts

0
src/domain/calls/v2/entities/call-log.entity.ts → src/domain/calls/entities/call-log.entity.ts

0
src/domain/calls/v2/entities/call-member.entity.ts → src/domain/calls/entities/call-member.entity.ts

47
src/domain/calls/entities/call.entity.ts

@ -1,29 +1,30 @@ @@ -1,29 +1,30 @@
import { Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm'
import { ICall } from '../typing/interfaces'
import { CallStatus } from '../typing/enums'
@Entity('calls')
export class Call implements ICall {
import {
Column,
CreateDateColumn,
Entity,
OneToMany,
PrimaryColumn,
UpdateDateColumn,
} from 'typeorm'
import { CallFinishedReason, CallStatus, ICallEntity, ICallMemberEntity } from '../typing'
import { CallMember } from './call-member.entity'
@Entity('callsv2')
export class Call implements ICallEntity {
@PrimaryColumn()
id: string
@Column({ type: 'int', array: true })
usersIds: number[]
@Column({ type: 'varchar', default: CallStatus.Calling })
status: CallStatus
@Column({ type: 'varchar', nullable: true })
finishedReason?: CallFinishedReason
@Column()
initiatorUserId: number
@Column({ type: 'int', array: true, default: [] })
hideForUsersIds: number[]
@Column({ type: 'varchar' })
status: CallStatus
@Column({ nullable: true })
title: string
@Column({ nullable: true })
accessToken?: string
finishedByUserId?: number
@Column({ type: 'timestamp with time zone', nullable: true })
finishedAt: string
@ -31,9 +32,15 @@ export class Call implements ICall { @@ -31,9 +32,15 @@ export class Call implements ICall {
@Column({ type: 'timestamp with time zone', nullable: true })
startAt: string
@CreateDateColumn({ type: 'timestamp', default: () => 'LOCALTIMESTAMP' })
@CreateDateColumn({ type: 'timestamp with time zone', default: () => 'LOCALTIMESTAMP' })
createdAt: string
@UpdateDateColumn({ type: 'timestamp', default: () => 'LOCALTIMESTAMP' })
@UpdateDateColumn({ type: 'timestamp with time zone', default: () => 'LOCALTIMESTAMP' })
updatedAt: string
@OneToMany(() => CallMember, member => member.call)
members?: CallMember[]
@Column({ type: 'int', array: true, default: [] })
hideForUsers?: number[]
}

6
src/domain/calls/entities/index.ts

@ -1,5 +1,7 @@ @@ -1,5 +1,7 @@
import { CallLog } from './call-log.entity'
import { CallMember } from './call-member.entity'
import { Call } from './call.entity'
export const CALLS_ENTITIES = [Call]
export const CALLS_ENTITIES_V2 = [Call, CallMember, CallLog]
export { Call }
export { Call, CallMember, CallLog }

0
src/domain/calls/v2/exceptions/access-call-wrong.exception.ts → src/domain/calls/exceptions/access-call-wrong.exception.ts

0
src/domain/calls/v2/exceptions/call-alerady-finished.exception.ts → src/domain/calls/exceptions/call-alerady-finished.exception.ts

0
src/domain/calls/v2/exceptions/call-already-ready-to-connect.exception.ts → src/domain/calls/exceptions/call-already-ready-to-connect.exception.ts

0
src/domain/calls/v2/exceptions/call-answered.exception.ts → src/domain/calls/exceptions/call-answered.exception.ts

0
src/domain/calls/v2/exceptions/call-not-found.exception.ts → src/domain/calls/exceptions/call-not-found.exception.ts

17
src/domain/calls/exceptions/cant-update-status.exception.ts

@ -1,17 +0,0 @@ @@ -1,17 +0,0 @@
import { DomainException } from 'src/shared'
import { CallStatus } from '../typing/enums'
export class CantUpdateStatusException extends DomainException {
constructor(private fromStatus: CallStatus, private toStatus: CallStatus) {
super({
key: 'cantUpdateStatus',
description: null,
})
this.setDescription(this.getDescription())
}
private getDescription() {
return `Cant update call entity from status: ${this.fromStatus} to status: ${this.toStatus}`
}
}

9
src/domain/calls/exceptions/index.ts

@ -1 +1,8 @@ @@ -1 +1,8 @@
export * from './cant-update-status.exception'
export * from './access-call-wrong.exception'
export * from './call-alerady-finished.exception'
export * from './call-already-ready-to-connect.exception'
export * from './call-answered.exception'
export * from './call-not-found.exception'
export * from './initiator-in-call.exception'
export * from './target-user-in-call.exception'
export * from './wrong-device.exception'

0
src/domain/calls/v2/exceptions/initiator-in-call.exception.ts → src/domain/calls/exceptions/initiator-in-call.exception.ts

0
src/domain/calls/v2/exceptions/target-user-in-call.exception.ts → src/domain/calls/exceptions/target-user-in-call.exception.ts

0
src/domain/calls/v2/exceptions/wrong-device.exception.ts → src/domain/calls/exceptions/wrong-device.exception.ts

0
src/domain/calls/v2/services/calls-actions-factory.ts → src/domain/calls/services/calls-actions-factory.ts

63
src/domain/calls/services/calls-ice-candidates.service.ts

@ -1,63 +0,0 @@ @@ -1,63 +0,0 @@
import { Inject, Injectable } from '@nestjs/common'
import { RedisService } from 'src/libs'
import { ICallsRepository } from '../typing/interfaces'
import { CALLS_REPOSITORY } from '../typing/consts'
import { OnEvent } from '@nestjs/event-emitter'
import { Events, IEventsPayloads } from 'src/core/enums'
import { CallStatus } from '../typing/enums'
@Injectable()
export class CallsIceCandidatesService {
constructor(
@Inject(CALLS_REPOSITORY)
private readonly callsRepository: ICallsRepository,
private readonly redisService: RedisService,
) {}
@OnEvent(Events.OnNewIceCandidate)
public async onIceCandidate(payload: IEventsPayloads['OnNewIceCandidate']) {
console.log('ICE CANDIDATE', payload)
if (payload.callId) {
const call = await this.getActiveCall(payload.userId)
if (!call) return
payload.callId = call.id
}
this.saveToRedis(payload.userId, payload.callId, payload.iceCandidates)
}
private async saveToRedis(userId: number, callId: string, candidates: any[]) {
const key = this.createKey(userId, callId)
const existCandidates = await this.getFromRedis(key)
existCandidates.push(...candidates)
console.log('SAVE TO REDIS', existCandidates.length)
await this.redisService.set(key, JSON.stringify(existCandidates), 1000 * 60 * 3)
}
private async getFromRedis(key: string) {
const data = await this.redisService.get(key)
if (!data) return []
if (typeof data === 'string') {
const result = JSON.parse(data)
if (Array.isArray(result)) return result
else return []
}
return []
}
private createKey(userId: number, callId: string) {
return `icecandidates/${userId}/${callId}`
}
private async getActiveCall(userId: number) {
const call = await this.callsRepository
.createQueryBuilder('it')
.where(':userId = ANY(it.usersIds)', { userId })
.andWhere('it.status = :status', { status: CallStatus.New })
.getOne()
return call
}
}

59
src/domain/calls/services/calls-read.service.ts

@ -0,0 +1,59 @@ @@ -0,0 +1,59 @@
import { Inject, Injectable } from '@nestjs/common'
import {
CALLS_MEMBERS_REPOSITORY,
CALLS_REPOSITORY,
CallsMembersRepository,
CallsRepository,
} from '../typing'
import { IPagination } from 'src/core/interfaces'
import { paginateAndGetMany } from 'src/shared'
@Injectable()
export class CallsReadService {
constructor(
@Inject(CALLS_REPOSITORY)
private readonly callsRepository: CallsRepository,
@Inject(CALLS_MEMBERS_REPOSITORY)
private readonly callsMembersRepository: CallsMembersRepository,
) {}
public async getCall(callId: string, userId: number) {
const call = await this.callsRepository.findOne({
where: { id: callId },
relations: ['members'],
})
if (!call) {
return null
}
const isMember = call?.members?.find(it => it.userId === userId)
if (!isMember) {
return null
}
return call
}
public async getList(userId: number, pagination: IPagination) {
const query = this.callsMembersRepository
.createQueryBuilder('it')
.leftJoinAndSelect('it.call', 'call')
.orderBy('call.createdAt', 'DESC')
.where('it.userId = :userId', { userId })
.andWhere('NOT :userId = ANY(call.hideForUsers)', { userId })
const { items, count } = await paginateAndGetMany(query, pagination, 'it')
for await (const [index, item] of items.entries()) {
const members = await this.callsMembersRepository.find({ callId: item.callId })
items[index].call.members = members
}
return {
items: items.map(it => it.call),
count,
}
}
}

301
src/domain/calls/services/calls.service.ts

@ -1,299 +1,88 @@ @@ -1,299 +1,88 @@
import { Inject, Injectable } from '@nestjs/common'
import { CallsActionsFactory } from './calls-actions-factory'
import {
CALLS_REPOSITORY,
CallsRepository,
IAnswerCallPayload,
ICallsRepository,
ICallEntity,
ICallsService,
ICancelCallPayload,
IFinishCallPayload,
INegotiationPayload,
IOnConnectedPayload,
IReadyToConnectPayload,
IStartCallPayload,
} from '../typing/interfaces'
import { CALLS_REPOSITORY } from '../typing/consts'
import { CallStatus } from '../typing/enums'
import { NOTIFICATIONS_SERVICE, REAL_TIME_SERVICE } from 'src/core/consts'
import { Notifications, Users, WebSockets } from 'src/core'
import { USERS_REPOSITORY } from 'src/domain/users/consts'
import { UsersRepository } from 'src/domain/users/repositories'
import { UserFullName } from 'src/domain/users/classes'
import { transformFileUrl } from 'src/shared/transforms'
SendIceCandidatesPayload,
SendRTCSessionPayload,
UpdateMediaSettingsPayload,
} from '../typing'
import { v4 as uuidv4 } from 'uuid'
import { CallDto } from '../dto'
import { CantUpdateStatusException } from '../exceptions'
import { RedisService } from 'src/libs'
type CallId = string
@Injectable()
export class CallsService implements ICallsService {
private readonly rtcMessagesTemp: Record<CallId, any> = {}
constructor(
@Inject(CALLS_REPOSITORY)
private readonly callsRepository: ICallsRepository,
@Inject(REAL_TIME_SERVICE)
private readonly realTimeService: WebSockets.Service,
@Inject(USERS_REPOSITORY)
private readonly usersRepository: UsersRepository,
@Inject(NOTIFICATIONS_SERVICE)
private readonly notificationsService: Notifications.INotificationsService,
private readonly redisService: RedisService,
private readonly callsRepository: CallsRepository,
private readonly callsActionsFactory: CallsActionsFactory,
) {}
public async getCall(callId: string) {
const callEntity = await this.callsRepository.findOne(callId)
if (!callEntity) throw new Error('Call doent exist')
return new CallDto(callEntity)
}
public async startCall(payload: IStartCallPayload) {
const callEntity = await this.callsRepository.save({
usersIds: payload.usersIds,
initiatorUserId: payload.initiatorUserId,
status: CallStatus.New,
duration: 0,
id: uuidv4(),
})
const callDto = new CallDto(callEntity)
this.rtcMessagesTemp[callDto.id] = payload.rtcMessage
await this.sendNewCallEvents(callDto)
return callDto
}
private async createCallToken(callId: string) {
const callToken = uuidv4()
await this.redisService.set(callToken, JSON.stringify({ callId: callId }))
return callToken
}
private async sendNewCallEvents(call: CallDto) {
const authToken = await this.createCallToken(call.id)
const fromUser = await this.usersRepository.findOne({
where: {
id: call.initiatorUserId,
},
relations: ['info'],
})
call.getRecipients().map(id => {
this.sendNewCallEventToUser(id, call, fromUser, authToken)
})
}
private async sendNewCallEventToUser(
targetUserId: number,
call: CallDto,
fromUser: Users.UserModel,
authToken: string,
) {
const expiredAt = new Date()
expiredAt.setMinutes(expiredAt.getMinutes() + 2)
this.notificationsService.sendVoipNotification(targetUserId, {
title: 'Новий виклик',
content: 'від користувача ' + UserFullName.getFromUser(fromUser),
data: {
uuid: call.id,
handle: fromUser.phoneNumber,
callerName: UserFullName.getFromUser(fromUser),
callerId: String(call.initiatorUserId),
type: 'income',
expiredAt: String(expiredAt.getTime()),
avatarUrl: fromUser.info.avatarUrl ? transformFileUrl(fromUser.info.avatarUrl) : '',
authToken,
},
})
}
public async getIncomeCall(callId: string, fromUserId: number, targetUserDeviceId: string) {
const call = await this.getCall(callId)
public async start(payload: IStartCallPayload): Promise<ICallEntity> {
const startCall = this.callsActionsFactory.startCall()
const fromUser = await this.usersRepository.findOne({
where: {
id: call.initiatorUserId,
},
relations: ['info'],
})
this.realTimeService.emitToUser(fromUserId, 'call/new', {
callId: call.id,
rtcMessage: this.rtcMessagesTemp[call.id],
callerId: call.initiatorUserId,
targetUserDeviceId,
from: {
type: 'personal',
title: UserFullName.getFromUser(fromUser),
avatarImageUrl: transformFileUrl(fromUser.info.avatarUrl),
},
})
this.sendAnsweredCall(call, targetUserDeviceId)
return await startCall.start(payload)
}
private sendAnsweredCall(call: CallDto, targetUserDeviceId: string) {
try {
const targetUsersIds = call.getRecipients()
public async answer(payload: IAnswerCallPayload): Promise<void> {
const answerCall = this.callsActionsFactory.answerCall()
targetUsersIds.forEach(userId => {
this.sendEventToUser(userId, 'callAnswered', targetUserDeviceId, {
callId: call.id,
})
})
} catch (e) {
console.log('Handle answer error', e)
}
await answerCall.answer(payload)
}
public async answerCall(payload: IAnswerCallPayload): Promise<CallDto> {
const call = await this.getCall(payload.callId)
this.realTimeService.emitToUser(call.initiatorUserId, 'call/answered', {
callId: call.id,
rtcMessage: payload.rtcMessage,
})
if (!call.statusDto.canUpdateStatus(CallStatus.InProccess)) {
throw new CantUpdateStatusException(call.status, CallStatus.InProccess)
}
public async readyToConnect(payload: IReadyToConnectPayload): Promise<void> {
const readyConnect = this.callsActionsFactory.readyToConnect()
const startAt = new Date().toISOString()
await this.callsRepository.update(payload.callId, {
status: CallStatus.InProccess,
startAt: startAt,
})
call.usersIds.map(id => {
this.realTimeService.emitToUser(id, 'call/start-timmer', {
callId: call.id,
startAt: startAt,
})
})
return call
await readyConnect.set(payload)
}
public async cancelCall(payload: ICancelCallPayload): Promise<void> {
const call = await this.getCall(payload.callId)
if (call.status !== CallStatus.New) throw new Error('Call not exist')
if (call.initiatorUserId !== payload.userId)
throw new Error('User doesnt initiator of call')
public async finish(payload: IFinishCallPayload): Promise<void> {
const finishCall = this.callsActionsFactory.finishCall()
await this.sendCallCanceledEvent(call, payload.userId)
await this.callsRepository.update(payload.callId, { status: CallStatus.Canceled })
await finishCall.finish(payload)
}
private sendCallCanceledEvent(call: CallDto, userId: number) {
const targetUsers = call.usersIds.filter(it => it !== userId)
public async sendRTCSessionDescription(payload: SendRTCSessionPayload) {
const sendRTCSession = this.callsActionsFactory.sendRTCSession()
targetUsers.map(userId => {
this.notificationsService.sendVoipNotification(
userId,
{
title: 'Виклик відмінений',
data: {
uuid: call.id,
type: 'cancelled',
},
},
{
toIos: false,
},
)
this.realTimeService.emitToUser(userId, 'call/canceled', {
callId: call.id,
finishedAt: new Date().toISOString(),
})
})
}
public async finishCall(payload: IFinishCallPayload): Promise<void> {
const call = await this.getCall(payload.callId)
await this.callsRepository.update(payload.callId, {
status: CallStatus.Finished,
finishedAt: new Date().toISOString(),
})
this.sendEventAboutCallEnd(call)
await sendRTCSession.send(payload)
}
public async rejectCall(callId: string): Promise<void> {
const call = await this.getCall(callId)
public async sendIceCandidates(payload: SendIceCandidatesPayload) {
const sendIceCandidate = this.callsActionsFactory.sendIceCandidates()
if (call.statusDto.canUpdateStatus(CallStatus.Rejected)) {
await this.callsRepository.update(callId, { status: CallStatus.Rejected })
// this.realTimeService.emitToUser(call.initiatorUserId, 'calls/rejected')
this.sendEventAboutCallEnd(call)
}
await sendIceCandidate.send(payload)
}
public async negotiation(payload: INegotiationPayload) {
const call = await this.callsRepository.findOne(payload.callId)
const targetUsers = call.usersIds.filter(it => it !== payload.userId)
public async onConnected(payload: IOnConnectedPayload) {
const onConnected = this.callsActionsFactory.onConnected()
targetUsers.map(id => {
this.realTimeService.emitToUser(id, 'call/negotiation', {
callId: call.id,
type: payload.type,
rtcMessage: payload.description,
})
})
await onConnected.handle(payload)
}
private sendEventToUser(
userId: number,
type: string,
execludeDeviceUuid: string,
data: any = {},
) {
try {
this.notificationsService.sendSlientNotification(
userId,
{
data: {
type,
...data,
},
},
{
execludeDeviceUuids: [execludeDeviceUuid],
},
)
} catch (e) {}
try {
this.realTimeService.emitToUser(userId, 'calls/answered-by-another-device', {
deviceUuid: execludeDeviceUuid,
})
} catch (e) {}
}
public async updateMediaSettings(payload: UpdateMediaSettingsPayload) {
const action = this.callsActionsFactory.updateMediaSettings()
private sendEventAboutCallEnd(call: CallDto) {
call.usersIds.map(userId => {
console.log('SENT END CALL', userId)
this.realTimeService.emitToUser(userId, 'call/end')
})
await action.update(payload)
}
public async deleteCall(callId: string, forUserId: number): Promise<void> {
public async deleteHistoryItem(userId: number, callId: string) {
const call = await this.callsRepository.findOne(callId)
if (!call) return
if (!call) return null
if (call.hideForUsersIds.includes(forUserId)) return
call.hideForUsers.push(userId)
call.hideForUsersIds.push(forUserId)
if (call.hideForUsers.length === 2) {
await this.callsRepository.delete(callId)
} else {
await this.callsRepository.save(call)
}
await this.callsRepository.save(call)
return
}
}

2
src/domain/calls/typing/consts/index.ts

@ -1,2 +0,0 @@ @@ -1,2 +0,0 @@
export const CALLS_REPOSITORY = Symbol('CALLS_REPOSITORY')
export const CALLS_SERVICE = Symbol('CALLS_SERVICE')

7
src/domain/calls/typing/enums/call-status.enum.ts

@ -1,7 +0,0 @@ @@ -1,7 +0,0 @@
export enum CallStatus {
New = 'n',
InProccess = 'i',
Canceled = 'c',
Finished = 'f',
Rejected = 'r',
}

1
src/domain/calls/typing/enums/index.ts

@ -1 +0,0 @@ @@ -1 +0,0 @@
export * from './call-status.enum'

3
src/domain/calls/v2/typing/index.ts → src/domain/calls/typing/index.ts

@ -32,6 +32,8 @@ export interface ICallEntity { @@ -32,6 +32,8 @@ export interface ICallEntity {
members?: ICallMemberEntity[]
accessToken?: string
hideForUsers?: number[]
}
export enum CallMemberStatus {
@ -56,6 +58,7 @@ export interface ICallMemberEntity { @@ -56,6 +58,7 @@ export interface ICallMemberEntity {
callId: string
status: CallMemberStatus
user?: Users.UserModel
call?: ICallEntity
micronOn?: boolean
speakerOn?: boolean

4
src/domain/calls/typing/interfaces/call-repository.interface.ts

@ -1,4 +0,0 @@ @@ -1,4 +0,0 @@
import { Repository } from 'typeorm'
import { ICall } from './call.interface'
export type ICallsRepository = Repository<ICall>

19
src/domain/calls/typing/interfaces/call.interface.ts

@ -1,19 +0,0 @@ @@ -1,19 +0,0 @@
import { Users } from 'src/core'
import { CallStatus } from '../enums'
export interface ICall {
id: string
usersIds: number[]
hideForUsersIds: number[]
initiatorUserId: number
finishedAt: string
startAt: string
title: string
status: CallStatus
createdAt: string
updatedAt: string
users?: Users.UserModel[]
}

41
src/domain/calls/typing/interfaces/calls-service.interface.ts

@ -1,41 +0,0 @@ @@ -1,41 +0,0 @@
import { ICall } from './call.interface'
export interface ICallsService {
startCall(payload: IStartCallPayload): Promise<ICall>
answerCall(payload: IAnswerCallPayload): Promise<ICall>
cancelCall(payload: ICancelCallPayload): Promise<void>
finishCall(payload: IFinishCallPayload): Promise<void>
deleteCall(callId: string, forUserId: number): Promise<void>
getIncomeCall(callId: string, fromUserId: number, targetUserDeviceId: string): Promise<void>
negotiation(payload: INegotiationPayload): Promise<void>
rejectCall(callId: string): Promise<void>
}
export interface IStartCallPayload {
usersIds: number[]
initiatorUserId: number
rtcMessage: any
title: string
}
export interface IAnswerCallPayload {
callId: string
userId: number
rtcMessage: any
}
export interface ICancelCallPayload {
userId: number
callId: string
}
export interface IFinishCallPayload {
userId: number
callId: string
}
export interface INegotiationPayload {
type: 'answer' | 'offer'
description: any
callId: string
userId: number
}

3
src/domain/calls/typing/interfaces/index.ts

@ -1,3 +0,0 @@ @@ -1,3 +0,0 @@
export * from './call-repository.interface'
export * from './call.interface'
export * from './calls-service.interface'

36
src/domain/calls/v2/callsv2.module.ts

@ -1,36 +0,0 @@ @@ -1,36 +0,0 @@
import { DynamicModule, Module } from '@nestjs/common'
import { UsersModule } from 'src/domain/users/users.module'
import { JwtModule, provideEntity } from 'src/libs'
import { CALLS_LOGS_REPOSITORY, CALLS_MEMBERS_REPOSITORY, CALLS_REPOSITORY } from './typing'
import { Call, CallLog, CallMember } from './entities'
import { CallsActionsFactory } from './services/calls-actions-factory'
import { RealTimeModule } from 'src/domain/real-time/real-time.module'
import { NotificationsModule } from 'src/domain/notifications/notifications.module'
import { CallsService } from './services/calls.service'
import { V2CallsController } from './controllers/calls.controller'
import { SessionsModule } from 'src/domain/sessions/sessions.module'
@Module({})
export class Callsv2Module {
static forRoot(): DynamicModule {
return {
module: Callsv2Module,
imports: [
SessionsModule.forFeature(),
JwtModule.forFeature(),
UsersModule.forFeature(),
RealTimeModule.forFeature(),
NotificationsModule.forFeature(),
],
providers: [
provideEntity(CALLS_REPOSITORY, Call),
provideEntity(CALLS_MEMBERS_REPOSITORY, CallMember),
provideEntity(CALLS_LOGS_REPOSITORY, CallLog),
CallsActionsFactory,
CallsService,
],
controllers: [V2CallsController],
}
}
}

43
src/domain/calls/v2/entities/call.entity.ts

@ -1,43 +0,0 @@ @@ -1,43 +0,0 @@
import {
Column,
CreateDateColumn,
Entity,
OneToMany,
PrimaryColumn,
UpdateDateColumn,
} from 'typeorm'
import { CallFinishedReason, CallStatus, ICallEntity, ICallMemberEntity } from '../typing'
import { CallMember } from './call-member.entity'
@Entity('callsv2')
export class Call implements ICallEntity {
@PrimaryColumn()
id: string
@Column({ type: 'varchar', default: CallStatus.Calling })
status: CallStatus
@Column({ type: 'varchar', nullable: true })
finishedReason?: CallFinishedReason
@Column()
initiatorUserId: number
@Column({ nullable: true })
finishedByUserId?: number
@Column({ type: 'timestamp with time zone', nullable: true })
finishedAt: string
@Column({ type: 'timestamp with time zone', nullable: true })
startAt: string
@CreateDateColumn({ type: 'timestamp with time zone', default: () => 'LOCALTIMESTAMP' })
createdAt: string
@UpdateDateColumn({ type: 'timestamp with time zone', default: () => 'LOCALTIMESTAMP' })
updatedAt: string
@OneToMany(() => CallMember, member => member.call)
members?: ICallMemberEntity[]
}

7
src/domain/calls/v2/entities/index.ts

@ -1,7 +0,0 @@ @@ -1,7 +0,0 @@
import { CallLog } from './call-log.entity'
import { CallMember } from './call-member.entity'
import { Call } from './call.entity'
export const CALLS_ENTITIES_V2 = [Call, CallMember, CallLog]
export { Call, CallMember, CallLog }

8
src/domain/calls/v2/exceptions/index.ts

@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
export * from './access-call-wrong.exception'
export * from './call-alerady-finished.exception'
export * from './call-already-ready-to-connect.exception'
export * from './call-answered.exception'
export * from './call-not-found.exception'
export * from './initiator-in-call.exception'
export * from './target-user-in-call.exception'
export * from './wrong-device.exception'

92
src/domain/calls/v2/services/calls.service.ts

@ -1,92 +0,0 @@ @@ -1,92 +0,0 @@
import { Inject, Injectable } from '@nestjs/common'
import { CallsActionsFactory } from './calls-actions-factory'
import {
CALLS_REPOSITORY,
CallsRepository,
IAnswerCallPayload,
ICallEntity,
ICallsService,
IFinishCallPayload,
IOnConnectedPayload,
IReadyToConnectPayload,
IStartCallPayload,
SendIceCandidatesPayload,
SendRTCSessionPayload,
UpdateMediaSettingsPayload,
} from '../typing'
@Injectable()
export class CallsService implements ICallsService {
constructor(
@Inject(CALLS_REPOSITORY)
private readonly callsRepository: CallsRepository,
private readonly callsActionsFactory: CallsActionsFactory,
) {}
public async start(payload: IStartCallPayload): Promise<ICallEntity> {
const startCall = this.callsActionsFactory.startCall()
return await startCall.start(payload)
}
public async answer(payload: IAnswerCallPayload): Promise<void> {
const answerCall = this.callsActionsFactory.answerCall()
await answerCall.answer(payload)
}
public async readyToConnect(payload: IReadyToConnectPayload): Promise<void> {
const readyConnect = this.callsActionsFactory.readyToConnect()
await readyConnect.set(payload)
}
public async finish(payload: IFinishCallPayload): Promise<void> {
const finishCall = this.callsActionsFactory.finishCall()
await finishCall.finish(payload)
}
public async sendRTCSessionDescription(payload: SendRTCSessionPayload) {
const sendRTCSession = this.callsActionsFactory.sendRTCSession()
await sendRTCSession.send(payload)
}
public async sendIceCandidates(payload: SendIceCandidatesPayload) {
const sendIceCandidate = this.callsActionsFactory.sendIceCandidates()
await sendIceCandidate.send(payload)
}
public async onConnected(payload: IOnConnectedPayload) {
const onConnected = this.callsActionsFactory.onConnected()
await onConnected.handle(payload)
}
public async updateMediaSettings(payload: UpdateMediaSettingsPayload) {
const action = this.callsActionsFactory.updateMediaSettings()
await action.update(payload)
}
public async getCall(callId: string, userId: number) {
const call = await this.callsRepository.findOne({
where: { id: callId },
relations: ['members'],
})
if (!call) {
return null
}
const isMember = call?.members?.find(it => it.userId === userId)
if (!isMember) {
return null
}
return call
}
}

13
src/domain/real-time/gateways/main.gateway.ts

@ -40,22 +40,15 @@ export class MainGateway { @@ -40,22 +40,15 @@ export class MainGateway {
await this.wsUsersService.joinUser(client, data)
}
@UseGuards(JwtWsGuard)
@SubscribeMessage('iceCandidate')
async iceCandidate(@MessageBody() data) {
// console.log('ICE CANDIDATE GATEWAY', data)
if (!data.iceCandidates || !data.iceCandidates.length) return
data.userId = data.user?.id
this.eventsEmitter.emit(Events.OnNewIceCandidate, data)
}
/***** Logger *****/
handleConnection(@ConnectedSocket() client: Socket) {
const accessToken = client.handshake.auth.accessToken
const deviceUuid = client.handshake.auth.deviceUuid
if (accessToken) {
const user = this.jwtCoreService.decodeToken(accessToken)
this.wsUsersService.joinUser(client, { user })
this.wsUsersService.joinUser(client, { user, deviceUuid })
}
this.logger.log('Connect new user to server')
}

1
src/domain/real-time/guards/jwt.guard.ts

@ -13,6 +13,7 @@ export class JwtWsGuard implements CanActivate { @@ -13,6 +13,7 @@ export class JwtWsGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
try {
const data = context.switchToWs().getData()
console.log('data', data)
const token = this.extractToken(data)

13
src/domain/real-time/services/ws-users.service.ts

@ -18,7 +18,7 @@ export class WsUsersService { @@ -18,7 +18,7 @@ export class WsUsersService {
* @param {Socket} client - сокет-клієнт
* @param {{user: any}} data - додаткові дані з даними користувача
*/
public joinUser(client: Socket, data: { user: any }) {
public joinUser(client: Socket, data: { user: any; deviceUuid?: string }) {
try {
const { user } = data
@ -34,6 +34,13 @@ export class WsUsersService { @@ -34,6 +34,13 @@ export class WsUsersService {
client.join(this.getUserRoom(user.id))
if (data.deviceUuid) {
client.join(this.getUserDeviceRoom(user.id, data.deviceUuid))
console.log(
`Socket connected room ${this.getUserDeviceRoom(user.id, data.deviceUuid)}`,
)
}
client.addListener('disconnect', () => this.disconnectUser(data))
} catch (e) {
this.logger.warn('Error join user')
@ -64,4 +71,8 @@ export class WsUsersService { @@ -64,4 +71,8 @@ export class WsUsersService {
private getUserRoom(userId: number) {
return `user-${userId}`
}
private getUserDeviceRoom(userId: number, deviceUuid: string) {
return `user-${userId}-${deviceUuid}`
}
}

4
src/domain/real-time/services/ws.service.ts

@ -25,4 +25,8 @@ export class WsService implements WebSockets.Service { @@ -25,4 +25,8 @@ export class WsService implements WebSockets.Service {
public emitToUser(userId: number, key: string, data?: any) {
this.wsServerService.emitToRoom(`user-${userId}`, key, data)
}
public emitToDevice(userId: number, deviceUuid: string, key: string, data?: any) {
this.wsServerService.emitToRoom(`user-${userId}-${deviceUuid}`, key, data)
}
}

10
src/domain/sessions/guards/ip.guard.ts

@ -14,24 +14,24 @@ export class IpGuard implements CanActivate { @@ -14,24 +14,24 @@ export class IpGuard implements CanActivate {
const ip = getIPFromRequest(req)
console.log('IP GUARD start', ip)
// console.log('IP GUARD start', ip)
// @ts-ignore
const existsInWhiteList = await global.checkIfIpInWhiteList(ip)
console.log('IP GUARD existsInWhiteList', existsInWhiteList)
// console.log('IP GUARD existsInWhiteList', existsInWhiteList)
if (existsInWhiteList) return true
// @ts-ignore
const existsInBlackList = await global.checkIfIpBlocked(ip)
console.log('IP GUARD existsInBlackList', existsInBlackList)
// console.log('IP GUARD existsInBlackList', existsInBlackList)
if (existsInBlackList) return false
const urlInWhiteList = this.checkUrlIsInWhiteList(req.url)
console.log('IP GUARD urlInWhiteList', urlInWhiteList)
// console.log('IP GUARD urlInWhiteList', urlInWhiteList)
if (urlInWhiteList) return true
const isBlockByFingreprint = await this.checkFingerprint(req, ip)
console.log('IP GUARD isBlockByFingreprint', isBlockByFingreprint)
// console.log('IP GUARD isBlockByFingreprint', isBlockByFingreprint)
if (isBlockByFingreprint) return false
return true

1
src/main.ts

@ -10,7 +10,6 @@ import { AppModule } from './app.module' @@ -10,7 +10,6 @@ import { AppModule } from './app.module'
import { IpGuard } from './domain/sessions/guards'
import { LoggerService } from './domain/logger'
import { DomainExceptionsFilter } from './shared/filters'
import { RedisIoAdapter } from './domain/real-time'
process.setMaxListeners(0)

49
src/rest/common/calls/calls-test.controller.ts

@ -1,49 +0,0 @@ @@ -1,49 +0,0 @@
import { Body, Controller, Inject, Post } from '@nestjs/common'
import { Notifications } from 'src/core'
import { NOTIFICATIONS_SERVICE } from 'src/core/consts'
import { DtoProperty } from 'src/shared'
class CallDto {
@DtoProperty()
userId: number
@DtoProperty()
callerId: number
}
@Controller('test/calls')
export class CallsTestController {
constructor(
@Inject(NOTIFICATIONS_SERVICE)
private readonly notificationsService: Notifications.INotificationsService,
) {}
@Post('call')
async call(@Body() dto: CallDto) {
this.notificationsService.sendVoipNotification(dto.userId, {
title: 'Новий виклик',
content: '',
data: {
uuid: '308da3fb-c398-48d1-a3f9-673b4201fb32',
handle: '380980717970',
callerName: 'First Last Middle',
callerId: String(dto.callerId),
type: 'income',
avatarUrl: 'https://picsum.photos/seed/picsum/200/300',
},
})
}
@Post('reject')
async reject(@Body() dto: CallDto) {
this.notificationsService.sendVoipNotification(dto.userId, {
title: 'Завершення виклик',
content: '',
data: {
uuid: '308da3fb-c398-48d1-a3f9-673b4201fb32',
callerId: String(dto.callerId),
type: 'reject',
},
})
}
}

113
src/rest/common/calls/calls.controller.ts

@ -1,113 +0,0 @@ @@ -1,113 +0,0 @@
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common'
import { ApiOperation, ApiTags } from '@nestjs/swagger'
import { RestCallsService } from './calls.service'
import { StartCallPayloadDto } from './dto/start-call.dto'
import { AuthGuard } from 'src/domain/sessions/decorators'
import { ApiImplictPagination, ReqPagination, ReqUser } from 'src/shared'
import { AnswerCallPayloadDto } from './dto/answer-call.dto'
import { IceCandidatePayloadDto } from './dto/ice-candidate.dto'
import { IPagination } from 'src/core/interfaces'
import { CancelCallPayloadDto, FinishCallDto, GetIncomeDataPayloadDto, RejectCallDto } from './dto'
import { NegotiationPayloadDto } from './dto/negotiation.dto'
@ApiTags('COMMONG | Calls')
@Controller('calls')
export class RestCallsController {
constructor(private readonly restCallsService: RestCallsService) {}
@ApiOperation({})
@AuthGuard()
@Post('/start')
startCall(@ReqUser() userId: number, @Body() dto: StartCallPayloadDto) {
return this.restCallsService.startCall(userId, dto)
}
@ApiOperation({})
@AuthGuard()
@Post('/income/:callId')
getIncomeData(
@ReqUser() userId: number,
@Param('callId') callId: string,
@Body() dto: GetIncomeDataPayloadDto,
) {
return this.restCallsService.getIncomeData(userId, callId, dto)
}
@ApiOperation({})
@AuthGuard()
@Post('/negotiation/:callId')
negotiation(
@ReqUser() userId: number,
@Param('callId') callId: string,
@Body() dto: NegotiationPayloadDto,
) {
return this.restCallsService.negotiation(userId, callId, dto)
}
@AuthGuard()
@Get('negotiation-possible/:callId')
public async isNegotiationPossible(@ReqUser() userId: number, @Param('callId') callId: string) {
return this.restCallsService.isNegotiationPossible(userId, callId)
}
@ApiOperation({})
@AuthGuard()
@Post('/answer')
answerCall(@ReqUser() userId: number, @Body() dto: AnswerCallPayloadDto) {
return this.restCallsService.answerCall(userId, dto)
}
@ApiOperation({})
@AuthGuard()
@Post('/cancel')
cancelCall(@ReqUser() userId: number, @Body() dto: CancelCallPayloadDto) {
return this.restCallsService.cancelCall(userId, dto)
}
@ApiOperation({})
@AuthGuard()
@Post('/reject')
rejectCall(@ReqUser() userId: number, @Body() dto: CancelCallPayloadDto) {
return this.restCallsService.reject(userId, dto.callId)
}
@ApiOperation({})
@Post('/reject-by-token')
rejectCallByToken(@Body() dto: RejectCallDto) {
return this.restCallsService.rejectByToken(dto)
}
@ApiOperation({})
@AuthGuard()
@Post('/finish')
finishCall(@ReqUser() userId: number, @Body() dto: FinishCallDto) {
return this.restCallsService.finishCall(userId, dto)
}
@ApiOperation({})
@AuthGuard()
@Post('/iceCandidate')
iceCandidate(@ReqUser() userId: number, @Body() dto: IceCandidatePayloadDto) {
return this.restCallsService.iceCandidate(userId, dto)
}
@AuthGuard()
@Get()
public async getList(@ReqPagination() pagination: IPagination, @ReqUser() userId: number) {
return this.restCallsService.getCalls(userId, pagination)
}
@ApiImplictPagination()
@AuthGuard()
@Get(':callId')
public async getCall(@Param('callId') callId: string) {
return this.restCallsService.getCall(callId)
}
@ApiOperation({})
@AuthGuard()
@Delete('/history/:callId')
deleteCallRecord(@Param('callId') callId: string, @ReqUser() userId: number) {
return this.restCallsService.deleteCall(userId, callId)
}
}

29
src/rest/common/calls/calls.module.ts

@ -1,29 +0,0 @@ @@ -1,29 +0,0 @@
import { DynamicModule, Module } from '@nestjs/common'
import { RestCallsService } from './calls.service'
import { CallsModule } from 'src/domain/calls/calls.module'
import { RestCallsController } from './calls.controller'
import { JwtModule, RedisModule } from 'src/libs'
import { NotificationsModule, RealTimeModule, UsersModule } from 'src/domain'
import { CallsTestController } from './calls-test.controller'
import { SecretModModule } from 'src/domain/secret-mod/secret-mod.module'
@Module({})
export class RestCallsModule {
static forRoot(): DynamicModule {
return {
module: RestCallsModule,
providers: [RestCallsService],
imports: [
CallsModule.forFeature(),
JwtModule.forFeature(),
RealTimeModule.forFeature(),
UsersModule.forFeature(),
RedisModule.forFeature(),
NotificationsModule.forFeature(),
SecretModModule.forFeature(),
],
controllers: [RestCallsController, CallsTestController],
}
}
}

202
src/rest/common/calls/calls.service.ts

@ -1,202 +0,0 @@ @@ -1,202 +0,0 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common'
import { CALLS_REPOSITORY, CALLS_SERVICE } from 'src/domain/calls/typing/consts'
import { ICallsRepository, ICallsService } from 'src/domain/calls/typing/interfaces'
import { StartCallPayloadDto } from './dto/start-call.dto'
import { AnswerCallPayloadDto } from './dto/answer-call.dto'
import { IceCandidatePayloadDto } from './dto/ice-candidate.dto'
import { REAL_TIME_SERVICE } from 'src/core/consts'
import { WebSockets } from 'src/core'
import { IUsersRepository } from 'src/domain/users/interfaces'
import { USERS_REPOSITORY } from 'src/domain/users/consts'
import { UserFullName } from 'src/domain/users/classes'
import { transformFileUrl } from 'src/shared/transforms'
import { CancelCallPayloadDto, FinishCallDto, GetIncomeDataPayloadDto, RejectCallDto } from './dto'
import { IPagination } from 'src/core/interfaces'
import { paginateAndGetMany } from 'src/shared'
import { NegotiationPayloadDto } from './dto/negotiation.dto'
import { RedisService } from 'src/libs'
import { SecretMod } from 'src/domain/secret-mod/typing'
import { SECRET_MOD_SERVICE } from 'src/domain/secret-mod/consts'
import * as _ from 'lodash'
@Injectable()
export class RestCallsService {
constructor(
@Inject(CALLS_SERVICE)
private callsService: ICallsService,
@Inject(CALLS_REPOSITORY)
private readonly callsRepository: ICallsRepository,
@Inject(REAL_TIME_SERVICE)
private readonly realTimeService: WebSockets.Service,
@Inject(USERS_REPOSITORY)
private readonly usersRepository: IUsersRepository,
@Inject(SECRET_MOD_SERVICE)
private readonly secretModService: SecretMod.Service,
private readonly redisService: RedisService,
) {}
public async startCall(userId: number, payload: StartCallPayloadDto) {
const targetUser = await this.usersRepository.findOne({
where: {
id: payload.targetUserId,
},
relations: ['info'],
})
const call = await this.callsService.startCall({
rtcMessage: payload.rtcMessage,
initiatorUserId: userId,
usersIds: [payload.targetUserId, userId],
title: UserFullName.getFromUser(targetUser),
})
const targetFrom = await this.usersRepository.findOne({
where: {
id: payload.targetUserId,
},
relations: ['info'],
})
return {
from: {
type: 'personal',
title: UserFullName.getFromUser(targetFrom),
avatarImageUrl: transformFileUrl(targetFrom.info.avatarUrl),
},
call,
}
}
public async answerCall(userId: number, payload: AnswerCallPayloadDto) {
console.log('ANSWER CALL', userId)
await this.callsService.answerCall({
userId,
callId: payload.callId,
rtcMessage: payload.rtcMessage,
})
}
public async iceCandidate(userId: number, dto: IceCandidatePayloadDto) {
console.log('ICE-CANDIDATE', userId)
this.realTimeService.emitToUser(dto.targetUserId, 'call/ICEcandidate', {
candidates: dto.candidates,
})
}
public async cancelCall(userId: number, dto: CancelCallPayloadDto) {
try {
await this.callsService.cancelCall({
userId,
callId: dto.callId,
})
} catch (e) {
console.log(e)
}
}
public async finishCall(userId: number, dto: FinishCallDto) {
await this.callsService.finishCall({
userId,
callId: dto.callId,
})
}
public async getIncomeData(userId: number, callId: string, dto: GetIncomeDataPayloadDto) {
await this.callsService.getIncomeCall(callId, userId, dto.targetDeviceId)
}
public async negotiation(userId: number, callId: string, dto: NegotiationPayloadDto) {
console.log('NEGOTIATION', userId)
await this.callsService.negotiation({
userId,
callId,
type: dto.type,
description: dto.description,
})
}
public async isNegotiationPossible(userId: number, callId: string) {
const call = await this.callsRepository.findOne(callId)
const targetUsers = call.usersIds.filter(it => it !== userId)
let possible = false
targetUsers.map(it => {
const isOnline = this.realTimeService.isUserOnline(it)
if (isOnline) possible = true
})
return possible
}
public async reject(userId: number, callId: string) {
console.log('REJECT')
await this.callsService.rejectCall(callId)
}
public async rejectByToken(dto: RejectCallDto) {
console.log('REJECT BY TOKEN', dto)
const dataJSON = await this.redisService.get(dto.authToken)
if (!dataJSON) throw new BadRequestException('')
const data = JSON.parse(dataJSON)
if (data.callId !== dto.uuid) {
throw new BadRequestException()
}
await this.callsService.rejectCall(data.callId)
}
public async deleteCall(userId: number, callId: string) {
await this.callsService.deleteCall(callId, userId)
}
public async getCalls(userId: number, pagination: IPagination) {
const ignoreUserIds = await this.secretModService.getHiddenUsersIds()
const query = this.callsRepository
.createQueryBuilder('it')
.where(':userId = ANY(it.usersIds)', { userId })
.andWhere('NOT :userId = ANY(it.hideForUsersIds)', { userId })
.orderBy('it.createdAt', 'DESC')
if (!_.isEmpty(ignoreUserIds)) {
query.andWhere('it.usersIds && ARRAY[:...ignoreUserIds]::integer[] = FALSE', {
ignoreUserIds,
})
}
if (pagination.searchString) {
query.andWhere('it.title ILIKE :ss', { ss: `%${pagination.searchString}%` })
}
const { items, count } = await paginateAndGetMany(query, pagination)
for await (const [index, item] of items.entries()) {
const users = await this.usersRepository.findByIds(item.usersIds, {
relations: ['info'],
})
users.map((_, i, arr) => {
if (arr[i].info.avatarUrl)
arr[i].info.avatarUrl = transformFileUrl(arr[i].info.avatarUrl)
})
items[index].users = users
}
return {
items,
count,
}
}
public async getCall(callUuid: string) {
return this.callsRepository.findOne(callUuid)
}
}

9
src/rest/common/calls/dto/answer-call.dto.ts

@ -1,9 +0,0 @@ @@ -1,9 +0,0 @@
import { DtoProperty } from 'src/shared'
export class AnswerCallPayloadDto {
@DtoProperty()
callId: string
@DtoProperty()
rtcMessage: any
}

6
src/rest/common/calls/dto/cancel-call.dto.ts

@ -1,6 +0,0 @@ @@ -1,6 +0,0 @@
import { DtoProperty } from 'src/shared'
export class CancelCallPayloadDto {
@DtoProperty()
callId: string
}

6
src/rest/common/calls/dto/finish-call.dto.ts

@ -1,6 +0,0 @@ @@ -1,6 +0,0 @@
import { DtoProperty } from 'src/shared'
export class FinishCallDto {
@DtoProperty()
callId: string
}

6
src/rest/common/calls/dto/get-income-data.dto.ts

@ -1,6 +0,0 @@ @@ -1,6 +0,0 @@
import { DtoProperty } from 'src/shared'
export class GetIncomeDataPayloadDto {
@DtoProperty()
targetDeviceId: string
}

9
src/rest/common/calls/dto/ice-candidate.dto.ts

@ -1,9 +0,0 @@ @@ -1,9 +0,0 @@
import { DtoProperty } from 'src/shared'
export class IceCandidatePayloadDto {
@DtoProperty()
targetUserId: number
@DtoProperty()
candidates: any[]
}

8
src/rest/common/calls/dto/index.ts

@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
export * from './answer-call.dto'
export * from './cancel-call.dto'
export * from './finish-call.dto'
export * from './get-income-data.dto'
export * from './ice-candidate.dto'
export * from './negotiation.dto'
export * from './reject-call.dto'
export * from './start-call.dto'

9
src/rest/common/calls/dto/negotiation.dto.ts

@ -1,9 +0,0 @@ @@ -1,9 +0,0 @@
import { DtoProperty } from 'src/shared'
export class NegotiationPayloadDto {
@DtoProperty()
type: 'answer' | 'offer'
@DtoProperty()
description: string
}

9
src/rest/common/calls/dto/reject-call.dto.ts

@ -1,9 +0,0 @@ @@ -1,9 +0,0 @@
import { DtoProperty } from 'src/shared'
export class RejectCallDto {
@DtoProperty()
authToken: string
@DtoProperty()
uuid: string
}

9
src/rest/common/calls/dto/start-call.dto.ts

@ -1,9 +0,0 @@ @@ -1,9 +0,0 @@
import { DtoProperty } from 'src/shared'
export class StartCallPayloadDto {
@DtoProperty()
targetUserId: number
@DtoProperty()
rtcMessage: any
}

2
src/rest/common/index.ts

@ -1,9 +1,7 @@ @@ -1,9 +1,7 @@
import { RestCallsModule } from './calls/calls.module'
import { CommonChatsModule } from './chats/chats.module'
import { CommonConfigsModule } from './configs/configs.module'
export const getRestCommonModules = () => [
CommonChatsModule.forRoot(),
CommonConfigsModule.forRoot(),
RestCallsModule.forRoot(),
]

Loading…
Cancel
Save