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

Merge pull request #1163 from weseek/feat/global-notification-slack

Feat/global notification slack
Sou Mizobuchi 6 лет назад
Родитель
Сommit
80f2fb7fe7

+ 14 - 2
src/server/crowi/index.js

@@ -98,13 +98,17 @@ Crowi.prototype.init = async function() {
     this.setupMailer(),
     this.setupMailer(),
     this.setupSlack(),
     this.setupSlack(),
     this.setupCsrf(),
     this.setupCsrf(),
-    this.setUpGlobalNotification(),
     this.setUpFileUpload(),
     this.setUpFileUpload(),
     this.setUpAcl(),
     this.setUpAcl(),
     this.setUpCustomize(),
     this.setUpCustomize(),
     this.setUpRestQiitaAPI(),
     this.setUpRestQiitaAPI(),
     this.setupUserGroup(),
     this.setupUserGroup(),
   ]);
   ]);
+
+  // globalNotification depends on slack and mailer
+  await Promise.all([
+    this.setUpGlobalNotification(),
+  ]);
 };
 };
 
 
 Crowi.prototype.initForTest = async function() {
 Crowi.prototype.initForTest = async function() {
@@ -127,12 +131,16 @@ Crowi.prototype.initForTest = async function() {
   //   this.setupMailer(),
   //   this.setupMailer(),
   //   this.setupSlack(),
   //   this.setupSlack(),
   //   this.setupCsrf(),
   //   this.setupCsrf(),
-  //   this.setUpGlobalNotification(),
   //   this.setUpFileUpload(),
   //   this.setUpFileUpload(),
     this.setUpAcl(),
     this.setUpAcl(),
   //   this.setUpCustomize(),
   //   this.setUpCustomize(),
   //   this.setUpRestQiitaAPI(),
   //   this.setUpRestQiitaAPI(),
   ]);
   ]);
+
+  // globalNotification depends on slack and mailer
+  // await Promise.all([
+  //   this.setUpGlobalNotification(),
+  // ]);
 };
 };
 
 
 Crowi.prototype.isPageId = function(pageId) {
 Crowi.prototype.isPageId = function(pageId) {
@@ -266,6 +274,10 @@ Crowi.prototype.getMailer = function() {
   return this.mailer;
   return this.mailer;
 };
 };
 
 
+Crowi.prototype.getSlack = function() {
+  return this.slack;
+};
+
 Crowi.prototype.getInterceptorManager = function() {
 Crowi.prototype.getInterceptorManager = function() {
   return this.interceptorManager;
   return this.interceptorManager;
 };
 };

+ 20 - 0
src/server/models/GlobalNotificationSetting.js

@@ -7,6 +7,26 @@ const GlobalNotificationSetting = require('./GlobalNotificationSetting/index');
 const GlobalNotificationSettingClass = GlobalNotificationSetting.class;
 const GlobalNotificationSettingClass = GlobalNotificationSetting.class;
 const GlobalNotificationSettingSchema = GlobalNotificationSetting.schema;
 const GlobalNotificationSettingSchema = GlobalNotificationSetting.schema;
 
 
+/**
+ * global notifcation event master
+ */
+GlobalNotificationSettingSchema.statics.EVENT = {
+  PAGE_CREATE: 'pageCreate',
+  PAGE_EDIT: 'pageEdit',
+  PAGE_DELETE: 'pageDelete',
+  PAGE_MOVE: 'pageMove',
+  PAGE_LIKE: 'pageLike',
+  COMMENT: 'comment',
+};
+
+/**
+ * global notifcation type master
+ */
+GlobalNotificationSettingSchema.statics.TYPE = {
+  MAIL: 'mail',
+  SLACK: 'slack',
+};
+
 module.exports = function(crowi) {
 module.exports = function(crowi) {
   GlobalNotificationSettingClass.crowi = crowi;
   GlobalNotificationSettingClass.crowi = crowi;
   GlobalNotificationSettingSchema.loadClass(GlobalNotificationSettingClass);
   GlobalNotificationSettingSchema.loadClass(GlobalNotificationSettingClass);

+ 8 - 3
src/server/models/GlobalNotificationSetting/GlobalNotificationMailSetting.js

@@ -9,9 +9,14 @@ module.exports = function(crowi) {
   GlobalNotificationSettingSchema.loadClass(GlobalNotificationSettingClass);
   GlobalNotificationSettingSchema.loadClass(GlobalNotificationSettingClass);
 
 
   const GlobalNotificationSettingModel = mongoose.model('GlobalNotificationSetting', GlobalNotificationSettingSchema);
   const GlobalNotificationSettingModel = mongoose.model('GlobalNotificationSetting', GlobalNotificationSettingSchema);
-  const GlobalNotificationMailSettingModel = GlobalNotificationSettingModel.discriminator('mail', new mongoose.Schema({
-    toEmail: String,
-  }, { discriminatorKey: 'type' }));
+  const GlobalNotificationMailSettingModel = GlobalNotificationSettingModel.discriminator(
+    GlobalNotificationSetting.schema.statics.TYPE.MAIL,
+    new mongoose.Schema({
+      toEmail: String,
+    }, {
+      discriminatorKey: 'type',
+    }),
+  );
 
 
   return GlobalNotificationMailSettingModel;
   return GlobalNotificationMailSettingModel;
 };
 };

+ 8 - 3
src/server/models/GlobalNotificationSetting/GlobalNotificationSlackSetting.js

@@ -9,9 +9,14 @@ module.exports = function(crowi) {
   GlobalNotificationSettingSchema.loadClass(GlobalNotificationSettingClass);
   GlobalNotificationSettingSchema.loadClass(GlobalNotificationSettingClass);
 
 
   const GlobalNotificationSettingModel = mongoose.model('GlobalNotificationSetting', GlobalNotificationSettingSchema);
   const GlobalNotificationSettingModel = mongoose.model('GlobalNotificationSetting', GlobalNotificationSettingSchema);
-  const GlobalNotificationSlackSettingModel = GlobalNotificationSettingModel.discriminator('slack', new mongoose.Schema({
-    slackChannels: String,
-  }, { discriminatorKey: 'type' }));
+  const GlobalNotificationSlackSettingModel = GlobalNotificationSettingModel.discriminator(
+    GlobalNotificationSetting.schema.statics.TYPE.SLACK,
+    new mongoose.Schema({
+      slackChannels: String,
+    }, {
+      discriminatorKey: 'type',
+    }),
+  );
 
 
   return GlobalNotificationSlackSettingModel;
   return GlobalNotificationSlackSettingModel;
 };
 };

+ 2 - 2
src/server/models/GlobalNotificationSetting/index.js

@@ -92,12 +92,13 @@ class GlobalNotificationSetting {
    * @param {string} path
    * @param {string} path
    * @param {string} event
    * @param {string} event
    */
    */
-  static async findSettingByPathAndEvent(path, event) {
+  static async findSettingByPathAndEvent(event, path, type) {
     const pathsToMatch = generatePathsToMatch(path);
     const pathsToMatch = generatePathsToMatch(path);
 
 
     const settings = await this.find({
     const settings = await this.find({
       triggerPath: { $in: pathsToMatch },
       triggerPath: { $in: pathsToMatch },
       triggerEvents: event,
       triggerEvents: event,
+      __t: type,
       isEnabled: true,
       isEnabled: true,
     })
     })
       .sort({ triggerPath: 1 });
       .sort({ triggerPath: 1 });
@@ -107,7 +108,6 @@ class GlobalNotificationSetting {
 
 
 }
 }
 
 
-
 module.exports = {
 module.exports = {
   class: GlobalNotificationSetting,
   class: GlobalNotificationSetting,
   schema: globalNotificationSettingSchema,
   schema: globalNotificationSettingSchema,

+ 31 - 11
src/server/routes/admin.js

@@ -331,14 +331,14 @@ module.exports = function(crowi, app) {
     let setting;
     let setting;
 
 
     switch (form.notifyToType) {
     switch (form.notifyToType) {
-      case 'mail':
+      case GlobalNotificationSetting.TYPE.MAIL:
         setting = new GlobalNotificationMailSetting(crowi);
         setting = new GlobalNotificationMailSetting(crowi);
         setting.toEmail = form.toEmail;
         setting.toEmail = form.toEmail;
         break;
         break;
-      // case 'slack':
-      //   setting = new GlobalNotificationSlackSetting(crowi);
-      //   setting.slackChannels = form.slackChannels;
-      //   break;
+      case GlobalNotificationSetting.TYPE.SLACK:
+        setting = new GlobalNotificationSlackSetting(crowi);
+        setting.slackChannels = form.slackChannels;
+        break;
       default:
       default:
         logger.error('GlobalNotificationSetting Type Error: undefined type');
         logger.error('GlobalNotificationSetting Type Error: undefined type');
         req.flash('errorMessage', 'Error occurred in creating a new global notification setting: undefined notification type');
         req.flash('errorMessage', 'Error occurred in creating a new global notification setting: undefined notification type');
@@ -354,24 +354,44 @@ module.exports = function(crowi, app) {
 
 
   actions.globalNotification.update = async(req, res) => {
   actions.globalNotification.update = async(req, res) => {
     const form = req.form.notificationGlobal;
     const form = req.form.notificationGlobal;
-    const setting = await GlobalNotificationSetting.findOne({ _id: form.id });
+
+    const models = {
+      [GlobalNotificationSetting.TYPE.MAIL]: GlobalNotificationMailSetting,
+      [GlobalNotificationSetting.TYPE.SLACK]: GlobalNotificationSlackSetting,
+    };
+
+    let setting = await GlobalNotificationSetting.findOne({ _id: form.id });
+    setting = setting.toObject();
+
+    // when switching from one type to another,
+    // remove toEmail from slack setting and slackChannels from mail setting
+    if (setting.__t !== form.notifyToType) {
+      setting = models[setting.__t].hydrate(setting);
+      setting.toEmail = undefined;
+      setting.slackChannels = undefined;
+      await setting.save();
+      setting = setting.toObject();
+    }
 
 
     switch (form.notifyToType) {
     switch (form.notifyToType) {
-      case 'mail':
+      case GlobalNotificationSetting.TYPE.MAIL:
+        setting = GlobalNotificationMailSetting.hydrate(setting);
         setting.toEmail = form.toEmail;
         setting.toEmail = form.toEmail;
         break;
         break;
-      // case 'slack':
-      //   setting.slackChannels = form.slackChannels;
-      //   break;
+      case GlobalNotificationSetting.TYPE.SLACK:
+        setting = GlobalNotificationSlackSetting.hydrate(setting);
+        setting.slackChannels = form.slackChannels;
+        break;
       default:
       default:
         logger.error('GlobalNotificationSetting Type Error: undefined type');
         logger.error('GlobalNotificationSetting Type Error: undefined type');
         req.flash('errorMessage', 'Error occurred in updating the global notification setting: undefined notification type');
         req.flash('errorMessage', 'Error occurred in updating the global notification setting: undefined notification type');
         return res.redirect('/admin/notification#global-notification');
         return res.redirect('/admin/notification#global-notification');
     }
     }
 
 
+    setting.__t = form.notifyToType;
     setting.triggerPath = form.triggerPath;
     setting.triggerPath = form.triggerPath;
     setting.triggerEvents = getNotificationEvents(form);
     setting.triggerEvents = getNotificationEvents(form);
-    setting.save();
+    await setting.save();
 
 
     return res.redirect('/admin/notification#global-notification');
     return res.redirect('/admin/notification#global-notification');
   };
   };

+ 17 - 5
src/server/routes/comment.js

@@ -3,6 +3,7 @@ module.exports = function(crowi, app) {
   const Comment = crowi.model('Comment');
   const Comment = crowi.model('Comment');
   const User = crowi.model('User');
   const User = crowi.model('User');
   const Page = crowi.model('Page');
   const Page = crowi.model('Page');
+  const GlobalNotificationSetting = crowi.model('GlobalNotificationSetting');
   const ApiResponse = require('../util/apiResponse');
   const ApiResponse = require('../util/apiResponse');
   const globalNotificationService = crowi.getGlobalNotificationService();
   const globalNotificationService = crowi.getGlobalNotificationService();
   const { body } = require('express-validator/check');
   const { body } = require('express-validator/check');
@@ -107,10 +108,19 @@ module.exports = function(crowi, app) {
       return res.json(ApiResponse.error('Current user is not accessible to this page.'));
       return res.json(ApiResponse.error('Current user is not accessible to this page.'));
     }
     }
 
 
-    const createdComment = await Comment.create(pageId, req.user._id, revisionId, comment, position, isMarkdown, replyTo)
-      .catch((err) => {
-        return res.json(ApiResponse.error(err));
-      });
+    let createdComment;
+    try {
+      createdComment = await Comment.create(pageId, req.user._id, revisionId, comment, position, isMarkdown, replyTo);
+
+      await Comment.populate(createdComment, [
+        { path: 'creator', model: 'User', select: User.USER_PUBLIC_FIELDS },
+      ]);
+
+    }
+    catch (err) {
+      logger.error(err);
+      return res.json(ApiResponse.error(err));
+    }
 
 
     // update page
     // update page
     const page = await Page.findOneAndUpdate({ _id: pageId }, {
     const page = await Page.findOneAndUpdate({ _id: pageId }, {
@@ -123,7 +133,9 @@ module.exports = function(crowi, app) {
     const path = page.path;
     const path = page.path;
 
 
     // global notification
     // global notification
-    globalNotificationService.notifyComment(createdComment, path);
+    globalNotificationService.fire(GlobalNotificationSetting.EVENT.COMMENT, path, req.user, {
+      comment: createdComment,
+    });
 
 
     // slack notification
     // slack notification
     if (slackNotificationForm.isSlackEnabled) {
     if (slackNotificationForm.isSlackEnabled) {

+ 8 - 5
src/server/routes/page.js

@@ -11,6 +11,7 @@ module.exports = function(crowi, app) {
   const Bookmark = crowi.model('Bookmark');
   const Bookmark = crowi.model('Bookmark');
   const PageTagRelation = crowi.model('PageTagRelation');
   const PageTagRelation = crowi.model('PageTagRelation');
   const UpdatePost = crowi.model('UpdatePost');
   const UpdatePost = crowi.model('UpdatePost');
+  const GlobalNotificationSetting = crowi.model('GlobalNotificationSetting');
 
 
   const ApiResponse = require('../util/apiResponse');
   const ApiResponse = require('../util/apiResponse');
   const getToday = require('../util/getToday');
   const getToday = require('../util/getToday');
@@ -607,7 +608,7 @@ module.exports = function(crowi, app) {
 
 
     // global notification
     // global notification
     try {
     try {
-      await globalNotificationService.notifyPageCreate(createdPage);
+      await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_CREATE, createdPage.path, req.user);
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
@@ -694,7 +695,7 @@ module.exports = function(crowi, app) {
 
 
     // global notification
     // global notification
     try {
     try {
-      await globalNotificationService.notifyPageEdit(page);
+      await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_EDIT, page.path, req.user);
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
@@ -862,7 +863,7 @@ module.exports = function(crowi, app) {
 
 
     try {
     try {
       // global notification
       // global notification
-      globalNotificationService.notifyPageLike(page, req.user);
+      await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_LIKE, page.path, req.user);
     }
     }
     catch (err) {
     catch (err) {
       logger.error('Like failed', err);
       logger.error('Like failed', err);
@@ -999,7 +1000,7 @@ module.exports = function(crowi, app) {
     res.json(ApiResponse.success(result));
     res.json(ApiResponse.success(result));
 
 
     // global notification
     // global notification
-    return globalNotificationService.notifyPageDelete(page);
+    await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_DELETE, page.path, req.user);
   };
   };
 
 
   /**
   /**
@@ -1104,7 +1105,9 @@ module.exports = function(crowi, app) {
     res.json(ApiResponse.success(result));
     res.json(ApiResponse.success(result));
 
 
     // global notification
     // global notification
-    globalNotificationService.notifyPageMove(page, req.body.path, req.user);
+    globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_MOVE, page.path, req.user, {
+      oldPath: req.body.path,
+    });
 
 
     return page;
     return page;
   };
   };

+ 0 - 189
src/server/service/global-notification.js

@@ -1,189 +0,0 @@
-const logger = require('@alias/logger')('growi:service:GlobalNotification');
-const nodePath = require('path');
-/**
- * the service class of GlobalNotificationSetting
- */
-class GlobalNotificationService {
-
-  constructor(crowi) {
-    this.crowi = crowi;
-    this.mailer = crowi.getMailer();
-    this.GlobalNotification = crowi.model('GlobalNotificationSetting');
-    this.User = crowi.model('User');
-    this.appTitle = crowi.appService.getAppTitle();
-  }
-
-  notifyByMail(notification, mailOption) {
-    this.mailer.send(Object.assign(mailOption, { to: notification.toEmail }));
-  }
-
-  notifyBySlack(notification, slackOption) {
-    // send slack notification here
-  }
-
-  sendNotification(notifications, option) {
-    notifications.forEach((notification) => {
-      if (notification.__t === 'mail') {
-        this.notifyByMail(notification, option.mail);
-      }
-      else if (notification.__t === 'slack') {
-        this.notifyBySlack(notification, option.slack);
-      }
-    });
-  }
-
-  /**
-   * send notification at page creation
-   * @memberof GlobalNotification
-   * @param {obejct} page
-   */
-  async notifyPageCreate(page) {
-    const notifications = await this.GlobalNotification.findSettingByPathAndEvent(page.path, 'pageCreate');
-    const lang = 'en-US'; // FIXME
-    const option = {
-      mail: {
-        subject: `#pageCreate - ${page.creator.username} created ${page.path}`,
-        template: nodePath.join(this.crowi.localeDir, `${lang}/notifications/pageCreate.txt`),
-        vars: {
-          appTitle: this.appTitle,
-          path: page.path,
-          username: page.creator.username,
-        },
-      },
-      slack: {},
-    };
-
-    logger.debug('notifyPageCreate', option);
-
-    this.sendNotification(notifications, option);
-  }
-
-  /**
-   * send notification at page edit
-   * @memberof GlobalNotification
-   * @param {obejct} page
-   */
-  async notifyPageEdit(page) {
-    const notifications = await this.GlobalNotification.findSettingByPathAndEvent(page.path, 'pageEdit');
-    const lang = 'en-US'; // FIXME
-    const option = {
-      mail: {
-        subject: `#pageEdit - ${page.creator.username} edited ${page.path}`,
-        template: nodePath.join(this.crowi.localeDir, `${lang}/notifications/pageEdit.txt`),
-        vars: {
-          appTitle: this.appTitle,
-          path: page.path,
-          username: page.creator.username,
-        },
-      },
-      slack: {},
-    };
-
-    logger.debug('notifyPageEdit', option);
-
-    this.sendNotification(notifications, option);
-  }
-
-  /**
-   * send notification at page deletion
-   * @memberof GlobalNotification
-   * @param {obejct} page
-   */
-  async notifyPageDelete(page) {
-    const notifications = await this.GlobalNotification.findSettingByPathAndEvent(page.path, 'pageDelete');
-    const lang = 'en-US'; // FIXME
-    const option = {
-      mail: {
-        subject: `#pageDelete - ${page.creator.username} deleted ${page.path}`, // FIXME
-        template: nodePath.join(this.crowi.localeDir, `${lang}/notifications/pageDelete.txt`),
-        vars: {
-          appTitle: this.appTitle,
-          path: page.path,
-          username: page.creator.username,
-        },
-      },
-      slack: {},
-    };
-
-    this.sendNotification(notifications, option);
-  }
-
-  /**
-   * send notification at page move
-   * @memberof GlobalNotification
-   * @param {obejct} page
-   */
-  async notifyPageMove(page, oldPagePath, user) {
-    const notifications = await this.GlobalNotification.findSettingByPathAndEvent(page.path, 'pageMove');
-    const lang = 'en-US'; // FIXME
-    const option = {
-      mail: {
-        subject: `#pageMove - ${user.username} moved ${page.path} to ${page.path}`, // FIXME
-        template: nodePath.join(this.crowi.localeDir, `${lang}/notifications/pageMove.txt`),
-        vars: {
-          appTitle: this.appTitle,
-          oldPath: oldPagePath,
-          newPath: page.path,
-          username: user.username,
-        },
-      },
-      slack: {},
-    };
-
-    this.sendNotification(notifications, option);
-  }
-
-  /**
-   * send notification at page like
-   * @memberof GlobalNotification
-   * @param {obejct} page
-   */
-  async notifyPageLike(page, user) {
-    const notifications = await this.GlobalNotification.findSettingByPathAndEvent(page.path, 'pageLike');
-    const lang = 'en-US'; // FIXME
-    const option = {
-      mail: {
-        subject: `#pageLike - ${user.username} liked ${page.path}`,
-        template: nodePath.join(this.crowi.localeDir, `${lang}/notifications/pageLike.txt`),
-        vars: {
-          appTitle: this.appTitle,
-          path: page.path,
-          username: page.creator.username,
-        },
-      },
-      slack: {},
-    };
-
-    this.sendNotification(notifications, option);
-  }
-
-  /**
-   * send notification at page comment
-   * @memberof GlobalNotification
-   * @param {obejct} page
-   * @param {obejct} comment
-   */
-  async notifyComment(comment, path) {
-    const notifications = await this.GlobalNotification.findSettingByPathAndEvent(path, 'comment');
-    const lang = 'en-US'; // FIXME
-    const user = await this.User.findOne({ _id: comment.creator });
-    const option = {
-      mail: {
-        subject: `#comment - ${user.username} commented on ${path}`,
-        template: nodePath.join(this.crowi.localeDir, `${lang}/notifications/comment.txt`),
-        vars: {
-          appTitle: this.appTitle,
-          path,
-          username: user.username,
-          comment: comment.comment,
-        },
-      },
-      slack: {},
-    };
-
-    this.sendNotification(notifications, option);
-  }
-
-}
-
-module.exports = GlobalNotificationService;

+ 120 - 0
src/server/service/global-notification/global-notification-mail.js

@@ -0,0 +1,120 @@
+const logger = require('@alias/logger')('growi:service:GlobalNotificationMailService'); // eslint-disable-line no-unused-vars
+const nodePath = require('path');
+
+/**
+ * sub service class of GlobalNotificationSetting
+ */
+class GlobalNotificationMailService {
+
+  constructor(crowi) {
+    this.crowi = crowi;
+    this.mailer = crowi.getMailer();
+    this.type = crowi.model('GlobalNotificationSetting').TYPE.MAIL;
+    this.event = crowi.model('GlobalNotificationSetting').EVENT;
+  }
+
+  /**
+   * send mail global notification
+   *
+   * @memberof GlobalNotificationMailService
+   *
+   * @param {string} event event name triggered
+   * @param {string} path path triggered the event
+   * @param {User} triggeredBy user who triggered the event
+   * @param {{ comment: Comment, oldPath: string }} _ event specific vars
+   */
+  async fire(event, path, triggeredBy, vars) {
+    const GlobalNotification = this.crowi.model('GlobalNotificationSetting');
+    const notifications = await GlobalNotification.findSettingByPathAndEvent(event, path, this.type);
+
+    const option = this.generateOption(event, path, triggeredBy, vars);
+
+    await Promise.all(notifications.map((notification) => {
+      return this.mailer.send({ ...option, to: notification.toEmail });
+    }));
+  }
+
+  /**
+   * fire global notification
+   *
+   * @memberof GlobalNotificationMailService
+   *
+   * @param {string} event event name triggered
+   * @param {string} path path triggered the event
+   * @param {User} triggeredBy user triggered the event
+   * @param {{ comment: Comment, oldPath: string }} _ event specific vars
+   *
+   * @return  {{ subject: string, template: string, vars: object }}
+   */
+  generateOption(event, path, triggeredBy, { comment, oldPath }) {
+    // validate for all events
+    if (event == null || path == null || triggeredBy == null) {
+      throw new Error(`invalid vars supplied to GlobalNotificationMailService.generateOption for event ${event}`);
+    }
+
+    const template = nodePath.join(this.crowi.localeDir, `${this.defaultLang}/notifications/${event}.txt`);
+    let subject;
+    let vars = {
+      appTitle: this.crowi.appService.getAppTitle(),
+      path,
+      username: triggeredBy.username,
+    };
+
+    switch (event) {
+      case this.event.PAGE_CREATE:
+        subject = `#${event} - ${triggeredBy.username} created ${path}`;
+        break;
+
+      case this.event.PAGE_EDIT:
+        subject = `#${event} - ${triggeredBy.username} edited ${path}`;
+        break;
+
+      case this.event.PAGE_DELETE:
+        subject = `#${event} - ${triggeredBy.username} deleted ${path}`;
+        break;
+
+      case this.event.PAGE_MOVE:
+        // validate for page move
+        if (oldPath == null) {
+          throw new Error(`invalid vars supplied to GlobalNotificationMailService.generateOption for event ${event}`);
+        }
+
+        subject = `#${event} - ${triggeredBy.username} moved ${oldPath} to ${path}`;
+        vars = {
+          ...vars,
+          oldPath,
+          newPath: path,
+        };
+        break;
+
+      case this.event.PAGE_LIKE:
+        subject = `#${event} - ${triggeredBy.username} liked ${path}`;
+        break;
+
+      case this.event.COMMENT:
+        // validate for comment
+        if (comment == null) {
+          throw new Error(`invalid vars supplied to GlobalNotificationMailService.generateOption for event ${event}`);
+        }
+
+        subject = `#${event} - ${triggeredBy.username} commented on ${path}`;
+        vars = {
+          ...vars,
+          comment: comment.comment,
+        };
+        break;
+
+      default:
+        throw new Error(`unknown global notificaiton event: ${event}`);
+    }
+
+    return {
+      subject,
+      template,
+      vars,
+    };
+  }
+
+}
+
+module.exports = GlobalNotificationMailService;

+ 131 - 0
src/server/service/global-notification/global-notification-slack.js

@@ -0,0 +1,131 @@
+const logger = require('@alias/logger')('growi:service:GlobalNotificationSlackService'); // eslint-disable-line no-unused-vars
+const urljoin = require('url-join');
+
+/**
+ * sub service class of GlobalNotificationSetting
+ */
+class GlobalNotificationSlackService {
+
+  constructor(crowi) {
+    this.crowi = crowi;
+    this.slack = crowi.getSlack();
+    this.type = crowi.model('GlobalNotificationSetting').TYPE.SLACK;
+    this.event = crowi.model('GlobalNotificationSetting').EVENT;
+  }
+
+  /**
+   * send slack global notification
+   *
+   * @memberof GlobalNotificationSlackService
+   *
+   * @param {string} event
+   * @param {string} path
+   * @param {User} triggeredBy user who triggered the event
+   * @param {{ comment: Comment, oldPath: string }} _ event specific vars
+   */
+  async fire(event, path, triggeredBy, vars) {
+    const GlobalNotification = this.crowi.model('GlobalNotificationSetting');
+    const notifications = await GlobalNotification.findSettingByPathAndEvent(event, path, this.type);
+
+    const messageBody = this.generateMessageBody(event, path, triggeredBy, vars);
+    const attachmentBody = this.generateAttachmentBody(event, path, triggeredBy, vars);
+
+    await Promise.all(notifications.map((notification) => {
+      return this.slack.sendGlobalNotification(messageBody, attachmentBody, notification.slackChannels);
+    }));
+  }
+
+  /**
+   * generate slack message body
+   *
+   * @memberof GlobalNotificationSlackService
+   *
+   * @param {string} event event name triggered
+   * @param {string} path path triggered the event
+   * @param {User} triggeredBy user triggered the event
+   * @param {{ comment: Comment, oldPath: string }} _ event specific vars
+   *
+   * @return  {string} slack message body
+   */
+  generateMessageBody(event, path, triggeredBy, { comment, oldPath }) {
+    const pageUrl = `<${urljoin(this.crowi.appService.getSiteUrl(), path)}|${path}>`;
+    const username = `<${urljoin(this.crowi.appService.getSiteUrl(), 'user', triggeredBy.username)}|${triggeredBy.username}>`;
+    let messageBody;
+
+    switch (event) {
+      case this.event.PAGE_CREATE:
+        messageBody = `:bell: ${username} created ${pageUrl}`;
+        break;
+      case this.event.PAGE_EDIT:
+        messageBody = `:bell: ${username} edited ${pageUrl}`;
+        break;
+      case this.event.PAGE_DELETE:
+        messageBody = `:bell: ${username} deleted ${pageUrl}`;
+        break;
+      case this.event.PAGE_MOVE:
+        // validate for page move
+        if (oldPath == null) {
+          throw new Error(`invalid vars supplied to GlobalNotificationSlackService.generateOption for event ${event}`);
+        }
+        // eslint-disable-next-line no-case-declarations
+        const oldPageUrl = `<${urljoin(this.crowi.appService.getSiteUrl(), oldPath)}|${oldPath}>`;
+        messageBody = `:bell: ${username} moved ${oldPageUrl} to ${pageUrl}`;
+        break;
+      case this.event.PAGE_LIKE:
+        messageBody = `:bell: ${username} liked ${pageUrl}`;
+        break;
+      case this.event.COMMENT:
+        // validate for comment
+        if (comment == null) {
+          throw new Error(`invalid vars supplied to GlobalNotificationSlackService.generateOption for event ${event}`);
+        }
+        messageBody = `:bell: ${username} commented on ${pageUrl}`;
+        break;
+      default:
+        throw new Error(`unknown global notificaiton event: ${event}`);
+    }
+
+    return messageBody;
+  }
+
+  /**
+   * generate slack attachment body
+   *
+   * @memberof GlobalNotificationSlackService
+   *
+   * @param {string} event event name triggered
+   * @param {string} path path triggered the event
+   * @param {User} triggeredBy user triggered the event
+   * @param {{ comment: Comment, oldPath: string }} _ event specific vars
+   *
+   * @return  {string} slack attachment body
+   */
+  generateAttachmentBody(event, path, triggeredBy, { comment, oldPath }) {
+    const attachmentBody = '';
+
+    // TODO: create attachment
+    // attachment body is intended for comment or page diff
+
+    // switch (event) {
+    //   case this.event.PAGE_CREATE:
+    //     break;
+    //   case this.event.PAGE_EDIT:
+    //     break;
+    //   case this.event.PAGE_DELETE:
+    //     break;
+    //   case this.event.PAGE_MOVE:
+    //     break;
+    //   case this.event.PAGE_LIKE:
+    //     break;
+    //   case this.event.COMMENT:
+    //     break;
+    //   default:
+    //     throw new Error(`unknown global notificaiton event: ${event}`);
+    // }
+
+    return attachmentBody;
+  }
+
+}
+
+module.exports = GlobalNotificationSlackService;

+ 44 - 0
src/server/service/global-notification/index.js

@@ -0,0 +1,44 @@
+const logger = require('@alias/logger')('growi:service:GlobalNotificationService');
+const GloabalNotificationSlack = require('./global-notification-slack');
+const GloabalNotificationMail = require('./global-notification-mail');
+
+/**
+ * service class of GlobalNotificationSetting
+ */
+class GlobalNotificationService {
+
+  constructor(crowi) {
+    this.crowi = crowi;
+    this.defaultLang = 'en-US'; // TODO: get defaultLang from app global config
+
+    this.gloabalNotificationMail = new GloabalNotificationMail(crowi);
+    this.gloabalNotificationSlack = new GloabalNotificationSlack(crowi);
+  }
+
+  /**
+   * fire global notification
+   *
+   * @memberof GlobalNotificationService
+   *
+   * @param {string} event event name triggered
+   * @param {string} path path triggered the event
+   * @param {User} triggeredBy user who triggered the event
+   * @param {object} vars event specific vars
+   */
+  async fire(event, path, triggeredBy, vars = {}) {
+    logger.debug(`global notficatoin event ${event} was triggered`);
+
+    // validation
+    if (event == null || path == null || triggeredBy == null) {
+      throw new Error(`invalid vars supplied to GlobalNotificationSlackService.generateOption for event ${event}`);
+    }
+
+    await Promise.all([
+      this.gloabalNotificationMail.fire(event, path, triggeredBy, vars),
+      this.gloabalNotificationSlack.fire(event, path, triggeredBy, vars),
+    ]);
+  }
+
+}
+
+module.exports = GlobalNotificationService;

+ 1 - 1
src/server/util/mailer.js

@@ -93,7 +93,7 @@ module.exports = function(crowi) {
     return mc;
     return mc;
   }
   }
 
 
-  function send(config, callback) {
+  function send(config, callback = () => {}) {
     if (mailer) {
     if (mailer) {
       const templateVars = config.vars || {};
       const templateVars = config.vars || {};
       return swig.renderFile(
       return swig.renderFile(

+ 32 - 0
src/server/util/slack.js

@@ -168,6 +168,32 @@ module.exports = function(crowi) {
     return message;
     return message;
   };
   };
 
 
+  /**
+   * For GlobalNotification
+   *
+   * @param {string} messageBody
+   * @param {string} attachmentBody
+   * @param {string} slackChannel
+  */
+  const prepareSlackMessageForGlobalNotification = async(messageBody, attachmentBody, slackChannel) => {
+    const appTitle = crowi.appService.getAppTitle();
+
+    const attachment = {
+      color: '#263a3c',
+      text: attachmentBody,
+      mrkdwn_in: ['text'],
+    };
+
+    const message = {
+      channel: `#${slackChannel}`,
+      username: appTitle,
+      text: messageBody,
+      attachments: [attachment],
+    };
+
+    return message;
+  };
+
   const getSlackMessageTextForPage = function(path, pageId, user, updateType) {
   const getSlackMessageTextForPage = function(path, pageId, user, updateType) {
     let text;
     let text;
     const url = crowi.appService.getSiteUrl();
     const url = crowi.appService.getSiteUrl();
@@ -204,6 +230,12 @@ module.exports = function(crowi) {
     return slackPost(messageObj);
     return slackPost(messageObj);
   };
   };
 
 
+  slack.sendGlobalNotification = async(messageBody, attachmentBody, slackChannel) => {
+    const messageObj = await prepareSlackMessageForGlobalNotification(messageBody, attachmentBody, slackChannel);
+
+    return slackPost(messageObj);
+  };
+
   const slackPost = (messageObj) => {
   const slackPost = (messageObj) => {
     // when incoming Webhooks is prioritized
     // when incoming Webhooks is prioritized
     if (configManager.getConfig('notification', 'slack:isIncomingWebhookPrioritized')) {
     if (configManager.getConfig('notification', 'slack:isIncomingWebhookPrioritized')) {

+ 5 - 6
src/server/views/admin/global-notification-detail.html

@@ -57,22 +57,21 @@
               <div class="form-group form-inline">
               <div class="form-group form-inline">
                 <h3>{{ t('notification_setting.notify_to') }}</h3>
                 <h3>{{ t('notification_setting.notify_to') }}</h3>
                 <div class="radio radio-primary">
                 <div class="radio radio-primary">
-                  <input type="radio" id="mail" name="notificationGlobal[notifyToType]" value="mail" {% if setting.__t == 'mail' %}checked{% endif %} checked>
+                  <input type="radio" id="mail" name="notificationGlobal[notifyToType]" value="mail" {% if setting.__t == 'mail' %}checked{% endif %}>
                   <label for="mail">
                   <label for="mail">
                     <p class="font-weight-bold">Email</p>
                     <p class="font-weight-bold">Email</p>
                   </label>
                   </label>
                 </div>
                 </div>
                 <div class="radio radio-primary">
                 <div class="radio radio-primary">
-                  <input type="radio" id="slack" name="notificationGlobal[notifyToType]" value="slack" {% if setting.__t == 'slack' %}checked{% endif %} disabled>
+                  <input type="radio" id="slack" name="notificationGlobal[notifyToType]" value="slack" {% if setting.__t == 'slack' %}checked{% endif %}>
                   <label for="slack">
                   <label for="slack">
                     <p class="font-weight-bold">Slack</p>
                     <p class="font-weight-bold">Slack</p>
                   </label>
                   </label>
                 </div>
                 </div>
               </div>
               </div>
 
 
-              <!-- <div class="form-group notify-to-option {% if setting.__t != 'mail' %}d-none{% endif %}" id="mail-input"> -->
-              <div class="form-group notify-to-option" id="mail-input">
-                <input class="form-control" type="text" name="notificationGlobal[toEmail]" value="{{ setting.toEmail || '' }}">
+              <div class="form-group notify-to-option {% if setting.__t != 'mail' %}d-none{% endif %}" id="mail-input">
+                <input class="form-control" type="text" name="notificationGlobal[toEmail]" placeholder="Email" value="{{ setting.toEmail || '' }}">
                 <p class="help">
                 <p class="help">
                   <b>Hint: </b>
                   <b>Hint: </b>
                   <a href="https://ifttt.com/create" target="_blank">{{ t('notification_setting.email.ifttt_link') }} <i class="icon-share-alt"></i></a>
                   <a href="https://ifttt.com/create" target="_blank">{{ t('notification_setting.email.ifttt_link') }} <i class="icon-share-alt"></i></a>
@@ -80,7 +79,7 @@
               </div>
               </div>
 
 
               <div class="form-group notify-to-option {% if setting.__t != 'slack' %}d-none{% endif %}" id="slack-input">
               <div class="form-group notify-to-option {% if setting.__t != 'slack' %}d-none{% endif %}" id="slack-input">
-                <input class="form-control" type="text" name="notificationGlobal[slackChannels]" value="{{ setting.slackChannels || '' }}" disabled>
+                <input class="form-control" type="text" name="notificationGlobal[slackChannels]" placeholder="Slack Channel" value="{{ setting.slackChannels || '' }}">
               </div>
               </div>
             </fieldset>
             </fieldset>