universo-platform-3d

Форк
0
1021 строка · 28.9 Кб
1
import {
2
  HttpException,
3
  Injectable,
4
  Logger,
5
  BadRequestException,
6
  NotFoundException,
7
  ConflictException
8
} from '@nestjs/common'
9
import { InjectModel } from '@nestjs/mongoose'
10
import { ApiProperty } from '@nestjs/swagger'
11
import { ObjectId, UpdateResult } from 'mongodb'
12
import mongoose, { Model, mongo } from 'mongoose'
13
import { v4 as uuidv4 } from 'uuid'
14
import { AssetPublicData } from '../asset/asset.schema'
15
import { CustomDataService } from '../custom-data/custom-data.service'
16
import { CreateCustomDataDto } from '../custom-data/dto/custom-data.dto'
17
import { PREMIUM_ACCESS } from '../option-sets/premium-tiers'
18
import { FileUploadService } from '../util/file-upload/file-upload.service'
19
import { getPublicPropertiesForMongooseQuery } from '../util/getPublicDataClassProperties'
20
import { UserEntityActionId, UserId } from '../util/mongo-object-id-helpers'
21
import { CreateUserAccessKeyDto } from './dto/create-user-access-key.dto'
22
import {
23
  AddRpmAvatarUrlDto,
24
  AddUserCartItemToUserCartDto,
25
  RemoveRpmAvatarUrlDto,
26
  UpdateUserAvatarDto,
27
  UpdateUserAvatarTypeDto,
28
  UpdateUserDeepLinkDto,
29
  UpdateUserProfileDto,
30
  UpdateUserTermsDto,
31
  UpdateUserTutorialDto,
32
  UpsertUserEntityActionDto
33
} from './dto/update-user.dto'
34
import { UploadProfileFileDto } from './dto/upload-profile-file.dto'
35
import {
36
  UserAccessKey,
37
  UserAccessKeyDocument
38
} from './models/user-access-key.schema'
39
import {
40
  ENTITY_TYPE,
41
  UserEntityAction,
42
  UserEntityActionDocument,
43
  USER_ENTITY_ACTION_TYPE
44
} from './models/user-entity-action.schema'
45
import { User, UserDocument, UserPublicData } from './user.schema'
46
import { UserSearch } from './user.search'
47
import { FirebaseAuthenticationService } from '../firebase/firebase-authentication.service'
48
import { UserCartItem } from './models/user-cart.schema'
49
import { AddUserSidebarTagDto } from './dto/add-user-sidebar-tag.dto'
50
import { IUserRecents } from './models/user-recents.schema'
51
import {
52
  Config,
53
  adjectives,
54
  animals,
55
  colors,
56
  uniqueNamesGenerator
57
} from 'unique-names-generator'
58
import { 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
 */
64
export class Friend {
65
  @ApiProperty({ type: 'string' })
66
  displayName = ''
67
  @ApiProperty({ type: 'string' })
68
  coverImage? = ''
69
  @ApiProperty({ type: 'string' })
70
  profileImage? = ''
71
  @ApiProperty({ type: 'string' })
72
  id? = ''
73
  @ApiProperty({ type: 'string' })
74
  _id = ''
75
}
76

77
export class UserWithPublicProfile extends UserPublicData {
78
  @ApiProperty({
79
    type: AssetPublicData,
80
    isArray: true
81
  })
82
  publicAssets: AssetPublicData[]
83
  // TODO: add back for usergroups impl
84
  // @ApiProperty({
85
  //   type: UserGroupPublicData,
86
  //   isArray: true
87
  // })
88
  // publicGroups: UserGroupPublicData[]
89
}
90

91
@Injectable()
92
export class UserService {
93
  private readonly logger = new Logger(UserService.name)
94

95
  constructor(
96
    private readonly firebaseAuthService: FirebaseAuthenticationService,
97
    @InjectModel(User.name) private userModel: Model<UserDocument>,
98
    @InjectModel(UserAccessKey.name)
99
    private userAccessKeyModel: Model<UserAccessKeyDocument>,
100
    @InjectModel(UserEntityAction.name)
101
    private userEntityActionModel: Model<UserEntityActionDocument>,
102
    private readonly userSearch: UserSearch,
103
    private readonly fileUploadService: FileUploadService,
104
    private readonly customDataService: CustomDataService
105
  ) {}
106
  searchForPublicUsers(searchQuery: string): Promise<UserPublicData[]> {
107
    const select = getPublicPropertiesForMongooseQuery(UserPublicData)
108

109
    if (!searchQuery) {
110
      return this.userModel
111
        .find({ deleted: { $exists: false } })
112
        .select(select)
113
        .limit(25)
114
        .exec()
115
    }
116

117
    const searchFilter = this.userSearch.getSearchFilter(searchQuery)
118

119
    //Users with deleted property should not appear in search results
120
    const filter = { ...searchFilter, deleted: { $exists: false } }
121

122
    return this.userModel.find(filter).select(select).exec()
123
  }
124

125
  findPublicUser(id: string): Promise<UserPublicData> {
126
    const select = getPublicPropertiesForMongooseQuery(UserPublicData)
127
    return this.userModel
128
      .findOne({ _id: id, deleted: { $exists: false } })
129
      .select(select)
130
      .populate('customData') // TODO this needs to be filtered with roles
131
      .exec()
132
  }
133

134
  /** @description Used for getting the user's additional profile data such as public assets and public groups */
135
  findPublicUserFullProfile(id: string): Promise<User> {
136
    const select = getPublicPropertiesForMongooseQuery(UserPublicData)
137
    return this.userModel
138
      .findOne({ _id: id, deleted: { $exists: false } })
139
      .select(select)
140
      .populate('customData') // TODO this needs to be filtered with roles
141
      .exec()
142
  }
143

144
  findPublicUserByEmail(email: string): Promise<User> {
145
    const select = getPublicPropertiesForMongooseQuery(UserPublicData)
146
    return this.userModel.findOne({ email: email }).select(select).exec()
147
  }
148

149
  async createUserWithEmailPassword(
150
    createUserWithEmailPasswordDto: CreateUserWithEmailPasswordDto
151
  ): Promise<UserDocument> {
152
    const { email, password, termsAgreedtoGeneralTOSandPP, displayName } =
153
      createUserWithEmailPasswordDto
154

155
    // disallow creation if they didn't agree to TOS/PP
156
    if (!termsAgreedtoGeneralTOSandPP) {
157
      throw 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

162
    const _id = new mongo.ObjectId()
163
    this.logger.log('createUserWithEmailPassword with id', _id.toHexString())
164

165
    try {
166
      await this.firebaseAuthService.createUser({
167
        uid: _id.toHexString(),
168
        password,
169
        displayName,
170
        email,
171
        emailVerified: false
172
      })
173

174
      const user = new this.userModel({
175
        _id: _id,
176
        firebaseUID: _id.toHexString(),
177
        displayName,
178
        email,
179
        emailVerified: false,
180
        termsAgreedtoGeneralTOSandPP
181
      })
182

183
      const createdUser = await user.save()
184

185
      return createdUser.toJSON()
186
    } catch (error) {
187
      this.logger.error(error)
188
      await this.removeAuthUserWhenErrored(_id.toHexString())
189
      throw new HttpException(error?.message || error, 400)
190
    }
191
  }
192

193
  async ensureMirrorUserExists(token: string) {
194
    try {
195
      const decodedToken = await this.firebaseAuthService.verifyIdToken(token)
196
      const firebaseUID = decodedToken.uid
197

198
      const _id = new mongo.ObjectId()
199

200
      if (!decodedToken) {
201
        throw new NotFoundException('User not found')
202
      }
203

204
      const user = await this.userModel.findOne({ firebaseUID }).exec()
205

206
      if (!user) {
207
        const displayName = this._generateUniqueUsername()
208
        const userModel = new this.userModel({
209
          _id: _id,
210
          firebaseUID,
211
          displayName,
212
          emailVerified: false
213
        })
214

215
        const user = await userModel.save()
216
        this.logger.log('createAnonymousUser with id', _id.toHexString())
217
        return user
218
      }
219
      return user
220
    } catch (error) {
221
      this.logger.error(error)
222
      throw new HttpException(error?.message || error, 400)
223
    }
224
  }
225

226
  private _generateUniqueUsername() {
227
    const config: Config = {
228
      dictionaries: [adjectives, colors, animals],
229
      separator: ' ',
230
      style: 'capital'
231
    }
232

233
    return 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
   */
240
  async findUser(userId: UserId): Promise<UserDocument> {
241
    return await this.userModel.findById(userId).populate('customData').exec()
242
  }
243

244
  async findOneAdmin(userId: UserId): Promise<UserDocument> {
245
    return await this.userModel.findById(userId).exec()
246
  }
247

248
  async getUserRecents(userId: UserId): Promise<IUserRecents> {
249
    const userRecents = await this.userModel
250
      .findOne({ _id: userId })
251
      .select('recents')
252
      .exec()
253

254
    if (!userRecents) {
255
      throw new NotFoundException('User not found')
256
    }
257

258
    return userRecents.recents as IUserRecents
259
  }
260

261
  /**
262
   * START Section: Friends and Friend Requests  ------------------------------------------------------
263
   */
264

265
  public async findUserFriendsAdmin(userId: UserId): Promise<Friend[]> {
266
    const [aggregationResult] = await this.userModel
267
      .aggregate([
268
        { $match: { _id: new ObjectId(userId) } },
269
        {
270
          $lookup: {
271
            from: 'users',
272
            localField: 'friends',
273
            foreignField: '_id',
274
            as: 'friends'
275
          }
276
        },
277
        {
278
          $project: {
279
            friends: {
280
              $filter: {
281
                input: '$friends',
282
                as: 'friend',
283
                cond: { $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': 1
294
          }
295
        }
296
      ])
297
      .exec()
298

299
    return aggregationResult
300
  }
301

302
  findFriendRequestsSentToMeAdmin(userId: UserId): Promise<Friend[]> {
303
    return this.userModel
304
      .find({
305
        sentFriendRequestsToUsers: { $in: [userId] },
306
        deleted: { $exists: false }
307
      })
308
      .select({ _id: 1, displayName: 1, profileImage: 1, coverImage: 1 })
309
      .exec()
310
  }
311

312
  async acceptFriendRequestAdmin(
313
    userId: UserId,
314
    userIdOfFriendRequestToAccept: UserId
315
  ): Promise<Friend[]> {
316
    // first ensure that the friend request actually exists
317
    const check = await this._checkIfFriendRequestExistsAdmin(
318
      userIdOfFriendRequestToAccept,
319
      userId // 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

322
    if (check) {
323
      // add to friends list for both users
324
      await this.userModel
325
        .findByIdAndUpdate(
326
          userId,
327
          { $addToSet: { friends: userIdOfFriendRequestToAccept } },
328
          { new: true }
329
        )
330
        .exec()
331
      await this.userModel
332
        .findByIdAndUpdate(
333
          userIdOfFriendRequestToAccept,
334
          { $addToSet: { friends: userId } },
335
          { new: true }
336
        )
337
        .exec()
338
      // remove the sentFriendRequestsToUsers from the user who sent the request
339
      await this.userModel
340
        .findByIdAndUpdate(
341
          userIdOfFriendRequestToAccept,
342
          { $pull: { sentFriendRequestsToUsers: userId } },
343
          { new: true }
344
        )
345
        .exec()
346

347
      return await this.findUserFriendsAdmin(userId)
348
    } else {
349
      throw new NotFoundException(
350
        'Friend request not found or you are already friends with this user'
351
      )
352
    }
353
  }
354

355
  async rejectFriendRequestAdmin(
356
    userId: UserId,
357
    userIdOfFriendRequestToReject: UserId
358
  ): Promise<Friend[]> {
359
    // first ensure that the friend request actually exists
360
    const check = await this._checkIfFriendRequestExistsAdmin(
361
      userIdOfFriendRequestToReject,
362
      userId // 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

365
    if (check) {
366
      // remove the sentFriendRequestsToUsers from the user who sent the request
367
      await this.userModel
368
        .findByIdAndUpdate(
369
          userIdOfFriendRequestToReject,
370
          { $pull: { sentFriendRequestsToUsers: userId } },
371
          { new: true }
372
        )
373
        .exec()
374

375
      // Note that the return here is different from acceptFriendRequestAdmin
376
      // instead, this returns the list of friend requests
377
      return await this.findFriendRequestsSentToMeAdmin(userId)
378
    } else {
379
      throw new NotFoundException('Friend request not found')
380
    }
381
  }
382

383
  async findSentFriendRequestsAdmin(userId: UserId): Promise<Friend[]> {
384
    const [aggregationResult] = await this.userModel
385
      .aggregate([
386
        { $match: { _id: new ObjectId(userId) } },
387
        {
388
          $lookup: {
389
            from: 'users',
390
            localField: 'sentFriendRequestsToUsers',
391
            foreignField: '_id',
392
            as: 'sentFriendRequestsToUsers'
393
          }
394
        },
395
        {
396
          $project: {
397
            sentFriendRequestsToUsers: {
398
              $filter: {
399
                input: '$sentFriendRequestsToUsers',
400
                as: 'request',
401
                cond: { $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': 1
412
          }
413
        }
414
      ])
415
      .exec()
416

417
    return aggregationResult.sentFriendRequestsToUsers
418
  }
419

420
  sendFriendRequestAdmin(
421
    requestingUserId: UserId,
422
    toUserId: UserId
423
  ): Promise<UserDocument> {
424
    return this.userModel
425
      .findByIdAndUpdate(
426
        requestingUserId,
427
        { $addToSet: { sentFriendRequestsToUsers: toUserId } },
428
        { new: true }
429
      )
430
      .select({ sentFriendRequestsToUsers: 1 })
431
      .exec()
432
  }
433

434
  private async _checkIfFriendRequestExistsAdmin(
435
    fromUserId: UserId,
436
    toUserId: UserId
437
  ): Promise<boolean> {
438
    const test = await this.userModel
439
      .find({
440
        _id: new ObjectId(fromUserId),
441
        sentFriendRequestsToUsers: { $in: [toUserId] }
442
      })
443
      .select({ _id: 1, displayName: 1, profileImage: 1, coverImage: 1 })
444
      .exec()
445
    if (test) {
446
      return true
447
    }
448
    return false
449
  }
450

451
  /**
452
   * @description Removes a friend and returns the updated friends list
453
   * @date 2023-06-30 00:19
454
   */
455
  async removeFriendAdmin(
456
    userId: UserId,
457
    friendUserIdToRemove: UserId
458
  ): Promise<Friend[]> {
459
    // remove from friends list for both users
460
    await this.userModel
461
      .findByIdAndUpdate(
462
        userId,
463
        { $pull: { friends: friendUserIdToRemove } },
464
        { new: true }
465
      )
466
      .exec()
467
    await this.userModel
468
      .findByIdAndUpdate(
469
        friendUserIdToRemove,
470
        { $pull: { friends: userId } },
471
        { new: true }
472
      )
473
      .exec()
474

475
    return await this.findUserFriendsAdmin(userId)
476
  }
477
  /**
478
   * END Section: Friends and Friend Requests  ------------------------------------------------------
479
   */
480

481
  /**
482
   * START Section: Cart  ------------------------------------------------------
483
   */
484

485
  async getUserCartAdmin(userId: UserId) {
486
    return 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
   */
494
  async addUserCartItemToUserCartAdmin(
495
    userId: UserId,
496
    dto: AddUserCartItemToUserCartDto
497
  ) {
498
    const cartItem = new UserCartItem()
499
    cartItem.entityType = dto.entityType
500
    cartItem.forEntity = new mongoose.Types.ObjectId(dto.forEntity)
501
    return await this.userModel
502
      .findByIdAndUpdate(
503
        userId,
504
        {
505
          $push: { cartItems: cartItem }
506
        },
507
        { new: true, select: { cartItems: 1 } }
508
      )
509
      .exec()
510
  }
511

512
  async removeAllUserCartItemsFromUserCartAdmin(userId: UserId) {
513
    return await this.userModel
514
      .findByIdAndUpdate(
515
        userId,
516
        {
517
          $set: {
518
            cartItems: []
519
          }
520
        },
521
        { new: true, select: { cartItems: 1 } }
522
      )
523
      .exec()
524
  }
525

526
  async removeUserCartItemFromUserCartAdmin(
527
    userId: UserId,
528
    cartItemId: string
529
  ) {
530
    return await this.userModel
531
      .findByIdAndUpdate(
532
        userId,
533
        {
534
          $pull: {
535
            cartItems: {
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
   */
554
  async findUserIncludingCustomData(id: string) {
555
    return await this.userModel.findById(id).populate('customData').exec()
556
  }
557

558
  findUserByDiscordId(discordUserId: string): Promise<User> {
559
    return this.userModel.findOne({ discordUserId: discordUserId }).exec()
560
  }
561

562
  findUserByEmail(email: string): Promise<User> {
563
    return this.userModel.findOne({ email: email }).exec()
564
  }
565

566
  async updateUserProfileAdmin(
567
    id: string,
568
    dto: UpdateUserProfileDto
569
  ): Promise<UserDocument> {
570
    let originalUser: UserDocument
571
    const firebaseUpdate = this.getFirebaseFieldsForUpdate(dto)
572
    const firebaseFields = Object.keys(firebaseUpdate)
573

574
    try {
575
      /** Update and save reference of old UserModel to handle errors */
576
      originalUser = await this.userModel.findByIdAndUpdate(id, dto).exec()
577

578
      /** Update firebase specific fields if present */
579
      if (firebaseFields.length) {
580
        await this.firebaseAuthService.updateUser(id, firebaseUpdate)
581
      }
582

583
      /** On success - return latest UserModel  */
584
      return this.findUser(id)
585
    } catch (error) {
586
      /** If Update succeeds but firebase fails, reset failed fields on mongodb User */
587
      if (firebaseFields.length && originalUser?._id) {
588
        this.undoUserUpdateOnError(id, firebaseFields, originalUser)
589
      }
590
      this.logger.error(error)
591
      throw new HttpException(error.message ?? error, 400)
592
    }
593
  }
594

595
  updateUserProfile(userId: UserId, dto: UpdateUserProfileDto) {
596
    return this.userModel.findByIdAndUpdate(userId, dto, { new: true }).exec()
597
  }
598

599
  updateUserTutorial(userId: UserId, dto: UpdateUserTutorialDto) {
600
    return this.userModel
601
      .findByIdAndUpdate(
602
        userId,
603
        {
604
          // the below is so nested properties don't get overwritten
605
          ...Object.fromEntries(
606
            Object.entries(dto).map(([key, value]) => [
607
              `tutorial.${key}`,
608
              value
609
            ])
610
          )
611
        },
612
        { new: true }
613
      )
614
      .exec()
615
  }
616

617
  updateDeepLink(userId: UserId, dto: UpdateUserDeepLinkDto) {
618
    return this.userModel
619
      .findByIdAndUpdate(
620
        userId,
621
        {
622
          deepLinkKey: dto.deepLinkKey,
623
          deepLinkValue: dto.deepLinkValue,
624
          deepLinkLastUpdatedAt: new Date()
625
        },
626
        { new: true }
627
      )
628
      .exec()
629
  }
630

631
  updateUserAvatar(id: string, dto: UpdateUserAvatarDto) {
632
    return this.userModel.findByIdAndUpdate(id, dto, { new: true }).exec()
633
  }
634

635
  updateUserTerms(id: string, dto: UpdateUserTermsDto) {
636
    return this.userModel.findByIdAndUpdate(id, dto, { new: true }).exec()
637
  }
638

639
  updateUserAvatarType(id: string, dto: UpdateUserAvatarTypeDto) {
640
    return this.userModel.findByIdAndUpdate(id, dto, { new: true }).exec()
641
  }
642

643
  updateUserRecentSpaces(id: string, spaces: string[]) {
644
    return this.userModel.findByIdAndUpdate(id, { 'recents.spaces': spaces })
645
  }
646

647
  updateUserRecentInstancedAssets(id: string, assets: string[]) {
648
    return this.userModel.findByIdAndUpdate(id, {
649
      'recents.assets.instanced': assets
650
    })
651
  }
652

653
  updateUserRecentScripts(id: string, scripts: string[]) {
654
    return this.userModel.findByIdAndUpdate(id, {
655
      'recents.scripts': scripts
656
    })
657
  }
658

659
  getUserFiveStarRatedSpaces(userId) {
660
    return this.userEntityActionModel
661
      .find({
662
        creator: new ObjectId(userId),
663
        entityType: ENTITY_TYPE.SPACE,
664
        actionType: USER_ENTITY_ACTION_TYPE.RATING,
665
        rating: 5
666
      })
667
      .select('forEntity')
668
      .exec()
669
  }
670

671
  addUserPremiumAccess(id: string, accessLevelToAdd: PREMIUM_ACCESS) {
672
    return this.userModel
673
      .findByIdAndUpdate(
674
        id,
675
        {
676
          $addToSet: {
677
            premiumAccess: accessLevelToAdd
678
          }
679
        },
680
        { new: true }
681
      )
682
      .exec()
683
  }
684

685
  removeUserPremiumAccess(id: string, accessLevelToRemove: PREMIUM_ACCESS) {
686
    return this.userModel
687
      .findByIdAndUpdate(
688
        id,
689
        {
690
          $pull: {
691
            premiumAccess: accessLevelToRemove
692
          }
693
        },
694
        { new: true }
695
      )
696
      .exec()
697
  }
698

699
  async getPublicEntityActionStats(entityId: string) {
700
    const pipeline = [
701
      {
702
        $match: { forEntity: new ObjectId(entityId) }
703
      },
704
      {
705
        $group: {
706
          _id: '$actionType',
707
          count: { $sum: 1 },
708
          ratingSum: { $sum: '$rating' },
709
          ratingAvg: { $avg: '$rating' }
710
        }
711
      },
712
      {
713
        $group: {
714
          _id: null,
715
          COUNT_LIKE: {
716
            $sum: { $cond: [{ $eq: ['$_id', 'LIKE'] }, '$count', 0] }
717
          },
718
          COUNT_FOLLOW: {
719
            $sum: { $cond: [{ $eq: ['$_id', 'FOLLOW'] }, '$count', 0] }
720
          },
721
          COUNT_SAVES: {
722
            $sum: { $cond: [{ $eq: ['$_id', 'SAVE'] }, '$count', 0] }
723
          },
724
          COUNT_RATING: {
725
            $sum: { $cond: [{ $eq: ['$_id', 'RATING'] }, '$count', 0] }
726
          },
727
          AVG_RATING: {
728
            $avg: { $cond: [{ $eq: ['$_id', 'RATING'] }, '$ratingAvg', null] }
729
          }
730
        }
731
      },
732
      {
733
        $project: {
734
          _id: 0,
735
          COUNT_LIKE: 1,
736
          COUNT_FOLLOW: 1,
737
          COUNT_SAVES: 1,
738
          COUNT_RATING: 1,
739
          AVG_RATING: 1
740
        }
741
      }
742
    ]
743
    const stats = await this.userEntityActionModel.aggregate(pipeline).exec()
744

745
    return stats[0]
746
  }
747

748
  findEntityActionsByUserForEntity(userId: UserId, entityId: string) {
749
    // validate mongo Ids
750
    if (!mongo.ObjectId.isValid(userId)) {
751
      throw new BadRequestException('User ID is not a valid Mongo ObjectId')
752
    }
753
    if (!mongo.ObjectId.isValid(entityId)) {
754
      throw new BadRequestException('Entity ID is not a valid Mongo ObjectId')
755
    }
756
    return this.userEntityActionModel
757
      .find({
758
        creator: new ObjectId(userId),
759
        forEntity: new ObjectId(entityId)
760
      })
761
      .exec()
762
  }
763

764
  upsertUserEntityAction(userId: string, dto: UpsertUserEntityActionDto) {
765
    const findData = {
766
      creator: new ObjectId(userId),
767
      forEntity: new ObjectId(dto.forEntity),
768
      entityType: dto.entityType,
769
      actionType: dto.actionType
770
    }
771
    const updateData: any = {}
772
    if (dto.rating !== undefined) {
773
      updateData.rating = dto.rating
774
    }
775
    return this.userEntityActionModel
776
      .findOneAndUpdate(findData, updateData, { new: true, upsert: true })
777
      .exec()
778
  }
779

780
  removeUserEntityAction(
781
    userId: UserId,
782
    userEntityActionId: UserEntityActionId
783
  ) {
784
    return this.userEntityActionModel
785
      .findOneAndRemove({ creator: userId, _id: userEntityActionId })
786
      .exec()
787
  }
788

789
  public uploadProfileImage({ file, userId }: UploadProfileFileDto) {
790
    const fileId = new ObjectId()
791
    const path = `${userId}/profile-images/${fileId.toString()}`
792
    return 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
   */
799
  addRpmAvatarUrl(id: string, dto: AddRpmAvatarUrlDto) {
800
    return this.userModel
801
      .findByIdAndUpdate(
802
        id,
803
        {
804
          $addToSet: {
805
            readyPlayerMeAvatarUrls: dto.rpmAvatarUrl
806
          }
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
   */
817
  removeRpmAvatarUrl(id: string, dto: RemoveRpmAvatarUrlDto) {
818
    return this.userModel
819
      .findByIdAndUpdate(
820
        id,
821
        {
822
          $pullAll: {
823
            readyPlayerMeAvatarUrls: [dto.rpmAvatarUrl]
824
          }
825
        },
826
        { new: true }
827
      )
828
      .exec()
829
  }
830

831
  createUserAccessKey(createSignUpKeyDto: CreateUserAccessKeyDto) {
832
    const keyName = uuidv4()
833
    const key = new this.userAccessKeyModel({
834
      ...createSignUpKeyDto,
835
      key: keyName
836
    })
837
    return key.save()
838
  }
839

840
  async checkUserAccessKeyExistence(name: string) {
841
    const check = await this.userAccessKeyModel.findOne({
842
      key: name,
843
      usedBy: {
844
        $exists: false // need to ensure it's not in use
845
      }
846
    })
847
    if (check) {
848
      return check
849
    } else {
850
      return false
851
    }
852
  }
853

854
  async setUserAccessKeyAsUsed(
855
    keyId: string,
856
    userId: string
857
  ): 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:44
859
    return await this.userAccessKeyModel
860
      .updateOne({ _id: keyId }, { usedBy: userId })
861
      .exec()
862
  }
863

864
  /**
865
   * START Section: Custom Data
866
   */
867
  async setCustomDataOnUser(
868
    userId: string,
869
    customDataDto: CreateCustomDataDto
870
  ) {
871
    const customDataDoc = await this.customDataService.createCustomData(
872
      userId,
873
      customDataDto
874
    )
875
    const doc = await this.userModel
876
      .findByIdAndUpdate(
877
        userId,
878
        {
879
          $set: {
880
            customData: customDataDoc.id
881
          }
882
        },
883
        { new: true }
884
      )
885
      .exec()
886
    return doc
887
  }
888

889
  /**
890
   * END Section: Custom Data
891
   */
892

893
  /** Extract Firebase specific properties to update */
894
  protected getFirebaseFieldsForUpdate(dto: UpdateUserProfileDto): any {
895
    const { email, displayName } = dto
896
    return {
897
      ...(email && { email }),
898
      ...(displayName && { displayName })
899
    }
900
  }
901

902
  protected undoUserUpdateOnError(
903
    id: string,
904
    firebaseFields: string[],
905
    originalUser: UserDocument
906
  ) {
907
    this.logger.warn(
908
      `Error Encountered. Attempting to revert User update for User ID: ${id}`
909
    )
910

911
    const revertFields = firebaseFields.reduce((result, field) => {
912
      if (originalUser[field]) {
913
        result[field] = originalUser[field]
914
      }
915
      return result
916
    }, {} as UpdateUserProfileDto)
917

918
    this.updateUserProfile(id, revertFields)
919
      .then(() =>
920
        this.logger.warn(
921
          `Attempted update of User ID: ${id} in mongo db successful. Update reverted.`
922
        )
923
      )
924
      .catch(() =>
925
        this.logger.warn(
926
          `Attempted update of User ID: ${id} in mongo db unsuccessful. Update not reverted.`
927
        )
928
      )
929
  }
930

931
  protected async removeAuthUserWhenErrored(_id: string) {
932
    this.logger.warn(
933
      `Error Encountered. Attempting to remove user from firebase db. ID: ${_id}`
934
    )
935
    await this.firebaseAuthService
936
      .deleteUser(_id)
937
      .then((result) => {
938
        this.logger.warn(
939
          `Attempted removal of user ${_id} from firebase db successful. ID: ${_id} deleted.`
940
        )
941
      })
942
      .catch((error) => {
943
        this.logger.error(
944
          `Attempted removal of user ${_id} from firebase db unsuccessful. ID: ${_id} not deleted!`
945
        )
946
      })
947
  }
948

949
  public async getUserSidebarTags(userId: UserId) {
950
    const user = await this.userModel
951
      .findOne({ _id: userId })
952
      .select('sidebarTags')
953
      .exec()
954

955
    if (!user) {
956
      throw new NotFoundException('User not found')
957
    }
958

959
    return user?.sidebarTags || []
960
  }
961

962
  public async addUserSidebarTag(
963
    userId: UserId,
964
    addUserSidebarTagDto: AddUserSidebarTagDto
965
  ) {
966
    const { sidebarTag } = addUserSidebarTagDto
967
    const sidebarTags = await this.getUserSidebarTags(userId)
968

969
    if (sidebarTags.length === 3) {
970
      throw new BadRequestException('User already has 3 sidebar tags')
971
    }
972

973
    if (sidebarTags.includes(sidebarTag)) {
974
      throw new ConflictException('User already has this sidebar tag')
975
    }
976

977
    sidebarTags.push(sidebarTag)
978
    await this.updateUserSidebarTags(userId, sidebarTags)
979

980
    return sidebarTag
981
  }
982

983
  public async deleteUserSidebarTag(userId: UserId, sidebarTag: string) {
984
    await this.userModel.updateOne(
985
      { _id: userId },
986
      { $pull: { sidebarTags: sidebarTag } }
987
    )
988

989
    return { userId, sidebarTag }
990
  }
991

992
  public async updateUserSidebarTags(userId: string, sidebarTags: string[]) {
993
    await this.userModel.updateOne({ _id: userId }, { $set: { sidebarTags } })
994
    return sidebarTags
995
  }
996

997
  public async updateUserLastActiveTimestamp(userId: string) {
998
    await 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
   */
1011
  public async markUserAsDeleted(userId: string) {
1012
    const updateResult = await this.userModel.updateOne({ _id: userId }, [
1013
      { $set: { deleted: true } },
1014
      { $unset: ['email', 'firebaseUID', 'discordUserId'] }
1015
    ])
1016

1017
    if (!updateResult.modifiedCount) {
1018
      throw new NotFoundException('User not found')
1019
    }
1020
  }
1021
}
1022

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.