activity.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import { DeleteWriteOpResultObject } from 'mongodb';
  2. import {
  3. Types, Document, Model, Schema, model,
  4. } from 'mongoose';
  5. import loggerFactory from '~/utils/logger';
  6. import ActivityDefine from '../util/activityDefine';
  7. import Crowi from '../crowi';
  8. const logger = loggerFactory('growi:models:activity');
  9. export interface ActivityDocument extends Document {
  10. _id: Types.ObjectId
  11. user: Types.ObjectId | any
  12. targetModel: string
  13. target: string
  14. action: string
  15. event: Types.ObjectId
  16. eventModel: string
  17. createdAt: Date
  18. getNotificationTargetUsers(): Promise<any[]>
  19. }
  20. export interface ActivityModel extends Model<ActivityDocument> {
  21. createByParameters(parameters: any): Promise<ActivityDocument>
  22. removeByParameters(parameters: any): any
  23. createByPageComment(comment: any): Promise<ActivityDocument>
  24. createByPageLike(page: any, user: any): Promise<ActivityDocument>
  25. removeByPageUnlike(page: any, user: any): Promise<DeleteWriteOpResultObject['result']>
  26. removeByPage(page: any): Promise<DeleteWriteOpResultObject['result']>
  27. findByUser(user: any): Promise<ActivityDocument[]>
  28. getActionUsersFromActivities(activities: ActivityDocument[]): any[]
  29. }
  30. export default (crowi: Crowi) => {
  31. const activityEvent = crowi.event('Activity');
  32. // TODO: add revision id
  33. const activitySchema = new Schema<ActivityDocument, ActivityModel>({
  34. user: {
  35. type: Schema.Types.ObjectId,
  36. ref: 'User',
  37. index: true,
  38. require: true,
  39. },
  40. targetModel: {
  41. type: String,
  42. require: true,
  43. enum: ActivityDefine.getSupportTargetModelNames(),
  44. },
  45. target: {
  46. type: Schema.Types.ObjectId,
  47. refPath: 'targetModel',
  48. require: true,
  49. },
  50. action: {
  51. type: String,
  52. require: true,
  53. enum: ActivityDefine.getSupportActionNames(),
  54. },
  55. event: {
  56. type: Schema.Types.ObjectId,
  57. refPath: 'eventModel',
  58. },
  59. eventModel: {
  60. type: String,
  61. enum: ActivityDefine.getSupportEventModelNames(),
  62. },
  63. createdAt: {
  64. type: Date,
  65. default: Date.now,
  66. },
  67. });
  68. activitySchema.index({ target: 1, action: 1 });
  69. activitySchema.index({
  70. user: 1, target: 1, action: 1, createdAt: 1,
  71. }, { unique: true });
  72. /**
  73. * @param {object} parameters
  74. * @return {Promise}
  75. */
  76. activitySchema.statics.createByParameters = function(parameters) {
  77. return Activity.create(parameters);
  78. };
  79. /**
  80. * @param {object} parameters
  81. */
  82. activitySchema.statics.removeByParameters = async function(parameters) {
  83. const activity = await Activity.findOne(parameters);
  84. activityEvent.emit('remove', activity);
  85. return Activity.deleteMany(parameters).exec();
  86. };
  87. /**
  88. * @param {Comment} comment
  89. * @return {Promise}
  90. */
  91. activitySchema.statics.createByPageComment = function(comment) {
  92. const parameters = {
  93. user: comment.creator,
  94. targetModel: ActivityDefine.MODEL_PAGE,
  95. target: comment.page,
  96. eventModel: ActivityDefine.MODEL_COMMENT,
  97. event: comment._id,
  98. action: ActivityDefine.ACTION_COMMENT,
  99. };
  100. return this.createByParameters(parameters);
  101. };
  102. /**
  103. * @param {Page} page
  104. * @param {User} user
  105. * @return {Promise}
  106. */
  107. activitySchema.statics.createByPageLike = function(page, user) {
  108. const parameters = {
  109. user: user._id,
  110. targetModel: ActivityDefine.MODEL_PAGE,
  111. target: page,
  112. action: ActivityDefine.ACTION_LIKE,
  113. };
  114. return this.createByParameters(parameters);
  115. };
  116. /**
  117. * @param {Page} page
  118. * @param {User} user
  119. * @return {Promise}
  120. */
  121. activitySchema.statics.removeByPageUnlike = function(page, user) {
  122. const parameters = {
  123. user,
  124. targetModel: ActivityDefine.MODEL_PAGE,
  125. target: page,
  126. action: ActivityDefine.ACTION_LIKE,
  127. };
  128. return this.removeByParameters(parameters);
  129. };
  130. /**
  131. * @param {Page} page
  132. * @return {Promise}
  133. */
  134. activitySchema.statics.removeByPage = async function(page) {
  135. const activities = await Activity.find({ target: page });
  136. for (const activity of activities) {
  137. activityEvent.emit('remove', activity);
  138. }
  139. return Activity.deleteMany({ target: page }).exec();
  140. };
  141. /**
  142. * @param {User} user
  143. * @return {Promise}
  144. */
  145. activitySchema.statics.findByUser = function(user) {
  146. return Activity.find({ user }).sort({ createdAt: -1 }).exec();
  147. };
  148. activitySchema.statics.getActionUsersFromActivities = function(activities) {
  149. return activities.map(({ user }) => user).filter((user, i, self) => self.indexOf(user) === i);
  150. };
  151. activitySchema.methods.getNotificationTargetUsers = async function() {
  152. const User = crowi.model('User');
  153. const Watcher = crowi.model('Watcher');
  154. const { user: actionUser, targetModel, target } = this;
  155. const model: any = await this.model(targetModel).findById(target);
  156. const [targetUsers, watchUsers, ignoreUsers] = await Promise.all([
  157. model.getNotificationTargetUsers(),
  158. Watcher.getWatchers((target as any) as Types.ObjectId),
  159. Watcher.getIgnorers((target as any) as Types.ObjectId),
  160. ]);
  161. const unique = array => Object.values(array.reduce((objects, object) => ({ ...objects, [object.toString()]: object }), {}));
  162. const filter = (array, pull) => {
  163. const ids = pull.map(object => object.toString());
  164. return array.filter(object => !ids.includes(object.toString()));
  165. };
  166. const notificationUsers = filter(unique([...targetUsers, ...watchUsers]), [...ignoreUsers, actionUser]);
  167. const activeNotificationUsers = await User.find({
  168. _id: { $in: notificationUsers },
  169. status: User.STATUS_ACTIVE,
  170. }).distinct('_id');
  171. return activeNotificationUsers;
  172. };
  173. /**
  174. * saved hook
  175. */
  176. activitySchema.post('save', async(savedActivity: ActivityDocument) => {
  177. const Notification = crowi.model('Notification');
  178. try {
  179. const notificationUsers = await savedActivity.getNotificationTargetUsers();
  180. await Promise.all(notificationUsers.map(user => Notification.upsertByActivity(user, savedActivity)));
  181. return;
  182. }
  183. catch (err) {
  184. logger.error(err);
  185. }
  186. });
  187. // because mongoose's 'remove' hook fired only when remove by a method of Document (not by a Model method)
  188. // move 'save' hook from mongoose's events to activityEvent if I have a time.
  189. activityEvent.on('remove', async(activity: ActivityDocument) => {
  190. const Notification = crowi.model('Notification');
  191. try {
  192. await Notification.removeActivity(activity);
  193. }
  194. catch (err) {
  195. logger.error(err);
  196. }
  197. });
  198. const Activity = model<ActivityDocument, ActivityModel>('Activity', activitySchema);
  199. return Activity;
  200. };