Vitalik
1 month ago
72 changed files with 267 additions and 1244 deletions
@ -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], |
||||
} |
||||
} |
||||
} |
||||
|
@ -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}` |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -1 +0,0 @@
@@ -1 +0,0 @@
|
||||
export * from './call.dto' |
@ -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 } |
||||
|
@ -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}` |
||||
} |
||||
} |
@ -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' |
||||
|
@ -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 |
||||
} |
||||
} |
@ -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, |
||||
} |
||||
} |
||||
} |
@ -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 |
||||
} |
||||
} |
||||
|
@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
|
||||
export const CALLS_REPOSITORY = Symbol('CALLS_REPOSITORY') |
||||
export const CALLS_SERVICE = Symbol('CALLS_SERVICE') |
@ -1,7 +0,0 @@
@@ -1,7 +0,0 @@
|
||||
export enum CallStatus { |
||||
New = 'n', |
||||
InProccess = 'i', |
||||
Canceled = 'c', |
||||
Finished = 'f', |
||||
Rejected = 'r', |
||||
} |
@ -1 +0,0 @@
@@ -1 +0,0 @@
|
||||
export * from './call-status.enum' |
@ -1,4 +0,0 @@
@@ -1,4 +0,0 @@
|
||||
import { Repository } from 'typeorm' |
||||
import { ICall } from './call.interface' |
||||
|
||||
export type ICallsRepository = Repository<ICall> |
@ -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[] |
||||
} |
@ -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 |
||||
} |
@ -1,3 +0,0 @@
@@ -1,3 +0,0 @@
|
||||
export * from './call-repository.interface' |
||||
export * from './call.interface' |
||||
export * from './calls-service.interface' |
@ -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], |
||||
} |
||||
} |
||||
} |
@ -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[] |
||||
} |
@ -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 } |
@ -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' |
@ -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 |
||||
} |
||||
} |
@ -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', |
||||
}, |
||||
}) |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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], |
||||
} |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -1,9 +0,0 @@
@@ -1,9 +0,0 @@
|
||||
import { DtoProperty } from 'src/shared' |
||||
|
||||
export class AnswerCallPayloadDto { |
||||
@DtoProperty() |
||||
callId: string |
||||
|
||||
@DtoProperty() |
||||
rtcMessage: any |
||||
} |
@ -1,6 +0,0 @@
@@ -1,6 +0,0 @@
|
||||
import { DtoProperty } from 'src/shared' |
||||
|
||||
export class CancelCallPayloadDto { |
||||
@DtoProperty() |
||||
callId: string |
||||
} |
@ -1,6 +0,0 @@
@@ -1,6 +0,0 @@
|
||||
import { DtoProperty } from 'src/shared' |
||||
|
||||
export class FinishCallDto { |
||||
@DtoProperty() |
||||
callId: string |
||||
} |
@ -1,6 +0,0 @@
@@ -1,6 +0,0 @@
|
||||
import { DtoProperty } from 'src/shared' |
||||
|
||||
export class GetIncomeDataPayloadDto { |
||||
@DtoProperty() |
||||
targetDeviceId: string |
||||
} |
@ -1,9 +0,0 @@
@@ -1,9 +0,0 @@
|
||||
import { DtoProperty } from 'src/shared' |
||||
|
||||
export class IceCandidatePayloadDto { |
||||
@DtoProperty() |
||||
targetUserId: number |
||||
|
||||
@DtoProperty() |
||||
candidates: any[] |
||||
} |
@ -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' |
@ -1,9 +0,0 @@
@@ -1,9 +0,0 @@
|
||||
import { DtoProperty } from 'src/shared' |
||||
|
||||
export class NegotiationPayloadDto { |
||||
@DtoProperty() |
||||
type: 'answer' | 'offer' |
||||
|
||||
@DtoProperty() |
||||
description: string |
||||
} |
@ -1,9 +0,0 @@
@@ -1,9 +0,0 @@
|
||||
import { DtoProperty } from 'src/shared' |
||||
|
||||
export class RejectCallDto { |
||||
@DtoProperty() |
||||
authToken: string |
||||
|
||||
@DtoProperty() |
||||
uuid: string |
||||
} |
@ -1,9 +0,0 @@
@@ -1,9 +0,0 @@
|
||||
import { DtoProperty } from 'src/shared' |
||||
|
||||
export class StartCallPayloadDto { |
||||
@DtoProperty() |
||||
targetUserId: number |
||||
|
||||
@DtoProperty() |
||||
rtcMessage: any |
||||
} |
@ -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…
Reference in new issue