Kaynağa Gözat

save notification on activity creation worked

Taichi Masuyama 4 yıl önce
ebeveyn
işleme
00de065d01

+ 1 - 0
packages/app/src/server/crowi/index.js

@@ -84,6 +84,7 @@ function Crowi() {
     tag: new (require('../events/tag'))(this),
     tag: new (require('../events/tag'))(this),
     admin: new (require('../events/admin'))(this),
     admin: new (require('../events/admin'))(this),
     comment: new (require('../events/comment'))(this),
     comment: new (require('../events/comment'))(this),
+    activity: new (require('../events/activity'))(),
   };
   };
 }
 }
 
 

+ 6 - 0
packages/app/src/server/events/activity.ts

@@ -10,4 +10,10 @@ export default class ActivityEvent extends EventEmitter {
     logger.info('onRemove activity event fired');
     logger.info('onRemove activity event fired');
   }
   }
 
 
+  onCreate(action: string, activity: any): void {
+    logger.info('onCreate activity event fired');
+  }
+
 }
 }
+
+module.exports = ActivityEvent;

+ 162 - 159
packages/app/src/server/models/activity.ts

@@ -2,6 +2,7 @@ import { DeleteWriteOpResultObject } from 'mongodb';
 import {
 import {
   Types, Document, Model, Schema,
   Types, Document, Model, Schema,
 } from 'mongoose';
 } from 'mongoose';
+import Crowi from '../crowi';
 
 
 import { getOrCreateModel, getModelSafely } from '../util/mongoose-utils';
 import { getOrCreateModel, getModelSafely } from '../util/mongoose-utils';
 import loggerFactory from '../../utils/logger';
 import loggerFactory from '../../utils/logger';
@@ -9,7 +10,6 @@ import ActivityDefine from '../util/activityDefine';
 
 
 import Watcher from './watcher';
 import Watcher from './watcher';
 // import { InAppNotification } from './in-app-notification';
 // import { InAppNotification } from './in-app-notification';
-import ActivityEvent from '../events/activity';
 
 
 const logger = loggerFactory('growi:models:activity');
 const logger = loggerFactory('growi:models:activity');
 
 
@@ -39,177 +39,180 @@ export interface ActivityModel extends Model<ActivityDocument> {
   getActionUsersFromActivities(activities: ActivityDocument[]): any[]
   getActionUsersFromActivities(activities: ActivityDocument[]): any[]
 }
 }
 
 
-// TODO: add revision id
-const activitySchema = new Schema<ActivityDocument, ActivityModel>({
-  user: {
-    type: Schema.Types.ObjectId,
-    ref: 'User',
-    index: true,
-    require: true,
-  },
-  targetModel: {
-    type: String,
-    require: true,
-    enum: ActivityDefine.getSupportTargetModelNames(),
-  },
-  target: {
-    type: Schema.Types.ObjectId,
-    refPath: 'targetModel',
-    require: true,
-  },
-  action: {
-    type: String,
-    require: true,
-    enum: ActivityDefine.getSupportActionNames(),
-  },
-  event: {
-    type: Schema.Types.ObjectId,
-    refPath: 'eventModel',
-  },
-  eventModel: {
-    type: String,
-    enum: ActivityDefine.getSupportEventModelNames(),
-  },
-  createdAt: {
-    type: Date,
-    default: Date.now,
-  },
-});
-activitySchema.index({ target: 1, action: 1 });
-activitySchema.index({
-  user: 1, target: 1, action: 1, createdAt: 1,
-}, { unique: true });
-
-/**
-   * @param {object} parameters
-   * @return {Promise}
-   */
-activitySchema.statics.createByParameters = function(parameters) {
-  return this.create(parameters);
-};
+module.exports = function(crowi: Crowi) {
+  const activityEvent = crowi.event('activity');
+
+  // TODO: add revision id
+  const activitySchema = new Schema<ActivityDocument, ActivityModel>({
+    user: {
+      type: Schema.Types.ObjectId,
+      ref: 'User',
+      index: true,
+      require: true,
+    },
+    targetModel: {
+      type: String,
+      require: true,
+      enum: ActivityDefine.getSupportTargetModelNames(),
+    },
+    target: {
+      type: Schema.Types.ObjectId,
+      refPath: 'targetModel',
+      require: true,
+    },
+    action: {
+      type: String,
+      require: true,
+      enum: ActivityDefine.getSupportActionNames(),
+    },
+    event: {
+      type: Schema.Types.ObjectId,
+      refPath: 'eventModel',
+    },
+    eventModel: {
+      type: String,
+      enum: ActivityDefine.getSupportEventModelNames(),
+    },
+    createdAt: {
+      type: Date,
+      default: Date.now,
+    },
+  });
+  activitySchema.index({ target: 1, action: 1 });
+  activitySchema.index({
+    user: 1, target: 1, action: 1, createdAt: 1,
+  }, { unique: true });
+
+  /**
+     * @param {object} parameters
+     * @return {Promise}
+     */
+  activitySchema.statics.createByParameters = function(parameters) {
+    return this.create(parameters);
+  };
 
 
-/**
-   * @param {object} parameters
-   */
-activitySchema.statics.removeByParameters = async function(parameters) {
-  const activityEvent = new ActivityEvent();
-  const activity = await this.findOne(parameters);
-  activityEvent.emit('remove', activity);
+  /**
+     * @param {object} parameters
+     */
+  activitySchema.statics.removeByParameters = async function(parameters) {
+    const activity = await this.findOne(parameters);
+    activityEvent.emit('remove', activity);
 
 
-  return this.deleteMany(parameters).exec();
-};
+    return this.deleteMany(parameters).exec();
+  };
 
 
-/**
-   * @param {Comment} comment
-   * @return {Promise}
-   */
-activitySchema.statics.createByPageComment = function(comment) {
-  const parameters = {
-    user: comment.creator,
-    targetModel: ActivityDefine.MODEL_PAGE,
-    target: comment.page,
-    eventModel: ActivityDefine.MODEL_COMMENT,
-    event: comment._id,
-    action: ActivityDefine.ACTION_COMMENT,
+  /**
+     * @param {Comment} comment
+     * @return {Promise}
+     */
+  activitySchema.statics.createByPageComment = function(comment) {
+    const parameters = {
+      user: comment.creator,
+      targetModel: ActivityDefine.MODEL_PAGE,
+      target: comment.page,
+      eventModel: ActivityDefine.MODEL_COMMENT,
+      event: comment._id,
+      action: ActivityDefine.ACTION_COMMENT,
+    };
+
+    return this.createByParameters(parameters);
   };
   };
 
 
-  return this.createByParameters(parameters);
-};
+  /**
+     * @param {Page} page
+     * @param {User} user
+     * @return {Promise}
+     */
+  activitySchema.statics.createByPageLike = function(page, user) {
+    const parameters = {
+      user: user._id,
+      targetModel: ActivityDefine.MODEL_PAGE,
+      target: page,
+      action: ActivityDefine.ACTION_LIKE,
+    };
+
+    return this.createByParameters(parameters);
+  };
 
 
-/**
-   * @param {Page} page
-   * @param {User} user
-   * @return {Promise}
-   */
-activitySchema.statics.createByPageLike = function(page, user) {
-  const parameters = {
-    user: user._id,
-    targetModel: ActivityDefine.MODEL_PAGE,
-    target: page,
-    action: ActivityDefine.ACTION_LIKE,
+  /**
+     * @param {Page} page
+     * @param {User} user
+     * @return {Promise}
+     */
+  activitySchema.statics.removeByPageUnlike = function(page, user) {
+    const parameters = {
+      user,
+      targetModel: ActivityDefine.MODEL_PAGE,
+      target: page,
+      action: ActivityDefine.ACTION_LIKE,
+    };
+
+    return this.removeByParameters(parameters);
   };
   };
 
 
-  return this.createByParameters(parameters);
-};
+  /**
+     * @param {Page} page
+     *
+     * @return {Promise}
+     */
+  activitySchema.statics.removeByPage = async function(page) {
+    // const activityEvent = new ActivityEvent();
+    const activities = await this.find({ target: page });
+    for (const activity of activities) {
+      // TODO: implement removeActivity when page deleted by GW-7481
+      // activityEvent.emit('remove', activity);
+    }
+    return this.deleteMany({ target: page }).exec();
+  };
 
 
-/**
-   * @param {Page} page
-   * @param {User} user
-   * @return {Promise}
-   */
-activitySchema.statics.removeByPageUnlike = function(page, user) {
-  const parameters = {
-    user,
-    targetModel: ActivityDefine.MODEL_PAGE,
-    target: page,
-    action: ActivityDefine.ACTION_LIKE,
+  /**
+     * @param {User} user
+     * @return {Promise}
+     */
+  activitySchema.statics.findByUser = function(user) {
+    return this.find({ user }).sort({ createdAt: -1 }).exec();
   };
   };
 
 
-  return this.removeByParameters(parameters);
-};
+  activitySchema.statics.getActionUsersFromActivities = function(activities) {
+    return activities.map(({ user }) => user).filter((user, i, self) => self.indexOf(user) === i);
+  };
 
 
-/**
-   * @param {Page} page
-   *
-   * @return {Promise}
-   */
-activitySchema.statics.removeByPage = async function(page) {
-  // const activityEvent = new ActivityEvent();
-  const activities = await this.find({ target: page });
-  for (const activity of activities) {
-    // TODO: implement removeActivity when page deleted by GW-7481
-    // activityEvent.emit('remove', activity);
-  }
-  return this.deleteMany({ target: page }).exec();
-};
+  activitySchema.methods.getNotificationTargetUsers = async function() {
+    const User = getModelSafely('User') || require('~/server/models/user')();
+    const { user: actionUser, targetModel, target } = this;
+
+    const model: any = await this.model(targetModel).findById(target);
+    const [targetUsers, watchUsers, ignoreUsers] = await Promise.all([
+      model.getNotificationTargetUsers(),
+      Watcher.getWatchers((target as any) as Types.ObjectId),
+      Watcher.getIgnorers((target as any) as Types.ObjectId),
+    ]);
+
+    const unique = array => Object.values(array.reduce((objects, object) => ({ ...objects, [object.toString()]: object }), {}));
+    const filter = (array, pull) => {
+      const ids = pull.map(object => object.toString());
+      return array.filter(object => !ids.includes(object.toString()));
+    };
+    const notificationUsers = filter(unique([...targetUsers, ...watchUsers]), [...ignoreUsers, actionUser]);
+    const activeNotificationUsers = await User.find({
+      _id: { $in: notificationUsers },
+      status: User.STATUS_ACTIVE,
+    }).distinct('_id');
+    return activeNotificationUsers;
+  };
 
 
-/**
-   * @param {User} user
-   * @return {Promise}
-   */
-activitySchema.statics.findByUser = function(user) {
-  return this.find({ user }).sort({ createdAt: -1 }).exec();
-};
+  activitySchema.post('save', async(savedActivity: ActivityDocument) => {
+    let targetUsers: Types.ObjectId[] = [];
+    try {
+      targetUsers = await savedActivity.getNotificationTargetUsers();
+    }
+    catch (err) {
+      logger.error(err);
+    }
 
 
-activitySchema.statics.getActionUsersFromActivities = function(activities) {
-  return activities.map(({ user }) => user).filter((user, i, self) => self.indexOf(user) === i);
-};
+    activityEvent.emit('create', targetUsers, savedActivity);
+  });
 
 
-activitySchema.methods.getNotificationTargetUsers = async function() {
-  const User = getModelSafely('User') || require('~/server/models/user')();
-  const { user: actionUser, targetModel, target } = this;
-
-  const model: any = await this.model(targetModel).findById(target);
-  const [targetUsers, watchUsers, ignoreUsers] = await Promise.all([
-    model.getNotificationTargetUsers(),
-    Watcher.getWatchers((target as any) as Types.ObjectId),
-    Watcher.getIgnorers((target as any) as Types.ObjectId),
-  ]);
-
-  const unique = array => Object.values(array.reduce((objects, object) => ({ ...objects, [object.toString()]: object }), {}));
-  const filter = (array, pull) => {
-    const ids = pull.map(object => object.toString());
-    return array.filter(object => !ids.includes(object.toString()));
-  };
-  const notificationUsers = filter(unique([...targetUsers, ...watchUsers]), [...ignoreUsers, actionUser]);
-  const activeNotificationUsers = await User.find({
-    _id: { $in: notificationUsers },
-    status: User.STATUS_ACTIVE,
-  }).distinct('_id');
-  return activeNotificationUsers;
-};
+  return getOrCreateModel<ActivityDocument, ActivityModel>('Activity', activitySchema);
 
 
-activitySchema.post('save', async(savedActivity: ActivityDocument) => {
-  try {
-    const targetUsers = await savedActivity.getNotificationTargetUsers();
-    const InAppNotification = mongoose.model('InAppNotification');
-    await InAppNotification.upsertByActivity(targetUsers, savedActivity);
-    return;
-  }
-  catch (err) {
-    logger.error(err);
-  }
-});
-
-const Activity = getOrCreateModel<ActivityDocument, ActivityModel>('Activity', activitySchema);
-export { Activity };
+};

+ 0 - 4
packages/app/src/server/models/comment.js

@@ -1,7 +1,3 @@
-// disable no-return-await for model functions
-/* eslint-disable no-return-await */
-import { Activity } from './activity';
-
 module.exports = function(crowi) {
 module.exports = function(crowi) {
   const debug = require('debug')('growi:models:comment');
   const debug = require('debug')('growi:models:comment');
   const mongoose = require('mongoose');
   const mongoose = require('mongoose');

+ 13 - 42
packages/app/src/server/models/in-app-notification.ts

@@ -5,13 +5,14 @@ import { subDays } from 'date-fns';
 import ActivityDefine from '../util/activityDefine';
 import ActivityDefine from '../util/activityDefine';
 import { getOrCreateModel, getModelSafely } from '../util/mongoose-utils';
 import { getOrCreateModel, getModelSafely } from '../util/mongoose-utils';
 import loggerFactory from '../../utils/logger';
 import loggerFactory from '../../utils/logger';
-import { Activity, ActivityDocument } from './activity';
+import { ActivityDocument } from './activity';
 
 
 const logger = loggerFactory('growi:models:inAppNotification');
 const logger = loggerFactory('growi:models:inAppNotification');
+const mongoose = require('mongoose');
 
 
-const STATUS_UNREAD = 'UNREAD';
-const STATUS_UNOPENED = 'UNOPENED';
-const STATUS_OPENED = 'OPENED';
+export const STATUS_UNREAD = 'UNREAD';
+export const STATUS_UNOPENED = 'UNOPENED';
+export const STATUS_OPENED = 'OPENED';
 const STATUSES = [STATUS_UNREAD, STATUS_UNOPENED, STATUS_OPENED];
 const STATUSES = [STATUS_UNREAD, STATUS_UNOPENED, STATUS_OPENED];
 
 
 export interface InAppNotificationDocument extends Document {
 export interface InAppNotificationDocument extends Document {
@@ -27,7 +28,7 @@ export interface InAppNotificationDocument extends Document {
 
 
 export interface InAppNotificationModel extends Model<InAppNotificationDocument> {
 export interface InAppNotificationModel extends Model<InAppNotificationDocument> {
   findLatestInAppNotificationsByUser(user: Types.ObjectId, skip: number, offset: number): Promise<InAppNotificationDocument[]>
   findLatestInAppNotificationsByUser(user: Types.ObjectId, skip: number, offset: number): Promise<InAppNotificationDocument[]>
-  upsertByActivity(userIds: Types.ObjectId[], activity: ActivityDocument, createdAt?: Date | null): Promise<InAppNotificationDocument | null>
+
   // commented out type 'Query' temporary to avoid ts error
   // commented out type 'Query' temporary to avoid ts error
   removeEmpty()/* : Query<any> */
   removeEmpty()/* : Query<any> */
   read(user) /* : Promise<Query<any>> */
   read(user) /* : Promise<Query<any>> */
@@ -80,9 +81,13 @@ const inAppNotificationSchema = new Schema<InAppNotificationDocument, InAppNotif
     default: Date.now,
     default: Date.now,
   },
   },
 });
 });
-inAppNotificationSchema.virtual('actionUsers').get(function(this: InAppNotificationDocument) {
-  return Activity.getActionUsersFromActivities((this.activities as any) as ActivityDocument[]);
-});
+
+// TODO: move this virtual property getter to the service layer if necessary 77893
+// inAppNotificationSchema.virtual('actionUsers').get(function(this: InAppNotificationDocument) {
+//   const Activity = getModelSafely('Activity') || require('../models/activity')(this.crowi);
+//   return Activity.getActionUsersFromActivities((this.activities as any) as ActivityDocument[]);
+// });
+
 const transform = (doc, ret) => {
 const transform = (doc, ret) => {
   // delete ret.activities
   // delete ret.activities
 };
 };
@@ -105,40 +110,6 @@ inAppNotificationSchema.statics.findLatestInAppNotificationsByUser = function(us
     .exec();
     .exec();
 };
 };
 
 
-inAppNotificationSchema.statics.upsertByActivity = async function(
-    users: Types.ObjectId[], activity: ActivityDocument, createdAt: Date | null,
-): Promise<InAppNotificationDocument | null> {
-  const {
-    _id: activityId, targetModel, target, action,
-  } = activity;
-  const now = createdAt || Date.now();
-  const lastWeek = subDays(now, 7);
-  const operations = users.map((user) => {
-    const filter = {
-      user, target, action, createdAt: { $gt: lastWeek },
-    };
-    const parameters = {
-      user,
-      targetModel,
-      target,
-      action,
-      status: STATUS_UNREAD,
-      createdAt: now,
-      $addToSet: { activities: activityId },
-    };
-    return {
-      updateOne: {
-        filter,
-        update: parameters,
-        upsert: true,
-      },
-    };
-  });
-
-  const resultObject = await this.bulkWrite(operations);
-  return resultObject?.result;
-};
-
 inAppNotificationSchema.statics.removeEmpty = function() {
 inAppNotificationSchema.statics.removeEmpty = function() {
   return InAppNotification.deleteMany({ activities: { $size: 0 } });
   return InAppNotification.deleteMany({ activities: { $size: 0 } });
 };
 };

+ 29 - 4
packages/app/src/server/service/activity.ts

@@ -1,23 +1,47 @@
+import { Types } from 'mongoose';
+import Crowi from '../crowi';
 import loggerFactory from '../../utils/logger';
 import loggerFactory from '../../utils/logger';
 
 
-import { Activity } from '../models/activity';
+import { ActivityDocument } from '../models/activity';
 
 
 import ActivityDefine from '../util/activityDefine';
 import ActivityDefine from '../util/activityDefine';
+import { getModelSafely } from '../util/mongoose-utils';
 
 
 
 
 const logger = loggerFactory('growi:service:ActivityService');
 const logger = loggerFactory('growi:service:ActivityService');
 
 
 class ActivityService {
 class ActivityService {
 
 
-  crowi: any;
+  crowi!: Crowi;
 
 
-  inAppNotificationService: any;
+  inAppNotificationService!: any;
+
+  activityEvent!: any;
 
 
   // commentEvent!: any;
   // commentEvent!: any;
 
 
-  constructor(crowi) {
+  constructor(crowi: Crowi) {
     this.crowi = crowi;
     this.crowi = crowi;
     this.inAppNotificationService = crowi.inAppNotificationService;
     this.inAppNotificationService = crowi.inAppNotificationService;
+    this.activityEvent = crowi.event('activity');
+
+    this.setUpEventListeners();
+  }
+
+  setUpEventListeners() {
+    this.initActivityEventListeners();
+  }
+
+  initActivityEventListeners() {
+    this.activityEvent.on('create', async(targetUsers: Types.ObjectId[], activity: ActivityDocument) => {
+      try {
+        await this.inAppNotificationService.upsertByActivity(targetUsers, activity);
+      }
+      catch (err) {
+        logger.error('Error occurred while saving InAppNotification');
+        throw err;
+      }
+    });
   }
   }
 
 
   /**
   /**
@@ -34,6 +58,7 @@ class ActivityService {
       action: ActivityDefine.ACTION_COMMENT,
       action: ActivityDefine.ACTION_COMMENT,
     };
     };
 
 
+    const Activity = getModelSafely('Activity') || require('../models/activity')(this.crowi);
     await Activity.removeByParameters(parameters);
     await Activity.removeByParameters(parameters);
     return;
     return;
   };
   };

+ 2 - 2
packages/app/src/server/service/comment.ts

@@ -1,6 +1,5 @@
 import loggerFactory from '../../utils/logger';
 import loggerFactory from '../../utils/logger';
-
-import { Activity } from '../models/activity';
+import { getModelSafely } from '../util/mongoose-utils';
 
 
 const InAppNotificationService = require('./in-app-notification');
 const InAppNotificationService = require('./in-app-notification');
 const ActivityService = require('./activity');
 const ActivityService = require('./activity');
@@ -30,6 +29,7 @@ class CommentService {
       this.commentEvent.onCreate();
       this.commentEvent.onCreate();
 
 
       try {
       try {
+        const Activity = getModelSafely('Activity') || require('../models/activity')(this.crowi);
         const activityLog = await Activity.createByPageComment(savedComment);
         const activityLog = await Activity.createByPageComment(savedComment);
         logger.info('Activity created', activityLog);
         logger.info('Activity created', activityLog);
       }
       }

+ 44 - 4
packages/app/src/server/service/in-app-notification.ts

@@ -1,24 +1,29 @@
+import { Types } from 'mongoose';
+import { subDays } from 'date-fns';
 import Crowi from '../crowi';
 import Crowi from '../crowi';
-import { InAppNotification } from '~/server/models/in-app-notification';
-import { Activity } from '~/server/models/activity';
+import { InAppNotification, InAppNotificationDocument, STATUS_UNREAD } from '~/server/models/in-app-notification';
+import { ActivityDocument } from '~/server/models/activity';
 
 
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:service:inAppNotification');
 const logger = loggerFactory('growi:service:inAppNotification');
 
 
 
 
-class InAppNotificationService {
+export default class InAppNotificationService {
 
 
-  crowi!: any;
+  crowi!: Crowi;
 
 
   socketIoService!: any;
   socketIoService!: any;
 
 
   commentEvent!: any;
   commentEvent!: any;
 
 
+  activityEvent!: any;
+
 
 
   constructor(crowi: Crowi) {
   constructor(crowi: Crowi) {
     this.crowi = crowi;
     this.crowi = crowi;
     this.socketIoService = crowi.socketIoService;
     this.socketIoService = crowi.socketIoService;
+    this.activityEvent = crowi.event('activity');
   }
   }
 
 
 
 
@@ -28,6 +33,41 @@ class InAppNotificationService {
     }
     }
   }
   }
 
 
+  upsertByActivity = async function(
+      users: Types.ObjectId[], activity: ActivityDocument, createdAt?: Date | null,
+  ): Promise<void> {
+    const {
+      _id: activityId, targetModel, target, action,
+    } = activity;
+    const now = createdAt || Date.now();
+    const lastWeek = subDays(now, 7);
+    const operations = users.map((user) => {
+      const filter = {
+        user, target, action, createdAt: { $gt: lastWeek },
+      };
+      const parameters = {
+        user,
+        targetModel,
+        target,
+        action,
+        status: STATUS_UNREAD,
+        createdAt: now,
+        $addToSet: { activities: activityId },
+      };
+      return {
+        updateOne: {
+          filter,
+          update: parameters,
+          upsert: true,
+        },
+      };
+    });
+
+    await InAppNotification.bulkWrite(operations);
+    logger.info('InAppNotification bulkWrite has run');
+    return;
+  }
+
 }
 }
 
 
 module.exports = InAppNotificationService;
 module.exports = InAppNotificationService;