diff --git a/config/defaults.yaml b/config/defaults.yaml index f3dd41ec..3eaef157 100644 --- a/config/defaults.yaml +++ b/config/defaults.yaml @@ -182,6 +182,9 @@ agora: - productID: eventType: secret: + ai: + server_cn: "http://43.131.39.44:8082" + server_en: "http://43.131.39.44:8081" whiteboard: app_id: diff --git a/config/test.yaml b/config/test.yaml index ba2f987a..90effbef 100644 --- a/config/test.yaml +++ b/config/test.yaml @@ -177,6 +177,8 @@ agora: - productID: eventType: secret: + ai: + server: "http://106.13.114.185:8081" whiteboard: app_id: "test/flat-server" diff --git a/src/constants/Config.ts b/src/constants/Config.ts index 203d8057..ff464efc 100644 --- a/src/constants/Config.ts +++ b/src/constants/Config.ts @@ -160,6 +160,10 @@ export const Agora = { enable: config.agora.messageNotification.enable, events: config.agora.messageNotification.events, }, + ai: { + server_cn: config.agora.ai.server_cn, + server_en: config.agora.ai.server_en, + } }; export const JWT = { diff --git a/src/dao/index.ts b/src/dao/index.ts index 4011a4fa..6e955064 100644 --- a/src/dao/index.ts +++ b/src/dao/index.ts @@ -19,6 +19,7 @@ import { UserPhoneModel } from "../model/user/Phone"; import { UserSensitiveModel } from "../model/user/Sensitive"; import { UserEmailModel } from "../model/user/Email"; import { UserPmiModel } from "../model/user/Pmi"; +import { UserAgreementModel } from "../model/user/Agreement"; export const UserDAO = DAOImplement(UserModel) as ReturnType>; @@ -42,6 +43,8 @@ export const UserSensitiveDAO = DAOImplement(UserSensitiveModel) as ReturnType< export const UserPmiDAO = DAOImplement(UserPmiModel) as ReturnType>; +export const UserAgreementDAO = DAOImplement(UserAgreementModel) as ReturnType>; + export const RoomDAO = DAOImplement(RoomModel) as ReturnType>; export const RoomUserDAO = DAOImplement(RoomUserModel) as ReturnType>; diff --git a/src/model/index.ts b/src/model/index.ts index 26cbb958..bc92cc91 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -20,6 +20,7 @@ import { OAuthUsersModel } from "./oauth/oauth-users"; import { UserSensitiveModel } from "./user/Sensitive"; import { UserEmailModel } from "./user/Email"; import { UserPmiModel } from "./user/Pmi"; +import { UserAgreementModel } from "./user/Agreement"; import { PartnerModel } from "./partner/Partner"; import { PartnerRoomModel } from "./partner/PartnerRoom"; @@ -34,6 +35,7 @@ export type Model = | UserEmailModel | UserSensitiveModel | UserPmiModel + | UserAgreementModel | RoomModel | RoomUserModel | RoomPeriodicConfigModel diff --git a/src/model/room/Room.ts b/src/model/room/Room.ts index cce04ae7..0addb78e 100644 --- a/src/model/room/Room.ts +++ b/src/model/room/Room.ts @@ -89,4 +89,9 @@ export class RoomModel extends Content { default: false, }) has_record: boolean; + + @Column({ + default: false, + }) + is_ai: boolean; } diff --git a/src/model/room/RoomUser.ts b/src/model/room/RoomUser.ts index 5dd0e0bb..9b954729 100644 --- a/src/model/room/RoomUser.ts +++ b/src/model/room/RoomUser.ts @@ -29,6 +29,12 @@ export class RoomUserModel extends Content { }) rtc_uid: string; + @Column({ + default: -1, + type: "int", + }) + grade: number; + @Index("room_users_is_delete_index") @Column({ default: false, diff --git a/src/model/user/Agreement.ts b/src/model/user/Agreement.ts new file mode 100644 index 00000000..eb86a4be --- /dev/null +++ b/src/model/user/Agreement.ts @@ -0,0 +1,26 @@ +import { Column, Entity, Index } from "typeorm"; +import { Content } from "../Content"; + +@Entity({ + name: "user_agreement", +}) +export class UserAgreementModel extends Content { + @Index("user_agreement_user_uuid_uindex", { + unique: true, + }) + @Column({ + length: 40, + }) + user_uuid: string; + + @Column({ + default: false, + }) + is_agree_collect_data: boolean; + + @Index("user_agreement_is_delete_index") + @Column({ + default: false, + }) + is_delete: boolean; +} diff --git a/src/thirdPartyService/TypeORMService.ts b/src/thirdPartyService/TypeORMService.ts index 2f2d15eb..59daf9a0 100644 --- a/src/thirdPartyService/TypeORMService.ts +++ b/src/thirdPartyService/TypeORMService.ts @@ -23,6 +23,7 @@ import { OAuthSecretsModel } from "../model/oauth/oauth-secrets"; import { OAuthUsersModel } from "../model/oauth/oauth-users"; import { UserEmailModel } from "../model/user/Email"; import { UserPmiModel } from "../model/user/Pmi"; +import { UserAgreementModel } from "../model/user/Agreement"; import { PartnerModel } from "../model/partner/Partner"; import { PartnerRoomModel } from "../model/partner/PartnerRoom"; @@ -44,6 +45,7 @@ export const dataSource = new DataSource({ UserEmailModel, UserSensitiveModel, UserPmiModel, + UserAgreementModel, RoomModel, RoomUserModel, RoomPeriodicConfigModel, diff --git a/src/utils/ParseConfig.ts b/src/utils/ParseConfig.ts index d4a828f7..18c06c81 100644 --- a/src/utils/ParseConfig.ts +++ b/src/utils/ParseConfig.ts @@ -202,6 +202,10 @@ type Config = { secret: string; }>; }; + ai: { + server_cn: string; + server_en: string; + } }; whiteboard: { app_id: string; diff --git a/src/v1/controller/agora/Router.ts b/src/v1/controller/agora/Router.ts index 0d039698..f56f614f 100644 --- a/src/v1/controller/agora/Router.ts +++ b/src/v1/controller/agora/Router.ts @@ -3,10 +3,16 @@ import { GenerateRTM } from "./token/RTM"; import { ControllerClass } from "../../../abstract/controller"; import { MessageCallback } from "./message/Callback"; import { RTMCensor } from "./rtm/censor"; +import { AgoraAIPing } from "./ai/ping"; +import { AgoraAIStart } from "./ai/start"; +import { AgoraAIStop } from "./ai/stop"; export const agoraRouters: Readonly>> = Object.freeze([ GenerateRTC, GenerateRTM, MessageCallback, RTMCensor, + AgoraAIPing, + AgoraAIStart, + AgoraAIStop ]); diff --git a/src/v1/controller/agora/ai/const.ts b/src/v1/controller/agora/ai/const.ts new file mode 100644 index 00000000..51513d1f --- /dev/null +++ b/src/v1/controller/agora/ai/const.ts @@ -0,0 +1,4 @@ +import { Agora } from "../../../../constants/Config"; + +export const AI_SERVER_URL_CN = Agora.ai.server_cn || "http://43.131.39.44:8082"; +export const AI_SERVER_URL_EN = Agora.ai.server_en || "http://43.131.39.44:8081"; \ No newline at end of file diff --git a/src/v1/controller/agora/ai/ping.ts b/src/v1/controller/agora/ai/ping.ts new file mode 100644 index 00000000..62bed14c --- /dev/null +++ b/src/v1/controller/agora/ai/ping.ts @@ -0,0 +1,63 @@ +import { FastifySchema, Response, ResponseError } from "../../../../types/Server"; +import { ax } from "../../../utils/Axios"; +import { AbstractController } from "../../../../abstract/controller"; +import { Controller } from "../../../../decorator/Controller"; +import { AI_SERVER_URL_CN, AI_SERVER_URL_EN } from "./const"; +import { Status } from "../../../../constants/Project"; + +@Controller({ + method: "post", + path: "agora/ai/ping", + auth: true, +}) +export class AgoraAIPing extends AbstractController { + public static readonly schema: FastifySchema = { + body: { + type: "object", + required: ["request_id", "channel_name", "language"], + properties: { + request_id: { + type: "string", + }, + channel_name: { + type: "string", + }, + language: { + type: "string", + } + }, + }, + }; + + public async execute(): Promise> { + const { request_id, channel_name, language } = this.body; + const api = language === "zh" ? AI_SERVER_URL_CN : AI_SERVER_URL_EN; + const res = await ax.post(`${api}/ping`, { + request_id, + channel_name + }, + { + headers: { + "Content-Type": "application/json" + } + } + ) + return { + status: Status.Success, + data: res.data, + } + + } + + public errorHandler(error: Error): ResponseError { + return this.autoHandlerError(error); + } +} + +interface RequestType { + body: { + request_id: string; + channel_name: string; + language: string; + }; +} diff --git a/src/v1/controller/agora/ai/start.ts b/src/v1/controller/agora/ai/start.ts new file mode 100644 index 00000000..a98e36af --- /dev/null +++ b/src/v1/controller/agora/ai/start.ts @@ -0,0 +1,72 @@ +import { FastifySchema, Response, ResponseError } from "../../../../types/Server"; +import { ax } from "../../../utils/Axios"; +import { AbstractController } from "../../../../abstract/controller"; +import { Controller } from "../../../../decorator/Controller"; +import { AI_SERVER_URL_CN, AI_SERVER_URL_EN } from "./const"; +import { Status } from "../../../../constants/Project"; +@Controller({ + method: "post", + path: "agora/ai/start", + auth: true, +}) +export class AgoraAIStart extends AbstractController { + public static readonly schema: FastifySchema = { + body: { + type: "object", + required: ["request_id", "channel_name", "user_uid", "language", "role"], + properties: { + request_id: { + type: "string", + }, + channel_name: { + type: "string", + }, + user_uid: { + type: "number", + }, + language: { + type: "string", + }, + role: { + type: "string", + }, + }, + }, + }; + + public async execute(): Promise> { + const { request_id, channel_name, user_uid, language, role } = this.body; + const api = language === "zh" ? AI_SERVER_URL_CN : AI_SERVER_URL_EN; + const res = await ax.post(`${api}/start`, { + request_id, + channel_name, + user_uid, + timbre_type: role + }, + { + headers: { + "Content-Type": "application/json" + } + } + ) + + return { + status: Status.Success, + data: res.data, + } + } + + public errorHandler(error: Error): ResponseError { + return this.autoHandlerError(error); + } +} + +interface RequestType { + body: { + request_id: string; + channel_name: string; + user_uid: number; + language: string; + role: string; + }; +} diff --git a/src/v1/controller/agora/ai/stop.ts b/src/v1/controller/agora/ai/stop.ts new file mode 100644 index 00000000..26d5c248 --- /dev/null +++ b/src/v1/controller/agora/ai/stop.ts @@ -0,0 +1,62 @@ +import { FastifySchema, Response, ResponseError } from "../../../../types/Server"; +import { ax } from "../../../utils/Axios"; +import { AbstractController } from "../../../../abstract/controller"; +import { Controller } from "../../../../decorator/Controller"; +import { AI_SERVER_URL_CN, AI_SERVER_URL_EN } from "./const"; +import { Status } from "../../../../constants/Project"; + +@Controller({ + method: "post", + path: "agora/ai/stop", + auth: true, +}) +export class AgoraAIStop extends AbstractController { + public static readonly schema: FastifySchema = { + body: { + type: "object", + required: ["request_id", "channel_name", "language"], + properties: { + request_id: { + type: "string", + }, + channel_name: { + type: "string", + }, + language: { + type: "string", + } + }, + }, + }; + + public async execute(): Promise> { + const { request_id, channel_name, language } = this.body; + const api = language === "zh" ? AI_SERVER_URL_CN : AI_SERVER_URL_EN; + const res = await ax.post(`${api}/start`, { + request_id, + channel_name, + }, + { + headers: { + "Content-Type": "application/json" + } + } + ) + return { + status: Status.Success, + data: res.data, + } + } + + public errorHandler(error: Error): ResponseError { + return this.autoHandlerError(error); + } +} + +interface RequestType { + body: { + request_id: string; + channel_name: string; + language: string; + }; +} diff --git a/src/v1/controller/room/Router.ts b/src/v1/controller/room/Router.ts index 681a27d9..501dfe7c 100644 --- a/src/v1/controller/room/Router.ts +++ b/src/v1/controller/room/Router.ts @@ -25,6 +25,8 @@ import { UpdatePeriodic } from "./update/Periodic"; import { RecordStarted } from "./record/Started"; import { RecordStopped } from "./record/Stopped"; import { ControllerClass } from "../../../abstract/controller"; +import { SetGrade } from "./grade/Set"; +import { GetGrade } from "./grade/Get"; export const roomRouters: Readonly>> = Object.freeze([ CreateOrdinary, @@ -53,4 +55,6 @@ export const roomRouters: Readonly>> = Object.fr RecordAgoraUpdateLayout, RecordAgoraStopped, RecordInfo, + SetGrade, + GetGrade ]); diff --git a/src/v1/controller/room/create/Ordinary.ts b/src/v1/controller/room/create/Ordinary.ts index 860f5c4f..b8caa59a 100644 --- a/src/v1/controller/room/create/Ordinary.ts +++ b/src/v1/controller/room/create/Ordinary.ts @@ -58,6 +58,10 @@ export class CreateOrdinary extends AbstractController({ + method: "post", + path: "user/grade/get", + auth: true, +}) +export class GetGrade extends AbstractController { + public static readonly schema: FastifySchema = { + body: { + type: "object", + required: ["roomUUID", "userUUID"], + properties: { + roomUUID: { + type: "string", + }, + userUUID: { + type: "string", + } + }, + }, + }; + + public async execute(): Promise> { + const { roomUUID, userUUID } = this.body; + + const roomUserInfo = await RoomUserDAO().findOne(["grade"], { + user_uuid: userUUID, + room_uuid: roomUUID, + }); + + if (roomUserInfo === undefined) { + return { + status: Status.Failed, + code: ErrorCode.UserNotFound, + }; + } + + return { + status: Status.Success, + data: { + grade: roomUserInfo.grade === -1 ? undefined : roomUserInfo.grade, + }, + }; + + } + + public errorHandler(error: Error): ResponseError { + return this.autoHandlerError(error); + } +} +type ResponseType = { + grade?: number; +}; + +interface RequestType { + body: { + roomUUID: string; + userUUID: string; + }; +} \ No newline at end of file diff --git a/src/v1/controller/room/grade/Set.ts b/src/v1/controller/room/grade/Set.ts new file mode 100644 index 00000000..f9e08038 --- /dev/null +++ b/src/v1/controller/room/grade/Set.ts @@ -0,0 +1,74 @@ +import { FastifySchema, Response, ResponseError } from "../../../../types/Server"; +import { Status } from "../../../../constants/Project"; +import { ErrorCode } from "../../../../ErrorCode"; +import { RoomUserDAO } from "../../../../dao"; +import { Controller } from "../../../../decorator/Controller"; +import { AbstractController } from "../../../../abstract/controller"; + +@Controller({ + method: "post", + path: "user/grade/set", + auth: true, +}) +export class SetGrade extends AbstractController { + public static readonly schema: FastifySchema = { + body: { + type: "object", + required: ["roomUUID", "userUUID", "grade"], + properties: { + roomUUID: { + type: "string", + }, + userUUID: { + type: "string", + }, + grade: { + type: "integer", + minimum: 0, + maximum: 5, + } + }, + }, + }; + + public async execute(): Promise> { + const { roomUUID, userUUID, grade } = this.body; + + const roomUserInfo = await RoomUserDAO().findOne(["id"], { + user_uuid: userUUID, + room_uuid: roomUUID, + }); + + if (roomUserInfo === undefined) { + return { + status: Status.Failed, + code: ErrorCode.UserNotFound, + }; + } + + await RoomUserDAO().update({ + grade, + },{ + room_uuid: roomUUID, + user_uuid: userUUID, + }); + + return { + status: Status.Success, + data: null, + }; + + } + + public errorHandler(error: Error): ResponseError { + return this.autoHandlerError(error); + } +} + +interface RequestType { + body: { + roomUUID: string; + userUUID: string; + grade: number; + }; +} \ No newline at end of file diff --git a/src/v1/controller/room/grade/__tests__/grade.test.ts b/src/v1/controller/room/grade/__tests__/grade.test.ts new file mode 100644 index 00000000..d10a773f --- /dev/null +++ b/src/v1/controller/room/grade/__tests__/grade.test.ts @@ -0,0 +1,102 @@ +import test from "ava"; +import { dataSource } from "../../../../../thirdPartyService/TypeORMService"; +import { v4 } from "uuid"; +import { createRoom, createRoomUser, createUsersRequest, getUserGradeRequest, setUserGradeRequest } from "./helpers/createUsersRequest"; +import { Status } from "../../../../../constants/Project"; + +const namespace = "[api][api-v1][api-v1-room][api-v1-room-info][api-v1-room-info-user]"; + +test.before(`${namespace} - initialize dataSource`, async () => { + await dataSource.initialize(); +}); + +test.after(`${namespace} - destroy dataSource`, async () => { + await dataSource.destroy(); +}); + +test(`${namespace} - show specify user info`, async ava => { + const [roomUUID] = [v4()]; + + const [ownerUUID, ...usersUUID] = await createRoomUser(roomUUID, 5); + + await createRoom(ownerUUID, roomUUID); + + const specifyUsers = usersUUID.splice(-2); + + const userRequest = createUsersRequest( + { + roomUUID, + usersUUID: specifyUsers, + }, + ownerUUID, + ); + + const result = await userRequest.execute(); + + ava.is(result.status, Status.Success); + + const data = (result as any).data; + + ava.is(Object.keys(data).length, 2); + + ava.not(data[specifyUsers[0]], undefined); +}); + +test(`${namespace} - show all user info`, async ava => { + const [roomUUID] = [v4()]; + + const [ownerUUID] = await createRoomUser(roomUUID, 4); + + await createRoom(ownerUUID, roomUUID); + + const userRequest = createUsersRequest( + { + roomUUID, + }, + ownerUUID, + ); + + const result = await userRequest.execute(); + + ava.is(result.status, Status.Success); + + const data = (result as any).data; + + ava.is(Object.keys(data).length, 4); +}); + +test(`${namespace} - test grade info`, async ava => { + const [roomUUID] = [v4()]; + + const [ownerUUID] = await createRoomUser(roomUUID, 4); + + await createRoom(ownerUUID, roomUUID); + + const setGradeRequest = setUserGradeRequest( + { + roomUUID, + userUUID: ownerUUID, + grade: 3, + } + ); + + const result = await setGradeRequest.execute(); + + ava.is(result.status, Status.Success); + + const getGradeRequest = getUserGradeRequest( + { + roomUUID, + userUUID: ownerUUID, + } + ); + + const result1 = await getGradeRequest.execute(); + + ava.is(result1.status, Status.Success); + + const data = (result1 as any).data; + + ava.is(data.grade, 3); +}); + diff --git a/src/v1/controller/room/grade/__tests__/helpers/createUsersRequest.ts b/src/v1/controller/room/grade/__tests__/helpers/createUsersRequest.ts new file mode 100644 index 00000000..07c0f0ce --- /dev/null +++ b/src/v1/controller/room/grade/__tests__/helpers/createUsersRequest.ts @@ -0,0 +1,120 @@ +import { UserInfo } from "../../../info/Users"; +import { Logger } from "../../../../../../logger"; +import { ControllerClassParams } from "../../../../../../abstract/controller"; +import { v4 } from "uuid"; +import { RoomDAO, RoomUserDAO, UserDAO } from "../../../../../../dao"; +import { RoomStatus, RoomType } from "../../../../../../model/room/Constants"; +import { Region } from "../../../../../../constants/Project"; +import cryptoRandomString from "crypto-random-string"; +import { SetGrade } from "../../Set"; +import { GetGrade } from "../../Get"; + +export const createUsersRequest = ( + body: { + roomUUID: string; + usersUUID?: string[]; + }, + userUUID: string, +): UserInfo => { + const logger = new Logger("test", {}, []); + + return new UserInfo({ + logger, + req: { + body: body, + user: { + userUUID, + }, + }, + reply: {}, + } as ControllerClassParams); +}; + +export const createRoom = async ( + ownerUUID: string, + roomUUID: string, + roomStatus: RoomStatus = RoomStatus.Stopped, + beginTime?: Date +): Promise => { + await RoomDAO().insert({ + room_uuid: roomUUID, + periodic_uuid: "", + room_status: roomStatus, + begin_time: beginTime || new Date(), + end_time: new Date(), + title: "test", + room_type: RoomType.BigClass, + region: Region.GB_LON, + owner_uuid: ownerUUID, + whiteboard_room_uuid: v4(), + }); +}; + +export const createRoomUser = async (roomUUID: string, count: number): Promise => { + const usersUUID = new Array(count).fill(null).map(() => v4()); + + const commands: Promise[] = []; + + for (const userUUID of usersUUID) { + commands.push( + UserDAO().insert({ + user_uuid: userUUID, + user_name: "test_name", + avatar_url: "xxx", + user_password: "", + }), + RoomUserDAO().insert({ + user_uuid: userUUID, + room_uuid: roomUUID, + rtc_uid: cryptoRandomString({ length: 6, type: "numeric" }), + grade: -1, + }), + ); + } + + await Promise.all(commands); + + return usersUUID; +}; + + +export const setUserGradeRequest = ( + body: { + roomUUID: string; + userUUID: string; + grade: number; + }, +): SetGrade => { + const logger = new Logger("test", {}, []); + + return new SetGrade({ + logger, + req: { + body: body, + user: { + userUUID: body.userUUID, + }, + }, + reply: {}, + } as ControllerClassParams); +}; + +export const getUserGradeRequest = ( + body: { + roomUUID: string; + userUUID: string; + }, +): GetGrade => { + const logger = new Logger("test", {}, []); + + return new GetGrade({ + logger, + req: { + body: body, + user: { + userUUID: body.userUUID, + }, + }, + reply: {}, + } as ControllerClassParams); +}; \ No newline at end of file diff --git a/src/v1/controller/room/info/Ordinary.ts b/src/v1/controller/room/info/Ordinary.ts index 540c7bc3..3bc6a335 100644 --- a/src/v1/controller/room/info/Ordinary.ts +++ b/src/v1/controller/room/info/Ordinary.ts @@ -52,6 +52,7 @@ export class OrdinaryInfo extends AbstractController "region", "periodic_uuid", "has_record", + "is_ai" ], { room_uuid: roomUUID, @@ -75,6 +76,7 @@ export class OrdinaryInfo extends AbstractController region, periodic_uuid: periodicUUID, has_record, + is_ai } = roomInfo; const userInfo = await UserDAO().findOne(["user_name"], { @@ -110,6 +112,7 @@ export class OrdinaryInfo extends AbstractController region, inviteCode, isPmi, + isAI: is_ai }, }, }; @@ -140,5 +143,6 @@ interface ResponseType { region: Region; inviteCode: string; isPmi: boolean; + isAI?: boolean; }; } diff --git a/src/v1/controller/room/info/Users.ts b/src/v1/controller/room/info/Users.ts index c8622ae1..765d3863 100644 --- a/src/v1/controller/room/info/Users.ts +++ b/src/v1/controller/room/info/Users.ts @@ -74,12 +74,13 @@ export class UserInfo extends AbstractController { const roomUsersInfo = await roomUsersInfoBasic.getRawMany(); const result: ResponseType = {}; - for (const { user_name, user_uuid, rtc_uid, avatar_url, is_delete } of roomUsersInfo) { + for (const { user_name, user_uuid, rtc_uid, avatar_url, is_delete, grade } of roomUsersInfo) { result[user_uuid] = { fake: user_name === null, name: user_name || user_uuid.slice(-8), rtcUID: is_delete ? -1 : Number(rtc_uid), avatarURL: avatar_url || generateAvatar(user_uuid), + grade }; } @@ -107,10 +108,10 @@ type ResponseType = { name: string; rtcUID: number; avatarURL: string; + grade: number; }; }; type Nullable = { [P in keyof T]: T[P] | null }; -type RoomUsersInfo = Pick & - Nullable>; +type RoomUsersInfo = Pick & Nullable>; diff --git a/src/v1/controller/room/join/Ordinary.ts b/src/v1/controller/room/join/Ordinary.ts index 79d528df..60001947 100644 --- a/src/v1/controller/room/join/Ordinary.ts +++ b/src/v1/controller/room/join/Ordinary.ts @@ -25,6 +25,7 @@ export const joinOrdinary = async ( "owner_uuid", "region", "begin_time", + "is_ai" ], { room_uuid: roomUUID, @@ -121,6 +122,7 @@ export const joinOrdinary = async ( rtmToken: await getRTMToken(userUUID), region: roomInfo.region, showGuide: roomInfo.owner_uuid === userUUID && (await showGuide(userUUID, roomUUID)), + isAI: roomInfo.is_ai, }, }; }; diff --git a/src/v1/controller/room/join/Type.ts b/src/v1/controller/room/join/Type.ts index 5d01b1d2..6b9bdddc 100644 --- a/src/v1/controller/room/join/Type.ts +++ b/src/v1/controller/room/join/Type.ts @@ -16,4 +16,5 @@ export type ResponseType = { rtmToken: string; region: Region; showGuide: boolean; + isAI?: boolean; }; diff --git a/src/v1/controller/room/list/index.ts b/src/v1/controller/room/list/index.ts index d3eecb26..2d5ed1d1 100644 --- a/src/v1/controller/room/list/index.ts +++ b/src/v1/controller/room/list/index.ts @@ -68,6 +68,7 @@ export class List extends AbstractController { hasRecord: !!room.hasRecord, inviteCode: inviteCodes[index] || room.periodicUUID || room.roomUUID, isPmi: false, + isAI: room.isAI }; }); @@ -102,6 +103,7 @@ export class List extends AbstractController { .addSelect("r.owner_uuid", "ownerUUID") .addSelect("r.room_status", "roomStatus") .addSelect("r.region", "region") + .addSelect("r.is_ai", "isAI") .addSelect("u.user_name", "ownerName") .addSelect("u.avatar_url", "ownerAvatarURL") .andWhere("ru.user_uuid = :userUUID", { @@ -182,4 +184,5 @@ export type ResponseType = Array<{ region: Region; inviteCode: string; isPmi: boolean; + isAI: boolean; }>; diff --git a/src/v1/controller/user/Router.ts b/src/v1/controller/user/Router.ts index ab7fdf10..37093a5a 100644 --- a/src/v1/controller/user/Router.ts +++ b/src/v1/controller/user/Router.ts @@ -17,6 +17,9 @@ import { BindingGithub } from "./binding/platform/github/Binding"; import { BindingGoogle } from "./binding/platform/google/Binding"; import { BindingEmail } from "./binding/platform/email/Binding"; import { BindingApple } from "./binding/platform/apple/Binding"; +import { AgreementSet } from "./agreement/Set"; +import { AgreementGet } from "./agreement/Get"; +import { AgreementGetToRtc } from "./agreement/GetToRtc"; export const userRouters: Readonly>> = Object.freeze([ Rename, @@ -38,4 +41,7 @@ export const userRouters: Readonly>> = Object.fr DeleteAccount, UploadAvatarStart, UploadAvatarFinish, + AgreementSet, + AgreementGet, + AgreementGetToRtc ]); diff --git a/src/v1/controller/user/agreement/Get.ts b/src/v1/controller/user/agreement/Get.ts new file mode 100644 index 00000000..1de0f5a0 --- /dev/null +++ b/src/v1/controller/user/agreement/Get.ts @@ -0,0 +1,47 @@ +import { AbstractController, ControllerClassParams } from "../../../../abstract/controller"; +import { Status } from "../../../../constants/Project"; +import { Controller } from "../../../../decorator/Controller"; +import { FastifySchema, Response, ResponseError } from "../../../../types/Server"; + +import { ServiceUserAgreement } from "../../../service/user/UserAgreement"; + +@Controller({ + method: "post", + path: "user/agreement/get", + auth: true, +}) +export class AgreementGet extends AbstractController { + public static readonly schema: FastifySchema = {}; + + public readonly svc: { + userAgreement: ServiceUserAgreement; + }; + + public constructor(params: ControllerClassParams) { + super(params); + + this.svc = { + userAgreement: new ServiceUserAgreement(this.userUUID), + }; + } + + public async execute(): Promise> { + const isAgree = await this.svc.userAgreement.isAgreeCollectData(); + return { + status: Status.Success, + data: { + isAgree + } + } + } + + public errorHandler(error: Error): ResponseError { + return this.autoHandlerError(error); + } +} + +interface RequestType {} + +interface ResponseType { + isAgree: boolean; +} diff --git a/src/v1/controller/user/agreement/GetToRtc.ts b/src/v1/controller/user/agreement/GetToRtc.ts new file mode 100644 index 00000000..fb00ab42 --- /dev/null +++ b/src/v1/controller/user/agreement/GetToRtc.ts @@ -0,0 +1,108 @@ +import { AbstractController, ControllerClassParams } from "../../../../abstract/controller"; +import { Status } from "../../../../constants/Project"; +import { Controller } from "../../../../decorator/Controller"; +import { FastifySchema, Response, ResponseError } from "../../../../types/Server"; + +import { ServiceUserAgreement } from "../../../service/user/UserAgreement"; +import { RoomUserModel } from "../../../../model/room/RoomUser"; +import { dataSource } from "../../../../thirdPartyService/TypeORMService"; +import { UserAgreementModel } from "./../../../../model/user/Agreement"; + +@Controller({ + method: "get", + path: "private-polic/get", + auth: false, + skipAutoHandle: false, + enable: true +}) +export class AgreementGetToRtc extends AbstractController { + public static readonly schema: FastifySchema = { + querystring: { + type: "object", + required: ["uid"], + properties: { + uid: { + type: "string" + }, + room_uuid: { + type: "string" + } + }, + }, + }; + + public readonly svc: { + userAgreement: ServiceUserAgreement; + }; + + public constructor(params: ControllerClassParams) { + super(params); + + this.svc = { + userAgreement: new ServiceUserAgreement(this.userUUID), + }; + } + + public async execute(): Promise> { + const rtcUidstr = this.querystring.uid; + const room_uuid = this.querystring.room_uuid; + const rtcUids = rtcUidstr.split(","); + const userAgreementMap:Map = new Map(rtcUids.map(rtc_uid => [rtc_uid, false])); + const length = rtcUids.length; + if (length > 0) { + let i = 0; + while (i < length) { + const j = i + 50; + const batchedRtcUids = rtcUids.slice(i, j); + const roomUserInfos = await this.getRoomUserInfos(room_uuid, batchedRtcUids); + const userUuids = roomUserInfos.map(user => user.user_uuid); + if (userUuids.length > 0) { + const userAgreements = await this.getUserAgreements(userUuids); + for (const userInfo of roomUserInfos) { + const { rtc_uid, user_uuid } = userInfo; + const userAgreement = userAgreements.find(ua => ua.user_uuid === user_uuid); + if (userAgreement) { + userAgreementMap.set(rtc_uid, userAgreement.is_agree_collect_data); + } else { + userAgreementMap.set(rtc_uid, true); + } + } + } + i = j; + } + } + return { + status: Status.Success, + data: Object.fromEntries(userAgreementMap) + } + } + + private async getRoomUserInfos(room_uuid: string, rtc_uids: string[]): Promise { + return dataSource + .createQueryBuilder(RoomUserModel, "ru") + .where("ru.room_uuid = :room_uuid", { room_uuid }) + .andWhere("ru.rtc_uid IN (:...rtc_uids)", { rtc_uids }) + .getMany(); + } + private async getUserAgreements(userUuids: string[]): Promise { + return dataSource + .createQueryBuilder(UserAgreementModel, "ua") + .where("ua.user_uuid IN (:...userUuids)", { userUuids }) + .getMany(); + } + + public errorHandler(error: Error): ResponseError { + return this.autoHandlerError(error); + } +} + +interface RequestType { + querystring: { + uid: string; + room_uuid: string; + }; +} + +interface ResponseType { + [key: string]: boolean; +} diff --git a/src/v1/controller/user/agreement/Set.ts b/src/v1/controller/user/agreement/Set.ts new file mode 100644 index 00000000..22025601 --- /dev/null +++ b/src/v1/controller/user/agreement/Set.ts @@ -0,0 +1,61 @@ +import { AbstractController, ControllerClassParams } from "../../../../abstract/controller"; +import { Status } from "../../../../constants/Project"; +import { Controller } from "../../../../decorator/Controller"; +import { FastifySchema, Response, ResponseError } from "../../../../types/Server"; + +import { ServiceUserAgreement } from "../../../service/user/UserAgreement"; + +@Controller({ + method: "post", + path: "user/agreement/set", + auth: true, +}) +export class AgreementSet extends AbstractController { + public static readonly schema: FastifySchema = { + body: { + type: "object", + required: ["isAgree"], + properties: { + isAgree: { + type: "boolean" + }, + }, + }, + }; + + public readonly svc: { + userAgreement: ServiceUserAgreement; + }; + + public constructor(params: ControllerClassParams) { + super(params); + + this.svc = { + userAgreement: new ServiceUserAgreement(this.userUUID), + }; + } + + public async execute(): Promise> { + await this.svc.userAgreement.set(this.body.isAgree); + return { + status: Status.Success, + data: { + userUUID: this.userUUID + } + }; + } + + public errorHandler(error: Error): ResponseError { + return this.autoHandlerError(error); + } +} + +interface RequestType { + body: { + isAgree: boolean; + }; +} + +interface ResponseType { + userUUID: string; +} diff --git a/src/v1/service/room/Room.ts b/src/v1/service/room/Room.ts index 236587e5..003478de 100644 --- a/src/v1/service/room/Room.ts +++ b/src/v1/service/room/Room.ts @@ -63,11 +63,12 @@ export class ServiceRoom { region?: Region; beginTime?: number | Date; endTime?: number | Date; + isAI?: boolean; }, t?: EntityManager, ): Promise { const region = Whiteboard.region as Region; - const { title, type, endTime } = data; + const { title, type, endTime, isAI } = data; const beginTime = data.beginTime || Date.now(); return await RoomDAO(t).insert({ @@ -81,6 +82,7 @@ export class ServiceRoom { begin_time: toDate(beginTime), end_time: endTime ? toDate(endTime) : addHours(1, beginTime), region, + is_ai: isAI, }); } diff --git a/src/v1/service/room/RoomUser.ts b/src/v1/service/room/RoomUser.ts index 77bf186a..6a6598ef 100644 --- a/src/v1/service/room/RoomUser.ts +++ b/src/v1/service/room/RoomUser.ts @@ -19,6 +19,7 @@ export class ServiceRoomUser { room_uuid: this.roomUUID, user_uuid: this.userUUID, rtc_uid: cryptoRandomString({ length: 6, type: "numeric" }), + grade: -1, }); } } diff --git a/src/v1/service/user/UserAgreement.ts b/src/v1/service/user/UserAgreement.ts new file mode 100644 index 00000000..7f90c23c --- /dev/null +++ b/src/v1/service/user/UserAgreement.ts @@ -0,0 +1,65 @@ +import { UserAgreementDAO } from "../../../dao"; +import { DeleteResult, EntityManager, InsertResult } from "typeorm"; +import { UpdateResult } from "typeorm/query-builder/result/UpdateResult"; + +export class ServiceUserAgreement { + constructor(private readonly userUUID: string) {} + public async isAgreeCollectData(): Promise { + const bol = await ServiceUserAgreement.hasCollectData(this.userUUID); + if (bol) { + const isAgree = await ServiceUserAgreement.isAgreeCollectData(this.userUUID); + return isAgree; + } + return true; + } + public async hasCollectData(): Promise { + return await ServiceUserAgreement.hasCollectData(this.userUUID); + } + public static async isAgreeCollectData(userUUID: string): Promise { + const result = await UserAgreementDAO().findOne(["is_agree_collect_data"], { + user_uuid: userUUID, + }); + return Boolean(result && result.is_agree_collect_data); + } + public static async hasCollectData(userUUID: string): Promise { + const result = await UserAgreementDAO().findOne(["user_uuid"], { + user_uuid: userUUID, + }); + return Boolean(result); + } + public async set( + is_agree_collect_data: boolean, + t?: EntityManager, + ): Promise { + const has = await this.hasCollectData(); + if (!has) { + return await this.create(is_agree_collect_data, t); + } + return await this.update(is_agree_collect_data, t); + } + public async create( + is_agree_collect_data: boolean, + t?: EntityManager, + ): Promise { + return await UserAgreementDAO(t).insert({ + user_uuid: this.userUUID, + is_agree_collect_data, + }); + } + public async update(is_agree_collect_data: boolean, t?: EntityManager): Promise { + return await UserAgreementDAO(t).update( + { + is_agree_collect_data, + }, + { + user_uuid: this.userUUID, + }, + ); + } + + public async physicalDeletion(t?: EntityManager): Promise { + return await UserAgreementDAO(t).physicalDeletion({ + user_uuid: this.userUUID, + }); + } +} diff --git a/src/v1/service/user/__tests__/userAgreement.test.ts b/src/v1/service/user/__tests__/userAgreement.test.ts new file mode 100644 index 00000000..402be7af --- /dev/null +++ b/src/v1/service/user/__tests__/userAgreement.test.ts @@ -0,0 +1,183 @@ +import test from "ava"; +import { dataSource } from "../../../../thirdPartyService/TypeORMService"; +import { RoomUserDAO, UserAgreementDAO } from "../../../../dao"; +import { v4 } from "uuid"; +import { ServiceUserAgreement } from "../UserAgreement"; +import cryptoRandomString from "crypto-random-string"; +import { RoomUserModel } from "../../../../model/room/RoomUser"; +import { UserAgreementModel } from "../../../../model/user/Agreement"; + +const namespace = "[service][service-user][service-user-agreement]"; + +test.before(`${namespace} - initialize dataSource`, async () => { + await dataSource.initialize(); +}); + +test.after(`${namespace} - destroy dataSource`, async () => { + await dataSource.destroy(); +}); + +test(`${namespace} - set user collect agreement`, async ava => { + + const userUUID = v4(); + const serviceUserAgreement = new ServiceUserAgreement(userUUID); + + await serviceUserAgreement.set(true); + + let result = await UserAgreementDAO().findOne(["is_agree_collect_data"], { + user_uuid: userUUID, + }); + + ava.not(result, undefined); + + ava.is(result?.is_agree_collect_data, true); + + await serviceUserAgreement.set(false); + + result = await UserAgreementDAO().findOne(["is_agree_collect_data"], { + user_uuid: userUUID, + }); + + ava.is(result?.is_agree_collect_data, false); +}); + +test(`${namespace} - has agree collect data`, async ava => { + const userUUID = v4(); + + await UserAgreementDAO().insert({ + user_uuid: userUUID, + is_agree_collect_data: true, + }); + + const serviceUserAgreement = new ServiceUserAgreement(userUUID); + + const bol = await serviceUserAgreement.hasCollectData(); + + ava.is(bol, true); + + const userUUID1 = v4(); + + const bol1 = await ServiceUserAgreement.hasCollectData(userUUID1); + + ava.is(bol1, false); + +}); + +test(`${namespace} - delete user agreement`, async ava => { + const userUUID = v4(); + + await UserAgreementDAO().insert({ + user_uuid: userUUID, + is_agree_collect_data: true, + }); + + const serviceUserAgreement = new ServiceUserAgreement(userUUID); + await serviceUserAgreement.physicalDeletion(); + + const result = await UserAgreementDAO().findOne(["is_agree_collect_data"], { + user_uuid: userUUID, + }); + + ava.is(result, undefined); + + const bol = await serviceUserAgreement.hasCollectData(); + + ava.is(bol, false); + +}); +test(`${namespace} - get agree collect data`, async ava => { + + const userUUID = v4(); + await UserAgreementDAO().insert({ + user_uuid: userUUID, + is_agree_collect_data: true, + }); + + const serviceUserAgreement = new ServiceUserAgreement(userUUID); + const result = await serviceUserAgreement.isAgreeCollectData(); + + ava.not(result, undefined); + ava.is(result, true); + + const serviceUserAgreement1 = new ServiceUserAgreement(v4()); + + const result1 = await serviceUserAgreement1.hasCollectData(); + ava.is(result1, false); + + const result2 = await serviceUserAgreement.isAgreeCollectData(); + ava.is(result2, true); +}); +test(`${namespace} - get user by rtc_uuid collect agreement`, async ava => { + + const userUUID = v4(); + const roomUUID = v4(); + const rtcUUID = cryptoRandomString({ length: 6, type: "numeric" }); + const rtcUUID1 = cryptoRandomString({ length: 6, type: "numeric" }); + + await Promise.all([ + RoomUserDAO().insert({ + room_uuid: roomUUID, + user_uuid: userUUID, + rtc_uid: rtcUUID, + }), + UserAgreementDAO().insert({ + user_uuid: userUUID, + is_agree_collect_data: true, + }), + ]); + + const result = await RoomUserDAO().findOne(["user_uuid"], { + rtc_uid: rtcUUID, + room_uuid: roomUUID, + }); + + ava.not(result, undefined); + + ava.is(result?.user_uuid, userUUID); + + const result1 = await UserAgreementDAO().findOne(["user_uuid", "is_agree_collect_data"], { + user_uuid: userUUID, + }); + + ava.not(result1, undefined); + + ava.is(result?.user_uuid, result1?.user_uuid); + + const rtcUids = [rtcUUID, rtcUUID1]; + const userAgreementMap:Map = new Map(rtcUids.map(rtc_uid => [rtc_uid, false])); + const length = rtcUids.length; + if (length > 0) { + let i = 0; + while (i < length) { + const j = i + 50; + const batchedRtcUids = rtcUids.slice(i, j); + const roomUserInfos = await dataSource + .createQueryBuilder(RoomUserModel, "ru") + .where("ru.room_uuid = :room_uuid", { room_uuid:roomUUID }) + .andWhere("ru.rtc_uid IN (:...rtc_uids)", { rtc_uids: batchedRtcUids }) + .getMany(); + const userUuids = roomUserInfos.map(user => user.user_uuid); + if (userUuids.length > 0) { + const userAgreements = await dataSource + .createQueryBuilder(UserAgreementModel, "ua") + .where("ua.user_uuid IN (:...userUuids)", { userUuids }) + .getMany();; + for (const userInfo of roomUserInfos) { + const { rtc_uid, user_uuid } = userInfo; + const userAgreement = userAgreements.find(ua => ua.user_uuid === user_uuid); + if (userAgreement) { + userAgreementMap.set(rtc_uid, userAgreement.is_agree_collect_data); + } else { + userAgreementMap.set(rtc_uid, true); + } + } + } + i = j; + } + } + const obj = Object.fromEntries(userAgreementMap); + + ava.is(result1?.is_agree_collect_data, obj?.[rtcUUID]); + + ava.is(false, obj?.[rtcUUID1]); +}); \ No newline at end of file diff --git a/src/v2/dao/index.ts b/src/v2/dao/index.ts index 262beb03..9f558859 100644 --- a/src/v2/dao/index.ts +++ b/src/v2/dao/index.ts @@ -13,6 +13,7 @@ import { UserPhoneModel } from "../../model/user/Phone"; import { UserEmailModel } from "../../model/user/Email"; import { UserSensitiveModel } from "../../model/user/Sensitive"; import { UserPmiModel } from "../../model/user/Pmi"; +import { UserAgreementModel } from "../../model/user/Agreement"; import { RoomModel } from "../../model/room/Room"; import { RoomUserModel } from "../../model/room/RoomUser"; import { RoomPeriodicConfigModel } from "../../model/room/RoomPeriodicConfig"; @@ -189,6 +190,7 @@ export const userPhoneDAO = new DAO(UserPhoneModel); export const userEmailDAO = new DAO(UserEmailModel); export const userSensitiveDAO = new DAO(UserSensitiveModel); export const userPmiDAO = new DAO(UserPmiModel); +export const userAgreementDAO = new DAO(UserAgreementModel); export const roomDAO = new DAO(RoomModel); export const roomUserDAO = new DAO(RoomUserModel); export const roomPeriodicConfigDAO = new DAO(RoomPeriodicConfigModel);