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

Merge pull request #134 from weseek/master

release v2.0.0
Yuki Takei 8 лет назад
Родитель
Сommit
d450d0dba2

+ 5 - 0
CHANGES.md

@@ -1,6 +1,11 @@
 CHANGES
 ========
 
+## 2.0.0
+
+* Feature: Enabled to integrate with Slack using Incoming Webhooks
+* Support: Upgrade all outdated libs
+
 ## 1.2.16
 
 * Improvement: Condition for creating portal

+ 2 - 0
README.md

@@ -12,6 +12,7 @@ crowi-plus [![Chat on Slack](https://crowi-plus-slackin.weseek.co.jp/badge.svg)]
 
 [![wercker status](https://app.wercker.com/status/39cdc49d067d65c39cb35d52ceae6dc1/s/master "wercker status")](https://app.wercker.com/project/byKey/39cdc49d067d65c39cb35d52ceae6dc1)
 [![dependencies status](https://david-dm.org/weseek/crowi-plus.svg)](https://david-dm.org/weseek/crowi-plus)
+[![devDependencies Status](https://david-dm.org/weseek/crowi-plus/dev-status.svg)](https://david-dm.org/weseek/crowi-plus?type=dev)
 [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
 
 
@@ -31,6 +32,7 @@ Why crowi-plus?
 * **Secure**
   * Upgrade jQuery to 3.x
   * Upgrade other insecure libs
+  * The official Crowi status is [![dependencies Status](https://david-dm.org/crowi/crowi/status.svg)](https://david-dm.org/crowi/crowi) [![devDependencies Status](https://david-dm.org/crowi/crowi/dev-status.svg)](https://david-dm.org/crowi/crowi?type=dev)
 * **[Docker Ready][dockerhub]**
 * **[Docker Compose Ready][docker-compose]**
   * [Multiple sites example](https://github.com/weseek/crowi-plus-docker-compose/tree/master/examples/multi-app)

+ 1 - 1
bin/generate-plugin-definitions-source.js

@@ -5,7 +5,7 @@
  */
 const fs = require('graceful-fs');
 const normalize = require('normalize-path');
-const swig = require('swig');
+const swig = require('swig-templates');
 const helpers = require('../config/helpers');
 
 const TEMPLATE = helpers.root('bin/templates/plugin-definitions.js.swig');

+ 0 - 1
config/webpack.common.js

@@ -8,7 +8,6 @@ const helpers = require('./helpers');
 /*
  * Webpack Plugins
  */
-// problem with copy-webpack-plugin
 const AssetsPlugin = require('assets-webpack-plugin');
 const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
 

+ 0 - 4
config/webpack.dev.js

@@ -12,10 +12,7 @@ const commonConfig = require('./webpack.common.js');
 /*
  * Webpack Plugins
  */
-// problem with copy-webpack-plugin
-const AssetsPlugin = require('assets-webpack-plugin');
 const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
-const CopyWebpackPlugin = require('copy-webpack-plugin');
 const DllBundlesPlugin = require('webpack-dll-bundles-plugin').DllBundlesPlugin;
 
 /*
@@ -24,7 +21,6 @@ const DllBundlesPlugin = require('webpack-dll-bundles-plugin').DllBundlesPlugin;
 const ENV = process.env.ENV = process.env.NODE_ENV = 'development';
 const HOST = process.env.HOST || '0.0.0.0';
 const PORT = process.env.PORT || 3000;
-const WATCH = helpers.hasProcessFlag('watch');
 
 /*
  * Webpack configuration

+ 7 - 1
lib/crowi/dev.js

@@ -3,6 +3,7 @@ const path = require('path');
 const webpack = require('webpack');
 const helpers = require('./helpers');
 
+const swig = require('swig-templates');
 const LRWebSocketServer = require('livereload-server/lib/server');
 
 class CrowiDev {
@@ -18,9 +19,14 @@ class CrowiDev {
   }
 
   init() {
+    this.initSwig();
     this.hackLRWebSocketServer();
   }
 
+  initSwig() {
+    swig.setDefaults({ cache: false });
+  }
+
   /**
    * prevent to crash socket with:
    * -------------------------------------------------
@@ -60,7 +66,7 @@ class CrowiDev {
   }
 
   setupEasyLiveReload(app) {
-    if (!helpers.hasProcessFlag('watch')) {
+    if (!helpers.hasProcessFlag('livereload')) {
       return;
     }
 

+ 2 - 3
lib/crowi/express-init.js

@@ -10,8 +10,7 @@ module.exports = function(crowi, app) {
     , session        = require('express-session')
     , basicAuth      = require('basic-auth-connect')
     , flash          = require('connect-flash')
-    , cons           = require('consolidate')
-    , swig           = require('swig')
+    , swig           = require('swig-templates')
     , webpackAssets  = require('express-webpack-assets')
     , i18next        = require('i18next')
     , i18nFsBackend  = require('i18next-node-fs-backend')
@@ -82,7 +81,7 @@ module.exports = function(crowi, app) {
   app.set('port', crowi.port);
   const staticOption = (crowi.node_env === 'production') ? {maxAge:'30d'} : {};
   app.use(express.static(crowi.publicDir, staticOption));
-  app.engine('html', cons.swig);
+  app.engine('html', swig.renderFile);
   app.use(webpackAssets(
     path.join(crowi.publicDir, 'js/webpack-assets.json'),
     { devMode: (crowi.node_env === 'development') })

+ 3 - 3
lib/crowi/index.js

@@ -25,7 +25,7 @@ function Crowi (rootdir, env)
   this.publicDir   = path.join(this.rootDir, 'public') + sep;
   this.libDir      = path.join(this.rootDir, 'lib') + sep;
   this.eventsDir   = path.join(this.libDir, 'events') + sep;
-  this.localeDir   = path.join(this.rootDir, 'locales') + sep;
+  this.localeDir   = path.join(this.libDir, 'locales') + sep;
   this.resourceDir = path.join(this.rootDir, 'resource') + sep;
   this.viewsDir    = path.join(this.libDir, 'views') + sep;
   this.mailDir     = path.join(this.viewsDir, 'mail') + sep;
@@ -153,7 +153,7 @@ Crowi.prototype.setupDatabase = function() {
     ;
 
   return new Promise(function(resolve, reject) {
-    mongoose.connect(mongoUri, function(e) {
+    mongoose.connect(mongoUri, { useMongoClient: true }, function(e) {
       if (e) {
         debug('DB Connect Error: ', e);
         debug('DB Connect Error: ', mongoUri);
@@ -378,7 +378,7 @@ Crowi.prototype.buildServer = function() {
   }
 
   if (env == 'development') {
-    //swig.setDefaults({ cache: false });
+    //swig.setDefaults({ cache: false });   // moved to dev.js -- 2017.07.09 Yuki Takei
     const morgan = require('morgan');
     app.use(morgan('dev'));
   }

+ 10 - 0
lib/form/admin/slackIwhSetting.js

@@ -0,0 +1,10 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field;
+
+module.exports = form(
+  field('slackIwhSetting[slack:incomingWebhookUrl]', 'Webhook URL'),
+  field('slackIwhSetting[slack:isIncomingWebhookPrioritized]', 'Prioritize Incoming Webhook than Slack App ').trim().toBooleanStrict()
+);
+

+ 2 - 2
lib/form/admin/slackSetting.js

@@ -4,7 +4,7 @@ var form = require('express-form')
   , field = form.field;
 
 module.exports = form(
-  field('slackSetting[slack:clientId]', 'clientId').is(/(\d+)\.(\d+)/).required(),
-  field('slackSetting[slack:clientSecret]', 'clientSecret').required().is(/([0-9a-f]+)/)
+  field('slackSetting[slack:clientId]', 'clientId'),
+  field('slackSetting[slack:clientSecret]', 'clientSecret')
 );
 

+ 1 - 0
lib/form/index.js

@@ -23,6 +23,7 @@ module.exports = {
     customlayout: require('./admin/customlayout'),
     customfeatures: require('./admin/customfeatures'),
     userInvite: require('./admin/userInvite'),
+    slackIwhSetting: require('./admin/slackIwhSetting'),
     slackSetting: require('./admin/slackSetting'),
   },
 };

+ 0 - 0
locales/en-US/translation.json → lib/locales/en-US/translation.json


+ 0 - 0
locales/ja/translation.json → lib/locales/ja/translation.json


+ 25 - 1
lib/models/config.js

@@ -338,6 +338,11 @@ module.exports = function(crowi) {
   };
 
   configSchema.statics.hasSlackConfig = function(config)
+  {
+    return Config.hasSlackAppConfig(config) || Config.hasSlackIwhUrl(config);
+  };
+
+  configSchema.statics.hasSlackAppConfig = function(config)
   {
     if (!config.notification) {
       return false;
@@ -350,9 +355,28 @@ module.exports = function(crowi) {
     return true;
   };
 
+  /**
+   * for Slack Incoming Webhooks
+   */
+  configSchema.statics.hasSlackIwhUrl = function(config)
+  {
+    if (!config.notification) {
+      return false;
+    }
+    return config.notification['slack:incomingWebhookUrl'];
+  };
+
+  configSchema.statics.isIncomingWebhookPrioritized = function(config)
+  {
+    if (!config.notification) {
+      return false;
+    }
+    return config.notification['slack:isIncomingWebhookPrioritized'];
+  };
+
   configSchema.statics.hasSlackToken = function(config)
   {
-    if (!this.hasSlackConfig(config)) {
+    if (!this.hasSlackAppConfig(config)) {
       return false;
     }
 

+ 55 - 8
lib/routes/admin.js

@@ -132,17 +132,22 @@ module.exports = function(crowi, app) {
     var config = crowi.getConfig();
     var UpdatePost = crowi.model('UpdatePost');
     var slackSetting = Config.setupCofigFormData('notification', config);
-    var hasSlackConfig = Config.hasSlackConfig(config);
+    var hasSlackAppConfig = Config.hasSlackAppConfig(config);
+    var hasSlackIwhUrl = Config.hasSlackIwhUrl(config);
     var hasSlackToken = Config.hasSlackToken(config);
     var slack = crowi.slack;
     var slackAuthUrl = '';
 
-    if (!Config.hasSlackConfig(req.config)) {
+    if (!Config.hasSlackAppConfig(req.config)) {
       slackSetting['slack:clientId'] = '';
       slackSetting['slack:clientSecret'] = '';
-    } else {
+    }
+    else {
       slackAuthUrl = slack.getAuthorizeURL();
     }
+    if (!Config.hasSlackIwhUrl(req.config)) {
+      slackSetting['slack:incomingWebhookUrl'] = '';
+    }
 
     if (req.session.slackSetting) {
       slackSetting = req.session.slackSetting;
@@ -154,7 +159,8 @@ module.exports = function(crowi, app) {
       return res.render('admin/notification', {
         settings,
         slackSetting,
-        hasSlackConfig,
+        hasSlackAppConfig,
+        hasSlackIwhUrl,
         hasSlackToken,
         slackAuthUrl
       });
@@ -169,8 +175,10 @@ module.exports = function(crowi, app) {
     if (req.form.isValid) {
       Config.updateNamespaceByArray('notification', slackSetting, function(err, config) {
         Config.updateConfigCache('notification', config);
+        req.flash('successMessage', ['Successfully Updated!']);
         req.session.slackSetting = null;
 
+        // Re-setup
         crowi.setupSlack().then(function() {
           return res.redirect('/admin/notification');
         });
@@ -186,13 +194,18 @@ module.exports = function(crowi, app) {
     var code = req.query.code;
     var config = crowi.getConfig();
 
-    if (!code || !Config.hasSlackConfig(req.config)) {
+    if (!code || !Config.hasSlackAppConfig(req.config)) {
       return res.redirect('/admin/notification');
     }
 
     var slack = crowi.slack;
-    var bot = slack.createBot();
-    bot.api.oauth.access({code}, function(err, data) {
+    var bot = slack.initAppBot(true);
+    var args = {
+      code,
+      client_id: config.notification['slack:clientId'],
+      client_secret: config.notification['slack:clientSecret'],
+    }
+    bot.api.oauth.access(args, function(err, data) {
       debug('oauth response', err, data);
       if (!data.ok || !data.access_token) {
         req.flash('errorMessage', ['Failed to fetch access_token. Please do connect again.']);
@@ -206,13 +219,27 @@ module.exports = function(crowi, app) {
             req.flash('successMessage', ['Successfully Connected!']);
           }
 
-          slack.createBot();
+          slack.initAppBot();
           return res.redirect('/admin/notification');
         });
       }
     });
   };
 
+  // app.post('/admin/notification/slackSetting/disconnect' , admin.notification.disconnectFromSlack);
+  actions.notification.disconnectFromSlack = function(req, res) {
+    const config = crowi.getConfig();
+    const slack = crowi.slack;
+
+    Config.updateNamespaceByArray('notification', {'slack:token': ''}, function(err, config) {
+      Config.updateConfigCache('notification', config);
+      req.flash('successMessage', ['Successfully Disconnected!']);
+
+      slack.initAppBot();
+      return res.redirect('/admin/notification');
+    });
+  };
+
   actions.search = {};
   actions.search.index = function(req, res) {
     var search = crowi.getSearcher();
@@ -224,6 +251,26 @@ module.exports = function(crowi, app) {
     });
   };
 
+  // app.post('/admin/notification/slackIwhSetting' , admin.notification.slackIwhSetting);
+  actions.notification.slackIwhSetting = function(req, res) {
+    var slackIwhSetting = req.form.slackIwhSetting;
+
+    if (req.form.isValid) {
+      Config.updateNamespaceByArray('notification', slackIwhSetting, function(err, config) {
+        Config.updateConfigCache('notification', config);
+        req.flash('successMessage', ['Successfully Updated!']);
+
+        // Re-setup
+        crowi.setupSlack().then(function() {
+          return res.redirect('/admin/notification#slack-incoming-webhooks');
+        });
+      });
+    } else {
+      req.flash('errorMessage', req.form.errors);
+      return res.redirect('/admin/notification#slack-incoming-webhooks');
+    }
+  };
+
   actions.search.buildIndex = function(req, res) {
     var search = crowi.getSearcher();
     if (!search) {

+ 2 - 0
lib/routes/index.js

@@ -64,8 +64,10 @@ module.exports = function(crowi, app) {
 
   // notification admin
   app.get('/admin/notification'              , loginRequired(crowi, app) , middleware.adminRequired() , admin.notification.index);
+  app.post('/admin/notification/slackIwhSetting', loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.slackIwhSetting, admin.notification.slackIwhSetting);
   app.post('/admin/notification/slackSetting', loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.slackSetting, admin.notification.slackSetting);
   app.get('/admin/notification/slackAuth'    , loginRequired(crowi, app) , middleware.adminRequired() , admin.notification.slackAuth);
+  app.get('/admin/notification/slackSetting/disconnect', loginRequired(crowi, app) , middleware.adminRequired() , admin.notification.disconnectFromSlack);
   app.post('/_api/admin/notification.add'    , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.api.notificationAdd);
   app.post('/_api/admin/notification.remove' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.api.notificationRemove);
   app.get('/_api/admin/users.search'         , loginRequired(crowi, app) , middleware.adminRequired() , admin.api.usersSearch);

+ 1 - 1
lib/util/mailer.js

@@ -7,7 +7,7 @@ module.exports = function(crowi) {
 
   var debug = require('debug')('crowi:lib:mailer')
     , nodemailer = require('nodemailer')
-    , swig = require('swig')
+    , swig = require('swig-templates')
     , config = crowi.getConfig()
     , mailConfig = {}
     , mailer = {}

+ 1 - 1
lib/util/middlewares.js

@@ -118,7 +118,7 @@ exports.swigFilters = function(app, swig) {
 
     swig.setFilter('datetz', function(input, format) {
       // timezone
-      var swigFilters = require('swig/lib/filters');
+      var swigFilters = require('swig-templates/lib/filters');
       return swigFilters.date(input, format, app.get('tzoffset'));
     });
 

+ 95 - 20
lib/util/slack.js

@@ -8,40 +8,86 @@ module.exports = function(crowi) {
   var debug = require('debug')('crowi:util:slack'),
     Config = crowi.model('Config'),
     Botkit = require('botkit'),
-    bot = null,
+    isDebugSlackbot = false,
+    appBot = null,                  // for Slack App
+    iwhBot = null,                  // for Slack Incoming Webhooks
     slack = {};
-  slack.controller = undefined;
+  slack.appController = undefined;  // for Slack App
+  slack.iwhController = undefined;  // for Slack Incoming Webhooks
 
-  slack.createBot = function() {
-    // alreay created
-    if (bot) {
-      return bot;
+  // isDebugSlackbot = true;           // for debug
+
+  slack.getBot = function() {
+    var config = crowi.getConfig();
+
+    // when incoming Webhooks is prioritized
+    if (Config.isIncomingWebhookPrioritized(config)) {
+      if (Config.hasSlackIwhUrl(config)) {
+        return iwhBot || slack.initIwhBot();
+      }
+      else if (Config.hasSlackToken(config)) {
+        return appBot || slack.initAppBot();
+      }
+    }
+    // else
+    else {
+      if (Config.hasSlackToken(config)) {
+        return appBot || slack.initAppBot();
+      }
+      else if (Config.hasSlackIwhUrl(config)) {
+        return iwhBot || slack.initIwhBot();
+      }
     }
 
+    return false;
+  };
+
+  slack.initAppBot = function(isClearToken) {
     var config = crowi.getConfig();
 
-    if (!slack.controller) {
+    if (!slack.appController) {
       slack.configureSlackApp();
     }
 
-    if (!slack.controller) {
+    if (!slack.appController) {
       return false;
     }
 
-    if (Config.hasSlackToken(config)) {
-      bot = slack.controller.spawn({token: config.notification['slack:token']});
+    if (!isClearToken && Config.hasSlackToken(config)) {
+      appBot = slack.appController.spawn({token: config.notification['slack:token']});
     } else {
-      bot = slack.controller.spawn();
+      appBot = slack.appController.spawn();
     }
-    return bot;
+    return appBot;
   };
 
+  slack.initIwhBot = function() {
+    var config = crowi.getConfig();
+
+    if (!slack.iwhController) {
+      slack.configureSlackIwh();
+    }
+
+    if (!slack.iwhController) {
+      return false;
+    }
+
+    iwhBot = slack.iwhController.spawn({
+      incoming_webhook: {
+        url: config.notification['slack:incomingWebhookUrl']
+      }
+    });
+
+    return iwhBot;
+  }
+
   slack.configureSlackApp = function ()
   {
     var config = crowi.getConfig();
-    if (Config.hasSlackConfig(config)) {
-      slack.controller = Botkit.slackbot();
-      slack.controller.configureSlackApp({
+
+    if (Config.hasSlackAppConfig(config)) {
+      slack.appController = Botkit.slackbot({debug: isDebugSlackbot});
+      slack.appController.configureSlackApp({
         clientId: config.notification['slack:clientId'],
         clientSecret: config.notification['slack:clientSecret'],
         redirectUri: slack.getSlackAuthCallbackUrl(),
@@ -54,6 +100,18 @@ module.exports = function(crowi) {
     return false;
   }
 
+  slack.configureSlackIwh = function ()
+  {
+    var config = crowi.getConfig();
+
+    if (Config.hasSlackIwhUrl(config)) {
+      slack.iwhController = Botkit.slackbot({debug: isDebugSlackbot});
+      return true;
+    }
+
+    return false;
+  }
+
   // hmmm
   slack.getSlackAuthCallbackUrl = function()
   {
@@ -64,22 +122,39 @@ module.exports = function(crowi) {
   }
 
   slack.getAuthorizeURL = function () {
-    if (!slack.controller) {
+    if (!slack.appController) {
       slack.configureSlackApp();
     }
 
-    if (!slack.controller) {
+    if (!slack.appController) {
       return '';
     }
 
-    return slack.controller.getAuthorizeURL();
+    return slack.appController.getAuthorizeURL();
   }
 
   slack.post = function (message) {
-    var bot = slack.createBot();
+    var bot = slack.getBot();
+    let sendMethod = undefined;
+
+    // use Slack App
+    if (bot === appBot) {
+      debug(`sendMethod: bot.api.chat.postMessage`);
+      sendMethod = bot.api.chat.postMessage;
+    }
+    // use Slack Incoming Webhooks
+    else if (bot === iwhBot) {
+      debug(`sendMethod: bot.sendWebhook`);
+      sendMethod = bot.sendWebhook;
+    }
+
+    if (sendMethod === undefined) {
+      debug(`sendMethod is undefined`);
+      return Promise.resolve();
+    }
 
     return new Promise(function(resolve, reject) {
-      bot.api.chat.postMessage(message, function(err, res) {
+      sendMethod(message, function(err, res) {
         if (err) {
           debug('Post error', err, res);
           debug('Sent data to slack is:', message);

+ 1 - 1
lib/util/swigFunctions.js

@@ -72,7 +72,7 @@ module.exports = function(crowi, app, req, locals) {
 
   locals.slackConfigured = function() {
     var config = crowi.getConfig()
-    if (Config.hasSlackToken(config)) {
+    if (Config.hasSlackToken(config) || Config.hasSlackIwhUrl(config)) {
       return true;
     }
     return false;

+ 15 - 1
lib/views/admin/app.html

@@ -242,7 +242,21 @@
       <form action="/_api/admin/settings/google" method="post" class="form-horizontal" id="googleSettingForm" role="form">
       <fieldset>
       <legend>Google 設定</legend>
-        <p class="well">Google プロジェクトの設定をすると、Google アカウントにコネクトして登録やログインが可能になります。</p>
+        <p class="well">
+          Google Cloud Platform の <a href="https://console.cloud.google.com/apis/credentials">API Manager</a>
+          から OAuth2 Client ID を作成すると、Google アカウントにコネクトして登録やログインが可能になります。
+        </p>
+
+        <ol class="help-block">
+          <li><a href="https://console.cloud.google.com/apis/credentials">API Manager</a> へアクセス</li>
+          <li>プロジェクトを作成していない場合は作成してください</li>
+          <li>「認証情報を作成」-> OAuthクライアントID</li>
+          <ol>
+            <li>「ウェブアプリケーション」を選択</li>
+            <li>承認済みのリダイレクトURLに、 <code>https://${crowi.host}/google/callback</code> を入力<br>
+            (<code>${crowi.host}</code>は環境に合わせて変更してください)</li>
+          </ol>
+        </ol>
 
         <div class="form-group">
           <label for="settingForm[google:clientId]" class="col-xs-3 control-label">Client ID</label>

+ 216 - 71
lib/views/admin/notification.html

@@ -18,12 +18,6 @@
     </div>
     <div class="col-md-9">
 
-      <ul class="nav nav-tabs">
-        <li class="active"><a href="#slack" data-toggle="tab"><i class="fa fa-slack"></i> Slack</a></li>
-      </ul>
-
-      <br>
-
       {% set smessage = req.flash('successMessage') %}
       {% if smessage.length %}
       <div class="alert alert-success">
@@ -42,46 +36,211 @@
       </div>
       {% endif %}
 
-      <form action="/admin/notification/slackSetting" method="post" class="form-horizontal" id="appSettingForm" role="form">
-      <fieldset>
-        <legend>Slack App Configuration</legend>
-        <div class="form-group">
-          <label for="slackSetting[slack:clientId]" class="col-xs-3 control-label">clientId</label>
-          <div class="col-xs-6">
-            <input class="form-control" type="text" name="slackSetting[slack:clientId]" value="{{ slackSetting['slack:clientId'] }}">
-          </div>
-        </div>
+      <ul class="nav nav-tabs" role="tablist">
+        <li class="active">
+          <a href="#slack-app" data-toggle="tab" role="tab"><i class="fa fa-slack"></i> Slack App</a>
+        </li>
+        <li role="tab">
+          <a href="#slack-incoming-webhooks" data-toggle="tab" role="tab"><i class="fa fa-slack"></i> Slack Incoming Webhooks</a>
+        </li>
+      </ul>
 
-        <div class="form-group">
-          <label for="slackSetting[slack:clientSecret]" class="col-xs-3 control-label">clientSecret</label>
-          <div class="col-xs-6">
-            <input class="form-control" type="text" name="slackSetting[slack:clientSecret]" value="{{ slackSetting['slack:clientSecret'] }}">
-          </div>
-        </div>
+      <div class="tab-content">
+        <div id="slack-app" class="tab-pane active" role="tabpanel" >
+
+          <form action="/admin/notification/slackSetting" method="post" class="form-horizontal" id="appSettingForm" role="form">
+            <fieldset>
+              <legend>Slack App Configuration</legend>
+
+              <p class="well text-warning">
+                <i class="fa fa-warning"></i> NOT RECOMMENDED
+                <br><br>
+                This is the way that compatible with the official Crowi,<br>
+                but not recommended in crowi-plus because it is too complex.
+                <br><br>
+                Please use <a href="#slack-incoming-webhooks" data-toggle="tab" onclick="activateTab('slack-incoming-webhooks')">Slack incomming webhooks Configuration</a> instead.
+              </p>
 
-        <div class="form-group">
-          <div class="col-xs-offset-3 col-xs-6">
-            <button type="submit" class="btn btn-primary">Submit</button>
+              <div class="form-group">
+                <label for="slackSetting[slack:clientId]" class="col-xs-3 control-label">clientId</label>
+                <div class="col-xs-6">
+                  <input class="form-control" type="text" name="slackSetting[slack:clientId]" value="{{ slackSetting['slack:clientId'] }}">
+                </div>
+              </div>
+
+              <div class="form-group">
+                <label for="slackSetting[slack:clientSecret]" class="col-xs-3 control-label">clientSecret</label>
+                <div class="col-xs-6">
+                  <input class="form-control" type="text" name="slackSetting[slack:clientSecret]" value="{{ slackSetting['slack:clientSecret'] }}">
+                </div>
+              </div>
+
+              <div class="form-group">
+                <div class="col-xs-offset-3 col-xs-6">
+                  <button type="submit" class="btn btn-primary">Submit</button>
+                </div>
+              </div>
+            </fieldset>
+            <input type="hidden" name="_csrf" value="{{ csrf() }}">
+          </form>
+
+          {% if hasSlackAppConfig %}
+          <div class="text-center">
+            {% if hasSlackToken %}
+            <p>Crowi and Slack is already <strong>connected</strong>. You can re-connect to refresh and overwirte the token with your Slack account.</p>
+            <a class="btn btn-warning" href="/admin/notification/slackSetting/disconnect">
+              <i class="fa fa-slack"></i> Disconnect from Slack
+            </a>
+            <a class="btn btn-default" href="{{ slackAuthUrl }}" target="_blank">
+              <i class="fa fa-slack"></i> Reconnect to Slack
+            </a>
+            {% else %}
+            <p>Slack clientId and clientSecret is configured. Now, you can connect with Slack.</p>
+            <a class="btn btn-primary" href="{{ slackAuthUrl }}" target="_blank">
+              <i class="fa fa-slack"></i> Connect to Slack
+            </a>
+            {% endif %}
           </div>
-        </div>
-      </fieldset>
-      <input type="hidden" name="_csrf" value="{{ csrf() }}">
-      </form>
-
-      {% if hasSlackConfig %}
-      <div class="text-center">
-        {% if hasSlackToken %}
-        <p>Crowi and Slack is already <strong>connected</strong>. You can re-connect to refresh and overwirte the token with your Slack account.</p>
-        <a class="btn btn-default" href="{{ slackAuthUrl }}">
-          <i class="fa fa-slack"></i> Reconnect to Slack
-        </a>
-        {% else %}
-        <p>Slack clientId and clientSecret is configured. Now, you can connect with Slack.</p>
-        <a class="btn btn-primary" href="{{ slackAuthUrl }}">
-          <i class="fa fa-slack"></i> Connect to Slack
-        </a>
-        {% endif %}
-      </div>
+
+          {% endif %}
+
+          {# {% if not hasSlackAppConfig %} #}
+          <hr>
+          <h3>
+            <i class="fa fa-question-circle" aria-hidden="true"></i>
+            <a href="#collapseHelpForApp" data-toggle="collapse">How to configure Slack App?</a>
+          </h3>
+
+          <ol id="collapseHelpForApp" class="collapse">
+            <li>
+              Register Slack App
+              <ol>
+                <li>
+                  Create App from <a href="https://api.slack.com/applications/new">this link</a>, and fill the form out as below:
+                  <dl class="dl-horizontal">
+                    <dt>App Name</dt> <dd><code>crowi-plus</code> </dd>
+                    <dt>Development Slack Team</dt> <dd>Select the team you want to notify to.</dd>
+                  </dl>
+                </li>
+                <li><strong>Save</strong> it.</li>
+              </ol>
+            </li>
+            <li>
+              Get App Credentials
+              <ol>
+                <li>Go To "Basic Information" page and make a note "Client ID" and "Client Secret".</li>
+              </ol>
+            </li>
+            <li>
+              Set Redirect URLs
+              <ol>
+                <li>Go to "OAuth &amp; Permissions" page.</li>
+                <li>Add <code><script>document.write(location.origin);</script>/admin/notification/slackAuth</code> .</li>
+                <li>Don't forget to <strong>save</strong>.</li>
+              </ol>
+            </li>
+            <li>
+              Set Permission Scopes to the App
+              <ol>
+                <li>Go to "OAuth &amp; Permissions" page.</li>
+                <li>Add "Send messages as crowi-plus"(<code>chat:write:bot</code>).</li>
+                <li>Don't forget to <strong>save</strong>.</li>
+              </ol>
+            </li>
+            <li>
+              Create a bot user
+              <ol>
+                <li>Go to "Bot Users" page and add.</li>
+              </ol>
+            </li>
+            <li>
+              Install the app
+              <ol>
+                <li>Go to "Install App to Your Team" page and install.</li>
+              </ol>
+            </li>
+            <li>
+              (At Team) Approve the app
+              <ol>
+                <li>Go to the management Apps page for the team you installed the app and approve crowi-plus.</li>
+              </ol>
+            </li>
+            <li>
+              (At Team) Invite the bot to your team
+              <ol>
+                <li>Invite the user you created in <code>4. Add a bot user</code> to the channel you notify to.</li>
+              </ol>
+            </li>
+            <li>
+              (At crowi-plus) Input "clientId" and "clientSecret" and submit on this page.
+            </li>
+            <li>
+              (At crowi-plus) Click "Connect to Slack" button to start OAuth process.
+            </li>
+          </ol>
+          {# {% endif %} #}
+
+
+        </div><!-- /#slack-app -->
+
+        <div id="slack-incoming-webhooks" class="tab-pane" role="tabpanel">
+
+          <form action="/admin/notification/slackIwhSetting" method="post" class="form-horizontal" id="appSettingForm" role="form">
+            <fieldset>
+              <legend>Slack Incoming Webhooks Configuration</legend>
+
+              <div class="form-group">
+                <label for="slackIwhSetting[slack:incomingWebhookUrl]" class="col-xs-3 control-label">Webhook URL</label>
+                <div class="col-xs-9">
+                  <input class="form-control" type="text" name="slackIwhSetting[slack:incomingWebhookUrl]" value="{{ slackSetting['slack:incomingWebhookUrl'] }}">
+                </div>
+              </div>
+
+              <div class="form-group">
+                <label for="slackIwhSetting[slack:isIncomingWebhookPrioritized]" class="col-xs-3 control-label"></label>
+                <div class="col-xs-9">
+                  <input type="checkbox" name="slackIwhSetting[slack:isIncomingWebhookPrioritized]" value="1"
+                    {% if slackSetting['slack:isIncomingWebhookPrioritized'] %}checked{% endif %}>
+                  Prioritize Incoming Webhook than Slack App
+                  <p class="help-block">Check this option and crowi-plus use Incoming Webhooks even if Slack App settings are enabled.</p>
+                </div>
+              </div>
+
+              <div class="form-group">
+                <div class="col-xs-offset-3 col-xs-6">
+                  <button type="submit" class="btn btn-primary">Submit</button>
+                </div>
+              </div>
+            </fieldset>
+            <input type="hidden" name="_csrf" value="{{ csrf() }}">
+          </form>
+
+          <hr>
+          <h3>
+            <i class="fa fa-question-circle" aria-hidden="true"></i>
+            <a href="#collapseHelpForIwh" data-toggle="collapse">How to configure Incoming Webhooks?</a>
+          </h3>
+
+          <ol id="collapseHelpForIwh" class="collapse">
+            <li>
+              (At Team) Add a hook
+              <ol>
+                <li>Go to <a href="https://slack.com/services/new/incoming-webhook">Incoming Webhooks Configuration page</a>.</li>
+                <li>Choose the default channel to post.</li>
+                <li>Add.</li>
+              </ol>
+            </li>
+            <li>
+              (At crowi-plus) Set Webhook URL
+              <ol>
+                <li>Input "Webhook URL" and submit on this page.</li>
+              </ol>
+            </li>
+          </ol>
+
+        </div><!-- /#slack-incoming-webhooks -->
+
+      </div><!-- /.tab-content -->
 
       <hr>
 
@@ -136,37 +295,23 @@
       </table>
 
 
-      {% endif %}
-
-      {% if not hasSlackConfig %}
-      <h3>How to configure Slack app for Crowi</h3>
-      <p>
-      Register Crowi as a Slack application, the notification feature for Slack can be enabled.
-      </p>
-      <h4>1. Register Slack App</h4>
-      <p>
-      Create App from <a href="https://api.slack.com/applications/new">this link</a>, and fill the form out as below:
-      </p>
-      <dl class="dl-horizontal">
-        <dt>App Name</dt> <dd><code>Crowi</code> </dd>
-        <dt>Icon</dt> <dd>Upload this image as the icon (Free to download and use it) =&gt; <a href="https://github.com/crowi/crowi/tree/master/resource/logo">Crowi Logo</a></dd>
-        <dt>Short description</dt> <dd><code>Crowi's Slack Notification Integration</code> </dd>
-        <dt>Long description</dt> <dd><code>Crowi's Slack Notification Integration</code> </dd>
-      </dl>
-      <p>
-      and <strong>Save</strong> it.
-      </p>
-
-      <h4>2. Get <code>clientId</code> and <code>clientSecret</code></h4>
-      <h4>3. After clientId and clientSecret set, click "Connect to Slack" button to start OAuth process.</h4>
-      <h4>4. Configure Slack on this notification setting screen</h4>
-      {% endif %}
-
-
-
     </div>
   </div>
 
+  <script>
+    window.addEventListener('load', function(e) {
+      // hash on page
+      if (location.hash) {
+        if (location.hash == '#slack-incoming-webhooks') {
+          activateTab('slack-incoming-webhooks');
+        }
+      }
+    });
+
+    function activateTab(tab){
+      $('.nav-tabs a[href="#' + tab + '"]').tab('show');
+    };
+  </script>
 </div>
 {% endblock content_main %}
 

+ 10 - 10
lib/views/crowi-plus/widget/header.html

@@ -1,7 +1,7 @@
 <div class="header-wrap">
   <header id="page-header">
     <div class="flex-title-line">
-      <div class="title-logo-container">
+      <div class="title-logo-container visible-lg visible-md">
         <a href="/">
           <img alt="Crowi" src="/logo/32x32_g.png" />
         </a>
@@ -12,15 +12,6 @@
       </div>
       {% if page %}
       <div class="flex-item-action">
-        {% if user %}
-        <span id="bookmark-button">
-          <p class="bookmark-link">
-            <i class="fa fa-star-o"></i>
-          </p>
-        </span>
-        {% endif %}
-      </div>
-      <div class="flex-item-action visible-xs visible-sm">
         {% if user %}
         <button
             data-csrftoken="{{ csrf() }}"
@@ -29,6 +20,15 @@
         ><i class="fa fa-thumbs-o-up"></i></button>
         {% endif %}
       </div>
+      <div class="flex-item-action">
+        {% if user %}
+        <span id="bookmark-button">
+          <p class="bookmark-link">
+            <i class="fa fa-star-o"></i>
+          </p>
+        </span>
+        {% endif %}
+      </div>
 
       <ul class="authors visible-md visible-lg">
         <li>

+ 21 - 25
package.json

@@ -1,6 +1,6 @@
 {
   "name": "crowi-plus",
-  "version": "1.2.16-RC",
+  "version": "2.0.0-RC",
   "description": "Enhanced Crowi",
   "tags": [
     "wiki",
@@ -33,7 +33,7 @@
     "prebuild:prod": "npm run plugin:def",
     "prestart": "npm run build:prod",
     "postserver:prod:container": "echo ---------------------------------------- && echo [WARNING] && echo   'server:prod:container' is deprecated. && echo   Please use 'sever:prod' && echo ----------------------------------------",
-    "server:dev:watch": "env-cmd config/env.dev.js node-dev --respawn app.js --watch",
+    "server:dev:watch": "env-cmd config/env.dev.js node-dev --respawn app.js --livereload",
     "server:dev": "env-cmd config/env.dev.js node app.js",
     "server:prod:container": "npm run server:prod",
     "server:prod": "node app.js --production | pino-clf common",
@@ -46,7 +46,7 @@
   "dependencies": {
     "assets-webpack-plugin": "~3.5.1",
     "async": "^2.3.0",
-    "aws-sdk": "~2.2.26",
+    "aws-sdk": "^2.80.0",
     "axios": "^0.16.1",
     "babel-core": "^6.24.0",
     "babel-loader": "^7.0.0",
@@ -55,23 +55,18 @@
     "basic-auth-connect": "~1.0.0",
     "body-parser": "^1.17.1",
     "bootstrap-sass": "~3.3.6",
-    "botkit": "~0.1.1",
+    "botkit": "~0.5.5",
     "check-node-version": "^2.0.1",
-    "cli": "~1.0.1",
-    "colors": "^1.1.2",
-    "commander": "~2.9.0",
     "connect-flash": "~0.1.1",
-    "connect-redis": "~2.1.0",
-    "consolidate": "~0.14.0",
+    "connect-redis": "^3.3.0",
     "cookie-parser": "^1.4.3",
-    "copy-webpack-plugin": "^4.0.0",
     "crowi-pluginkit": "^1.1.0",
     "csrf": "~3.0.3",
     "css-loader": "^0.28.0",
     "debug": "~2.6.0",
     "diff": "^3.2.0",
     "diff2html": "^2.3.0",
-    "elasticsearch": "^12.1.3",
+    "elasticsearch": "^13.2.0",
     "emojify.js": "^1.1.0",
     "env-cmd": "^5.0.0",
     "express": "~4.15.2",
@@ -80,13 +75,13 @@
     "express-session": "~1.15.0",
     "express-webpack-assets": "0.0.2",
     "file-loader": "^0.11.1",
-    "googleapis": "=12.3.0",
+    "googleapis": "^20.0.1",
     "graceful-fs": "^4.1.11",
     "highlight.js": "^9.10.0",
-    "i18next": "~4.1.0",
-    "i18next-express-middleware": "~1.0.2",
-    "i18next-node-fs-backend": "~0.1.3",
-    "i18next-sprintf-postprocessor": "~0.2.2",
+    "i18next": "^8.4.2",
+    "i18next-express-middleware": "^1.0.5",
+    "i18next-node-fs-backend": "^1.0.0",
+    "i18next-sprintf-postprocessor": "^0.2.2",
     "inline-attachment": "~2.0.3",
     "jquery.cookie": "~1.4.1",
     "marked": "~0.3.6",
@@ -94,11 +89,11 @@
     "method-override": "~2.3.1",
     "mkdirp": "~0.5.1",
     "moment": "^2.18.0",
-    "mongoose": "^4.9.4",
+    "mongoose": "^4.11.1",
     "mongoose-paginate": "5.0.x",
     "multer": "~1.3.0",
     "node-sass": "^4.5.0",
-    "nodemailer": "~2.7.0",
+    "nodemailer": "^4.0.1",
     "nodemailer-ses-transport": "~1.5.0",
     "normalize-path": "^2.1.1",
     "optimize-js-plugin": "0.0.4",
@@ -113,24 +108,25 @@
     "reveal.js": "^3.5.0",
     "rimraf": "^2.6.1",
     "sass-loader": "^6.0.3",
-    "socket.io": "~1.7.0",
-    "socket.io-client": "~1.7.0",
+    "socket.io": "^2.0.3",
+    "socket.io-client": "^2.0.3",
     "style-loader": "^0.18.2",
-    "swig": "~1.4.0",
+    "swig-templates": "^2.0.2",
     "toastr": "^2.1.2",
     "uglifycss": "^0.0.27",
-    "webpack": "~2.6.1",
+    "webpack": "^3.1.0",
     "webpack-dll-bundles-plugin": "^1.0.0-beta.5",
     "webpack-merge": "~4.1.0"
   },
   "devDependencies": {
-    "chai": "^3.5.0",
-    "concurrently": "^3.4.0",
+    "chai": "^4.0.2",
+    "cli": "~1.0.1",
+    "colors": "^1.1.2",
+    "commander": "^2.11.0",
     "easy-livereload": "^1.2.0",
     "mocha": "^3.2.0",
     "morgan": "^1.8.2",
     "node-dev": "^3.1.3",
-    "proxyquire": "^1.7.11",
     "sinon": "^2.1.0",
     "sinon-chai": "^2.9.0"
   },

+ 0 - 1
test/crowi/crowi.test.js

@@ -2,7 +2,6 @@ var chai = require('chai')
   , expect = chai.expect
   , sinon = require('sinon')
   , sinonChai = require('sinon-chai')
-  , proxyquire = require('proxyquire')
 
   , path = require('path')
   ;

+ 1 - 1
test/models/user.test.js

@@ -33,7 +33,7 @@ describe('User', function () {
 
         User.findUsersByPartOfEmail('ao', {})
         .then(function(userData) {
-          expect(userData).to.be.array;
+          expect(userData).to.be.a('array');
           expect(userData[0]).to.instanceof(User);
           expect(userData[0].email).to.equal('aoi@example.com');
           done();

Разница между файлами не показана из-за своего большого размера
+ 445 - 236
yarn.lock


Некоторые файлы не были показаны из-за большого количества измененных файлов