Browse Source

Merge pull request #5412 from weseek/feat/gw7598-GROWI-and-Slack-user-linking-accounts

feat: gw7598 growi and slack user linking accounts
Yuki Takei 4 years ago
parent
commit
5cd0722c48

+ 11 - 0
packages/app/src/client/services/PersonalContainer.js

@@ -30,6 +30,7 @@ export default class PersonalContainer extends Container {
       uploadedPictureSrc: this.getUploadedPictureSrc(this.appContainer.currentUser),
       uploadedPictureSrc: this.getUploadedPictureSrc(this.appContainer.currentUser),
       externalAccounts: [],
       externalAccounts: [],
       apiToken: '',
       apiToken: '',
+      slackMemberId: '',
     };
     };
 
 
   }
   }
@@ -55,6 +56,7 @@ export default class PersonalContainer extends Container {
         lang: currentUser.lang,
         lang: currentUser.lang,
         isGravatarEnabled: currentUser.isGravatarEnabled,
         isGravatarEnabled: currentUser.isGravatarEnabled,
         apiToken: currentUser.apiToken,
         apiToken: currentUser.apiToken,
+        slackMemberId: currentUser.slackMemberId,
       });
       });
     }
     }
     catch (err) {
     catch (err) {
@@ -114,6 +116,13 @@ export default class PersonalContainer extends Container {
     this.setState({ email: inputValue });
     this.setState({ email: inputValue });
   }
   }
 
 
+  /**
+   * Change Slack Member ID
+   */
+  changeSlackMemberId(inputValue) {
+    this.setState({ slackMemberId: inputValue });
+  }
+
   /**
   /**
    * Change isEmailPublished
    * Change isEmailPublished
    */
    */
@@ -147,6 +156,7 @@ export default class PersonalContainer extends Container {
         email: this.state.email,
         email: this.state.email,
         isEmailPublished: this.state.isEmailPublished,
         isEmailPublished: this.state.isEmailPublished,
         lang: this.state.lang,
         lang: this.state.lang,
+        slackMemberId: this.state.slackMemberId,
       });
       });
       const { updatedUser } = response.data;
       const { updatedUser } = response.data;
 
 
@@ -155,6 +165,7 @@ export default class PersonalContainer extends Container {
         email: updatedUser.email,
         email: updatedUser.email,
         isEmailPublished: updatedUser.isEmailPublished,
         isEmailPublished: updatedUser.isEmailPublished,
         lang: updatedUser.lang,
         lang: updatedUser.lang,
+        slackMemberId: updatedUser.slackMemberId,
       });
       });
     }
     }
     catch (err) {
     catch (err) {

+ 13 - 0
packages/app/src/components/Me/BasicInfoSettings.jsx

@@ -128,6 +128,19 @@ class BasicInfoSettings extends React.Component {
             }
             }
           </div>
           </div>
         </div>
         </div>
+        <div className="form-group row">
+          <label htmlFor="userForm[slackMemberId]" className="text-left text-md-right col-md-3 col-form-label">{t('Slack Member ID')}</label>
+          <div className="col-md-6">
+            <input
+              className="form-control"
+              type="text"
+              key={personalContainer.state.slackMemberId}
+              name="userForm[slackMemberId]"
+              defaultValue={personalContainer.state.slackMemberId}
+              onChange={(e) => { personalContainer.changeSlackMemberId(e.target.value) }}
+            />
+          </div>
+        </div>
 
 
         <div className="row my-3">
         <div className="row my-3">
           <div className="offset-4 col-5">
           <div className="offset-4 col-5">

+ 17 - 0
packages/app/src/server/models/user.js

@@ -47,6 +47,7 @@ module.exports = function(crowi) {
     name: { type: String },
     name: { type: String },
     username: { type: String, required: true, unique: true },
     username: { type: String, required: true, unique: true },
     email: { type: String, unique: true, sparse: true },
     email: { type: String, unique: true, sparse: true },
+    slackMemberId: { type: String, unique: true },
     // === Crowi settings
     // === Crowi settings
     // username: { type: String, index: true },
     // username: { type: String, index: true },
     // email: { type: String, required: true, index: true },
     // email: { type: String, required: true, index: true },
@@ -689,6 +690,22 @@ module.exports = function(crowi) {
     user.save();
     user.save();
   };
   };
 
 
+  userSchema.statics.findUserBySlackMemberId = async function(slackMemberId) {
+    const user = this.findOne({ slackMemberId });
+    if (user == null) {
+      throw new Error('User not found');
+    }
+    return user;
+  };
+
+  userSchema.statics.findUsersBySlackMemberIds = async function(slackMemberIds) {
+    const users = this.find({ slackMemberId: { $in: slackMemberIds } });
+    if (users.length === 0) {
+      throw new Error('No user found');
+    }
+    return users;
+  };
+
   class UserUpperLimitException {
   class UserUpperLimitException {
 
 
     constructor() {
     constructor() {

+ 2 - 0
packages/app/src/server/routes/apiv3/personal-setting.js

@@ -84,6 +84,7 @@ module.exports = (crowi) => {
         }),
         }),
       body('lang').isString().isIn(listLocaleIds()),
       body('lang').isString().isIn(listLocaleIds()),
       body('isEmailPublished').isBoolean(),
       body('isEmailPublished').isBoolean(),
+      body('slackMemberId').optional().isString(),
     ],
     ],
     imageType: [
     imageType: [
       body('isGravatarEnabled').isBoolean(),
       body('isGravatarEnabled').isBoolean(),
@@ -226,6 +227,7 @@ module.exports = (crowi) => {
       user.email = req.body.email;
       user.email = req.body.email;
       user.lang = req.body.lang;
       user.lang = req.body.lang;
       user.isEmailPublished = req.body.isEmailPublished;
       user.isEmailPublished = req.body.isEmailPublished;
+      user.slackMemberId = req.body.slackMemberId;
 
 
       const updatedUser = await user.save();
       const updatedUser = await user.save();
       req.i18n.changeLanguage(req.body.lang);
       req.i18n.changeLanguage(req.body.lang);

+ 2 - 2
packages/app/src/server/service/slack-command-handler/create-page-service.js

@@ -11,7 +11,7 @@ class CreatePageService {
     this.crowi = crowi;
     this.crowi = crowi;
   }
   }
 
 
-  async createPageInGrowi(interactionPayloadAccessor, path, contentsBody, respondUtil) {
+  async createPageInGrowi(interactionPayloadAccessor, path, contentsBody, respondUtil, userId) {
     const Page = this.crowi.model('Page');
     const Page = this.crowi.model('Page');
     const reshapedContentsBody = reshapeContentsBody(contentsBody);
     const reshapedContentsBody = reshapeContentsBody(contentsBody);
 
 
@@ -20,7 +20,7 @@ class CreatePageService {
     const normalizedPath = pathUtils.normalizePath(sanitizedPath);
     const normalizedPath = pathUtils.normalizePath(sanitizedPath);
 
 
     // generate a dummy id because Operation to create a page needs ObjectId
     // generate a dummy id because Operation to create a page needs ObjectId
-    const dummyObjectIdOfUser = new mongoose.Types.ObjectId();
+    const dummyObjectIdOfUser = userId != null ? userId : new mongoose.Types.ObjectId();
     const page = await Page.create(normalizedPath, reshapedContentsBody, dummyObjectIdOfUser, {});
     const page = await Page.create(normalizedPath, reshapedContentsBody, dummyObjectIdOfUser, {});
 
 
     // Send a message when page creation is complete
     // Send a message when page creation is complete

+ 37 - 4
packages/app/src/server/service/slack-command-handler/keep.js

@@ -12,6 +12,7 @@ module.exports = (crowi) => {
   const createPageService = new CreatePageService(crowi);
   const createPageService = new CreatePageService(crowi);
   const BaseSlackCommandHandler = require('./slack-command-handler');
   const BaseSlackCommandHandler = require('./slack-command-handler');
   const handler = new BaseSlackCommandHandler();
   const handler = new BaseSlackCommandHandler();
+  const { User } = crowi.models;
 
 
   handler.handleCommand = async function(growiCommand, client, body, respondUtil) {
   handler.handleCommand = async function(growiCommand, client, body, respondUtil) {
     await respondUtil.respond({
     await respondUtil.respond({
@@ -32,8 +33,9 @@ module.exports = (crowi) => {
   handler.createPage = async function(client, payload, interactionPayloadAccessor, respondUtil) {
   handler.createPage = async function(client, payload, interactionPayloadAccessor, respondUtil) {
     let result = [];
     let result = [];
     const channelId = payload.channel.id; // this must exist since the type is always block_actions
     const channelId = payload.channel.id; // this must exist since the type is always block_actions
-    const userChannelId = payload.user.id;
+    const user = await User.findUserBySlackMemberId(payload.user.id);
 
 
+    const userId = user != null ? user._id : null;
     // validate form
     // validate form
     const { path, oldest, newest } = await this.keepValidateForm(client, payload, interactionPayloadAccessor);
     const { path, oldest, newest } = await this.keepValidateForm(client, payload, interactionPayloadAccessor);
     // get messages
     // get messages
@@ -43,7 +45,7 @@ module.exports = (crowi) => {
 
 
     const contentsBody = cleanedContents.join('');
     const contentsBody = cleanedContents.join('');
     // create and send url message
     // create and send url message
-    await this.keepCreatePageAndSendPreview(client, interactionPayloadAccessor, path, userChannelId, contentsBody, respondUtil);
+    await this.keepCreatePageAndSendPreview(client, interactionPayloadAccessor, path, userId, contentsBody, respondUtil);
   };
   };
 
 
   handler.keepValidateForm = async function(client, payload, interactionPayloadAccessor) {
   handler.keepValidateForm = async function(client, payload, interactionPayloadAccessor) {
@@ -137,10 +139,41 @@ module.exports = (crowi) => {
     return result;
     return result;
   };
   };
 
 
+  /**
+   * Get all growi users from messages
+   * @param {*} messages (array of messages)
+   * @returns users object with matching Slack Member ID
+   */
+  handler.getGrowiUsersFromMessages = async function(messages) {
+    const users = messages.map((message) => {
+      return message.user;
+    });
+    const growiUsers = await User.findUsersBySlackMemberIds(users);
+    return growiUsers;
+  };
+  /**
+   * Convert slack member ID to growi user if slack member ID is found in messages
+   * @param {*} messages
+   */
+  handler.injectGrowiUsernameToMessages = async function(messages) {
+    const growiUsers = await this.getGrowiUsersFromMessages(messages);
+
+    messages.map(async(message) => {
+      const growiUser = growiUsers.find(user => user.slackMemberId === message.user);
+      if (growiUser != null) {
+        message.user = `${growiUser.name} (@${growiUser.username})`;
+      }
+      else {
+        message.user = `This slack member ID is not registered (${message.user})`;
+      }
+    });
+  };
+
   handler.keepCleanMessages = async function(messages) {
   handler.keepCleanMessages = async function(messages) {
     const cleanedContents = [];
     const cleanedContents = [];
     let lastMessage = {};
     let lastMessage = {};
     const grwTzoffset = crowi.appService.getTzoffset() * 60;
     const grwTzoffset = crowi.appService.getTzoffset() * 60;
+    await this.injectGrowiUsernameToMessages(messages);
     messages
     messages
       .sort((a, b) => {
       .sort((a, b) => {
         return a.ts - b.ts;
         return a.ts - b.ts;
@@ -164,8 +197,8 @@ module.exports = (crowi) => {
     return cleanedContents;
     return cleanedContents;
   };
   };
 
 
-  handler.keepCreatePageAndSendPreview = async function(client, interactionPayloadAccessor, path, userChannelId, contentsBody, respondUtil) {
-    await createPageService.createPageInGrowi(interactionPayloadAccessor, path, contentsBody, respondUtil);
+  handler.keepCreatePageAndSendPreview = async function(client, interactionPayloadAccessor, path, userId, contentsBody, respondUtil) {
+    await createPageService.createPageInGrowi(interactionPayloadAccessor, path, contentsBody, respondUtil, userId);
 
 
     // TODO: contentsBody text characters must be less than 3001
     // TODO: contentsBody text characters must be less than 3001
     // send preview to dm
     // send preview to dm