Просмотр исходного кода

Merge branch 'support/133781-refactor-where-to-get-users-to-be-notified' into dev/7.0.x

WNomunomu 2 лет назад
Родитель
Сommit
8f9a258052

+ 0 - 2
apps/app/src/interfaces/in-app-notification.ts

@@ -8,8 +8,6 @@ export enum InAppNotificationStatuses {
   STATUS_OPENED = 'OPENED',
 }
 
-// TODO: do not use any type
-// https://redmine.weseek.co.jp/issues/120632
 export interface IInAppNotification<T = unknown> {
   user: IUser
   targetModel: SupportedTargetModelType

+ 2 - 2
apps/app/src/server/models/activity.ts

@@ -112,8 +112,8 @@ activitySchema.statics.createByParameters = async function(parameters): Promise<
 };
 
 // When using this method, ensure that activity updates are allowed using ActivityService.shoudUpdateActivity
-activitySchema.statics.updateByParameters = async function(activityId: string, parameters): Promise<IActivity> {
-  const activity = await this.findOneAndUpdate({ _id: activityId }, parameters, { new: true }) as unknown as IActivity;
+activitySchema.statics.updateByParameters = async function(activityId: string, parameters): Promise<ActivityDocument | null> {
+  const activity = await this.findOneAndUpdate({ _id: activityId }, parameters, { new: true }).exec();
 
   return activity;
 };

+ 3 - 1
apps/app/src/server/routes/apiv3/bookmarks.js

@@ -1,6 +1,7 @@
 import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 import { serializeBookmarkSecurely } from '~/server/models/serializers/bookmark-serializer';
+import { preNotifyService } from '~/server/service/pre-notify';
 import loggerFactory from '~/utils/logger';
 
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
@@ -301,7 +302,8 @@ module.exports = (crowi) => {
       target: page,
       action: bool ? SupportedAction.ACTION_PAGE_BOOKMARK : SupportedAction.ACTION_PAGE_UNBOOKMARK,
     };
-    activityEvent.emit('update', res.locals.activity._id, parameters, page);
+
+    activityEvent.emit('update', res.locals.activity._id, parameters, page, preNotifyService.generatePreNotify);
 
     return res.apiv3({ bookmark });
   });

+ 4 - 1
apps/app/src/server/routes/apiv3/page.js

@@ -14,6 +14,7 @@ import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import { excludeReadOnlyUser } from '~/server/middlewares/exclude-read-only-user';
 import Subscription from '~/server/models/subscription';
 import UserGroup from '~/server/models/user-group';
+import { preNotifyService } from '~/server/service/pre-notify';
 import { divideByType } from '~/server/util/granted-group';
 import loggerFactory from '~/utils/logger';
 
@@ -362,7 +363,9 @@ module.exports = (crowi) => {
       target: page,
       action: isLiked ? SupportedAction.ACTION_PAGE_LIKE : SupportedAction.ACTION_PAGE_UNLIKE,
     };
-    activityEvent.emit('update', res.locals.activity._id, parameters, page);
+
+    activityEvent.emit('update', res.locals.activity._id, parameters, page, preNotifyService.generatePreNotify);
+
 
     res.apiv3({ result });
 

+ 3 - 1
apps/app/src/server/routes/apiv3/pages.js

@@ -6,6 +6,7 @@ import { normalizePath, addHeadingSlash, attachTitleHeader } from '@growi/core/d
 
 import { SupportedTargetModel, SupportedAction } from '~/interfaces/activity';
 import { subscribeRuleNames } from '~/interfaces/in-app-notification';
+import { preNotifyService } from '~/server/service/pre-notify';
 import loggerFactory from '~/utils/logger';
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
@@ -849,7 +850,8 @@ module.exports = (crowi) => {
         target: page,
         action: SupportedAction.ACTION_PAGE_DUPLICATE,
       };
-      activityEvent.emit('update', res.locals.activity._id, parameters, page);
+
+      activityEvent.emit('update', res.locals.activity._id, parameters, page, preNotifyService.generatePreNotify);
 
       return res.apiv3(result);
     });

+ 10 - 1
apps/app/src/server/routes/comment.js

@@ -3,6 +3,8 @@ import { Comment, CommentEvent, commentEvent } from '~/features/comment/server';
 import { SupportedAction, SupportedTargetModel, SupportedEventModel } from '~/interfaces/activity';
 import loggerFactory from '~/utils/logger';
 
+import { preNotifyService } from '../service/pre-notify';
+
 /**
  * @swagger
  *  tags:
@@ -266,7 +268,14 @@ module.exports = function(crowi, app) {
       event: createdComment,
       action: SupportedAction.ACTION_COMMENT_CREATE,
     };
-    activityEvent.emit('update', res.locals.activity._id, parameters, page);
+
+    const getAdditionalTargetUsers = async(activity) => {
+      const mentionedUsers = await crowi.commentService.getMentionedUsers(activity.event);
+
+      return mentionedUsers;
+    };
+
+    activityEvent.emit('update', res.locals.activity._id, parameters, page, preNotifyService.generatePreNotify, getAdditionalTargetUsers);
 
     res.json(ApiResponse.success({ comment: createdComment }));
 

+ 9 - 2
apps/app/src/server/routes/login.js

@@ -44,13 +44,20 @@ module.exports = function(crowi, app) {
   }
 
   async function sendNotificationToAllAdmins(user) {
-    const adminUsers = await User.findAdmins();
+
     const activity = await activityService.createActivity({
       action: SupportedAction.ACTION_USER_REGISTRATION_APPROVAL_REQUEST,
       target: user,
       targetModel: SupportedTargetModel.MODEL_USER,
     });
-    await activityEvent.emit('updated', activity, user, adminUsers);
+
+    const preNotify = async(props) => {
+      const adminUsers = await User.findAdmins();
+
+      props.push(...adminUsers);
+    };
+
+    await activityEvent.emit('updated', activity, user, preNotify);
     return;
   }
 

+ 5 - 1
apps/app/src/server/routes/page.js

@@ -7,6 +7,7 @@ import loggerFactory from '~/utils/logger';
 
 import { PathAlreadyExistsError } from '../models/errors';
 import UpdatePost from '../models/update-post';
+import { preNotifyService } from '../service/pre-notify';
 
 const { serializePageSecurely } = require('../models/serializers/page-serializer');
 const { serializeRevisionSecurely } = require('../models/serializers/revision-serializer');
@@ -522,7 +523,10 @@ module.exports = function(crowi, app) {
       target: page,
       action: SupportedAction.ACTION_PAGE_UPDATE,
     };
-    activityEvent.emit('update', res.locals.activity._id, parameters, { path: page.path, creator: page.creator._id.toString() });
+
+    activityEvent.emit(
+      'update', res.locals.activity._id, parameters, { path: page.path, creator: page.creator._id.toString() }, preNotifyService.generatePreNotify,
+    );
   };
 
   /**

+ 18 - 5
apps/app/src/server/service/activity.ts

@@ -1,16 +1,18 @@
-import type { Ref, IPage, IUser } from '@growi/core';
+import type { IPage } from '@growi/core';
 import mongoose from 'mongoose';
 
 import {
   IActivity, SupportedActionType, AllSupportedActions, ActionGroupSize,
   AllEssentialActions, AllSmallGroupActions, AllMediumGroupActions, AllLargeGroupActions,
 } from '~/interfaces/activity';
-import Activity from '~/server/models/activity';
+import Activity, { ActivityDocument } from '~/server/models/activity';
 
 import loggerFactory from '../../utils/logger';
 import Crowi from '../crowi';
 
 
+import type { GeneratePreNotify, GetAdditionalTargetUsers } from './pre-notify';
+
 const logger = loggerFactory('growi:service:ActivityService');
 
 const parseActionString = (actionsString: string): SupportedActionType[] => {
@@ -39,8 +41,10 @@ class ActivityService {
   }
 
   initActivityEventListeners(): void {
-    this.activityEvent.on('update', async(activityId: string, parameters, target?: IPage, descendantsSubscribedUsers?: Ref<IUser>[]) => {
-      let activity: IActivity;
+    this.activityEvent.on('update', async(
+        activityId: string, parameters, target: IPage, generatePreNotify?: GeneratePreNotify, getAdditionalTargetUsers?: GetAdditionalTargetUsers,
+    ) => {
+      let activity: ActivityDocument;
       const shoudUpdate = this.shoudUpdateActivity(parameters.action);
 
       if (shoudUpdate) {
@@ -52,7 +56,16 @@ class ActivityService {
           return;
         }
 
-        this.activityEvent.emit('updated', activity, target, descendantsSubscribedUsers);
+        if (generatePreNotify != null) {
+          const preNotify = generatePreNotify(activity, getAdditionalTargetUsers);
+
+          this.activityEvent.emit('updated', activity, target, preNotify);
+
+          return;
+        }
+
+        this.activityEvent.emit('updated', activity, target);
+
       }
     });
   }

+ 20 - 36
apps/app/src/server/service/in-app-notification.ts

@@ -1,14 +1,12 @@
 import type {
-  HasObjectId, Ref, IUser,
+  HasObjectId, IUser, IPage,
 } from '@growi/core';
 import { SubscriptionStatusType } from '@growi/core';
 import { subDays } from 'date-fns';
 import { Types, FilterQuery, UpdateQuery } from 'mongoose';
 
-import { AllEssentialActions, SupportedAction } from '~/interfaces/activity';
+import { AllEssentialActions } from '~/interfaces/activity';
 import { InAppNotificationStatuses, PaginateResult } from '~/interfaces/in-app-notification';
-import * as pageSerializers from '~/models/serializers/in-app-notification-snapshot/page';
-import * as userSerializers from '~/models/serializers/in-app-notification-snapshot/user';
 import { ActivityDocument } from '~/server/models/activity';
 import {
   InAppNotification,
@@ -21,12 +19,14 @@ import loggerFactory from '~/utils/logger';
 import Crowi from '../crowi';
 import { RoomPrefix, getRoomNameWithId } from '../util/socket-io-helpers';
 
+import { generateSnapshot } from './in-app-notification/in-app-notification-utils';
+import { preNotifyService, type PreNotify } from './pre-notify';
+
 
 const { STATUS_UNREAD, STATUS_UNOPENED, STATUS_OPENED } = InAppNotificationStatuses;
 
 const logger = loggerFactory('growi:service:inAppNotification');
 
-
 export default class InAppNotificationService {
 
   crowi!: Crowi;
@@ -49,13 +49,11 @@ export default class InAppNotificationService {
   }
 
   initActivityEventListeners(): void {
-    // TODO: do not use any type
-    // https://redmine.weseek.co.jp/issues/120632
-    this.activityEvent.on('updated', async(activity: ActivityDocument, target: any, users?: Ref<IUser>[]) => {
+    this.activityEvent.on('updated', async(activity: ActivityDocument, target: IUser | IPage, preNotify: PreNotify) => {
       try {
         const shouldNotification = activity != null && target != null && (AllEssentialActions as ReadonlyArray<string>).includes(activity.action);
         if (shouldNotification) {
-          await this.createInAppNotification(activity, target, users);
+          await this.createInAppNotification(activity, target, preNotify);
         }
       }
       catch (err) {
@@ -199,38 +197,24 @@ export default class InAppNotificationService {
     return;
   };
 
-  // TODO: do not use any type
-  // https://redmine.weseek.co.jp/issues/120632
-  createInAppNotification = async function(activity: ActivityDocument, target, users?: Ref<IUser>[]): Promise<void> {
-    if (activity.action === SupportedAction.ACTION_USER_REGISTRATION_APPROVAL_REQUEST) {
-      const snapshot = userSerializers.stringifySnapshot(target);
-      await this.upsertByActivity(users, activity, snapshot);
-      await this.emitSocketIo(users);
-      return;
-    }
+  createInAppNotification = async function(activity: ActivityDocument, target: IUser | IPage, preNotify: PreNotify): Promise<void> {
 
     const shouldNotification = activity != null && target != null && (AllEssentialActions as ReadonlyArray<string>).includes(activity.action);
-    const snapshot = pageSerializers.stringifySnapshot(target);
+
+    const targetModel = activity.targetModel;
+
+    const snapshot = generateSnapshot(targetModel, target);
+
     if (shouldNotification) {
-      let mentionedUsers: IUser[] = [];
-      if (activity.action === SupportedAction.ACTION_COMMENT_CREATE) {
-        mentionedUsers = await this.crowi.commentService.getMentionedUsers(activity.event);
-      }
-      const notificationTargetUsers = await activity?.getNotificationTargetUsers();
-      let notificationDescendantsUsers = [];
-      if (users != null) {
-        const User = this.crowi.model('User');
-        const descendantsUsers = users.filter(item => (item.toString() !== activity.user._id.toString()));
-        notificationDescendantsUsers = await User.find({
-          _id: { $in: descendantsUsers },
-          status: User.STATUS_ACTIVE,
-        }).distinct('_id');
-      }
-      await this.upsertByActivity([...notificationTargetUsers, ...mentionedUsers, ...notificationDescendantsUsers], activity, snapshot);
-      await this.emitSocketIo([...notificationTargetUsers, notificationDescendantsUsers]);
+      const props = preNotifyService.generateInitialPreNotifyProps();
+
+      await preNotify(props);
+
+      await this.upsertByActivity(props.notificationTargetUsers, activity, snapshot);
+      await this.emitSocketIo(props.notificationTargetUsers);
     }
     else {
-      throw Error('No activity to notify');
+      throw Error('no activity to notify');
     }
     return;
   };

+ 19 - 0
apps/app/src/server/service/in-app-notification/in-app-notification-utils.ts

@@ -0,0 +1,19 @@
+import type { IUser, IPage } from '@growi/core';
+
+import { SupportedTargetModel } from '~/interfaces/activity';
+import * as pageSerializers from '~/models/serializers/in-app-notification-snapshot/page';
+
+const isIPage = (targetModel: string, target: IUser | IPage): target is IPage => {
+  return targetModel === SupportedTargetModel.MODEL_PAGE;
+};
+
+export const generateSnapshot = (targetModel: string, target: IUser | IPage) => {
+
+  let snapshot;
+
+  if (isIPage(targetModel, target)) {
+    snapshot = pageSerializers.stringifySnapshot(target);
+  }
+
+  return snapshot;
+};

+ 41 - 15
apps/app/src/server/service/page.ts

@@ -2,7 +2,7 @@ import pathlib from 'path';
 import { Readable, Writable } from 'stream';
 
 import type {
-  Ref, HasObjectId, IUserHasId,
+  Ref, HasObjectId, IUserHasId, IUser,
   IPage, IPageInfo, IPageInfoAll, IPageInfoForEntity, IPageWithMeta, IGrantedGroup,
 } from '@growi/core';
 import { PageGrant, PageStatus } from '@growi/core';
@@ -29,6 +29,7 @@ import {
   type CreateMethod, type PageCreateOptions, type PageModel, type PageDocument, pushRevision, PageQueryBuilder,
 } from '~/server/models/page';
 import { createBatchStream } from '~/server/util/batch-stream';
+import { getModelSafely } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 import { prepareDeleteConfigValuesForCalc } from '~/utils/page-delete-config';
 
@@ -45,6 +46,8 @@ import UserGroupRelation from '../models/user-group-relation';
 import { V5ConversionError } from '../models/vo/v5-conversion-error';
 import { divideByType } from '../util/granted-group';
 
+import { preNotifyService } from './pre-notify';
+
 const debug = require('debug')('growi:services:page');
 
 const logger = loggerFactory('growi:services:page');
@@ -443,7 +446,9 @@ class PageService {
       throw err;
     }
     if (page.descendantCount < 1) {
-      this.activityEvent.emit('updated', activity, page);
+      const preNotify = preNotifyService.generatePreNotify(activity);
+
+      this.activityEvent.emit('updated', activity, page, preNotify);
     }
     return renamedPage;
   }
@@ -548,8 +553,11 @@ class PageService {
     // update descendants first
       const descendantsSubscribedSets = new Set();
       await this.renameDescendantsWithStream(page, newPagePath, user, options, false, descendantsSubscribedSets);
-      const descendantsSubscribedUsers = Array.from(descendantsSubscribedSets);
-      this.activityEvent.emit('updated', activity, page, descendantsSubscribedUsers);
+      const descendantsSubscribedUsers = Array.from(descendantsSubscribedSets) as Ref<IUser>[];
+
+      const preNotify = preNotifyService.generatePreNotify(activity, () => { return descendantsSubscribedUsers });
+
+      this.activityEvent.emit('updated', activity, page, preNotify);
     }
     catch (err) {
       logger.warn(err);
@@ -1481,7 +1489,9 @@ class PageService {
       })();
     }
     else {
-      this.activityEvent.emit('updated', activity, page);
+      const preNotify = preNotifyService.generatePreNotify(activity);
+
+      this.activityEvent.emit('updated', activity, page, preNotify);
     }
 
     return deletedPage;
@@ -1517,8 +1527,11 @@ class PageService {
     const descendantsSubscribedSets = new Set();
     await this.deleteDescendantsWithStream(page, user, false, descendantsSubscribedSets);
 
-    const descendantsSubscribedUsers = Array.from(descendantsSubscribedSets);
-    this.activityEvent.emit('updated', activity, page, descendantsSubscribedUsers);
+    const descendantsSubscribedUsers = Array.from(descendantsSubscribedSets) as Ref<IUser>[];
+
+    const preNotify = preNotifyService.generatePreNotify(activity, () => { return descendantsSubscribedUsers });
+
+    this.activityEvent.emit('updated', activity, page, preNotify);
 
     await PageOperation.findByIdAndDelete(pageOpId);
 
@@ -1829,7 +1842,9 @@ class PageService {
       })();
     }
     else {
-      this.activityEvent.emit('updated', activity, page);
+      const preNotify = preNotifyService.generatePreNotify(activity);
+
+      this.activityEvent.emit('updated', activity, page, preNotify);
     }
 
     return;
@@ -1838,8 +1853,11 @@ class PageService {
   async deleteCompletelyRecursivelyMainOperation(page, user, options, pageOpId: ObjectIdLike, activity?): Promise<void> {
     const descendantsSubscribedSets = new Set();
     await this.deleteCompletelyDescendantsWithStream(page, user, options, false, descendantsSubscribedSets);
-    const descendantsSubscribedUsers = Array.from(descendantsSubscribedSets);
-    this.activityEvent.emit('updated', activity, page, descendantsSubscribedUsers);
+    const descendantsSubscribedUsers = Array.from(descendantsSubscribedSets) as Ref<IUser>[];
+
+    const preNotify = preNotifyService.generatePreNotify(activity, () => { return descendantsSubscribedUsers });
+
+    this.activityEvent.emit('updated', activity, page, preNotify);
 
     await PageOperation.findByIdAndDelete(pageOpId);
 
@@ -1882,9 +1900,11 @@ class PageService {
 
     const descendantsSubscribedSets = new Set();
     const pages = await this.deleteCompletelyDescendantsWithStream(page, user, options, true, descendantsSubscribedSets);
-    const descendantsSubscribedUsers = Array.from(descendantsSubscribedSets);
+    const descendantsSubscribedUsers = Array.from(descendantsSubscribedSets) as Ref<IUser>[];
 
-    this.activityEvent.emit('updated', activity, page, descendantsSubscribedUsers);
+    const preNotify = preNotifyService.generatePreNotify(activity, () => { return descendantsSubscribedUsers });
+
+    this.activityEvent.emit('updated', activity, page, preNotify);
 
     return pages;
   }
@@ -2161,7 +2181,10 @@ class PageService {
 
     if (!isRecursively) {
       await this.updateDescendantCountOfAncestors(parent._id, 1, true);
-      this.activityEvent.emit('updated', activity, page);
+
+      const preNotify = preNotifyService.generatePreNotify(activity);
+
+      this.activityEvent.emit('updated', activity, page, preNotify);
     }
     else {
       let pageOp;
@@ -2207,8 +2230,11 @@ class PageService {
 
     const descendantsSubscribedSets = new Set();
     await this.revertDeletedDescendantsWithStream(page, user, options, false, descendantsSubscribedSets);
-    const descendantsSubscribedUsers = Array.from(descendantsSubscribedSets);
-    this.activityEvent.emit('updated', activity, page, descendantsSubscribedUsers);
+    const descendantsSubscribedUsers = Array.from(descendantsSubscribedSets) as Ref<IUser>[];
+
+    const preNotify = preNotifyService.generatePreNotify(activity, () => { return descendantsSubscribedUsers });
+
+    this.activityEvent.emit('updated', activity, page, preNotify);
 
     const newPath = Page.getRevertDeletedPageName(page.path);
     // normalize parent of descendant pages

+ 66 - 0
apps/app/src/server/service/pre-notify.ts

@@ -0,0 +1,66 @@
+import type {
+  IPage, IUser, Ref,
+} from '@growi/core';
+
+import { ActivityDocument } from '../models/activity';
+import Subscription from '../models/subscription';
+import { getModelSafely } from '../util/mongoose-utils';
+
+export type PreNotifyProps = {
+  notificationTargetUsers?: Ref<IUser>[],
+}
+
+export type PreNotify = (props: PreNotifyProps) => Promise<void>;
+export type GeneratePreNotify = (activity: ActivityDocument, getAdditionalTargetUsers?: (activity?: ActivityDocument) => Ref<IUser>[]) => PreNotify;
+
+export type GetAdditionalTargetUsers = (activity: ActivityDocument) => Ref<IUser>[];
+
+interface IPreNotifyService {
+  generateInitialPreNotifyProps: (PreNotifyProps) => { notificationTargetUsers?: Ref<IUser>[] },
+  generatePreNotify: GeneratePreNotify
+}
+
+class PreNotifyService implements IPreNotifyService {
+
+  generateInitialPreNotifyProps = (): PreNotifyProps => {
+
+    const initialPreNotifyProps: Ref<IUser>[] = [];
+
+    return { notificationTargetUsers: initialPreNotifyProps };
+  };
+
+  generatePreNotify = (activity: ActivityDocument, getAdditionalTargetUsers?: GetAdditionalTargetUsers): PreNotify => {
+
+    const preNotify = async(props: PreNotifyProps) => {
+      const { notificationTargetUsers } = props;
+
+      const User = getModelSafely('User') || require('~/server/models/user')();
+      const actionUser = activity.user;
+      const target = activity.target;
+      const subscribedUsers = await Subscription.getSubscription(target as unknown as Ref<IPage>);
+      const notificationUsers = subscribedUsers.filter(item => (item.toString() !== actionUser._id.toString()));
+      const activeNotificationUsers = await User.find({
+        _id: { $in: notificationUsers },
+        status: User.STATUS_ACTIVE,
+      }).distinct('_id');
+
+      if (getAdditionalTargetUsers == null) {
+        notificationTargetUsers?.push(...activeNotificationUsers);
+      }
+      else {
+        const AdditionalTargetUsers = getAdditionalTargetUsers(activity);
+
+        notificationTargetUsers?.push(
+          ...activeNotificationUsers,
+          ...AdditionalTargetUsers,
+        );
+      }
+
+    };
+
+    return preNotify;
+  };
+
+}
+
+export const preNotifyService = new PreNotifyService();