Compare commits
120 Commits
Author | SHA1 | Date |
---|---|---|
Vitalik | af05c70a57 | 1 month ago |
Vitalik | 138f5805a1 | 1 month ago |
Vitalik | 7c058862ca | 2 months ago |
Vitalik | aba40fbe17 | 2 months ago |
Vitalik | dcdb0a0cbd | 2 months ago |
Vitalik | 3e3914fc78 | 2 months ago |
Vitalik | ced8a853a5 | 2 months ago |
Vitalik | e490da2f01 | 2 months ago |
Vitalik | e35e2737ad | 2 months ago |
Vitalik | 944dc81d34 | 2 months ago |
Vitalik | 09f374d539 | 2 months ago |
Vitalik | ccc7936906 | 2 months ago |
Vitalik | 281edaa954 | 2 months ago |
Vitalik | f40fb39435 | 3 months ago |
Vitalik | b4f7f2d3a5 | 3 months ago |
Vitalik | 6778a46c5f | 3 months ago |
Vitalik | 268dc0323e | 3 months ago |
Vitalik | f770fccf0c | 3 months ago |
Vitalik | 9339ca9b4f | 3 months ago |
Vitalik | 9d531d854c | 3 months ago |
Vitalik | 3dafeaae31 | 3 months ago |
Vitalik | d2853d78f9 | 3 months ago |
Vitalik | 658d0ea100 | 3 months ago |
Vitalik | 686a41164e | 3 months ago |
Vitalik | ac9e398fd9 | 3 months ago |
Vitalik | fe4132312d | 3 months ago |
Vitalik | bb5de0262a | 3 months ago |
Vitalik | 183234bb45 | 3 months ago |
Vitalik | 7d4324f128 | 3 months ago |
Vitalik | 85ac99e8f0 | 3 months ago |
Vitalik | d022c843df | 3 months ago |
Vitalik | d5e0ee9fa8 | 3 months ago |
Vitalik | 04b51aa61a | 3 months ago |
Vitalik | c3545c2771 | 3 months ago |
Vitalik | c4400cbcdb | 3 months ago |
Vitalik | 3fe40653ba | 3 months ago |
Vitalik | 1dd268d808 | 3 months ago |
Vitalik | f59a924ad0 | 4 months ago |
Vitalik | e5403b78ba | 4 months ago |
Vitalik | 7cdedf3de7 | 4 months ago |
Vitalik | 84ec35551e | 4 months ago |
Vitalik | aec27fb0ba | 4 months ago |
Vitalik | 6fec0ae2d1 | 4 months ago |
Vitalik | 51c1b6bd42 | 4 months ago |
Vitalik | 51b904e2f9 | 4 months ago |
Vitalik | 46cf94464d | 4 months ago |
Vitalik | 8a4a56a714 | 4 months ago |
Vitalik | e851b601f9 | 4 months ago |
Vitalik | a81a6ebc06 | 4 months ago |
Vitalik | c7c6489571 | 4 months ago |
Vitalik | 5e0a266de3 | 4 months ago |
Vitalik | 23abf6a2e3 | 4 months ago |
Vitalik | 77003dfa14 | 4 months ago |
Vitalik | c665ba71b9 | 4 months ago |
Vitalik | 46b1721bdf | 4 months ago |
Vitalik | 7c35fbf421 | 4 months ago |
Vitalik | 51fcf2cf0f | 4 months ago |
Vitalik | a310e108ed | 4 months ago |
Vitalik | 4621d3f631 | 4 months ago |
Vitalik | 7a2b0b48dd | 4 months ago |
Vitalik | fa343d66f1 | 4 months ago |
Vitalik | 81c574bd5e | 4 months ago |
Vitalik | 77e8c7a680 | 5 months ago |
Vitalik | 3a81b2caf5 | 5 months ago |
Vitalik | dcc4c61fcf | 5 months ago |
Vitalik | bcf1813fbd | 6 months ago |
Vitalik | 1dd017c38f | 6 months ago |
Vitalik | 7392b12fc3 | 6 months ago |
Yevhen Romanenko | 5446f10680 | 6 months ago |
Vitalik Yatsenko | fab8c4547d | 7 months ago |
Vitalik | 5b2e0ed921 | 7 months ago |
Yevhen Romanenko | 96c6ac79fe | 7 months ago |
Vitalik | 808a279932 | 7 months ago |
Oksana Stepanenko | 1134f4de6e | 7 months ago |
Vitalik | 135a3ddea5 | 7 months ago |
Vitalik | f1f44e0815 | 7 months ago |
Vitalik | fcfbabccd0 | 7 months ago |
Vitalik | 19ac028e9b | 8 months ago |
Vitalik | d3e14a2381 | 8 months ago |
Vitalik | 894a6093c9 | 8 months ago |
Vitalik | e1d49b343b | 8 months ago |
Vitalik | 981151b0c3 | 8 months ago |
Yevhen Romanenko | 1b8d07921d | 8 months ago |
Vitalik | 015f79250b | 8 months ago |
Vitalik | b26879f82e | 8 months ago |
Oksana Stepanenko | 329a6b2fa7 | 8 months ago |
Vitalik | 14500bf678 | 8 months ago |
Vitalik | 87808ff03a | 8 months ago |
Vitalik | a6f932a492 | 8 months ago |
Vitalik | 7e7baf6afb | 8 months ago |
Vitalik | aa60f8e468 | 8 months ago |
Vitalik | 93c6d9a70a | 8 months ago |
Vitalik | 03abeda566 | 8 months ago |
Vitalik | ee8136382c | 8 months ago |
Vitalik | f906d87a3f | 8 months ago |
Vitalik | 24168af92a | 8 months ago |
Vitalik | 4860665b7b | 9 months ago |
Vitalik | 4f13623ea7 | 9 months ago |
Vitalik | 0b1dc3c4e5 | 9 months ago |
Yevhen Romanenko | 0d85bd289a | 9 months ago |
Vitalik | c9ac276d4f | 9 months ago |
Vitalik | 32b48344d6 | 9 months ago |
Vitalik | 3e86af8b89 | 9 months ago |
Vitalik | aace04211c | 9 months ago |
Vitalik | f7b8e6165c | 9 months ago |
Vitalik | 5225a3625b | 9 months ago |
Vitalik | b747de0376 | 9 months ago |
Vitalik | ddbc591ff4 | 9 months ago |
Vitalik | f158bcc58f | 9 months ago |
Vitalik | cf6d9997c1 | 9 months ago |
Oksana Stepanenko | 835ea8c0eb | 9 months ago |
Vitalik | bceac154bf | 9 months ago |
Oksana Stepanenko | 217ce8ebcc | 9 months ago |
YaroslavBerkuta | 9fb407db95 | 9 months ago |
Vitalik | cc6413929a | 9 months ago |
Vitalik | c880ed2713 | 9 months ago |
Vitalik Yatsenko | ea3544f2fa | 9 months ago |
Vitalik | 752d330bf7 | 9 months ago |
Vitalik | 96eecc4263 | 9 months ago |
Vitalik | 0e08a9b801 | 9 months ago |
304 changed files with 16209 additions and 18829 deletions
@ -1,51 +1,112 @@
@@ -1,51 +1,112 @@
|
||||
APP_VERSION_IOS=2.2 |
||||
APP_VERSION_ANDROID=2.2 |
||||
|
||||
AUTO_SEED_ENABLED=true |
||||
TEST_ENV=test |
||||
SYSTEMS_USERS_PHONES=380980717970 |
||||
|
||||
DATABASE_DB=taskme |
||||
DATABASE_USER=taskme |
||||
DATABASE_PASS=taskme |
||||
DATABASE_HOST=taskme-postgres |
||||
DATABASE_PORT=5432 |
||||
DATABASE_HOST=localhost |
||||
DATABASE_PORT=5001 |
||||
|
||||
# OLD DATABASE |
||||
OLD_DATABASE_DB=false |
||||
AUTO_DUMP_OLD_DB_ENABLED=false |
||||
|
||||
# REDIS |
||||
REDIS_PASS=12345 |
||||
REDIS_HOST=taskme-redis |
||||
REDIS_PORT=6379 |
||||
REDIS_HOST=localhost |
||||
REDIS_PORT=5002 |
||||
|
||||
LOCAL_HASH_SALT=SAD130DL23DL23DL02 |
||||
CHAT_MESSAGES_CRYPT_SALT=912DK2M1D0912-LS,12IDJ32MUFDM2,1D0 |
||||
|
||||
# MINIO |
||||
MINIO_SECRET_KEY=K2FCDDS4JJ8DHUYa8ba7550cfff942b08 |
||||
MINIO_ACCESS_KEY=V42FCG42DK2FDDS4JJ8DHUYG |
||||
MINIO_HOST=taskme-minio |
||||
MINIO_PORT=9000 |
||||
MINIO_URL_PREFIX=http://localhost |
||||
MINIO_HOST=localhost |
||||
MINIO_PORT=5003 |
||||
MINIO_URL_PREFIX=http://taskme-fs.work-jetup.site |
||||
MINIO_FILES_URL_PREFIX=http://taskme-fs.work-jetup.site |
||||
MINIO_BUCKET=rws |
||||
MINIO_PRIVATE_BUCKET=rwsprivate |
||||
|
||||
# IMGPROXY |
||||
IMGPROXY_KEY=d37e5654e66c027dc66937aaa0faf7c76445d730d797f91edcaaddf4648fb971c1cf7cd5135d36ed92a12992eee85df1534b6f243029e31270bf86457a21c920 |
||||
IMGPROXY_SALT=e5516246fd072573ad7a9eb2339a18c417000cbc1ce5a13d3752673d77a746089997c423888cd29b50271c29b9f11b5fdb4917eb5ca53fe9f87b2ecd635bde17 |
||||
|
||||
IMGPROXY_BASE_URL=http://taskme-imgs.work-jetup.site |
||||
|
||||
# SMS |
||||
SMS_TEST_MODE=true |
||||
SMS_ALPHASMS_API_KEY=ea1503353d709f10b06c0962fa1f8e4e13614489 |
||||
SMS_ALPHASMS_FROM=Task me |
||||
|
||||
#JWT |
||||
JWT_KEY=ASD12JD12M0,9-D1S21 |
||||
JWT_PAYLOAD_KEY=1829JS12,W0-L12, |
||||
JWT_LIFE_TIME=10s |
||||
|
||||
AUTO_SEED_ENABLED=true |
||||
|
||||
# MAILER |
||||
MAILER_PROTOCOL=smtp |
||||
MAILER_TEST_MODE=true |
||||
MAILER_DOMAIN=mail.ubg.ua |
||||
MAILER_PORT= |
||||
MAILER_SECURE= |
||||
MAILER_LOGIN= |
||||
MAILER_PASSWORD= |
||||
|
||||
#JWT |
||||
JWT_KEY=ASD12JD12M0,9-D1S21 |
||||
JWT_PAYLOAD_KEY=1829JS12,W0-L12, |
||||
MAILER_PORT=465 |
||||
MAILER_SECURE=true |
||||
MAILER_LOGIN=taskme@rwsbank.com.ua |
||||
MAILER_PASSWORD=BsZH@Rgxtfz4pk |
||||
|
||||
# PUSH |
||||
PUSH_NOTIFICATIONS_SERVICE=one-signal |
||||
PUSH_APP_ID=ae1524a2-a4b5-4640-9f35-efa8f2cc2c78 |
||||
PUSH_REST_API_KEY=MjI2MWEyYzctYzFhZC00YjQzLWE2NzUtYjg1ZDQ3NDcwODVh |
||||
|
||||
PUSH_APP_ID=5e1a5e18-33e5-4ed3-8423-45b1abc354c6 |
||||
PUSH_REST_API_KEY=YjVmMjhhYzYtYmQzNy00MzMwLTk2ODEtNzFjYTIwN2NmYjQy |
||||
PUSH_NOTIFICATION_ANDROID_CALLS_CHANNEL_ID=ffc845a6-4a7e-410e-95e5-ac32bb404448 |
||||
|
||||
VOIP_PUSH_NOTIFICATIONS_APP_ID=42e07e75-a735-4351-af0b-912a47831a5a |
||||
VOIP_PUSH_NOTIFICATIONS_REST_API_KEY=YmUyZDM5OTktYTRlYy00Y2Y5LThhZTItNjg1MDNmOWJlZDdm |
||||
|
||||
|
||||
# IN PX |
||||
ALLOWED_AVATAR_WIDTH=200 |
||||
# IN PX |
||||
ALLOWED_AVATAR_HEIGHT=200 |
||||
# IN KB |
||||
ALLOWED_AVATAR_SIZE=500 |
||||
# ONLY IMAGE FILES TYPES DIVIDED BY COMMA |
||||
ALLOWED_AVATAR_TYPES='jpg,svg,png,webp' |
||||
# IN KB |
||||
ALLOWED_TASK_FILES_SIZE=100000 |
||||
# ANY FILES TYPES DIVIDED BY COMMA |
||||
ALLOWED_TASK_FILES_TYPES='jpeg,jpg,png,svg,webp,tiff,pdf,txt,doc,docx,xls,xlsx' |
||||
# IN KB |
||||
ALLOWED_CHAT_FILES_SIZE=50000 |
||||
# IN KB |
||||
ALLOWED_CHAT_VIDEOS_SIZE=100000 |
||||
|
||||
|
||||
# BLOCK IPS OPTIONS |
||||
MAX_ATTEMPS_BY_IP=50 |
||||
MAX_ATTEMPS_BY_FINGERPRINT=5 |
||||
MAX_ATTEMPS_BY_TIME=10 |
||||
MAX_GAP_FOR_TIME_ATTEMPS=3000 # in miliseconds |
||||
|
||||
|
||||
CRON_ENABLED=false |
||||
|
||||
|
||||
APNS_VOIP_CERT_PART=/var/www/certs/rws/certs/voip-base.p12 |
||||
APNS_VOIP_CERT_PASS=12345678 |
||||
IOS_BUNDLE_ID=com.app.taskme.voip |
||||
APNS_VOIP_IS_DEV=true |
||||
|
||||
MAX_TASKS_BATCH_SIZE=20 |
||||
PERCENT_OF_TASK_TOTAL_DURATION=20 |
||||
|
||||
FIREBASE_CONFIG_JFON_PARH=/var/www/certs/taskme-dae7e-firebase-adminsdk-du13i-f49f0a0624.json |
||||
|
||||
|
||||
SENTRY_DNS=https://23cf7a02dc8578b656e4ba2e8b6f397a@o402114.ingest.us.sentry.io/450722074669875223 |
||||
SENTRY_ENVIROMENT=local |
||||
|
||||
|
||||
SUPERADMIN_KEYS=1234567890 |
@ -1,9 +1,8 @@
@@ -1,9 +1,8 @@
|
||||
ssh root@185.69.154.136 " |
||||
cd /home/rws/api-rws && |
||||
cd /var/www/rws/api-rws && |
||||
git reset --hard && |
||||
git pull origin stage && |
||||
npm run build && |
||||
docker-compose -f docker-compose.stage.yml up -d && |
||||
docker restart api-rws_taskme-api_1 |
||||
pm2 restart all |
||||
|
||||
" |
@ -0,0 +1,58 @@
@@ -0,0 +1,58 @@
|
||||
version: '3' |
||||
|
||||
services: |
||||
taskme-postgres: |
||||
image: postgres:11 |
||||
restart: always |
||||
|
||||
ports: |
||||
- 3303:5432 |
||||
|
||||
environment: |
||||
POSTGRES_PASSWORD: ${DATABASE_PASS} |
||||
POSTGRES_USER: ${DATABASE_USER} |
||||
POSTGRES_DB: ${DATABASE_DB} |
||||
|
||||
taskme-redis: |
||||
image: 'redis:4-alpine' |
||||
command: redis-server --requirepass ${REDIS_PASS} |
||||
ports: |
||||
- '6379:6379' |
||||
|
||||
taskme-minio: |
||||
hostname: taskme-minio |
||||
image: minio/minio:RELEASE.2021-09-18T18-09-59Z |
||||
container_name: taskme-minio |
||||
|
||||
volumes: |
||||
- './taskme/data/:/data' |
||||
- './taskme/config:/root/.minio' |
||||
|
||||
ports: |
||||
- 5003:9000 |
||||
- 5004:9001 |
||||
environment: |
||||
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY} |
||||
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY} |
||||
command: server --console-address ":9001" /data |
||||
|
||||
taskme-createbuckets: |
||||
image: minio/mc |
||||
depends_on: |
||||
- taskme-minio |
||||
entrypoint: > |
||||
/bin/sh -c " |
||||
sleep 10; |
||||
/usr/bin/mc config host add data http://${MINIO_HOST}:${MINIO_PORT} ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}; |
||||
/usr/bin/mc mb data/${MINIO_BUCKET}; |
||||
/usr/bin/mc policy set public data/${MINIO_BUCKET}; |
||||
exit 0; |
||||
" |
||||
taskme-imgproxy: |
||||
image: 'darthsim/imgproxy:latest' |
||||
ports: |
||||
- '5005:8080' |
||||
environment: |
||||
IMGPROXY_KEY: ${IMGPROXY_KEY} |
||||
IMGPROXY_SALT: ${IMGPROXY_SALT} |
||||
IMGPROXY_MAX_SRC_FILE_SIZE: 10485760 |
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
version: '3' |
||||
|
||||
services: |
||||
taskme-redis: |
||||
image: redis:latest |
||||
restart: always |
||||
ports: |
||||
- '6379:6379' |
||||
volumes: |
||||
- /var/www/redis:/root/redis |
||||
- /var/www/redisredis.conf:/usr/local/etc/redis/redis.conf |
||||
environment: |
||||
- REDIS_PASSWORD=${REDIS_PASS} |
||||
- REDIS_PORT=6379 |
||||
- REDIS_DATABASES=16 |
||||
|
||||
taskme-minio: |
||||
hostname: taskme-minio |
||||
image: minio/minio:RELEASE.2021-09-18T18-09-59Z |
||||
container_name: taskme-minio |
||||
|
||||
volumes: |
||||
- './taskme/data/:/data' |
||||
- './taskme/config:/root/.minio' |
||||
ports: |
||||
- 9000:9000 |
||||
- 9001:9001 |
||||
environment: |
||||
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY} |
||||
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY} |
||||
command: server --console-address ":9001" /data |
||||
|
||||
taskme-imgproxy: |
||||
image: 'darthsim/imgproxy:latest' |
||||
ports: |
||||
- '5005:8080' |
||||
environment: |
||||
IMGPROXY_KEY: ${IMGPROXY_KEY} |
||||
IMGPROXY_SALT: ${IMGPROXY_SALT} |
||||
IMGPROXY_MAX_SRC_FILE_SIZE: 10485760 |
@ -1,34 +1,26 @@
@@ -1,34 +1,26 @@
|
||||
server { |
||||
|
||||
server_name tasks-fs.rwsbank.com.ua; |
||||
|
||||
server_name temp-tasks-fs.rwsbank.com.ua tasks-fs.rwsbank.com.ua; |
||||
location / { |
||||
proxy_pass "http://127.0.0.1:9000"; |
||||
} |
||||
|
||||
location =/taskme { |
||||
deny all; |
||||
} |
||||
|
||||
|
||||
|
||||
listen [::]:443 ssl ipv6only=on; |
||||
listen 443 ssl; |
||||
ssl_certificate /etc/ssl/rwsbank/ssl-bundle.crt; |
||||
ssl_certificate_key /etc/ssl/rwsbank/com.key; |
||||
} |
||||
|
||||
|
||||
|
||||
|
||||
server { |
||||
if ($host = tasks-fs.rwsbank.com.ua) { |
||||
return 301 https://$host$request_uri; |
||||
} |
||||
if ($host = temp-tasks-fs.rwsbank.com.ua) { |
||||
return 301 https://$host$request_uri; |
||||
} |
||||
listen 80; |
||||
listen [::]:80; |
||||
server_name tasks-fs.rwsbank.com.ua; |
||||
return 404; |
||||
|
||||
|
||||
server_name tasks-fs.rwsbank.com.ua temp-tasks-fs.rwsbank.com.ua; |
||||
return 404; |
||||
} |
@ -1,6 +1,7 @@
@@ -1,6 +1,7 @@
|
||||
import { ExceptionKeys } from '../enums' |
||||
|
||||
export interface DomainErrorParams { |
||||
key: ExceptionKeys |
||||
key: ExceptionKeys | string |
||||
description: string |
||||
metadata?: any |
||||
} |
||||
|
@ -1,7 +1,6 @@
@@ -1,7 +1,6 @@
|
||||
import { ActivitiesService } from './activities.service' |
||||
import { ActivityLogsService } from './activity-logs.service' |
||||
import { ActivitiesEventsHandlerService } from './activity-events.service' |
||||
|
||||
export const ACTIVITY_SERVICES = [ActivityLogsService, ActivitiesEventsHandlerService] |
||||
export const ACTIVITY_SERVICES = [ActivityLogsService] |
||||
|
||||
export { ActivitiesService } |
||||
|
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
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' |
||||
import { CallsReadService } from './services/calls-read.service' |
||||
|
||||
@Module({}) |
||||
export class CallsModule { |
||||
static forRoot(): DynamicModule { |
||||
return { |
||||
module: CallsModule, |
||||
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, |
||||
CallsReadService, |
||||
], |
||||
controllers: [V2CallsController], |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,162 @@
@@ -0,0 +1,162 @@
|
||||
import { Body, Controller, Delete, Get, Inject, Param, Post } from '@nestjs/common' |
||||
import { ApiTags } from '@nestjs/swagger' |
||||
import { CallsService } from '../services/calls.service' |
||||
|
||||
import { |
||||
CallFinishedReason, |
||||
CALLS_REPOSITORY, |
||||
CallsRepository, |
||||
IAnswerCallPayload, |
||||
IOnConnectedPayload, |
||||
IReadyToConnectPayload, |
||||
IStartCallPayload, |
||||
SendIceCandidatesPayload, |
||||
SendRTCSessionPayload, |
||||
UpdateMediaSettingsPayload, |
||||
} from '../typing' |
||||
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') |
||||
@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) { |
||||
payload.initiatorId = userId |
||||
return this.callsService.start(payload) |
||||
} |
||||
|
||||
@Post('answer') |
||||
@AuthGuard() |
||||
public answer(@Body() payload: IAnswerCallPayload, @ReqUser() userId: number) { |
||||
payload.userId = userId |
||||
return this.callsService.answer(payload) |
||||
} |
||||
|
||||
@Post('ready-to-connect') |
||||
@AuthGuard() |
||||
public readyToConnect(@Body() payload: IReadyToConnectPayload, @ReqUser() userId: number) { |
||||
payload.userId = userId |
||||
return this.callsService.readyToConnect(payload) |
||||
} |
||||
|
||||
@Post('reject') |
||||
@AuthGuard() |
||||
public reject(@Body() dto: RejectCallDto, @ReqUser() userId: number) { |
||||
return this.callsService.finish({ |
||||
callId: dto.callId, |
||||
userId, |
||||
reason: CallFinishedReason.Rejected, |
||||
}) |
||||
} |
||||
|
||||
@Post('cancel') |
||||
@AuthGuard() |
||||
public cancel(@Body() dto: RejectCallDto, @ReqUser() userId: number) { |
||||
return this.callsService.finish({ |
||||
callId: dto.callId, |
||||
userId, |
||||
reason: CallFinishedReason.Canceled, |
||||
}) |
||||
} |
||||
|
||||
// TODO
|
||||
@Post('reject-by-token') |
||||
public async rejectByToken(@Body() dto: RejectCallByTokenDto) { |
||||
const call = await this.callsRepository.findOne(dto.uuid) |
||||
if (call.accessToken !== dto.authToken) throw new AccessCallWrongException() |
||||
|
||||
return this.callsService.finish({ |
||||
callId: dto.uuid, |
||||
userId: 1, |
||||
reason: CallFinishedReason.Rejected, |
||||
}) |
||||
} |
||||
|
||||
@Post('end') |
||||
@AuthGuard() |
||||
public async end(@Body() dto: EndCallDto, @ReqUser() userId: number) { |
||||
console.log('END DTO', dto) |
||||
return this.callsService.finish({ |
||||
callId: dto.callId, |
||||
userId, |
||||
reason: CallFinishedReason.EndCall, |
||||
}) |
||||
} |
||||
|
||||
@Post('disconnected') |
||||
@AuthGuard() |
||||
public async disconnected(@Body() dto: EndCallDto, @ReqUser() userId: number) { |
||||
return this.callsService.finish({ |
||||
callId: dto.callId, |
||||
userId, |
||||
reason: CallFinishedReason.FailedConnect, |
||||
}) |
||||
} |
||||
|
||||
@Post('rtc-session-description') |
||||
@AuthGuard() |
||||
public sendRTCSessionDescription( |
||||
@Body() payload: SendRTCSessionPayload, |
||||
@ReqUser() userId: number, |
||||
) { |
||||
payload.userId = userId |
||||
return this.callsService.sendRTCSessionDescription(payload) |
||||
} |
||||
|
||||
@Post('rtc-ice-candidates') |
||||
@AuthGuard() |
||||
public sendIceCandidates(@Body() payload: SendIceCandidatesPayload, @ReqUser() userId: number) { |
||||
payload.userId = userId |
||||
return this.callsService.sendIceCandidates(payload) |
||||
} |
||||
|
||||
@Post('connected') |
||||
@AuthGuard() |
||||
public onConnected(@Body() payload: IOnConnectedPayload, @ReqUser() userId: number) { |
||||
payload.userId = userId |
||||
return this.callsService.onConnected(payload) |
||||
} |
||||
|
||||
@Post('media-settings') |
||||
@AuthGuard() |
||||
public updateMediaSettings( |
||||
@Body() payload: UpdateMediaSettingsPayload, |
||||
@ReqUser() userId: number, |
||||
) { |
||||
payload.userId = userId |
||||
return this.callsService.updateMediaSettings(payload) |
||||
} |
||||
|
||||
@Get(':callId') |
||||
@AuthGuard() |
||||
public getCall(@Param('callId') callId: string, @ReqUser() userId: number) { |
||||
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) |
||||
} |
||||
} |
@ -0,0 +1,72 @@
@@ -0,0 +1,72 @@
|
||||
import { CallAnsweredException } from '../exceptions' |
||||
import { CallSocketEvent, CallStatus, IAnswerCallPayload, ICallEntity } from '../typing' |
||||
import { CallAction } from './call-action' |
||||
|
||||
export class AnswerCall extends CallAction { |
||||
protected payload: IAnswerCallPayload |
||||
protected call: ICallEntity |
||||
|
||||
public async answer(payload: IAnswerCallPayload) { |
||||
this.payload = payload |
||||
this.call = await this.getCall(payload.callId) |
||||
|
||||
await this.validate() |
||||
await this.validateCall(this.call) |
||||
await this.updateMember() |
||||
await this.sendEvents() |
||||
await this.sendVoipEventsForAndroid() |
||||
|
||||
await this.info(this.call.id, 'Answered call', payload.userId) |
||||
} |
||||
|
||||
protected async validate() { |
||||
if (this.call.status !== CallStatus.Calling) { |
||||
throw new CallAnsweredException() |
||||
} |
||||
} |
||||
|
||||
protected async updateMember() { |
||||
await this.callsMembersRepository.update( |
||||
{ |
||||
callId: this.payload.callId, |
||||
userId: this.payload.userId, |
||||
}, |
||||
{ |
||||
deviceUuid: this.payload.deviceUuid, |
||||
}, |
||||
) |
||||
} |
||||
|
||||
protected async sendEvents() { |
||||
const members = await this.callsMembersRepository.find({ |
||||
where: { callId: this.call.id }, |
||||
select: ['userId', 'deviceUuid'], |
||||
}) |
||||
|
||||
this.emitToMembers(members, CallSocketEvent.Answered, { |
||||
call: this.call, |
||||
}) |
||||
} |
||||
|
||||
private sendVoipEventsForAndroid() { |
||||
const targetUsers = this.call.members |
||||
.filter(it => it.userId !== this.payload.userId) |
||||
.map(it => it.userId) |
||||
|
||||
targetUsers.map(userId => { |
||||
this.notificationsService.sendVoipNotification( |
||||
userId, |
||||
{ |
||||
title: 'Виклик прийнято на іншому девайсі', |
||||
data: { |
||||
uuid: this.call.id, |
||||
type: 'cancelled', |
||||
}, |
||||
}, |
||||
{ |
||||
toIos: false, |
||||
}, |
||||
) |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,103 @@
@@ -0,0 +1,103 @@
|
||||
import { IUsersRepository } from 'src/domain/users/interfaces' |
||||
import { |
||||
CallsLogsRepository, |
||||
CallsMembersRepository, |
||||
CallsRepository, |
||||
CallStatus, |
||||
ICallEntity, |
||||
ICallMemberEntity, |
||||
} from '../typing' |
||||
import { Notifications, WebSockets } from 'src/core' |
||||
import { CallNotFoundException } from '../exceptions/call-not-found.exception' |
||||
import { CallAlreadyFinishedException } from '../exceptions' |
||||
|
||||
export class CallAction { |
||||
protected callsRepository: CallsRepository |
||||
protected callsLogsRepository: CallsLogsRepository |
||||
protected callsMembersRepository: CallsMembersRepository |
||||
protected usersRepository: IUsersRepository |
||||
protected notificationsService: Notifications.INotificationsService |
||||
protected realTimeService: WebSockets.Service |
||||
|
||||
public setRepositories( |
||||
callsRepository: CallsRepository, |
||||
callsLogsRepository: CallsLogsRepository, |
||||
callsMembersRepository: CallsMembersRepository, |
||||
) { |
||||
this.callsLogsRepository = callsLogsRepository |
||||
this.callsRepository = callsRepository |
||||
this.callsMembersRepository = callsMembersRepository |
||||
return this |
||||
} |
||||
|
||||
public setUsersRepositories(usersRepository: IUsersRepository) { |
||||
this.usersRepository = usersRepository |
||||
return this |
||||
} |
||||
|
||||
public setNotificationsService(notificationsService: Notifications.INotificationsService) { |
||||
this.notificationsService = notificationsService |
||||
} |
||||
|
||||
public setRealTimeService(realTimeService: WebSockets.Service) { |
||||
this.realTimeService = realTimeService |
||||
} |
||||
|
||||
protected async getCall(callId: string) { |
||||
const call = await this.callsRepository.findOne({ |
||||
where: { id: callId }, |
||||
relations: ['members'], |
||||
}) |
||||
return call |
||||
} |
||||
|
||||
protected validateCall(call: ICallEntity) { |
||||
if (!call) { |
||||
throw new CallNotFoundException() |
||||
} |
||||
|
||||
if (call.status === CallStatus.Finished) { |
||||
throw new CallAlreadyFinishedException() |
||||
} |
||||
} |
||||
|
||||
protected async info(callId: string, description: string, userId?: number) { |
||||
try { |
||||
await this.callsLogsRepository.insert({ |
||||
callId, |
||||
description, |
||||
userId, |
||||
level: 'info', |
||||
}) |
||||
} catch (e) { |
||||
console.log(e) |
||||
} |
||||
} |
||||
|
||||
protected async error(callId: string, description: string, userId?: number) { |
||||
try { |
||||
await this.callsLogsRepository.insert({ |
||||
callId, |
||||
description, |
||||
userId, |
||||
level: 'error', |
||||
}) |
||||
} catch (e) { |
||||
console.log(e) |
||||
} |
||||
} |
||||
|
||||
protected async populateUsersToSend(call: ICallEntity, userId: number) { |
||||
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,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
import { v4 as uuidv4 } from 'uuid' |
||||
|
||||
export class CallToken { |
||||
static create() { |
||||
return uuidv4() |
||||
} |
||||
} |
@ -0,0 +1,88 @@
@@ -0,0 +1,88 @@
|
||||
import { |
||||
CallFinishedReason, |
||||
CallMemberStatus, |
||||
CallSocketEvent, |
||||
CallStatus, |
||||
ICallEntity, |
||||
IFinishCallPayload, |
||||
} from '../typing' |
||||
import { CallAction } from './call-action' |
||||
|
||||
export class FinishCall extends CallAction { |
||||
protected payload: IFinishCallPayload |
||||
protected call: ICallEntity |
||||
|
||||
public async finish(payload: IFinishCallPayload) { |
||||
this.payload = payload |
||||
this.call = await this.getCall(payload.callId) |
||||
|
||||
console.log('call', this.payload) |
||||
await this.validateCall(this.call) |
||||
await this.updateMemberStatuses() |
||||
await this.updateCall() |
||||
|
||||
this.call = await this.getCall(payload.callId) |
||||
|
||||
await this.sendEvents() |
||||
|
||||
await this.info(this.call.id, `Finished call, reason ${payload.reason}`, payload.userId) |
||||
|
||||
if ([CallFinishedReason.Canceled, CallFinishedReason.Rejected]) { |
||||
this.sendCallCanceledEvent() |
||||
} |
||||
} |
||||
|
||||
private async updateMemberStatuses() { |
||||
await this.callsMembersRepository.update( |
||||
{ callId: this.call.id }, |
||||
{ status: CallMemberStatus.Closed }, |
||||
) |
||||
} |
||||
|
||||
private async updateCall() { |
||||
await this.callsRepository.update( |
||||
{ id: this.call.id }, |
||||
{ |
||||
finishedAt: new Date().toISOString(), |
||||
finishedByUserId: this.payload.userId, |
||||
finishedReason: this.payload.reason, |
||||
status: CallStatus.Finished, |
||||
}, |
||||
) |
||||
} |
||||
|
||||
private async sendEvents() { |
||||
const members = await this.callsMembersRepository.find({ |
||||
where: { callId: this.call.id }, |
||||
select: ['userId'], |
||||
}) |
||||
|
||||
members.forEach(it => { |
||||
this.realTimeService.emitToUser(it.userId, CallSocketEvent.Finished, { |
||||
call: this.call, |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
private sendCallCanceledEvent() { |
||||
const targetUsers = this.call.members |
||||
.filter(it => it.userId !== this.payload.userId) |
||||
.map(it => it.userId) |
||||
|
||||
targetUsers.map(userId => { |
||||
this.notificationsService.sendVoipNotification( |
||||
userId, |
||||
{ |
||||
title: 'Виклик відмінений', |
||||
data: { |
||||
uuid: this.call.id, |
||||
type: 'cancelled', |
||||
}, |
||||
}, |
||||
{ |
||||
toIos: false, |
||||
}, |
||||
) |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,73 @@
@@ -0,0 +1,73 @@
|
||||
import { |
||||
CallMemberStatus, |
||||
CallSocketEvent, |
||||
CallStatus, |
||||
ICallEntity, |
||||
ICallMemberEntity, |
||||
IOnConnectedPayload, |
||||
} from '../typing' |
||||
import { CallAction } from './call-action' |
||||
|
||||
export class OnConnected extends CallAction { |
||||
protected payload: IOnConnectedPayload |
||||
protected call: ICallEntity |
||||
protected member: ICallMemberEntity |
||||
|
||||
public async handle(payload: IOnConnectedPayload) { |
||||
this.payload = payload |
||||
|
||||
this.call = await this.getCall(this.payload.callId) |
||||
this.member = await this.callsMembersRepository.findOne({ |
||||
userId: this.payload.userId, |
||||
callId: this.payload.callId, |
||||
}) |
||||
|
||||
await this.validateCall(this.call) |
||||
await this.updateMemberStatus() |
||||
|
||||
await this.info(this.call.id, 'On call', payload.userId) |
||||
|
||||
const isAllConnected = await this.checkIsAllUsersInCallStatus() |
||||
if (!isAllConnected) return |
||||
|
||||
await this.updateCall() |
||||
|
||||
this.sendEvents() |
||||
|
||||
await this.info(this.call.id, 'Call updated to status in progress') |
||||
} |
||||
|
||||
protected async updateMemberStatus() { |
||||
await this.callsMembersRepository.update(this.member.id, { |
||||
status: CallMemberStatus.Call, |
||||
}) |
||||
} |
||||
|
||||
protected async checkIsAllUsersInCallStatus() { |
||||
const members = await this.callsMembersRepository.find({ |
||||
callId: this.payload.callId, |
||||
}) |
||||
|
||||
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 }) |
||||
} |
||||
} |
@ -0,0 +1,86 @@
@@ -0,0 +1,86 @@
|
||||
import { CallAlreadyReadyToConnectException } from '../exceptions/call-already-ready-to-connect.exception' |
||||
import { WrongDeviceException } from '../exceptions/wrong-device.exception' |
||||
import { |
||||
CallMemberStatus, |
||||
CallSocketEvent, |
||||
CallStatus, |
||||
ICallEntity, |
||||
ICallMemberEntity, |
||||
IReadyToConnectPayload, |
||||
} from '../typing' |
||||
import { CallAction } from './call-action' |
||||
|
||||
export class ReadyToConnect extends CallAction { |
||||
protected call: ICallEntity |
||||
protected payload: IReadyToConnectPayload |
||||
protected member: ICallMemberEntity |
||||
|
||||
public async set(payload: IReadyToConnectPayload) { |
||||
this.payload = payload |
||||
this.call = await this.getCall(this.payload.callId) |
||||
this.member = await this.callsMembersRepository.findOne({ |
||||
userId: this.payload.userId, |
||||
callId: this.payload.callId, |
||||
}) |
||||
|
||||
await this.validateCall(this.call) |
||||
await this.validate() |
||||
await this.updateMemberStatus() |
||||
|
||||
await this.info(this.call.id, 'Ready to connect', payload.userId) |
||||
|
||||
const isAllConnected = await this.checkIsAllUsersInConnectingStatus() |
||||
if (!isAllConnected) return |
||||
|
||||
await this.updateCall() |
||||
await this.sendEvents() |
||||
|
||||
await this.info(this.call.id, 'Call updated to status connecting') |
||||
} |
||||
|
||||
protected async validate() { |
||||
if (this.member.deviceUuid !== this.payload.deviceUuid) { |
||||
throw new WrongDeviceException() |
||||
} |
||||
|
||||
if ( |
||||
this.member.status !== CallMemberStatus.IncomingCall && |
||||
this.member.status !== CallMemberStatus.OutgoingCall |
||||
) { |
||||
throw new CallAlreadyReadyToConnectException() |
||||
} |
||||
} |
||||
|
||||
protected async updateMemberStatus() { |
||||
await this.callsMembersRepository.update(this.member.id, { |
||||
status: CallMemberStatus.Connecting, |
||||
}) |
||||
} |
||||
|
||||
protected async checkIsAllUsersInConnectingStatus() { |
||||
const members = await this.callsMembersRepository.find({ |
||||
callId: this.payload.callId, |
||||
}) |
||||
|
||||
const isAllConnecting = members.every(it => it.status === CallMemberStatus.Connecting) |
||||
|
||||
return isAllConnecting |
||||
} |
||||
|
||||
protected async updateCall() { |
||||
await this.callsRepository.update( |
||||
{ id: this.payload.callId }, |
||||
{ status: CallStatus.Connection, startAt: new Date().toISOString() }, |
||||
) |
||||
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.ReadyToConnect, { call: this.call }) |
||||
} |
||||
} |
@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
|
||||
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: ICallMemberEntity[] = [] |
||||
|
||||
public async send(payload: SendIceCandidatesPayload) { |
||||
this.payload = payload |
||||
this.call = await this.getCall(payload.callId) |
||||
this.usersIdsToSend = await this.populateUsersToSend(this.call, this.payload.userId) |
||||
|
||||
await this.sendEvents() |
||||
|
||||
this.info(this.call.id, 'Send ice candidates', this.payload.userId) |
||||
} |
||||
|
||||
protected async sendEvents() { |
||||
this.emitToMembers(this.usersIdsToSend, CallSocketEvent.RTCIceCandidates, { |
||||
call: this.call, |
||||
iceCandidates: this.payload.iceCandidates, |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
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: ICallMemberEntity[] = [] |
||||
|
||||
public async send(payload: SendRTCSessionPayload) { |
||||
this.payload = payload |
||||
|
||||
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) |
||||
} |
||||
|
||||
protected async isNeedAnswer() { |
||||
return this.call.initiatorUserId !== this.payload.userId |
||||
} |
||||
|
||||
protected async sendEvents() { |
||||
this.emitToMembers(this.usersIdsToSend, CallSocketEvent.RTCSessionDescription, { |
||||
call: this.call, |
||||
mustAnswer: this.isNeedAnswer(), |
||||
sessionDescription: this.payload.sessionDescription, |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,144 @@
@@ -0,0 +1,144 @@
|
||||
import { UserFullName } from 'src/domain/users/classes' |
||||
import { TargetUserInCallException } from '../exceptions' |
||||
import { |
||||
CallFinishedReason, |
||||
CallMemberStatus, |
||||
CallStatus, |
||||
ICallEntity, |
||||
IStartCallPayload, |
||||
} from '../typing' |
||||
import { CallAction } from './call-action' |
||||
import { CallToken } from './call-token' |
||||
import { transformFileUrl } from 'src/shared/transforms' |
||||
|
||||
export class StartCall extends CallAction { |
||||
protected payload: IStartCallPayload |
||||
protected call: ICallEntity |
||||
|
||||
public async start(payload: IStartCallPayload) { |
||||
this.payload = payload |
||||
|
||||
await this.validate() |
||||
await this.createEntity() |
||||
await this.createMembers() |
||||
await this.reloadCallEntity() |
||||
await this.info(this.call.id, 'Start call', this.payload.initiatorId) |
||||
|
||||
await this.sendEventToTargetUser() |
||||
|
||||
return this.call |
||||
} |
||||
|
||||
private async validate() { |
||||
const isInitiatorInCall = await this.userInCall(this.payload.initiatorId) |
||||
|
||||
if (isInitiatorInCall) { |
||||
await this.stopExistInitiatorCall() |
||||
} |
||||
|
||||
const targetInCall = await this.userInCall(this.payload.targetId) |
||||
if (targetInCall) { |
||||
throw new TargetUserInCallException() |
||||
} |
||||
} |
||||
|
||||
private async userInCall(userId: number) { |
||||
const member = await this.callsMembersRepository |
||||
.createQueryBuilder('it') |
||||
.where('it.userId = :userId', { userId }) |
||||
.andWhere('it.status <> :status', { status: CallMemberStatus.Closed }) |
||||
.getOne() |
||||
|
||||
if (!member) return false |
||||
|
||||
const call = await this.callsRepository.findOne({ id: member.callId }) |
||||
if (call.status === CallStatus.Finished) return false |
||||
|
||||
return true |
||||
} |
||||
|
||||
private async createEntity() { |
||||
this.call = await this.callsRepository.save({ |
||||
id: CallToken.create(), |
||||
status: CallStatus.Calling, |
||||
initiatorUserId: this.payload.initiatorId, |
||||
accessToken: CallToken.create(), |
||||
}) |
||||
} |
||||
|
||||
private async createMembers() { |
||||
const user = await this.usersRepository.findOne({ |
||||
where: { id: this.payload.initiatorId }, |
||||
relations: ['info'], |
||||
}) |
||||
await this.callsMembersRepository.insert({ |
||||
userId: this.payload.initiatorId, |
||||
status: CallMemberStatus.OutgoingCall, |
||||
callId: this.call.id, |
||||
deviceUuid: this.payload.deviceUuid, |
||||
avatarUrl: user.info.avatarUrl ? transformFileUrl(user.info.avatarUrl) : null, |
||||
name: `${user.info.lastName} ${user.info.firstName}`, |
||||
}) |
||||
const user2 = await this.usersRepository.findOne({ |
||||
where: { id: this.payload.targetId }, |
||||
relations: ['info'], |
||||
}) |
||||
await this.callsMembersRepository.insert({ |
||||
userId: this.payload.targetId, |
||||
status: CallMemberStatus.IncomingCall, |
||||
callId: this.call.id, |
||||
avatarUrl: user2.info.avatarUrl ? transformFileUrl(user2.info.avatarUrl) : null, |
||||
name: `${user2.info.lastName} ${user2.info.firstName}`, |
||||
}) |
||||
} |
||||
|
||||
private async reloadCallEntity() { |
||||
this.call = await this.getCall(this.call?.id) |
||||
} |
||||
|
||||
private async sendEventToTargetUser() { |
||||
const fromUser = await this.usersRepository.findOne({ |
||||
where: { |
||||
id: this.payload.initiatorId, |
||||
}, |
||||
relations: ['info'], |
||||
}) |
||||
|
||||
const expiredAt = new Date() |
||||
expiredAt.setMinutes(expiredAt.getMinutes() + 2) |
||||
this.notificationsService.sendVoipNotification(this.payload.targetId, { |
||||
title: 'Новий виклик', |
||||
content: 'від користувача ' + UserFullName.getFromUser(fromUser), |
||||
data: { |
||||
uuid: this.call.id, |
||||
handle: fromUser.phoneNumber, |
||||
callerName: UserFullName.getFromUser(fromUser), |
||||
callerId: String(this.payload.initiatorId), |
||||
type: 'income', |
||||
expiredAt: String(expiredAt.getTime()), |
||||
avatarUrl: fromUser.info.avatarUrl ? transformFileUrl(fromUser.info.avatarUrl) : '', |
||||
authToken: this.call.accessToken, |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
private async stopExistInitiatorCall() { |
||||
const members = await this.callsMembersRepository |
||||
.createQueryBuilder('it') |
||||
.where('it.userId = :userId', { userId: this.payload.initiatorId }) |
||||
.andWhere('it.status <> :status', { status: CallMemberStatus.Closed }) |
||||
.getMany() |
||||
|
||||
for await (const member of members) { |
||||
await this.callsRepository.update(member.callId, { |
||||
status: CallStatus.Finished, |
||||
finishedReason: CallFinishedReason.FailedConnect, |
||||
finishedByUserId: null, |
||||
}) |
||||
await this.callsMembersRepository.update( |
||||
{ callId: member.callId }, |
||||
{ status: CallMemberStatus.Closed }, |
||||
) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
|
||||
import { CallSocketEvent, ICallEntity, UpdateMediaSettingsPayload } from '../typing' |
||||
import { CallAction } from './call-action' |
||||
|
||||
export class UpdateMediaSettings extends CallAction { |
||||
protected payload: UpdateMediaSettingsPayload |
||||
protected call: ICallEntity |
||||
protected usersIdsToSend: number[] = [] |
||||
|
||||
public async update(payload: UpdateMediaSettingsPayload) { |
||||
this.payload = payload |
||||
|
||||
this.call = await this.getCall(payload.callId) |
||||
|
||||
await this.updateMember() |
||||
|
||||
this.call = await this.getCall(payload.callId) |
||||
|
||||
await this.sendEvents() |
||||
} |
||||
|
||||
private async updateMember() { |
||||
await this.callsMembersRepository.update( |
||||
{ userId: this.payload.userId }, |
||||
{ |
||||
cameraOn: this.payload.cameraOn, |
||||
micronOn: this.payload.micronOn, |
||||
speakerOn: this.payload.speakerOn, |
||||
}, |
||||
) |
||||
} |
||||
|
||||
protected async sendEvents() { |
||||
this.emitToMembers(this.call.members, CallSocketEvent.CallUpdated, { call: this.call }) |
||||
} |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
export interface EndCallDto { |
||||
callId: string |
||||
} |
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
import { Expose } from 'class-transformer' |
||||
|
||||
export class RejectCallDto { |
||||
@Expose() |
||||
callId: string |
||||
} |
||||
|
||||
export class RejectCallByTokenDto { |
||||
@Expose() |
||||
authToken: string |
||||
|
||||
@Expose() |
||||
uuid: string |
||||
} |
@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
|
||||
import { |
||||
Column, |
||||
CreateDateColumn, |
||||
Entity, |
||||
JoinColumn, |
||||
ManyToOne, |
||||
PrimaryGeneratedColumn, |
||||
} from 'typeorm' |
||||
import { ICallLogEntity } from '../typing' |
||||
import { Call } from './call.entity' |
||||
|
||||
@Entity('callsLogs') |
||||
export class CallLog implements ICallLogEntity { |
||||
@PrimaryGeneratedColumn() |
||||
id: number |
||||
|
||||
@Column({}) |
||||
callId: string |
||||
|
||||
@Column() |
||||
level?: string |
||||
|
||||
@Column({ type: 'varchar' }) |
||||
description: string |
||||
|
||||
@Column({ nullable: true }) |
||||
userId?: number |
||||
|
||||
@CreateDateColumn({ type: 'timestamp with time zone', default: () => 'LOCALTIMESTAMP' }) |
||||
createdAt: string |
||||
|
||||
@ManyToOne(() => Call, { onDelete: 'CASCADE' }) |
||||
@JoinColumn({ name: 'callId' }) |
||||
call: Call |
||||
} |
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm' |
||||
import { CallMemberStatus, ICallMemberEntity } from '../typing' |
||||
import { Call } from './call.entity' |
||||
import { User } from 'src/domain/users/entities' |
||||
|
||||
@Entity('callsMembers') |
||||
export class CallMember implements ICallMemberEntity { |
||||
@PrimaryGeneratedColumn() |
||||
id: number |
||||
|
||||
@Column({ nullable: false }) |
||||
userId: number |
||||
|
||||
@Column({ nullable: true }) |
||||
avatarUrl?: string |
||||
|
||||
@Column({ nullable: false }) |
||||
name: string |
||||
|
||||
@Column({ nullable: true }) |
||||
deviceUuid: string |
||||
|
||||
@Column({ nullable: false }) |
||||
callId: string |
||||
|
||||
@Column({ nullable: false }) |
||||
status: CallMemberStatus |
||||
|
||||
@Column({ type: 'boolean', default: true }) |
||||
micronOn: boolean |
||||
|
||||
@Column({ type: 'boolean', default: false }) |
||||
speakerOn: boolean |
||||
|
||||
@Column({ type: 'boolean', default: true }) |
||||
cameraOn: boolean |
||||
|
||||
@ManyToOne(() => Call, call => call.members, { onDelete: 'CASCADE' }) |
||||
@JoinColumn({ name: 'callId' }) |
||||
call: Call |
||||
|
||||
@ManyToOne(() => User, { onDelete: 'SET NULL' }) |
||||
@JoinColumn({ name: 'userId' }) |
||||
user: User |
||||
} |
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
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?: CallMember[] |
||||
|
||||
@Column({ type: 'int', array: true, default: [] }) |
||||
hideForUsers?: number[] |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
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 } |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
import { DomainException } from 'src/shared' |
||||
|
||||
export class AccessCallWrongException extends DomainException { |
||||
constructor() { |
||||
super({ |
||||
key: 'accessCallWrong', |
||||
description: 'Access call wrong', |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
import { DomainException } from 'src/shared' |
||||
|
||||
export class CallAlreadyFinishedException extends DomainException { |
||||
constructor() { |
||||
super({ |
||||
key: 'callAlreadyFinished', |
||||
description: 'Call already finished', |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
import { DomainException } from 'src/shared' |
||||
|
||||
export class CallAlreadyReadyToConnectException extends DomainException { |
||||
constructor() { |
||||
super({ |
||||
key: 'callAlreadyReadyToConnect', |
||||
description: 'User already set ready to connect', |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
import { DomainException } from 'src/shared' |
||||
|
||||
export class CallAnsweredException extends DomainException { |
||||
constructor() { |
||||
super({ |
||||
key: 'callAnswered', |
||||
description: 'Call answered', |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
import { DomainException } from 'src/shared' |
||||
|
||||
export class CallNotFoundException extends DomainException { |
||||
constructor() { |
||||
super({ |
||||
key: 'callNotFound', |
||||
description: 'Call not found', |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
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,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
import { DomainException } from 'src/shared' |
||||
|
||||
export class InitiatorInCallException extends DomainException { |
||||
constructor() { |
||||
super({ |
||||
key: 'initiatorInCall', |
||||
description: 'Initiator in call', |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
import { DomainException } from 'src/shared' |
||||
|
||||
export class TargetUserInCallException extends DomainException { |
||||
constructor() { |
||||
super({ |
||||
key: 'targetUserInCall', |
||||
description: 'Target user in call', |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
import { DomainException } from 'src/shared' |
||||
|
||||
export class WrongDeviceException extends DomainException { |
||||
constructor() { |
||||
super({ |
||||
key: 'wrongDevice', |
||||
description: 'Call was started on another device', |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,89 @@
@@ -0,0 +1,89 @@
|
||||
import { Inject, Injectable } from '@nestjs/common' |
||||
import { |
||||
CALLS_LOGS_REPOSITORY, |
||||
CALLS_MEMBERS_REPOSITORY, |
||||
CALLS_REPOSITORY, |
||||
CallsLogsRepository, |
||||
CallsMembersRepository, |
||||
CallsRepository, |
||||
} from '../typing' |
||||
import { StartCall } from '../core/start-call' |
||||
import { CallAction } from '../core/call-action' |
||||
import { Notifications, WebSockets } from 'src/core' |
||||
import { NOTIFICATIONS_SERVICE, REAL_TIME_SERVICE } from 'src/core/consts' |
||||
import { UsersRepository } from 'src/domain/users/repositories' |
||||
import { USERS_REPOSITORY } from 'src/domain/users/consts' |
||||
import { FinishCall } from '../core/finish-call' |
||||
import { AnswerCall } from '../core/answer-call' |
||||
import { ReadyToConnect } from '../core/ready-to-connect' |
||||
import { SendRTCSession } from '../core/send-rtc-session' |
||||
import { SendIceCandidates } from '../core/send-ice-candidates' |
||||
import { OnConnected } from '../core/on-connected' |
||||
import { UpdateMediaSettings } from '../core/update-media-settings' |
||||
|
||||
@Injectable() |
||||
export class CallsActionsFactory { |
||||
constructor( |
||||
@Inject(CALLS_REPOSITORY) |
||||
private readonly callsRepository: CallsRepository, |
||||
|
||||
@Inject(CALLS_MEMBERS_REPOSITORY) |
||||
private readonly callsMembersRepository: CallsMembersRepository, |
||||
|
||||
@Inject(CALLS_LOGS_REPOSITORY) |
||||
private readonly callsLogsRepository: CallsLogsRepository, |
||||
|
||||
@Inject(REAL_TIME_SERVICE) |
||||
private readonly realTimeService: WebSockets.Service, |
||||
|
||||
@Inject(USERS_REPOSITORY) |
||||
private readonly usersRepository: UsersRepository, |
||||
|
||||
@Inject(NOTIFICATIONS_SERVICE) |
||||
private readonly notificationsService: Notifications.INotificationsService, |
||||
) {} |
||||
|
||||
public startCall() { |
||||
return this.injectDependencies(new StartCall()) |
||||
} |
||||
|
||||
public finishCall() { |
||||
return this.injectDependencies(new FinishCall()) |
||||
} |
||||
|
||||
public answerCall() { |
||||
return this.injectDependencies(new AnswerCall()) |
||||
} |
||||
|
||||
public readyToConnect() { |
||||
return this.injectDependencies(new ReadyToConnect()) |
||||
} |
||||
|
||||
public sendRTCSession() { |
||||
return this.injectDependencies(new SendRTCSession()) |
||||
} |
||||
|
||||
public sendIceCandidates() { |
||||
return this.injectDependencies(new SendIceCandidates()) |
||||
} |
||||
|
||||
public onConnected() { |
||||
return this.injectDependencies(new OnConnected()) |
||||
} |
||||
|
||||
public updateMediaSettings() { |
||||
return this.injectDependencies(new UpdateMediaSettings()) |
||||
} |
||||
|
||||
private injectDependencies<T extends CallAction>(obj: T) { |
||||
obj.setRepositories( |
||||
this.callsRepository, |
||||
this.callsLogsRepository, |
||||
this.callsMembersRepository, |
||||
) |
||||
obj.setUsersRepositories(this.usersRepository) |
||||
obj.setRealTimeService(this.realTimeService) |
||||
obj.setNotificationsService(this.notificationsService) |
||||
return obj |
||||
} |
||||
} |
@ -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, |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,88 @@
@@ -0,0 +1,88 @@
|
||||
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 deleteHistoryItem(userId: number, callId: string) { |
||||
const call = await this.callsRepository.findOne(callId) |
||||
if (!call) return null |
||||
|
||||
call.hideForUsers.push(userId) |
||||
|
||||
if (call.hideForUsers.length === 2) { |
||||
await this.callsRepository.delete(callId) |
||||
} else { |
||||
await this.callsRepository.save(call) |
||||
} |
||||
|
||||
return |
||||
} |
||||
} |
@ -0,0 +1,150 @@
@@ -0,0 +1,150 @@
|
||||
import { Users } from 'src/core' |
||||
import { Repository } from 'typeorm' |
||||
|
||||
export enum CallStatus { |
||||
Calling = 'calling', |
||||
Connection = 'connection', |
||||
InProgress = 'inProgress', |
||||
Finished = 'finished', |
||||
} |
||||
|
||||
export enum CallFinishedReason { |
||||
Canceled = 'canceled', |
||||
Rejected = 'rejected', |
||||
FailedConnect = 'failedConnect', |
||||
EndCall = 'endcall', |
||||
} |
||||
|
||||
export interface ICallEntity { |
||||
id: string |
||||
|
||||
initiatorUserId: number |
||||
finishedByUserId?: number |
||||
|
||||
finishedAt: string |
||||
startAt: string |
||||
createdAt: string |
||||
updatedAt: string |
||||
|
||||
status: CallStatus |
||||
finishedReason?: CallFinishedReason |
||||
|
||||
members?: ICallMemberEntity[] |
||||
|
||||
accessToken?: string |
||||
|
||||
hideForUsers?: number[] |
||||
} |
||||
|
||||
export enum CallMemberStatus { |
||||
OutgoingCall = 'outgoingCall', |
||||
IncomingCall = 'incomingCall', |
||||
|
||||
Connecting = 'connecting', |
||||
|
||||
Call = 'call', |
||||
|
||||
Closed = 'closed', |
||||
} |
||||
|
||||
export interface ICallMemberEntity { |
||||
id: number |
||||
userId: number |
||||
|
||||
avatarUrl?: string |
||||
name: string |
||||
|
||||
deviceUuid: string |
||||
callId: string |
||||
status: CallMemberStatus |
||||
user?: Users.UserModel |
||||
call?: ICallEntity |
||||
|
||||
micronOn?: boolean |
||||
speakerOn?: boolean |
||||
cameraOn?: boolean |
||||
} |
||||
|
||||
export interface ICallLogEntity { |
||||
id: number |
||||
callId: string |
||||
description: string |
||||
|
||||
userId?: number |
||||
createdAt: string |
||||
level?: string |
||||
} |
||||
|
||||
export const CALLS_REPOSITORY = Symbol('CALLS_REPOSITORY') |
||||
export const CALLS_MEMBERS_REPOSITORY = Symbol('CALLS_MEMBERS_REPOSITORY') |
||||
export const CALLS_LOGS_REPOSITORY = Symbol('CALLS_LOGS_REPOSITORY') |
||||
|
||||
export type CallsRepository = Repository<ICallEntity> |
||||
export type CallsMembersRepository = Repository<ICallMemberEntity> |
||||
export type CallsLogsRepository = Repository<ICallLogEntity> |
||||
|
||||
export interface IStartCallPayload { |
||||
initiatorId: number |
||||
targetId: number |
||||
deviceUuid: string |
||||
} |
||||
|
||||
export interface IFinishCallPayload { |
||||
userId: number |
||||
callId: string |
||||
reason: CallFinishedReason |
||||
} |
||||
|
||||
export interface IAnswerCallPayload { |
||||
userId: number |
||||
callId: string |
||||
deviceUuid: string |
||||
} |
||||
|
||||
export interface IReadyToConnectPayload { |
||||
userId: number |
||||
callId: string |
||||
deviceUuid: string |
||||
} |
||||
|
||||
export interface IOnConnectedPayload { |
||||
userId: number |
||||
callId: string |
||||
} |
||||
|
||||
export interface ICallsService { |
||||
start(payload: IStartCallPayload): Promise<ICallEntity> |
||||
answer(payload: IAnswerCallPayload): Promise<void> |
||||
readyToConnect(payload: IReadyToConnectPayload): Promise<void> |
||||
finish(payload: IFinishCallPayload): Promise<void> |
||||
} |
||||
|
||||
export enum CallSocketEvent { |
||||
Answered = '/v2call/answered', |
||||
ReadyToConnect = '/v2call/ready-to-connect', |
||||
Finished = '/v2call/finished', |
||||
RTCSessionDescription = '/v2call/rtc-session-description', |
||||
RTCIceCandidates = '/v2call/rtc-ice-candidates', |
||||
CallUpdated = '/v2call/call-updated', |
||||
} |
||||
|
||||
export interface SendRTCSessionPayload { |
||||
userId: number |
||||
callId: string |
||||
sessionDescription: any |
||||
} |
||||
|
||||
export interface SendIceCandidatesPayload { |
||||
userId: number |
||||
callId: string |
||||
iceCandidates: any[] |
||||
} |
||||
|
||||
export interface UpdateMediaSettingsPayload { |
||||
userId: number |
||||
callId: string |
||||
|
||||
micronOn?: boolean |
||||
speakerOn?: boolean |
||||
cameraOn?: boolean |
||||
} |
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
import { OnChatMessageViewListener } from './on-chat-message-view.listener' |
||||
import { OnChatNewMessageListener } from './on-chat-new-message.listener' |
||||
import { OnNewChatListener } from './on-new-chat.listener' |
||||
import { OnReadChatListener } from './on-read-chat.listener' |
||||
import { OnSecretModTurnListener } from './on-secret-mod-turn.listener' |
||||
import { OnSecretRecordsChangedListener } from './on-secret-records-changed.listener' |
||||
|
||||
export const CHATS_LISTENERS = [ |
||||
OnReadChatListener, |
||||
OnChatMessageViewListener, |
||||
OnChatNewMessageListener, |
||||
OnNewChatListener, |
||||
OnSecretModTurnListener, |
||||
OnSecretRecordsChangedListener, |
||||
] |
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
import { Injectable } from '@nestjs/common' |
||||
import { OnEvent } from '@nestjs/event-emitter' |
||||
import { Events, IEventsPayloads } from 'src/core/enums' |
||||
import { ChatsEventsService } from '../services/chats-events.service' |
||||
import { Chats } from 'src/core' |
||||
import * as _ from 'lodash' |
||||
|
||||
@Injectable() |
||||
export class OnChatMessageViewListener { |
||||
constructor(private readonly chatsEventsService: ChatsEventsService) {} |
||||
|
||||
@OnEvent(Events.OnChatMessageView) |
||||
public async onChatMessageView(payload: IEventsPayloads['OnChatMessageView']) { |
||||
const existEvents = await this.chatsEventsService.getExistEvents( |
||||
payload.messageIds, |
||||
payload.userId, |
||||
Chats.ChatMessageEventType.View, |
||||
) |
||||
|
||||
for (const id of payload.messageIds) { |
||||
if (!_.find(existEvents, event => event.messageId === id)) |
||||
await this.chatsEventsService.addEvent({ |
||||
messageId: id, |
||||
userId: payload.userId, |
||||
type: Chats.ChatMessageEventType.View, |
||||
}) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
import { Injectable } from '@nestjs/common' |
||||
import { OnEvent } from '@nestjs/event-emitter' |
||||
import { Events, IEventsPayloads } from 'src/core/enums' |
||||
import * as _ from 'lodash' |
||||
import { ChatsCacheService } from '../services' |
||||
|
||||
@Injectable() |
||||
export class OnChatNewMessageListener { |
||||
constructor(private readonly chatsCacheService: ChatsCacheService) {} |
||||
|
||||
@OnEvent(Events.OnNewMessage) |
||||
public async removeCache(payload: IEventsPayloads['OnNewMessage']) { |
||||
await this.chatsCacheService.remove(payload.chatId) |
||||
} |
||||
} |
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
import { Injectable } from '@nestjs/common' |
||||
import { OnEvent } from '@nestjs/event-emitter' |
||||
import { Events, IEventsPayloads } from 'src/core/enums' |
||||
import * as _ from 'lodash' |
||||
import { ChatsCacheService } from '../services' |
||||
|
||||
@Injectable() |
||||
export class OnChatNewMessageListener { |
||||
constructor(private readonly chatsCacheService: ChatsCacheService) {} |
||||
|
||||
@OnEvent(Events.OnNewMessage) |
||||
public async removeCache(payload: IEventsPayloads['OnNewMessage']) { |
||||
await this.chatsCacheService.remove(payload.chatId) |
||||
} |
||||
} |
@ -0,0 +1,47 @@
@@ -0,0 +1,47 @@
|
||||
import { Inject, Injectable } from '@nestjs/common' |
||||
import { OnEvent } from '@nestjs/event-emitter' |
||||
import { Events, IEventsPayloads } from 'src/core/enums' |
||||
import * as _ from 'lodash' |
||||
import { SECRET_MOD_SERVICE } from 'src/domain/secret-mod/consts' |
||||
import { CHATS_MEMBERS_REPOSITORY } from '../consts' |
||||
import { IChatsMembersRepository } from '../interfaces' |
||||
import { SecretMod } from 'src/domain/secret-mod/typing' |
||||
import { ChatsSecretService } from '../services/chats-secret.service' |
||||
|
||||
@Injectable() |
||||
export class OnNewChatListener { |
||||
constructor( |
||||
@Inject(SECRET_MOD_SERVICE) |
||||
private readonly secretModService: SecretMod.Service, |
||||
|
||||
@Inject(CHATS_MEMBERS_REPOSITORY) |
||||
private readonly chatsMembersRepository: IChatsMembersRepository, |
||||
|
||||
private readonly chatsSecretService: ChatsSecretService, |
||||
) {} |
||||
|
||||
@OnEvent(Events.OnNewChat) |
||||
public async checkForHiddenUsers(payload: IEventsPayloads['OnNewChat']) { |
||||
const dbResult = await this.chatsMembersRepository |
||||
.createQueryBuilder('it') |
||||
.distinct(true) |
||||
.select('it.userId', 'userId') |
||||
.where('it.chatId = :chatId', { chatId: payload.chatId }) |
||||
.getRawMany() |
||||
|
||||
const usersIds = dbResult.map(it => Number(it.userId)) |
||||
|
||||
const hiddenUsersIds = await this.secretModService.getHiddenUsersIds() |
||||
const hasIntersections = usersIds.reduce((result, it) => { |
||||
if (result === true) return true |
||||
if (hiddenUsersIds.includes(it)) { |
||||
return true |
||||
} |
||||
return false |
||||
}, false) |
||||
|
||||
if (!hasIntersections) return |
||||
|
||||
this.chatsSecretService.allocateSecretChats() |
||||
} |
||||
} |
@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
|
||||
import { Injectable } from '@nestjs/common' |
||||
import { ChatsEventsService } from '../services/chats-events.service' |
||||
import { OnEvent } from '@nestjs/event-emitter' |
||||
import { Events, IEventsPayloads } from 'src/core/enums' |
||||
import { Chats } from 'src/core' |
||||
import { ChatsAccessoryService } from '../services/chats-accessory.service' |
||||
import * as _ from 'lodash' |
||||
import { ChatsCacheService } from '../services' |
||||
|
||||
@Injectable() |
||||
export class OnReadChatListener { |
||||
constructor( |
||||
private readonly chatsEventsService: ChatsEventsService, |
||||
private readonly chatsAccessoryService: ChatsAccessoryService, |
||||
private readonly chatsCacheService: ChatsCacheService, |
||||
) {} |
||||
|
||||
@OnEvent(Events.OnReadChat) |
||||
public async addEvemts(payload: IEventsPayloads['OnReadChat']) { |
||||
const unreadMessages = (await this.chatsAccessoryService.getChatUnreadMessages( |
||||
payload.chatId, |
||||
payload.userId, |
||||
false, |
||||
)) as Chats.IChatMessage[] |
||||
|
||||
if (_.isEmpty(unreadMessages)) return |
||||
|
||||
const messagesIds = unreadMessages.map(it => it.id) |
||||
|
||||
for (const id of messagesIds) { |
||||
await this.chatsEventsService.addEvent({ |
||||
messageId: id, |
||||
userId: payload.userId, |
||||
type: Chats.ChatMessageEventType.View, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
@OnEvent(Events.OnReadChat) |
||||
public async clearCache(payload: IEventsPayloads['OnReadChat']) { |
||||
await this.chatsCacheService.remove(payload.chatId) |
||||
} |
||||
} |
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
import { Injectable } from '@nestjs/common' |
||||
import { OnEvent } from '@nestjs/event-emitter' |
||||
import { Events, IEventsPayloads } from 'src/core/enums' |
||||
import * as _ from 'lodash' |
||||
import { ChatsSecretService } from '../services/chats-secret.service' |
||||
|
||||
@Injectable() |
||||
export class OnSecretModTurnListener { |
||||
constructor(private readonly chatsSecretService: ChatsSecretService) {} |
||||
|
||||
@OnEvent(Events.OnSecretModTurn) |
||||
public async addEvemts(payload: IEventsPayloads['OnSecretModTurn']) { |
||||
if (payload.isActive) { |
||||
await this.chatsSecretService.allocateSecretChats() |
||||
} else { |
||||
await this.chatsSecretService.clear() |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
import { Injectable } from '@nestjs/common' |
||||
import { OnEvent } from '@nestjs/event-emitter' |
||||
import { Events } from 'src/core/enums' |
||||
import * as _ from 'lodash' |
||||
import { ChatsSecretService } from '../services/chats-secret.service' |
||||
|
||||
@Injectable() |
||||
export class OnSecretRecordsChangedListener { |
||||
constructor(private readonly chatsSecretService: ChatsSecretService) {} |
||||
|
||||
@OnEvent(Events.OnSecretRecordsChanged) |
||||
public async addEvemts() { |
||||
await this.chatsSecretService.allocateSecretChats() |
||||
} |
||||
} |
@ -0,0 +1,81 @@
@@ -0,0 +1,81 @@
|
||||
import { Injectable } from '@nestjs/common' |
||||
import { Chats } from 'src/core' |
||||
import { RedisService } from 'src/libs' |
||||
|
||||
@Injectable() |
||||
export class ChatsCacheService { |
||||
constructor(private readonly redisService: RedisService) {} |
||||
|
||||
public async save(chatItem: Chats.IChat, userId: number) { |
||||
await this.redisService.set( |
||||
this.getKey(chatItem.id, userId), |
||||
JSON.stringify(chatItem), |
||||
100000, |
||||
) |
||||
|
||||
await this.updateCacheMetadata(chatItem.id, userId) |
||||
} |
||||
|
||||
public async getFromCache(chatId: number, userId: number, clearAfterGet = false) { |
||||
try { |
||||
const key = this.getKey(chatId, userId) |
||||
const data = await this.redisService.get(key) |
||||
|
||||
if (!data) return null |
||||
const chat = JSON.parse(data) |
||||
|
||||
if (chat.id !== chatId) throw new Error('Invalid cache') |
||||
|
||||
if (clearAfterGet) { |
||||
this.redisService.del(key) |
||||
} |
||||
|
||||
return chat |
||||
} catch (e) { |
||||
return null |
||||
} |
||||
} |
||||
|
||||
public async remove(chatId: number) { |
||||
const usersIds = await this.getChatCacheMetadata(chatId) |
||||
if (!usersIds.length) { |
||||
return null |
||||
} |
||||
|
||||
for await (const userId of usersIds) { |
||||
await this.redisService.del(this.getKey(chatId, userId)) |
||||
} |
||||
|
||||
await this.redisService.del(this.getMetadataKey(chatId)) |
||||
} |
||||
|
||||
private getKey(chatId: number, userId: number) { |
||||
return `chat-cache/id:${chatId};userId:${userId}` |
||||
} |
||||
|
||||
private async updateCacheMetadata(chatId: number, userId: number) { |
||||
const exist = await this.getChatCacheMetadata(chatId) |
||||
|
||||
await this.setCacheMetadata(chatId, [...exist, userId]) |
||||
} |
||||
|
||||
private async setCacheMetadata(chatId: number, usersIds: number[]) { |
||||
await this.redisService.set(this.getMetadataKey(chatId), [...new Set(usersIds)].join(',')) |
||||
} |
||||
|
||||
private async getChatCacheMetadata(chatId: number): Promise<number[]> { |
||||
try { |
||||
const exist = await this.redisService.get(this.getMetadataKey(chatId)) |
||||
|
||||
if (!exist || typeof exist !== 'string') return [] |
||||
|
||||
return exist.split(',').map(Number) |
||||
} catch (e) { |
||||
return [] |
||||
} |
||||
} |
||||
|
||||
private getMetadataKey(chatId: number) { |
||||
return `chat-cache/metadata/id:${chatId}` |
||||
} |
||||
} |
@ -0,0 +1,126 @@
@@ -0,0 +1,126 @@
|
||||
import { Inject, Injectable } from '@nestjs/common' |
||||
import { Chats } from 'src/core' |
||||
import { ChatsAccessoryService } from './chats-accessory.service' |
||||
import { CHATS_MESSAGES_REPOSITORY } from '../consts' |
||||
import { IChatsMessagesRepository } from '../interfaces' |
||||
import { ChatsCryptService } from './chats-crypt.service' |
||||
import { transformFileUrl } from 'src/shared/transforms' |
||||
import * as _ from 'lodash' |
||||
|
||||
@Injectable() |
||||
export class ChatsForwardMessagesService implements Chats.IChatForwardService { |
||||
constructor( |
||||
private readonly chatsAccessoryService: ChatsAccessoryService, |
||||
private readonly chatsCryptService: ChatsCryptService, |
||||
|
||||
@Inject(CHATS_MESSAGES_REPOSITORY) |
||||
private readonly chatsMessagesRepository: IChatsMessagesRepository, |
||||
) {} |
||||
|
||||
public async forwardMessages({ chatId, messagesIds, userId }: Chats.IForwardMessagesPayload) { |
||||
const chat = await this.chatsAccessoryService.selectChatData(chatId, ['salt', 'type', 'id']) |
||||
await this.chatsAccessoryService.isUserChatMember(chatId, userId) |
||||
|
||||
const originMessages = await this.loadOriginMessages(messagesIds) |
||||
let forwardedMessages = await this.saveNewMessages( |
||||
originMessages, |
||||
chat.salt, |
||||
chatId, |
||||
userId, |
||||
) |
||||
forwardedMessages = forwardedMessages.sort((a, b) => a.id - b.id) |
||||
|
||||
await this.chatsAccessoryService.afterSendManyMessages(chatId, forwardedMessages, chat.type) |
||||
} |
||||
|
||||
private async loadOriginMessages(messagesIds: number[]) { |
||||
const messages = await this.chatsMessagesRepository.findByIds(messagesIds, { |
||||
select: ['id', 'type', 'chatId', 'userId', 'isPined', 'createdAt', 'content'], |
||||
}) |
||||
const originChat = await this.chatsAccessoryService.selectChatData(messages[0].chatId, [ |
||||
'salt', |
||||
'type', |
||||
]) |
||||
|
||||
return await Promise.all( |
||||
messages.map(async message => { |
||||
return await this.prepareOriginMessage(message, originChat.salt) |
||||
}), |
||||
) |
||||
} |
||||
|
||||
private async prepareOriginMessage( |
||||
message: Chats.IChatMessage, |
||||
chatSalt: string, |
||||
): Promise<Chats.ITransformedMessage> { |
||||
const transformedMessage: Chats.ITransformedMessage = { |
||||
...message, |
||||
content: null, |
||||
events: null, |
||||
} |
||||
const decoded = await this.chatsCryptService.decodeMessage(message.content, chatSalt) |
||||
transformedMessage.content = JSON.parse(decoded) |
||||
transformedMessage.content = _.omit(transformedMessage.content, 'replyToMessage') |
||||
|
||||
if (this.getFileUrl(transformedMessage)) this.transformUrl(transformedMessage) |
||||
|
||||
return transformedMessage |
||||
} |
||||
|
||||
private async saveNewMessages( |
||||
originMessages: Chats.ITransformedMessage[], |
||||
chatSalt: string, |
||||
chatId: number, |
||||
userId: number, |
||||
) { |
||||
const preparedMessages = [] |
||||
|
||||
const createdAt = new Date().getTime() |
||||
|
||||
await Promise.all( |
||||
originMessages.map(async (item, index) => { |
||||
const message = await this.prepareNewMessage(item, chatSalt) |
||||
preparedMessages[index] = { |
||||
...message, |
||||
chatId, |
||||
userId, |
||||
createdAt: new Date(createdAt + index).toISOString(), |
||||
} |
||||
}), |
||||
) |
||||
|
||||
const result = await this.chatsMessagesRepository.save(preparedMessages) |
||||
|
||||
return result |
||||
} |
||||
|
||||
private async prepareNewMessage(originalMessage: Chats.ITransformedMessage, chatSalt: string) { |
||||
const messageContent: Record<string, any> = { originalMessage } |
||||
const encodedMessage = await this.chatsCryptService.encodeMessage( |
||||
JSON.stringify(messageContent), |
||||
chatSalt, |
||||
) |
||||
|
||||
return { |
||||
content: encodedMessage, |
||||
type: Chats.MessageType.Forwarded, |
||||
salt: chatSalt, |
||||
} |
||||
} |
||||
|
||||
private transformUrl(message: Chats.ITransformedMessage) { |
||||
if (message.content.fileUrl) |
||||
message.content.fileUrl = transformFileUrl(message.content.fileUrl) |
||||
} |
||||
|
||||
private getFileUrl(message: Chats.ITransformedMessage) { |
||||
if ( |
||||
(message.type === Chats.MessageType.Audio || |
||||
message.type === Chats.MessageType.File || |
||||
message.type === Chats.MessageType.Image || |
||||
message.type === Chats.MessageType.Video) && |
||||
message.content |
||||
) |
||||
return message.content.fileUrl |
||||
} |
||||
} |
@ -0,0 +1,72 @@
@@ -0,0 +1,72 @@
|
||||
import { Inject, Injectable } from '@nestjs/common' |
||||
import { SECRET_MOD_SERVICE } from 'src/domain/secret-mod/consts' |
||||
import { SecretMod } from 'src/domain/secret-mod/typing' |
||||
import { RedisService } from 'src/libs' |
||||
import { IChatsMembersRepository } from '../interfaces' |
||||
import { CHATS_MEMBERS_REPOSITORY } from '../consts' |
||||
|
||||
@Injectable() |
||||
export class ChatsSecretService { |
||||
private REDIS_KEY = 'secret-chats' |
||||
|
||||
constructor( |
||||
@Inject(SECRET_MOD_SERVICE) |
||||
private readonly secretModService: SecretMod.Service, |
||||
|
||||
@Inject(CHATS_MEMBERS_REPOSITORY) |
||||
private readonly chatsMembersRepository: IChatsMembersRepository, |
||||
|
||||
private readonly redisService: RedisService, |
||||
) {} |
||||
|
||||
public async allocateSecretChats() { |
||||
const hiddenUsersIds = await this.secretModService.getHiddenUsersIds() |
||||
if (!hiddenUsersIds) { |
||||
await this.clear() |
||||
} |
||||
|
||||
const result = await this.chatsMembersRepository |
||||
.createQueryBuilder('it') |
||||
.select('it.chatId', 'chatId') |
||||
.distinct(true) |
||||
.where('it.userId = ANY(:hiddenUsersIds)', { hiddenUsersIds }) |
||||
.getRawMany() |
||||
|
||||
const chatIds = result.map(it => it.chatId).sort((a, b) => a - b) |
||||
|
||||
await this.saveToRedis(chatIds) |
||||
} |
||||
|
||||
public async clear() { |
||||
await this.saveToRedis([]) |
||||
} |
||||
|
||||
public async getHiddenChatIds(requestedUserId?: number): Promise<number[]> { |
||||
if (!(await this.secretModService.isActive())) return [] |
||||
|
||||
const ids = await this.getFromRedis() |
||||
|
||||
return ids |
||||
} |
||||
|
||||
private async saveToRedis(ids: number[]) { |
||||
await this.redisService.set(this.REDIS_KEY, JSON.stringify(ids)) |
||||
} |
||||
|
||||
private async getFromRedis() { |
||||
try { |
||||
const data = await this.redisService.get(this.REDIS_KEY) |
||||
if (!data) return [] |
||||
|
||||
const result = JSON.parse(data) |
||||
return Array.isArray(result) ? result : [] |
||||
} catch (e) { |
||||
return [] |
||||
} |
||||
} |
||||
|
||||
public async isChatHidden(chatId: number) { |
||||
const ids = await this.getHiddenChatIds() |
||||
return ids.includes(chatId) |
||||
} |
||||
} |
@ -1,49 +0,0 @@
@@ -1,49 +0,0 @@
|
||||
import { Test } from '@nestjs/testing' |
||||
import { RedisService } from 'src/libs' |
||||
import { NotFoundException } from 'src/shared' |
||||
import { mockRedisService } from 'src/shared/mocks' |
||||
import { FACTORIES_REPOSITORY } from '../../consts' |
||||
import { IFactoriesRepository } from '../../interfaces' |
||||
import { FactoriesValidatorService } from '../../services' |
||||
|
||||
describe('Factories validator service', () => { |
||||
let service: FactoriesValidatorService |
||||
let repository: MockType<IFactoriesRepository> |
||||
|
||||
beforeEach(async () => { |
||||
const module = await Test.createTestingModule({ |
||||
providers: [ |
||||
FactoriesValidatorService, |
||||
{ |
||||
provide: RedisService, |
||||
useValue: mockRedisService(), |
||||
}, |
||||
{ |
||||
provide: FACTORIES_REPOSITORY, |
||||
useValue: { |
||||
findOne: jest.fn(), |
||||
}, |
||||
}, |
||||
], |
||||
}).compile() |
||||
|
||||
service = module.get(FactoriesValidatorService) |
||||
repository = module.get(FACTORIES_REPOSITORY) |
||||
}) |
||||
|
||||
it('should throw error if parent does not exist', async () => { |
||||
repository.findOne.mockReturnValueOnce(null) |
||||
|
||||
expect(service.validateParentId(3)).rejects.toBeInstanceOf(NotFoundException) |
||||
}) |
||||
|
||||
it('should return void if parent exists', async () => { |
||||
repository.findOne.mockReturnValueOnce({}) |
||||
|
||||
expect(service.validateParentId(3)).resolves.toBeUndefined() |
||||
}) |
||||
|
||||
it('should return void if no parent id provided', async () => { |
||||
expect(service.validateParentId(null)).resolves.toBeUndefined() |
||||
}) |
||||
}) |
@ -1,77 +0,0 @@
@@ -1,77 +0,0 @@
|
||||
import { Test } from '@nestjs/testing' |
||||
import { RedisService } from 'src/libs' |
||||
import { NotFoundException } from 'src/shared' |
||||
import { mockRedisService } from 'src/shared/mocks' |
||||
import { FACTORIES_REPOSITORY } from '../../consts' |
||||
import { IFactoriesRepository } from '../../interfaces' |
||||
import { FactoryTreesService } from '../../services' |
||||
|
||||
const factoryMock = { |
||||
id: 3, |
||||
authorId: 2, |
||||
name: 'Some factory', |
||||
shortName: 'sf', |
||||
path: ' | 6 | 4 | ', |
||||
} |
||||
|
||||
describe('Factory threes service', () => { |
||||
let service: FactoryTreesService |
||||
let repository: MockType<IFactoriesRepository> |
||||
|
||||
beforeEach(async () => { |
||||
const module = await Test.createTestingModule({ |
||||
providers: [ |
||||
FactoryTreesService, |
||||
{ |
||||
provide: RedisService, |
||||
useValue: mockRedisService(), |
||||
}, |
||||
{ |
||||
provide: FACTORIES_REPOSITORY, |
||||
useValue: { |
||||
findOne: jest.fn(), |
||||
find: jest.fn(), |
||||
}, |
||||
}, |
||||
], |
||||
}).compile() |
||||
|
||||
service = module.get(FactoryTreesService) |
||||
repository = module.get(FACTORIES_REPOSITORY) |
||||
}) |
||||
|
||||
it('should generate three list path', async () => { |
||||
repository.findOne.mockReturnValueOnce(factoryMock) |
||||
|
||||
expect(await service.generateThreeListPath(2)).toBe(' | 6 | 4 | 3 | ') |
||||
}) |
||||
|
||||
it('should throw an exception if no parent generating three list path', async () => { |
||||
repository.findOne.mockReturnValueOnce(null) |
||||
|
||||
expect(service.generateThreeListPath(2)).rejects.toBeInstanceOf(NotFoundException) |
||||
}) |
||||
|
||||
it('should check if factory is ', async () => { |
||||
expect(service.isAncestor(2, factoryMock)).toBeFalsy() |
||||
}) |
||||
|
||||
it('should check if id belongs to the ancestor of entity ', async () => { |
||||
expect(service.isAncestor(2, factoryMock)).toBeFalsy() |
||||
expect(service.isAncestor(4, factoryMock)).toBeTruthy() |
||||
expect(service.isAncestor(6, factoryMock)).toBeTruthy() |
||||
}) |
||||
|
||||
it('should return three like list with children', async () => { |
||||
repository.find |
||||
.mockReturnValueOnce([factoryMock]) |
||||
.mockReturnValueOnce([ |
||||
{ ...factoryMock, id: 10 }, |
||||
{ ...factoryMock, id: 12 }, |
||||
]) |
||||
.mockReturnValue([]) |
||||
|
||||
const res = await service.getTreeLikeList(1) |
||||
expect(res[0].children).toHaveLength(2) |
||||
}) |
||||
}) |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
export * from './logger-exception.filter' |
@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common' |
||||
import { HttpAdapterHost } from '@nestjs/core' |
||||
import { LoggerService } from '../services' |
||||
|
||||
@Catch() |
||||
export class LoggerExceptionsFilter implements ExceptionFilter { |
||||
constructor( |
||||
private readonly httpAdapterHost: HttpAdapterHost, |
||||
private readonly loggerService: LoggerService, |
||||
) {} |
||||
|
||||
catch(exception: unknown, host: ArgumentsHost): void { |
||||
const { httpAdapter } = this.httpAdapterHost |
||||
|
||||
if (!httpAdapter) { |
||||
throw new Error('HttpAdapter is not available.') |
||||
} |
||||
|
||||
const ctx = host.switchToHttp() |
||||
|
||||
const httpStatus = |
||||
exception instanceof HttpException |
||||
? exception.getStatus() |
||||
: HttpStatus.INTERNAL_SERVER_ERROR |
||||
|
||||
if ( |
||||
httpStatus >= HttpStatus.BAD_REQUEST && |
||||
httpStatus <= HttpStatus.HTTP_VERSION_NOT_SUPPORTED |
||||
) { |
||||
this.loggerService.logException(exception) |
||||
} |
||||
|
||||
const responseBody = { |
||||
statusCode: httpStatus, |
||||
timestamp: new Date().toISOString(), |
||||
path: httpAdapter.getRequestUrl(ctx.getRequest()), |
||||
} |
||||
|
||||
httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus) |
||||
|
||||
throw exception |
||||
} |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
export * from './services' |
||||
export * from './typing' |
||||
export * from './filter' |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
import { Global, Module } from '@nestjs/common' |
||||
import { LoggerService } from './services' |
||||
|
||||
@Global() |
||||
@Module({ |
||||
providers: [LoggerService], |
||||
exports: [LoggerService], |
||||
}) |
||||
export class LoggerModule {} |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
export * from './logger.service' |
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
import * as Sentry from '@sentry/node' |
||||
import { LoggerMessage } from '../typing/enums' |
||||
import { getEnv } from 'src/shared' |
||||
|
||||
export class LoggerService { |
||||
constructor() { |
||||
this.init() |
||||
} |
||||
|
||||
private init(): void { |
||||
Sentry.init({ |
||||
dsn: getEnv('SENTRY_DNS'), |
||||
environment: getEnv('SENTRY_ENVIROMENT'), |
||||
integrations: [], |
||||
tracesSampleRate: 1.0, |
||||
profilesSampleRate: 1.0, |
||||
}) |
||||
} |
||||
|
||||
public logEvent( |
||||
message: LoggerMessage, |
||||
user?: { id: string }, |
||||
extra?: any, |
||||
tags?: Record<string, string>, |
||||
): void { |
||||
try { |
||||
Sentry.captureEvent({ |
||||
message, |
||||
user, |
||||
extra, |
||||
level: 'log', |
||||
tags, |
||||
}) |
||||
} catch (error) { |
||||
console.error('Error logging event:', error) |
||||
} |
||||
} |
||||
|
||||
public logException(error: any, extra?: any, tags?: Record<string, string>): void { |
||||
try { |
||||
Sentry.captureException(error, { extra, tags }) |
||||
} catch (error) { |
||||
console.error('Error logging exception:', error) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
export * from './logger-messages.enum' |
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
export enum LoggerMessage { |
||||
UserLoggedOut = 'user logged out', |
||||
|
||||
UserLoggedIn = 'user logged in', |
||||
|
||||
UserStatusChanged = 'user status changed', |
||||
|
||||
RefReshToken = 'refresh token', |
||||
|
||||
FinishSession = 'finish session', |
||||
|
||||
FinishSessionByToken = 'finish session by token', |
||||
|
||||
FinishAllUserSessions = 'finish all user sessions', |
||||
} |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
export * from './enums' |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue