in-app-notification.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import type {
  2. HasObjectId, IUser, IPage,
  3. } from '@growi/core';
  4. import { SubscriptionStatusType } from '@growi/core';
  5. import { subDays } from 'date-fns/subDays';
  6. import type { Types, FilterQuery, UpdateQuery } from 'mongoose';
  7. import { AllEssentialActions } from '~/interfaces/activity';
  8. import type { PaginateResult } from '~/interfaces/in-app-notification';
  9. import { InAppNotificationStatuses } from '~/interfaces/in-app-notification';
  10. import type { ActivityDocument } from '~/server/models/activity';
  11. import type { InAppNotificationDocument } from '~/server/models/in-app-notification';
  12. import {
  13. InAppNotification,
  14. } from '~/server/models/in-app-notification';
  15. import InAppNotificationSettings from '~/server/models/in-app-notification-settings';
  16. import Subscription from '~/server/models/subscription';
  17. import loggerFactory from '~/utils/logger';
  18. import type Crowi from '../crowi';
  19. import { generateSnapshot } from './in-app-notification/in-app-notification-utils';
  20. import { preNotifyService, type PreNotify } from './pre-notify';
  21. import { RoomPrefix, getRoomNameWithId } from './socket-io/helper';
  22. const { STATUS_UNOPENED, STATUS_OPENED } = InAppNotificationStatuses;
  23. const logger = loggerFactory('growi:service:inAppNotification');
  24. export default class InAppNotificationService {
  25. crowi!: Crowi;
  26. socketIoService!: any;
  27. activityEvent!: any;
  28. constructor(crowi: Crowi) {
  29. this.crowi = crowi;
  30. this.activityEvent = crowi.event('activity');
  31. this.socketIoService = crowi.socketIoService;
  32. this.emitSocketIo = this.emitSocketIo.bind(this);
  33. this.upsertByActivity = this.upsertByActivity.bind(this);
  34. this.getUnreadCountByUser = this.getUnreadCountByUser.bind(this);
  35. this.createInAppNotification = this.createInAppNotification.bind(this);
  36. this.initActivityEventListeners();
  37. }
  38. initActivityEventListeners(): void {
  39. this.activityEvent.on('updated', async(activity: ActivityDocument, target: IUser | IPage, preNotify: PreNotify) => {
  40. try {
  41. const shouldNotification = activity != null && target != null && (AllEssentialActions as ReadonlyArray<string>).includes(activity.action);
  42. if (shouldNotification) {
  43. await this.createInAppNotification(activity, target, preNotify);
  44. }
  45. }
  46. catch (err) {
  47. logger.error('Create InAppNotification failed', err);
  48. }
  49. });
  50. }
  51. emitSocketIo = async(targetUsers) => {
  52. if (this.socketIoService.isInitialized) {
  53. targetUsers.forEach(async(userId) => {
  54. // emit to the room for each user
  55. await this.socketIoService.getDefaultSocket()
  56. .in(getRoomNameWithId(RoomPrefix.USER, userId))
  57. .emit('notificationUpdated');
  58. });
  59. }
  60. };
  61. upsertByActivity = async function(
  62. users: Types.ObjectId[], activity: ActivityDocument, snapshot: string, createdAt?: Date | null,
  63. ): Promise<void> {
  64. const {
  65. _id: activityId, targetModel, target, action,
  66. } = activity;
  67. const now = createdAt || Date.now();
  68. const lastWeek = subDays(now, 7);
  69. const operations = users.map((user) => {
  70. const filter: FilterQuery<InAppNotificationDocument> = {
  71. user, target, action, createdAt: { $gt: lastWeek }, snapshot,
  72. };
  73. const parameters: UpdateQuery<InAppNotificationDocument> = {
  74. user,
  75. targetModel,
  76. target,
  77. action,
  78. status: STATUS_UNOPENED,
  79. createdAt: now,
  80. snapshot,
  81. $addToSet: { activities: activityId },
  82. };
  83. return {
  84. updateOne: {
  85. filter,
  86. update: parameters,
  87. upsert: true,
  88. },
  89. };
  90. });
  91. await InAppNotification.bulkWrite(operations);
  92. logger.info('InAppNotification bulkWrite has run');
  93. return;
  94. };
  95. getLatestNotificationsByUser = async(
  96. userId: Types.ObjectId,
  97. queryOptions: {offset: number, limit: number, status?: InAppNotificationStatuses},
  98. ): Promise<PaginateResult<InAppNotificationDocument>> => {
  99. const { limit, offset, status } = queryOptions;
  100. try {
  101. const pagenateOptions = { user: userId };
  102. if (status != null) {
  103. Object.assign(pagenateOptions, { status });
  104. }
  105. // TODO: import @types/mongoose-paginate-v2 and use PaginateResult as a type after upgrading mongoose v6.0.0
  106. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  107. const paginationResult = await (InAppNotification as any).paginate(
  108. pagenateOptions,
  109. {
  110. sort: { createdAt: -1 },
  111. limit,
  112. offset,
  113. populate: [
  114. { path: 'user' },
  115. { path: 'target' },
  116. { path: 'activities', populate: { path: 'user' } },
  117. ],
  118. },
  119. );
  120. return paginationResult;
  121. }
  122. catch (err) {
  123. logger.error('Error', err);
  124. throw new Error(err);
  125. }
  126. };
  127. open = async function(user: IUser & HasObjectId, id: Types.ObjectId): Promise<void> {
  128. const query = { _id: id, user: user._id };
  129. const parameters = { status: STATUS_OPENED };
  130. const options = { new: true };
  131. await InAppNotification.findOneAndUpdate(query, parameters, options);
  132. return;
  133. };
  134. updateAllNotificationsAsOpened = async function(user: IUser & HasObjectId): Promise<void> {
  135. const filter = { user: user._id, status: STATUS_UNOPENED };
  136. const options = { status: STATUS_OPENED };
  137. await InAppNotification.updateMany(filter, options);
  138. return;
  139. };
  140. getUnreadCountByUser = async function(user: Types.ObjectId): Promise<number| undefined> {
  141. const query = { user, status: STATUS_UNOPENED };
  142. try {
  143. const count = await InAppNotification.countDocuments(query);
  144. return count;
  145. }
  146. catch (err) {
  147. logger.error('Error on getUnreadCountByUser', err);
  148. throw err;
  149. }
  150. };
  151. createSubscription = async function(userId: Types.ObjectId, pageId: Types.ObjectId, targetRuleName: string): Promise<void> {
  152. const query = { userId };
  153. const inAppNotificationSettings = await InAppNotificationSettings.findOne(query);
  154. if (inAppNotificationSettings != null) {
  155. const subscribeRule = inAppNotificationSettings.subscribeRules.find(subscribeRule => subscribeRule.name === targetRuleName);
  156. if (subscribeRule != null && subscribeRule.isEnabled) {
  157. await Subscription.subscribeByPageId(userId, pageId, SubscriptionStatusType.SUBSCRIBE);
  158. }
  159. }
  160. return;
  161. };
  162. createInAppNotification = async function(activity: ActivityDocument, target: IUser | IPage, preNotify: PreNotify): Promise<void> {
  163. const shouldNotification = activity != null && target != null && (AllEssentialActions as ReadonlyArray<string>).includes(activity.action);
  164. const targetModel = activity.targetModel;
  165. const snapshot = generateSnapshot(targetModel, target);
  166. if (shouldNotification) {
  167. const props = preNotifyService.generateInitialPreNotifyProps();
  168. await preNotify(props);
  169. await this.upsertByActivity(props.notificationTargetUsers, activity, snapshot);
  170. await this.emitSocketIo(props.notificationTargetUsers);
  171. }
  172. else {
  173. throw Error('no activity to notify');
  174. }
  175. return;
  176. };
  177. }
  178. module.exports = InAppNotificationService;