universo-platform-3d
1021 строка · 28.9 Кб
1import {2HttpException,3Injectable,4Logger,5BadRequestException,6NotFoundException,7ConflictException
8} from '@nestjs/common'9import { InjectModel } from '@nestjs/mongoose'10import { ApiProperty } from '@nestjs/swagger'11import { ObjectId, UpdateResult } from 'mongodb'12import mongoose, { Model, mongo } from 'mongoose'13import { v4 as uuidv4 } from 'uuid'14import { AssetPublicData } from '../asset/asset.schema'15import { CustomDataService } from '../custom-data/custom-data.service'16import { CreateCustomDataDto } from '../custom-data/dto/custom-data.dto'17import { PREMIUM_ACCESS } from '../option-sets/premium-tiers'18import { FileUploadService } from '../util/file-upload/file-upload.service'19import { getPublicPropertiesForMongooseQuery } from '../util/getPublicDataClassProperties'20import { UserEntityActionId, UserId } from '../util/mongo-object-id-helpers'21import { CreateUserAccessKeyDto } from './dto/create-user-access-key.dto'22import {23AddRpmAvatarUrlDto,24AddUserCartItemToUserCartDto,25RemoveRpmAvatarUrlDto,26UpdateUserAvatarDto,27UpdateUserAvatarTypeDto,28UpdateUserDeepLinkDto,29UpdateUserProfileDto,30UpdateUserTermsDto,31UpdateUserTutorialDto,32UpsertUserEntityActionDto
33} from './dto/update-user.dto'34import { UploadProfileFileDto } from './dto/upload-profile-file.dto'35import {36UserAccessKey,37UserAccessKeyDocument
38} from './models/user-access-key.schema'39import {40ENTITY_TYPE,41UserEntityAction,42UserEntityActionDocument,43USER_ENTITY_ACTION_TYPE44} from './models/user-entity-action.schema'45import { User, UserDocument, UserPublicData } from './user.schema'46import { UserSearch } from './user.search'47import { FirebaseAuthenticationService } from '../firebase/firebase-authentication.service'48import { UserCartItem } from './models/user-cart.schema'49import { AddUserSidebarTagDto } from './dto/add-user-sidebar-tag.dto'50import { IUserRecents } from './models/user-recents.schema'51import {52Config,53adjectives,54animals,55colors,56uniqueNamesGenerator
57} from 'unique-names-generator'58import { CreateUserWithEmailPasswordDto } from '../auth/dto/CreateUserWithEmailPasswordDto'59
60/**
61* @description The shape of the data retrieved when looking up a friend request. This is via `select` in mongoose
62* @date 2023-06-28 22:21
63*/
64export class Friend {65@ApiProperty({ type: 'string' })66displayName = ''67@ApiProperty({ type: 'string' })68coverImage? = ''69@ApiProperty({ type: 'string' })70profileImage? = ''71@ApiProperty({ type: 'string' })72id? = ''73@ApiProperty({ type: 'string' })74_id = ''75}
76
77export class UserWithPublicProfile extends UserPublicData {78@ApiProperty({79type: AssetPublicData,80isArray: true81})82publicAssets: AssetPublicData[]83// TODO: add back for usergroups impl84// @ApiProperty({85// type: UserGroupPublicData,86// isArray: true87// })88// publicGroups: UserGroupPublicData[]89}
90
91@Injectable()92export class UserService {93private readonly logger = new Logger(UserService.name)94
95constructor(96private readonly firebaseAuthService: FirebaseAuthenticationService,97@InjectModel(User.name) private userModel: Model<UserDocument>,98@InjectModel(UserAccessKey.name)99private userAccessKeyModel: Model<UserAccessKeyDocument>,100@InjectModel(UserEntityAction.name)101private userEntityActionModel: Model<UserEntityActionDocument>,102private readonly userSearch: UserSearch,103private readonly fileUploadService: FileUploadService,104private readonly customDataService: CustomDataService105) {}106searchForPublicUsers(searchQuery: string): Promise<UserPublicData[]> {107const select = getPublicPropertiesForMongooseQuery(UserPublicData)108
109if (!searchQuery) {110return this.userModel111.find({ deleted: { $exists: false } })112.select(select)113.limit(25)114.exec()115}116
117const searchFilter = this.userSearch.getSearchFilter(searchQuery)118
119//Users with deleted property should not appear in search results120const filter = { ...searchFilter, deleted: { $exists: false } }121
122return this.userModel.find(filter).select(select).exec()123}124
125findPublicUser(id: string): Promise<UserPublicData> {126const select = getPublicPropertiesForMongooseQuery(UserPublicData)127return this.userModel128.findOne({ _id: id, deleted: { $exists: false } })129.select(select)130.populate('customData') // TODO this needs to be filtered with roles131.exec()132}133
134/** @description Used for getting the user's additional profile data such as public assets and public groups */135findPublicUserFullProfile(id: string): Promise<User> {136const select = getPublicPropertiesForMongooseQuery(UserPublicData)137return this.userModel138.findOne({ _id: id, deleted: { $exists: false } })139.select(select)140.populate('customData') // TODO this needs to be filtered with roles141.exec()142}143
144findPublicUserByEmail(email: string): Promise<User> {145const select = getPublicPropertiesForMongooseQuery(UserPublicData)146return this.userModel.findOne({ email: email }).select(select).exec()147}148
149async createUserWithEmailPassword(150createUserWithEmailPasswordDto: CreateUserWithEmailPasswordDto151): Promise<UserDocument> {152const { email, password, termsAgreedtoGeneralTOSandPP, displayName } =153createUserWithEmailPasswordDto
154
155// disallow creation if they didn't agree to TOS/PP156if (!termsAgreedtoGeneralTOSandPP) {157throw new BadRequestException(158'You must agree to the Terms of Service and Privacy Policy to create an account: https://www.themirror.space/terms, https://www.themirror.space/privacy'159)160}161
162const _id = new mongo.ObjectId()163this.logger.log('createUserWithEmailPassword with id', _id.toHexString())164
165try {166await this.firebaseAuthService.createUser({167uid: _id.toHexString(),168password,169displayName,170email,171emailVerified: false172})173
174const user = new this.userModel({175_id: _id,176firebaseUID: _id.toHexString(),177displayName,178email,179emailVerified: false,180termsAgreedtoGeneralTOSandPP
181})182
183const createdUser = await user.save()184
185return createdUser.toJSON()186} catch (error) {187this.logger.error(error)188await this.removeAuthUserWhenErrored(_id.toHexString())189throw new HttpException(error?.message || error, 400)190}191}192
193async ensureMirrorUserExists(token: string) {194try {195const decodedToken = await this.firebaseAuthService.verifyIdToken(token)196const firebaseUID = decodedToken.uid197
198const _id = new mongo.ObjectId()199
200if (!decodedToken) {201throw new NotFoundException('User not found')202}203
204const user = await this.userModel.findOne({ firebaseUID }).exec()205
206if (!user) {207const displayName = this._generateUniqueUsername()208const userModel = new this.userModel({209_id: _id,210firebaseUID,211displayName,212emailVerified: false213})214
215const user = await userModel.save()216this.logger.log('createAnonymousUser with id', _id.toHexString())217return user218}219return user220} catch (error) {221this.logger.error(error)222throw new HttpException(error?.message || error, 400)223}224}225
226private _generateUniqueUsername() {227const config: Config = {228dictionaries: [adjectives, colors, animals],229separator: ' ',230style: 'capital'231}232
233return uniqueNamesGenerator(config)234}235
236/**237* @deprecated since this uses populate and does have role checks. Use findOneAdmin
238* @date 2023-08-13 11:03
239*/
240async findUser(userId: UserId): Promise<UserDocument> {241return await this.userModel.findById(userId).populate('customData').exec()242}243
244async findOneAdmin(userId: UserId): Promise<UserDocument> {245return await this.userModel.findById(userId).exec()246}247
248async getUserRecents(userId: UserId): Promise<IUserRecents> {249const userRecents = await this.userModel250.findOne({ _id: userId })251.select('recents')252.exec()253
254if (!userRecents) {255throw new NotFoundException('User not found')256}257
258return userRecents.recents as IUserRecents259}260
261/**262* START Section: Friends and Friend Requests ------------------------------------------------------
263*/
264
265public async findUserFriendsAdmin(userId: UserId): Promise<Friend[]> {266const [aggregationResult] = await this.userModel267.aggregate([268{ $match: { _id: new ObjectId(userId) } },269{270$lookup: {271from: 'users',272localField: 'friends',273foreignField: '_id',274as: 'friends'275}276},277{278$project: {279friends: {280$filter: {281input: '$friends',282as: 'friend',283cond: { $ne: ['$$friend.deleted', true] }284}285}286}287},288{289$project: {290'friends._id': 1,291'friends.displayName': 1,292'friends.profileImage': 1,293'friends.coverImage': 1294}295}296])297.exec()298
299return aggregationResult300}301
302findFriendRequestsSentToMeAdmin(userId: UserId): Promise<Friend[]> {303return this.userModel304.find({305sentFriendRequestsToUsers: { $in: [userId] },306deleted: { $exists: false }307})308.select({ _id: 1, displayName: 1, profileImage: 1, coverImage: 1 })309.exec()310}311
312async acceptFriendRequestAdmin(313userId: UserId,314userIdOfFriendRequestToAccept: UserId315): Promise<Friend[]> {316// first ensure that the friend request actually exists317const check = await this._checkIfFriendRequestExistsAdmin(318userIdOfFriendRequestToAccept,319userId // note that the param order is flipped for the _checkIfFriendRequestExists method because we want to check that the friend request was sent to the current userId (most likely use case for this method)320)321
322if (check) {323// add to friends list for both users324await this.userModel325.findByIdAndUpdate(326userId,327{ $addToSet: { friends: userIdOfFriendRequestToAccept } },328{ new: true }329)330.exec()331await this.userModel332.findByIdAndUpdate(333userIdOfFriendRequestToAccept,334{ $addToSet: { friends: userId } },335{ new: true }336)337.exec()338// remove the sentFriendRequestsToUsers from the user who sent the request339await this.userModel340.findByIdAndUpdate(341userIdOfFriendRequestToAccept,342{ $pull: { sentFriendRequestsToUsers: userId } },343{ new: true }344)345.exec()346
347return await this.findUserFriendsAdmin(userId)348} else {349throw new NotFoundException(350'Friend request not found or you are already friends with this user'351)352}353}354
355async rejectFriendRequestAdmin(356userId: UserId,357userIdOfFriendRequestToReject: UserId358): Promise<Friend[]> {359// first ensure that the friend request actually exists360const check = await this._checkIfFriendRequestExistsAdmin(361userIdOfFriendRequestToReject,362userId // note that the param order is flipped for the _checkIfFriendRequestExists method because we want to check that the friend request was sent to the current userId (most likely use case for this method)363)364
365if (check) {366// remove the sentFriendRequestsToUsers from the user who sent the request367await this.userModel368.findByIdAndUpdate(369userIdOfFriendRequestToReject,370{ $pull: { sentFriendRequestsToUsers: userId } },371{ new: true }372)373.exec()374
375// Note that the return here is different from acceptFriendRequestAdmin376// instead, this returns the list of friend requests377return await this.findFriendRequestsSentToMeAdmin(userId)378} else {379throw new NotFoundException('Friend request not found')380}381}382
383async findSentFriendRequestsAdmin(userId: UserId): Promise<Friend[]> {384const [aggregationResult] = await this.userModel385.aggregate([386{ $match: { _id: new ObjectId(userId) } },387{388$lookup: {389from: 'users',390localField: 'sentFriendRequestsToUsers',391foreignField: '_id',392as: 'sentFriendRequestsToUsers'393}394},395{396$project: {397sentFriendRequestsToUsers: {398$filter: {399input: '$sentFriendRequestsToUsers',400as: 'request',401cond: { $ne: ['$$request.deleted', true] }402}403}404}405},406{407$project: {408'sentFriendRequestsToUsers._id': 1,409'sentFriendRequestsToUsers.displayName': 1,410'sentFriendRequestsToUsers.profileImage': 1,411'sentFriendRequestsToUsers.coverImage': 1412}413}414])415.exec()416
417return aggregationResult.sentFriendRequestsToUsers418}419
420sendFriendRequestAdmin(421requestingUserId: UserId,422toUserId: UserId423): Promise<UserDocument> {424return this.userModel425.findByIdAndUpdate(426requestingUserId,427{ $addToSet: { sentFriendRequestsToUsers: toUserId } },428{ new: true }429)430.select({ sentFriendRequestsToUsers: 1 })431.exec()432}433
434private async _checkIfFriendRequestExistsAdmin(435fromUserId: UserId,436toUserId: UserId437): Promise<boolean> {438const test = await this.userModel439.find({440_id: new ObjectId(fromUserId),441sentFriendRequestsToUsers: { $in: [toUserId] }442})443.select({ _id: 1, displayName: 1, profileImage: 1, coverImage: 1 })444.exec()445if (test) {446return true447}448return false449}450
451/**452* @description Removes a friend and returns the updated friends list
453* @date 2023-06-30 00:19
454*/
455async removeFriendAdmin(456userId: UserId,457friendUserIdToRemove: UserId458): Promise<Friend[]> {459// remove from friends list for both users460await this.userModel461.findByIdAndUpdate(462userId,463{ $pull: { friends: friendUserIdToRemove } },464{ new: true }465)466.exec()467await this.userModel468.findByIdAndUpdate(469friendUserIdToRemove,470{ $pull: { friends: userId } },471{ new: true }472)473.exec()474
475return await this.findUserFriendsAdmin(userId)476}477/**478* END Section: Friends and Friend Requests ------------------------------------------------------
479*/
480
481/**482* START Section: Cart ------------------------------------------------------
483*/
484
485async getUserCartAdmin(userId: UserId) {486return await this.userModel.findById(userId).select({ cartItems: 1 }).exec()487}488
489/**490* @description add a UserCartItem to a User's cart
491* Admin call because the user should be determined by the JWT. Someone else should never be able to add to someone else's cart
492* @date 2023-07-09 16:19
493*/
494async addUserCartItemToUserCartAdmin(495userId: UserId,496dto: AddUserCartItemToUserCartDto497) {498const cartItem = new UserCartItem()499cartItem.entityType = dto.entityType500cartItem.forEntity = new mongoose.Types.ObjectId(dto.forEntity)501return await this.userModel502.findByIdAndUpdate(503userId,504{505$push: { cartItems: cartItem }506},507{ new: true, select: { cartItems: 1 } }508)509.exec()510}511
512async removeAllUserCartItemsFromUserCartAdmin(userId: UserId) {513return await this.userModel514.findByIdAndUpdate(515userId,516{517$set: {518cartItems: []519}520},521{ new: true, select: { cartItems: 1 } }522)523.exec()524}525
526async removeUserCartItemFromUserCartAdmin(527userId: UserId,528cartItemId: string529) {530return await this.userModel531.findByIdAndUpdate(532userId,533{534$pull: {535cartItems: {536_id: new mongoose.Types.ObjectId(cartItemId)537}538}539},540{ new: true, select: { cartItems: 1 } }541)542.exec()543}544
545/**546* END Section: Cart ------------------------------------------------------
547*/
548
549// TODO need to lock down permissions for custom data. Also, users shouldn't be able to see each other unless they used the public methods like findPublicUserFullProfile()550/**551*
552* @deprecated This needs role checks and we want to avoid extra lookups. I'm not sure if customData is the best way to go. 2023-09-11 16:28:43
553*/
554async findUserIncludingCustomData(id: string) {555return await this.userModel.findById(id).populate('customData').exec()556}557
558findUserByDiscordId(discordUserId: string): Promise<User> {559return this.userModel.findOne({ discordUserId: discordUserId }).exec()560}561
562findUserByEmail(email: string): Promise<User> {563return this.userModel.findOne({ email: email }).exec()564}565
566async updateUserProfileAdmin(567id: string,568dto: UpdateUserProfileDto569): Promise<UserDocument> {570let originalUser: UserDocument571const firebaseUpdate = this.getFirebaseFieldsForUpdate(dto)572const firebaseFields = Object.keys(firebaseUpdate)573
574try {575/** Update and save reference of old UserModel to handle errors */576originalUser = await this.userModel.findByIdAndUpdate(id, dto).exec()577
578/** Update firebase specific fields if present */579if (firebaseFields.length) {580await this.firebaseAuthService.updateUser(id, firebaseUpdate)581}582
583/** On success - return latest UserModel */584return this.findUser(id)585} catch (error) {586/** If Update succeeds but firebase fails, reset failed fields on mongodb User */587if (firebaseFields.length && originalUser?._id) {588this.undoUserUpdateOnError(id, firebaseFields, originalUser)589}590this.logger.error(error)591throw new HttpException(error.message ?? error, 400)592}593}594
595updateUserProfile(userId: UserId, dto: UpdateUserProfileDto) {596return this.userModel.findByIdAndUpdate(userId, dto, { new: true }).exec()597}598
599updateUserTutorial(userId: UserId, dto: UpdateUserTutorialDto) {600return this.userModel601.findByIdAndUpdate(602userId,603{604// the below is so nested properties don't get overwritten605...Object.fromEntries(606Object.entries(dto).map(([key, value]) => [607`tutorial.${key}`,608value
609])610)611},612{ new: true }613)614.exec()615}616
617updateDeepLink(userId: UserId, dto: UpdateUserDeepLinkDto) {618return this.userModel619.findByIdAndUpdate(620userId,621{622deepLinkKey: dto.deepLinkKey,623deepLinkValue: dto.deepLinkValue,624deepLinkLastUpdatedAt: new Date()625},626{ new: true }627)628.exec()629}630
631updateUserAvatar(id: string, dto: UpdateUserAvatarDto) {632return this.userModel.findByIdAndUpdate(id, dto, { new: true }).exec()633}634
635updateUserTerms(id: string, dto: UpdateUserTermsDto) {636return this.userModel.findByIdAndUpdate(id, dto, { new: true }).exec()637}638
639updateUserAvatarType(id: string, dto: UpdateUserAvatarTypeDto) {640return this.userModel.findByIdAndUpdate(id, dto, { new: true }).exec()641}642
643updateUserRecentSpaces(id: string, spaces: string[]) {644return this.userModel.findByIdAndUpdate(id, { 'recents.spaces': spaces })645}646
647updateUserRecentInstancedAssets(id: string, assets: string[]) {648return this.userModel.findByIdAndUpdate(id, {649'recents.assets.instanced': assets650})651}652
653updateUserRecentScripts(id: string, scripts: string[]) {654return this.userModel.findByIdAndUpdate(id, {655'recents.scripts': scripts656})657}658
659getUserFiveStarRatedSpaces(userId) {660return this.userEntityActionModel661.find({662creator: new ObjectId(userId),663entityType: ENTITY_TYPE.SPACE,664actionType: USER_ENTITY_ACTION_TYPE.RATING,665rating: 5666})667.select('forEntity')668.exec()669}670
671addUserPremiumAccess(id: string, accessLevelToAdd: PREMIUM_ACCESS) {672return this.userModel673.findByIdAndUpdate(674id,675{676$addToSet: {677premiumAccess: accessLevelToAdd678}679},680{ new: true }681)682.exec()683}684
685removeUserPremiumAccess(id: string, accessLevelToRemove: PREMIUM_ACCESS) {686return this.userModel687.findByIdAndUpdate(688id,689{690$pull: {691premiumAccess: accessLevelToRemove692}693},694{ new: true }695)696.exec()697}698
699async getPublicEntityActionStats(entityId: string) {700const pipeline = [701{702$match: { forEntity: new ObjectId(entityId) }703},704{705$group: {706_id: '$actionType',707count: { $sum: 1 },708ratingSum: { $sum: '$rating' },709ratingAvg: { $avg: '$rating' }710}711},712{713$group: {714_id: null,715COUNT_LIKE: {716$sum: { $cond: [{ $eq: ['$_id', 'LIKE'] }, '$count', 0] }717},718COUNT_FOLLOW: {719$sum: { $cond: [{ $eq: ['$_id', 'FOLLOW'] }, '$count', 0] }720},721COUNT_SAVES: {722$sum: { $cond: [{ $eq: ['$_id', 'SAVE'] }, '$count', 0] }723},724COUNT_RATING: {725$sum: { $cond: [{ $eq: ['$_id', 'RATING'] }, '$count', 0] }726},727AVG_RATING: {728$avg: { $cond: [{ $eq: ['$_id', 'RATING'] }, '$ratingAvg', null] }729}730}731},732{733$project: {734_id: 0,735COUNT_LIKE: 1,736COUNT_FOLLOW: 1,737COUNT_SAVES: 1,738COUNT_RATING: 1,739AVG_RATING: 1740}741}742]743const stats = await this.userEntityActionModel.aggregate(pipeline).exec()744
745return stats[0]746}747
748findEntityActionsByUserForEntity(userId: UserId, entityId: string) {749// validate mongo Ids750if (!mongo.ObjectId.isValid(userId)) {751throw new BadRequestException('User ID is not a valid Mongo ObjectId')752}753if (!mongo.ObjectId.isValid(entityId)) {754throw new BadRequestException('Entity ID is not a valid Mongo ObjectId')755}756return this.userEntityActionModel757.find({758creator: new ObjectId(userId),759forEntity: new ObjectId(entityId)760})761.exec()762}763
764upsertUserEntityAction(userId: string, dto: UpsertUserEntityActionDto) {765const findData = {766creator: new ObjectId(userId),767forEntity: new ObjectId(dto.forEntity),768entityType: dto.entityType,769actionType: dto.actionType770}771const updateData: any = {}772if (dto.rating !== undefined) {773updateData.rating = dto.rating774}775return this.userEntityActionModel776.findOneAndUpdate(findData, updateData, { new: true, upsert: true })777.exec()778}779
780removeUserEntityAction(781userId: UserId,782userEntityActionId: UserEntityActionId783) {784return this.userEntityActionModel785.findOneAndRemove({ creator: userId, _id: userEntityActionId })786.exec()787}788
789public uploadProfileImage({ file, userId }: UploadProfileFileDto) {790const fileId = new ObjectId()791const path = `${userId}/profile-images/${fileId.toString()}`792return this.fileUploadService.uploadFilePublic({ file, path })793}794
795/**796* @description Adds an RPM url to readyPlayerMeAvatarUrls. $addToSet forces uniqueness so there aren't duplicates
797* @date 2022-06-18 12:04
798*/
799addRpmAvatarUrl(id: string, dto: AddRpmAvatarUrlDto) {800return this.userModel801.findByIdAndUpdate(802id,803{804$addToSet: {805readyPlayerMeAvatarUrls: dto.rpmAvatarUrl806}807},808{ new: true }809)810.exec()811}812
813/**814* @description Removes an RPM url from readyPlayerMeAvatarUrls.
815* @date 2022-06-18 12:07
816*/
817removeRpmAvatarUrl(id: string, dto: RemoveRpmAvatarUrlDto) {818return this.userModel819.findByIdAndUpdate(820id,821{822$pullAll: {823readyPlayerMeAvatarUrls: [dto.rpmAvatarUrl]824}825},826{ new: true }827)828.exec()829}830
831createUserAccessKey(createSignUpKeyDto: CreateUserAccessKeyDto) {832const keyName = uuidv4()833const key = new this.userAccessKeyModel({834...createSignUpKeyDto,835key: keyName836})837return key.save()838}839
840async checkUserAccessKeyExistence(name: string) {841const check = await this.userAccessKeyModel.findOne({842key: name,843usedBy: {844$exists: false // need to ensure it's not in use845}846})847if (check) {848return check849} else {850return false851}852}853
854async setUserAccessKeyAsUsed(855keyId: string,856userId: string857): Promise<UpdateResult> {858// @ts-ignore. The error was: Type '"ObjectID"' is not assignable to type '"ObjectId"' with importing from Mongo vs Mongoose. Not worth debugging 2023-03-28 01:41:44859return await this.userAccessKeyModel860.updateOne({ _id: keyId }, { usedBy: userId })861.exec()862}863
864/**865* START Section: Custom Data
866*/
867async setCustomDataOnUser(868userId: string,869customDataDto: CreateCustomDataDto870) {871const customDataDoc = await this.customDataService.createCustomData(872userId,873customDataDto
874)875const doc = await this.userModel876.findByIdAndUpdate(877userId,878{879$set: {880customData: customDataDoc.id881}882},883{ new: true }884)885.exec()886return doc887}888
889/**890* END Section: Custom Data
891*/
892
893/** Extract Firebase specific properties to update */894protected getFirebaseFieldsForUpdate(dto: UpdateUserProfileDto): any {895const { email, displayName } = dto896return {897...(email && { email }),898...(displayName && { displayName })899}900}901
902protected undoUserUpdateOnError(903id: string,904firebaseFields: string[],905originalUser: UserDocument906) {907this.logger.warn(908`Error Encountered. Attempting to revert User update for User ID: ${id}`909)910
911const revertFields = firebaseFields.reduce((result, field) => {912if (originalUser[field]) {913result[field] = originalUser[field]914}915return result916}, {} as UpdateUserProfileDto)917
918this.updateUserProfile(id, revertFields)919.then(() =>920this.logger.warn(921`Attempted update of User ID: ${id} in mongo db successful. Update reverted.`922)923)924.catch(() =>925this.logger.warn(926`Attempted update of User ID: ${id} in mongo db unsuccessful. Update not reverted.`927)928)929}930
931protected async removeAuthUserWhenErrored(_id: string) {932this.logger.warn(933`Error Encountered. Attempting to remove user from firebase db. ID: ${_id}`934)935await this.firebaseAuthService936.deleteUser(_id)937.then((result) => {938this.logger.warn(939`Attempted removal of user ${_id} from firebase db successful. ID: ${_id} deleted.`940)941})942.catch((error) => {943this.logger.error(944`Attempted removal of user ${_id} from firebase db unsuccessful. ID: ${_id} not deleted!`945)946})947}948
949public async getUserSidebarTags(userId: UserId) {950const user = await this.userModel951.findOne({ _id: userId })952.select('sidebarTags')953.exec()954
955if (!user) {956throw new NotFoundException('User not found')957}958
959return user?.sidebarTags || []960}961
962public async addUserSidebarTag(963userId: UserId,964addUserSidebarTagDto: AddUserSidebarTagDto965) {966const { sidebarTag } = addUserSidebarTagDto967const sidebarTags = await this.getUserSidebarTags(userId)968
969if (sidebarTags.length === 3) {970throw new BadRequestException('User already has 3 sidebar tags')971}972
973if (sidebarTags.includes(sidebarTag)) {974throw new ConflictException('User already has this sidebar tag')975}976
977sidebarTags.push(sidebarTag)978await this.updateUserSidebarTags(userId, sidebarTags)979
980return sidebarTag981}982
983public async deleteUserSidebarTag(userId: UserId, sidebarTag: string) {984await this.userModel.updateOne(985{ _id: userId },986{ $pull: { sidebarTags: sidebarTag } }987)988
989return { userId, sidebarTag }990}991
992public async updateUserSidebarTags(userId: string, sidebarTags: string[]) {993await this.userModel.updateOne({ _id: userId }, { $set: { sidebarTags } })994return sidebarTags995}996
997public async updateUserLastActiveTimestamp(userId: string) {998await this.userModel.updateOne(999{ _id: userId },1000{ $set: { lastActiveTimestamp: new Date() } }1001)1002}1003
1004/**1005* @description This method removes all personally identifiable information from the user and marks the user as deleted.
1006* When a user requests to delete an account, it is marked with the deleted: true property.
1007* A user with the deleted: true property will be filtered out of all search results.
1008* After we check for any compliance/investigative issues (e.g. in case of a report from another user), we remove the account
1009* @date 2023-12-22 17:31
1010*/
1011public async markUserAsDeleted(userId: string) {1012const updateResult = await this.userModel.updateOne({ _id: userId }, [1013{ $set: { deleted: true } },1014{ $unset: ['email', 'firebaseUID', 'discordUserId'] }1015])1016
1017if (!updateResult.modifiedCount) {1018throw new NotFoundException('User not found')1019}1020}1021}
1022