Ver código fonte

Merge pull request #4242 from weseek/imprv/replace-relationmock-to-relation

Imprv: replace relationmock to relation
Sizma yosimaz 4 anos atrás
pai
commit
2593139577

+ 1 - 1
packages/app/src/components/Admin/SlackIntegration/ManageCommandsProcess.jsx

@@ -131,7 +131,7 @@ const ManageCommandsProcess = ({
 
 
   const updateCommandsHandler = async(e) => {
   const updateCommandsHandler = async(e) => {
     try {
     try {
-      await apiv3Put(`/slack-integration-settings/${slackAppIntegrationId}/supported-commands`, {
+      await apiv3Put(`/slack-integration-settings/slack-app-integrations/${slackAppIntegrationId}/supported-commands`, {
         permissionsForBroadcastUseCommands: permissionsForBroadcastUseCommandsState,
         permissionsForBroadcastUseCommands: permissionsForBroadcastUseCommandsState,
         permissionsForSingleUseCommands: permissionsForSingleUseCommandsState,
         permissionsForSingleUseCommands: permissionsForSingleUseCommandsState,
       });
       });

+ 0 - 3
packages/app/src/server/models/index.js

@@ -17,7 +17,4 @@ module.exports = {
   GlobalNotificationSlackSetting: require('./GlobalNotificationSetting/GlobalNotificationSlackSetting'),
   GlobalNotificationSlackSetting: require('./GlobalNotificationSetting/GlobalNotificationSlackSetting'),
   ShareLink: require('./share-link'),
   ShareLink: require('./share-link'),
   SlackAppIntegration: require('./slack-app-integration'),
   SlackAppIntegration: require('./slack-app-integration'),
-  // MOCK DATA DELETE THIS GW-6972 ---------------
-  SlackAppIntegrationMock: require('./slack-app-integration-mock'),
-  // MOCK DATA DELETE THIS GW-6972 ---------------
 };
 };

+ 0 - 51
packages/app/src/server/models/slack-app-integration-mock.js

@@ -1,51 +0,0 @@
-const crypto = require('crypto');
-const mongoose = require('mongoose');
-
-const permittedChannelsForEachCommandSchema = new mongoose.Schema({
-  channelsObject: Map,
-});
-
-
-const schema = new mongoose.Schema({
-  tokenGtoP: { type: String, required: true, unique: true },
-  tokenPtoG: { type: String, required: true, unique: true },
-  permissionsForBroadcastUseCommands: Map,
-  permissionsForSingleUseCommands: Map,
-});
-
-class SlackAppIntegrationMock {
-
-  static generateAccessTokens() {
-    const now = new Date().getTime();
-    const hasher1 = crypto.createHash('sha512');
-    const hasher2 = crypto.createHash('sha512');
-    const tokenGtoP = hasher1.update(`gtop${now.toString()}${process.env.SALT_FOR_GTOP_TOKEN}`).digest('base64');
-    const tokenPtoG = hasher2.update(`ptog${now.toString()}${process.env.SALT_FOR_PTOG_TOKEN}`).digest('base64');
-    return [tokenGtoP, tokenPtoG];
-  }
-
-  static async generateUniqueAccessTokens() {
-    let duplicateTokens;
-    let tokenGtoP;
-    let tokenPtoG;
-    let generateTokens;
-
-    do {
-      generateTokens = this.generateAccessTokens();
-      tokenGtoP = generateTokens[0];
-      tokenPtoG = generateTokens[1];
-      // eslint-disable-next-line no-await-in-loop
-      duplicateTokens = await this.findOne({ $or: [{ tokenGtoP }, { tokenPtoG }] });
-    } while (duplicateTokens != null);
-
-
-    return { tokenGtoP, tokenPtoG };
-  }
-
-}
-
-module.exports = function(crowi) {
-  SlackAppIntegrationMock.crowi = crowi;
-  schema.loadClass(SlackAppIntegrationMock);
-  return mongoose.model('SlackAppIntegrationMock', schema);
-};

+ 3 - 4
packages/app/src/server/models/slack-app-integration.js

@@ -1,12 +1,13 @@
 const crypto = require('crypto');
 const crypto = require('crypto');
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
 
 
+
 const schema = new mongoose.Schema({
 const schema = new mongoose.Schema({
   tokenGtoP: { type: String, required: true, unique: true },
   tokenGtoP: { type: String, required: true, unique: true },
   tokenPtoG: { type: String, required: true, unique: true },
   tokenPtoG: { type: String, required: true, unique: true },
   isPrimary: { type: Boolean, unique: true, sparse: true },
   isPrimary: { type: Boolean, unique: true, sparse: true },
-  supportedCommandsForBroadcastUse: { type: [String], default: [] },
-  supportedCommandsForSingleUse: { type: [String], default: [] },
+  permissionsForBroadcastUseCommands: Map,
+  permissionsForSingleUseCommands: Map,
 });
 });
 
 
 class SlackAppIntegration {
 class SlackAppIntegration {
@@ -47,9 +48,7 @@ class SlackAppIntegration {
 }
 }
 
 
 module.exports = function(crowi) {
 module.exports = function(crowi) {
-
   SlackAppIntegration.crowi = crowi;
   SlackAppIntegration.crowi = crowi;
-
   schema.loadClass(SlackAppIntegration);
   schema.loadClass(SlackAppIntegration);
   return mongoose.model('SlackAppIntegration', schema);
   return mongoose.model('SlackAppIntegration', schema);
 };
 };

+ 24 - 54
packages/app/src/server/routes/apiv3/slack-integration-settings.js

@@ -2,7 +2,6 @@ import { SlackbotType } from '@growi/slack';
 
 
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
 const express = require('express');
 const express = require('express');
 const { body, query, param } = require('express-validator');
 const { body, query, param } = require('express-validator');
@@ -55,9 +54,7 @@ module.exports = (crowi) => {
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const csrf = require('../../middlewares/csrf')(crowi);
   const csrf = require('../../middlewares/csrf')(crowi);
   const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
   const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
-
-  const SlackAppIntegration = mongoose.model('SlackAppIntegration');
-  const SlackAppIntegrationMock = mongoose.model('SlackAppIntegrationMock');
+  const SlackAppIntegration = crowi.model('SlackAppIntegration');
 
 
   const validator = {
   const validator = {
     botType: [
     botType: [
@@ -378,35 +375,33 @@ module.exports = (crowi) => {
    *          200:
    *          200:
    *            description: Succeeded to create slack app integration
    *            description: Succeeded to create slack app integration
    */
    */
-  router.put('/slack-app-integrations', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
-    const SlackAppIntegrationMock = mongoose.model('SlackAppIntegrationMock');
-    const SlackAppIntegrationMockRecordsNum = await SlackAppIntegrationMock.countDocuments();
-    if (SlackAppIntegrationMockRecordsNum >= 10) {
+  router.post('/slack-app-integrations', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
+    const SlackAppIntegrationRecordsNum = await SlackAppIntegration.countDocuments();
+    if (SlackAppIntegrationRecordsNum >= 10) {
       const msg = 'Not be able to create more than 10 slack workspace integration settings';
       const msg = 'Not be able to create more than 10 slack workspace integration settings';
       logger.error('Error', msg);
       logger.error('Error', msg);
       return res.apiv3Err(new ErrorV3(msg, 'create-slackAppIntegeration-failed'), 500);
       return res.apiv3Err(new ErrorV3(msg, 'create-slackAppIntegeration-failed'), 500);
     }
     }
 
 
-    const { tokenGtoP, tokenPtoG } = await SlackAppIntegrationMock.generateUniqueAccessTokens();
+    const { tokenGtoP, tokenPtoG } = await SlackAppIntegration.generateUniqueAccessTokens();
     try {
     try {
-      // MOCK DATA DELETE THIS GW-6972 ---------------
-      /* This code represents the creation of the new SlackAppIntegration model instance. */
       const initialSupportedCommandsForBroadcastUse = new Map();
       const initialSupportedCommandsForBroadcastUse = new Map();
       const initialSupportedCommandsForSingleUse = new Map();
       const initialSupportedCommandsForSingleUse = new Map();
+
       defaultSupportedCommandsNameForBroadcastUse.forEach((commandName) => {
       defaultSupportedCommandsNameForBroadcastUse.forEach((commandName) => {
         initialSupportedCommandsForBroadcastUse.set(commandName, true);
         initialSupportedCommandsForBroadcastUse.set(commandName, true);
       });
       });
       defaultSupportedCommandsNameForSingleUse.forEach((commandName) => {
       defaultSupportedCommandsNameForSingleUse.forEach((commandName) => {
         initialSupportedCommandsForSingleUse.set(commandName, true);
         initialSupportedCommandsForSingleUse.set(commandName, true);
       });
       });
-      const slackAppTokensMOCK = await SlackAppIntegrationMock.create({
+
+      const slackAppTokens = await SlackAppIntegration.create({
         tokenGtoP,
         tokenGtoP,
         tokenPtoG,
         tokenPtoG,
         permissionsForBroadcastUseCommands: initialSupportedCommandsForBroadcastUse,
         permissionsForBroadcastUseCommands: initialSupportedCommandsForBroadcastUse,
         permissionsForSingleUseCommands: initialSupportedCommandsForSingleUse,
         permissionsForSingleUseCommands: initialSupportedCommandsForSingleUse,
       });
       });
-      // MOCK DATA DELETE THIS GW-6972 ---------------
-      return res.apiv3(slackAppTokensMOCK, 200);
+      return res.apiv3(slackAppTokens, 200);
     }
     }
     catch (error) {
     catch (error) {
       const msg = 'Error occurred during creating slack integration settings procedure';
       const msg = 'Error occurred during creating slack integration settings procedure';
@@ -429,7 +424,6 @@ module.exports = (crowi) => {
    *            description: Succeeded to delete access tokens for slack
    *            description: Succeeded to delete access tokens for slack
    */
    */
   router.delete('/slack-app-integrations/:id', validator.deleteIntegration, apiV3FormValidator, async(req, res) => {
   router.delete('/slack-app-integrations/:id', validator.deleteIntegration, apiV3FormValidator, async(req, res) => {
-    const SlackAppIntegration = mongoose.model('SlackAppIntegration');
     const { id } = req.params;
     const { id } = req.params;
 
 
     try {
     try {
@@ -559,53 +553,33 @@ module.exports = (crowi) => {
    */
    */
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
   router.put('/slack-app-integrations/:id/supported-commands', loginRequiredStrictly, adminRequired, csrf, validator.updateSupportedCommands, apiV3FormValidator, async(req, res) => {
   router.put('/slack-app-integrations/:id/supported-commands', loginRequiredStrictly, adminRequired, csrf, validator.updateSupportedCommands, apiV3FormValidator, async(req, res) => {
-    const { supportedCommandsForBroadcastUse, supportedCommandsForSingleUse } = req.body;
+    const { permissionsForBroadcastUseCommands, permissionsForSingleUseCommands } = req.body;
     const { id } = req.params;
     const { id } = req.params;
 
 
+    const updatePermissionsForBroadcastUseCommands = new Map(Object.entries(permissionsForBroadcastUseCommands));
+    const updatePermissionsForSingleUseCommands = new Map(Object.entries(permissionsForSingleUseCommands));
+
     try {
     try {
-      // NOT MOCK DATA BUT REFER THIS GW-7006
       const slackAppIntegration = await SlackAppIntegration.findByIdAndUpdate(
       const slackAppIntegration = await SlackAppIntegration.findByIdAndUpdate(
         id,
         id,
-        { supportedCommandsForBroadcastUse, supportedCommandsForSingleUse },
-        { new: true },
-      );
-
-      // MOCK DATA MODIFY THIS GW-6972 ---------------
-      /**
-       * this code represents the update operation using request from client (slackapp integration settings page)
-       * , then send request to proxy to update cache
-       * permittedChannelsForEachCommandFromClient represents the data sent from client
-       */
-      const SlackAppIntegrationMock = mongoose.model('SlackAppIntegrationMock');
-      // MOCK DATA FROM CLIENT assume that these data were sent from client
-      const permissionsForBroadcastUseCommandsFromClient = {
-        search: false,
-      };
-      const permissionsForSingleUseCommandsFromClient = {
-        create: ['random'],
-      };
-      const slackAppIntegrationMock = await SlackAppIntegrationMock.findOneAndUpdate(
-        // MOCK DATA USE id IN req.params LIKE ABOVE
-        { tokenPtoG: slackAppIntegration.tokenPtoG },
         {
         {
-          permissionsForBroadcastUseCommands: permissionsForBroadcastUseCommandsFromClient,
-          permissionsForSingleUseCommands: permissionsForSingleUseCommandsFromClient,
+          permissionsForBroadcastUseCommands: updatePermissionsForBroadcastUseCommands,
+          permissionsForSingleUseCommands: updatePermissionsForSingleUseCommands,
         },
         },
         { new: true },
         { new: true },
       );
       );
 
 
       await requestToProxyServer(
       await requestToProxyServer(
-        slackAppIntegrationMock.tokenGtoP,
+        slackAppIntegration.tokenGtoP,
         'put',
         'put',
         '/g2s/supported-commands',
         '/g2s/supported-commands',
         {
         {
-          permissionsForBroadcastUseCommands: slackAppIntegrationMock.permissionsForBroadcastUseCommands,
-          permissionsForSingleUseCommands: slackAppIntegrationMock.permissionsForSingleUseCommands,
+          permissionsForBroadcastUseCommands: slackAppIntegration.permissionsForBroadcastUseCommands,
+          permissionsForSingleUseCommands: slackAppIntegration.permissionsForSingleUseCommands,
         },
         },
       );
       );
-      // MOCK DATA MODIFY THIS GW-6972 ---------------
 
 
-      return res.apiv3({ slackAppIntegrationMock });
+      return res.apiv3({});
     }
     }
     catch (error) {
     catch (error) {
       const msg = `Error occured in updating settings. Cause: ${error.message}`;
       const msg = `Error occured in updating settings. Cause: ${error.message}`;
@@ -643,23 +617,19 @@ module.exports = (crowi) => {
     const { id } = req.params;
     const { id } = req.params;
     let slackBotToken;
     let slackBotToken;
     try {
     try {
-      // MOCK DATA DELETE THIS GW-6972 ---------------
-      const SlackAppIntegrationMock = mongoose.model('SlackAppIntegrationMock');
-      const slackAppIntegrationMock = await SlackAppIntegrationMock.findOne({ _id: id });
-      // MOCK DATA DELETE THIS GW-6972 ---------------
-      if (slackAppIntegrationMock == null) {
+      const slackAppIntegration = await SlackAppIntegration.findOne({ _id: id });
+      if (slackAppIntegration == null) {
         const msg = 'Could not find SlackAppIntegration by id';
         const msg = 'Could not find SlackAppIntegration by id';
         return res.apiv3Err(new ErrorV3(msg, 'find-slackAppIntegration-failed'), 400);
         return res.apiv3Err(new ErrorV3(msg, 'find-slackAppIntegration-failed'), 400);
       }
       }
 
 
-      // USE MOCK DATA HERE FOR cache creation at /relation-test GW-7021
       const result = await requestToProxyServer(
       const result = await requestToProxyServer(
-        slackAppIntegrationMock.tokenGtoP,
+        slackAppIntegration.tokenGtoP,
         'post',
         'post',
         '/g2s/relation-test',
         '/g2s/relation-test',
         {
         {
-          permissionsForBroadcastUseCommands: slackAppIntegrationMock.permissionsForBroadcastUseCommands,
-          permissionsForSingleUseCommands: slackAppIntegrationMock.permissionsForSingleUseCommands,
+          permissionsForBroadcastUseCommands: slackAppIntegration.permissionsForBroadcastUseCommands,
+          permissionsForSingleUseCommands: slackAppIntegration.permissionsForSingleUseCommands,
         },
         },
       );
       );
 
 

+ 10 - 24
packages/app/src/server/routes/apiv3/slack-integration.js

@@ -8,8 +8,7 @@ const { verifySlackRequest, generateWebClient, getSupportedGrowiActionsRegExps }
 
 
 const logger = loggerFactory('growi:routes:apiv3:slack-integration');
 const logger = loggerFactory('growi:routes:apiv3:slack-integration');
 const router = express.Router();
 const router = express.Router();
-const SlackAppIntegration = mongoose.model('SlackAppIntegrationMock');
-const SlackAppIntegrationMock = mongoose.model('SlackAppIntegrationMock');
+const SlackAppIntegration = mongoose.model('SlackAppIntegration');
 const { respondIfSlackbotError } = require('../../service/slack-command-handler/respond-if-slackbot-error');
 const { respondIfSlackbotError } = require('../../service/slack-command-handler/respond-if-slackbot-error');
 
 
 module.exports = (crowi) => {
 module.exports = (crowi) => {
@@ -27,16 +26,14 @@ module.exports = (crowi) => {
       return res.status(400).send({ message });
       return res.status(400).send({ message });
     }
     }
 
 
-    // const slackAppIntegrationCount = await SlackAppIntegrationMock.countDocuments({ tokenPtoG });
-    // MOCK DATA MODIFY THIS WITH SlackAppIntegration GW-7006 --------------
-    const slackAppIntegrationMockCount = await SlackAppIntegrationMock.countDocuments({ tokenPtoG });
+    const SlackAppIntegrationCount = await SlackAppIntegration.countDocuments({ tokenPtoG });
 
 
     logger.debug('verifyAccessTokenFromProxy', {
     logger.debug('verifyAccessTokenFromProxy', {
       tokenPtoG,
       tokenPtoG,
-      slackAppIntegrationMockCount,
+      SlackAppIntegrationCount,
     });
     });
 
 
-    if (slackAppIntegrationMockCount === 0) {
+    if (SlackAppIntegrationCount === 0) {
       return res.status(403).send({
       return res.status(403).send({
         message: 'The access token that identifies the request source is slackbot-proxy is invalid. Did you setup with `/growi register`.\n'
         message: 'The access token that identifies the request source is slackbot-proxy is invalid. Did you setup with `/growi register`.\n'
         + 'Or did you delete registration for GROWI ? if so, the link with GROWI has been disconnected. '
         + 'Or did you delete registration for GROWI ? if so, the link with GROWI has been disconnected. '
@@ -58,14 +55,9 @@ module.exports = (crowi) => {
 
 
     const tokenPtoG = req.headers['x-growi-ptog-tokens'];
     const tokenPtoG = req.headers['x-growi-ptog-tokens'];
 
 
-    // const relation = await SlackAppIntegration.findOne({ tokenPtoG });
-    // MOCK DATA DELETE THIS GW-6972 ---------------
-    const SlackAppIntegrationMock = mongoose.model('SlackAppIntegrationMock');
-    const slackAppIntegrationMock = await SlackAppIntegrationMock.findOne({ tokenPtoG });
-    const permissionsForBroadcastUseCommands = slackAppIntegrationMock.permissionsForBroadcastUseCommands;
-    const permissionsForSingleUseCommands = slackAppIntegrationMock.permissionsForSingleUseCommands;
-    // MOCK DATA DELETE THIS GW-6972 ---------------
-    // const { supportedCommandsForBroadcastUse, supportedCommandsForSingleUse } = relation;
+    const slackAppIntegration = await SlackAppIntegration.findOne({ tokenPtoG });
+    const permissionsForBroadcastUseCommands = slackAppIntegration.permissionsForBroadcastUseCommands;
+    const permissionsForSingleUseCommands = slackAppIntegration.permissionsForSingleUseCommands;
 
 
     // get command name from req.body
     // get command name from req.body
     let command = '';
     let command = '';
@@ -169,7 +161,6 @@ module.exports = (crowi) => {
 
 
     const tokenPtoG = req.headers['x-growi-ptog-tokens'];
     const tokenPtoG = req.headers['x-growi-ptog-tokens'];
     const client = await slackIntegrationService.generateClientByTokenPtoG(tokenPtoG);
     const client = await slackIntegrationService.generateClientByTokenPtoG(tokenPtoG);
-
     return handleCommands(req, res, client);
     return handleCommands(req, res, client);
   });
   });
 
 
@@ -224,15 +215,10 @@ module.exports = (crowi) => {
 
 
   router.get('/supported-commands', verifyAccessTokenFromProxy, async(req, res) => {
   router.get('/supported-commands', verifyAccessTokenFromProxy, async(req, res) => {
     const tokenPtoG = req.headers['x-growi-ptog-tokens'];
     const tokenPtoG = req.headers['x-growi-ptog-tokens'];
-    // MOCK DATA DELETE THIS GW-6972 ---------
-    const SlackAppIntegrationMock = mongoose.model('SlackAppIntegrationMock');
-    const slackAppIntegrationMock = await SlackAppIntegrationMock.findOne({ tokenPtoG });
-    const { permissionsForBroadcastUseCommands, permissionsForSingleUseCommands } = slackAppIntegrationMock;
-    return res.apiv3({ permissionsForBroadcastUseCommands, permissionsForSingleUseCommands });
-    // MOCK DATA DELETE THIS GW-6972 ---------
+    const slackAppIntegration = await SlackAppIntegration.findOne({ tokenPtoG });
+    const { permissionsForBroadcastUseCommands, permissionsForSingleUseCommands } = slackAppIntegration;
 
 
-    // const slackAppIntegration = await SlackAppIntegration.findOne({ tokenPtoG });
-    // return res.send(slackAppIntegration);
+    return res.apiv3({ permissionsForBroadcastUseCommands, permissionsForSingleUseCommands });
   });
   });
 
 
   return router;
   return router;

+ 2 - 3
packages/app/src/server/service/slack-integration.ts

@@ -128,10 +128,9 @@ export class SlackIntegrationService implements S2sMessageHandlable {
   async generateClientByTokenPtoG(tokenPtoG: string): Promise<WebClient> {
   async generateClientByTokenPtoG(tokenPtoG: string): Promise<WebClient> {
     this.isCheckTypeValid();
     this.isCheckTypeValid();
 
 
-    // const SlackAppIntegration = mongoose.model('SlackAppIntegration');
-    const SlackAppIntegrationMock = mongoose.model('SlackAppIntegrationMock');
+    const SlackAppIntegration = mongoose.model('SlackAppIntegration');
 
 
-    const slackAppIntegration = await SlackAppIntegrationMock.findOne({ tokenPtoG });
+    const slackAppIntegration = await SlackAppIntegration.findOne({ tokenPtoG });
 
 
     if (slackAppIntegration == null) {
     if (slackAppIntegration == null) {
       throw new Error('No SlackAppIntegration exists that corresponds to the tokenPtoG specified.');
       throw new Error('No SlackAppIntegration exists that corresponds to the tokenPtoG specified.');

+ 9 - 24
packages/slackbot-proxy/src/controllers/growi-to-slack.ts

@@ -16,7 +16,6 @@ import { WebclientRes, AddWebclientResponseToRes } from '~/middlewares/growi-to-
 import { GrowiReq } from '~/interfaces/growi-to-slack/growi-req';
 import { GrowiReq } from '~/interfaces/growi-to-slack/growi-req';
 import { InstallationRepository } from '~/repositories/installation';
 import { InstallationRepository } from '~/repositories/installation';
 import { RelationRepository } from '~/repositories/relation';
 import { RelationRepository } from '~/repositories/relation';
-import { RelationMockRepository } from '~/repositories/relation-mock';
 import { OrderRepository } from '~/repositories/order';
 import { OrderRepository } from '~/repositories/order';
 
 
 import { InstallerService } from '~/services/InstallerService';
 import { InstallerService } from '~/services/InstallerService';
@@ -37,11 +36,8 @@ export class GrowiToSlackCtrl {
   @Inject()
   @Inject()
   installationRepository: InstallationRepository;
   installationRepository: InstallationRepository;
 
 
-  // @Inject()
-  // relationRepository: RelationRepository;
-
   @Inject()
   @Inject()
-  relationMockRepository: RelationMockRepository;
+  relationRepository: RelationRepository;
 
 
   @Inject()
   @Inject()
   orderRepository: OrderRepository;
   orderRepository: OrderRepository;
@@ -76,7 +72,7 @@ export class GrowiToSlackCtrl {
     const { tokenGtoPs } = req;
     const { tokenGtoPs } = req;
 
 
     // retrieve Relation with Installation
     // retrieve Relation with Installation
-    const relations = await this.relationMockRepository.createQueryBuilder('relation')
+    const relations = await this.relationRepository.createQueryBuilder('relation')
       .where('relation.tokenGtoP IN (:...tokens)', { tokens: tokenGtoPs })
       .where('relation.tokenGtoP IN (:...tokens)', { tokens: tokenGtoPs })
       .leftJoinAndSelect('relation.installation', 'installation')
       .leftJoinAndSelect('relation.installation', 'installation')
       .getMany();
       .getMany();
@@ -103,9 +99,7 @@ export class GrowiToSlackCtrl {
     // asserted (tokenGtoPs.length > 0) by verifyGrowiToSlackRequest
     // asserted (tokenGtoPs.length > 0) by verifyGrowiToSlackRequest
     const { tokenGtoPs } = req;
     const { tokenGtoPs } = req;
 
 
-    // MOCK DATA SO FAR BUT THIS CAN BE USED AS AN ACTUAL CODE AS WELL GW 6972 -----------
     const { permissionsForBroadcastUseCommands, permissionsForSingleUseCommands } = req.body;
     const { permissionsForBroadcastUseCommands, permissionsForSingleUseCommands } = req.body;
-    // MOCK DATA SO FAR BUT THIS CAN BE USED AS AN ACTUAL CODE AS WELL GW 6972 -----------
 
 
     if (tokenGtoPs.length !== 1) {
     if (tokenGtoPs.length !== 1) {
       throw createError(400, 'installation is invalid');
       throw createError(400, 'installation is invalid');
@@ -113,11 +107,9 @@ export class GrowiToSlackCtrl {
 
 
     const tokenGtoP = tokenGtoPs[0];
     const tokenGtoP = tokenGtoPs[0];
 
 
-    // MOCK DATA MODIFY THIS GW 6972 -----------
-    const relation = await this.relationMockRepository.update(
+    const relation = await this.relationRepository.update(
       { tokenGtoP }, { permissionsForBroadcastUseCommands, permissionsForSingleUseCommands },
       { tokenGtoP }, { permissionsForBroadcastUseCommands, permissionsForSingleUseCommands },
     );
     );
-    // MOCK DATA MODIFY THIS GW 6972 -----------
 
 
     return res.send({ relation });
     return res.send({ relation });
   }
   }
@@ -134,7 +126,7 @@ export class GrowiToSlackCtrl {
     const tokenGtoP = tokenGtoPs[0];
     const tokenGtoP = tokenGtoPs[0];
 
 
     // retrieve relation with Installation
     // retrieve relation with Installation
-    const relation = await this.relationMockRepository.createQueryBuilder('relation')
+    const relation = await this.relationRepository.createQueryBuilder('relation')
       .where('tokenGtoP = :token', { token: tokenGtoP })
       .where('tokenGtoP = :token', { token: tokenGtoP })
       .leftJoinAndSelect('relation.installation', 'installation')
       .leftJoinAndSelect('relation.installation', 'installation')
       .getOne();
       .getOne();
@@ -157,6 +149,7 @@ export class GrowiToSlackCtrl {
       }
       }
 
 
       const status = await getConnectionStatus(token);
       const status = await getConnectionStatus(token);
+
       if (status.error != null) {
       if (status.error != null) {
         throw createError(400, `failed to get connection. err: ${status.error}`);
         throw createError(400, `failed to get connection. err: ${status.error}`);
       }
       }
@@ -201,12 +194,7 @@ export class GrowiToSlackCtrl {
     // temporary cache for 48 hours
     // temporary cache for 48 hours
     const expiredAtCommands = addHours(new Date(), 48);
     const expiredAtCommands = addHours(new Date(), 48);
 
 
-    // MOCK DATA DELETE THIS GW-6972 7004 ---------------
-    /**
-     * this code represents the creation of cache (Relation schema) using request from GROWI
-     */
-    // Transaction is not considered because it is used infrequently
-    const response = await this.relationMockRepository.createQueryBuilder('relation')
+    const response = await this.relationRepository.createQueryBuilder('relation')
       .insert()
       .insert()
       .values({
       .values({
         installation: order.installation,
         installation: order.installation,
@@ -223,13 +211,10 @@ export class GrowiToSlackCtrl {
         overwrite: ['tokenGtoP', 'tokenPtoG', 'permissionsForBroadcastUseCommands', 'permissionsForSingleUseCommands'],
         overwrite: ['tokenGtoP', 'tokenPtoG', 'permissionsForBroadcastUseCommands', 'permissionsForSingleUseCommands'],
       })
       })
       .execute();
       .execute();
-    // MOCK DATA DELETE THIS GW-6972 7004 ---------------
 
 
-    // Find the generated relation
-    // const generatedRelation = await this.relationMockRepository.findOne({ id: response.identifiers[0].id });
-    const generatedRelationMock = await this.relationMockRepository.findOne({ id: response.identifiers[0].id });
+    const generatedRelation = await this.relationRepository.findOne({ id: response.identifiers[0].id });
 
 
-    return res.send({ relation: generatedRelationMock, slackBotToken: token });
+    return res.send({ relation: generatedRelation, slackBotToken: token });
   }
   }
 
 
   injectGrowiUri(req: GrowiReq, growiUri: string): void {
   injectGrowiUri(req: GrowiReq, growiUri: string): void {
@@ -276,7 +261,7 @@ export class GrowiToSlackCtrl {
     const tokenGtoP = tokenGtoPs[0];
     const tokenGtoP = tokenGtoPs[0];
 
 
     // retrieve relation with Installation
     // retrieve relation with Installation
-    const relation = await this.relationMockRepository.createQueryBuilder('relation')
+    const relation = await this.relationRepository.createQueryBuilder('relation')
       .where('tokenGtoP = :token', { token: tokenGtoP })
       .where('tokenGtoP = :token', { token: tokenGtoP })
       .leftJoinAndSelect('relation.installation', 'installation')
       .leftJoinAndSelect('relation.installation', 'installation')
       .getOne();
       .getOne();

+ 10 - 15
packages/slackbot-proxy/src/controllers/slack.ts

@@ -13,12 +13,10 @@ import {
   InvalidGrowiCommandError, requiredScopes, postWelcomeMessage, REQUEST_TIMEOUT_FOR_PTOG,
   InvalidGrowiCommandError, requiredScopes, postWelcomeMessage, REQUEST_TIMEOUT_FOR_PTOG,
 } from '@growi/slack';
 } from '@growi/slack';
 
 
-// import { Relation } from '~/entities/relation';
-import { RelationMock } from '~/entities/relation-mock';
+import { Relation } from '~/entities/relation';
 import { SlackOauthReq } from '~/interfaces/slack-to-growi/slack-oauth-req';
 import { SlackOauthReq } from '~/interfaces/slack-to-growi/slack-oauth-req';
 import { InstallationRepository } from '~/repositories/installation';
 import { InstallationRepository } from '~/repositories/installation';
-// import { RelationRepository } from '~/repositories/relation';
-import { RelationMockRepository } from '~/repositories/relation-mock';
+import { RelationRepository } from '~/repositories/relation';
 import { OrderRepository } from '~/repositories/order';
 import { OrderRepository } from '~/repositories/order';
 import { AddSigningSecretToReq } from '~/middlewares/slack-to-growi/add-signing-secret-to-req';
 import { AddSigningSecretToReq } from '~/middlewares/slack-to-growi/add-signing-secret-to-req';
 import {
 import {
@@ -74,11 +72,8 @@ export class SlackCtrl {
   @Inject()
   @Inject()
   installationRepository: InstallationRepository;
   installationRepository: InstallationRepository;
 
 
-  // @Inject()
-  // relationRepository: RelationRepository;
-
   @Inject()
   @Inject()
-  relationMockRepository: RelationMockRepository;
+  relationRepository: RelationRepository;
 
 
   @Inject()
   @Inject()
   orderRepository: OrderRepository;
   orderRepository: OrderRepository;
@@ -102,13 +97,13 @@ export class SlackCtrl {
    * @param body
    * @param body
    * @returns
    * @returns
    */
    */
-  private async sendCommand(growiCommand: GrowiCommand, relations: RelationMock[], body: any) {
+  private async sendCommand(growiCommand: GrowiCommand, relations: Relation[], body: any) {
     if (relations.length === 0) {
     if (relations.length === 0) {
       throw new Error('relations must be set');
       throw new Error('relations must be set');
     }
     }
 
 
     const botToken = relations[0].installation?.data.bot?.token; // relations[0] should be exist
     const botToken = relations[0].installation?.data.bot?.token; // relations[0] should be exist
-    const promises = relations.map((relation: RelationMock) => {
+    const promises = relations.map((relation: Relation) => {
       // generate API URL
       // generate API URL
       const url = new URL('/_api/v3/slack-integration/proxied/commands', relation.growiUri);
       const url = new URL('/_api/v3/slack-integration/proxied/commands', relation.growiUri);
       return axios.post(url.toString(), {
       return axios.post(url.toString(), {
@@ -179,7 +174,7 @@ export class SlackCtrl {
     const installationId = authorizeResult.enterpriseId || authorizeResult.teamId;
     const installationId = authorizeResult.enterpriseId || authorizeResult.teamId;
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     const installation = await this.installationRepository.findByTeamIdOrEnterpriseId(installationId!);
     const installation = await this.installationRepository.findByTeamIdOrEnterpriseId(installationId!);
-    const relations = await this.relationMockRepository.createQueryBuilder('relation')
+    const relations = await this.relationRepository.createQueryBuilder('relation')
       .where('relation.installationId = :id', { id: installation?.id })
       .where('relation.installationId = :id', { id: installation?.id })
       .leftJoinAndSelect('relation.installation', 'installation')
       .leftJoinAndSelect('relation.installation', 'installation')
       .getMany();
       .getMany();
@@ -212,8 +207,8 @@ export class SlackCtrl {
 
 
     const baseDate = new Date();
     const baseDate = new Date();
 
 
-    const allowedRelationsForSingleUse:RelationMock[] = [];
-    const allowedRelationsForBroadcastUse:RelationMock[] = [];
+    const allowedRelationsForSingleUse:Relation[] = [];
+    const allowedRelationsForBroadcastUse:Relation[] = [];
     const disallowedGrowiUrls: Set<string> = new Set();
     const disallowedGrowiUrls: Set<string> = new Set();
 
 
     // check permission
     // check permission
@@ -321,7 +316,7 @@ export class SlackCtrl {
     }
     }
 
 
     // check permission
     // check permission
-    const relations = await this.relationMockRepository.createQueryBuilder('relation')
+    const relations = await this.relationRepository.createQueryBuilder('relation')
       .where('relation.installationId = :id', { id: installation?.id })
       .where('relation.installationId = :id', { id: installation?.id })
       .leftJoinAndSelect('relation.installation', 'installation')
       .leftJoinAndSelect('relation.installation', 'installation')
       .getMany();
       .getMany();
@@ -336,7 +331,7 @@ export class SlackCtrl {
     }
     }
 
 
 
 
-    const allowedRelations:RelationMock[] = [];
+    const allowedRelations:Relation[] = [];
     const disallowedGrowiUrls: Set<string> = new Set();
     const disallowedGrowiUrls: Set<string> = new Set();
     let notAllowedCommandName!:string;
     let notAllowedCommandName!:string;
     const actionId:string = payload?.actions?.[0].action_id;
     const actionId:string = payload?.actions?.[0].action_id;

+ 0 - 51
packages/slackbot-proxy/src/entities/relation-mock.ts

@@ -1,51 +0,0 @@
-import { differenceInMilliseconds } from 'date-fns';
-import {
-  Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn, ManyToOne, Index,
-} from 'typeorm';
-import { Installation } from './installation';
-
-interface PermissionSettingsInterface {
-  [commandName: string]: boolean | string[],
-}
-
-@Entity()
-@Index(['installation', 'growiUri'], { unique: true })
-export class RelationMock {
-
-  @PrimaryGeneratedColumn()
-  readonly id: number;
-
-  @CreateDateColumn()
-  readonly createdAt: Date;
-
-  @UpdateDateColumn()
-  readonly updatedAt: Date;
-
-  @ManyToOne(() => Installation)
-  readonly installation: Installation;
-
-  @Column()
-  @Index({ unique: true })
-  tokenGtoP: string;
-
-  @Column()
-  @Index()
-  tokenPtoG: string;
-
-  @Column()
-  growiUri: string;
-
-  @Column({ type: 'json' })
-  permissionsForBroadcastUseCommands: PermissionSettingsInterface;
-
-  @Column({ type: 'json' })
-  permissionsForSingleUseCommands: PermissionSettingsInterface;
-
-  @Column({ type: 'timestamp' })
-  expiredAtCommands: Date;
-
-  getDistanceInMillisecondsToExpiredAt(baseDate:Date):number {
-    return differenceInMilliseconds(this.expiredAtCommands, baseDate);
-  }
-
-}

+ 8 - 5
packages/slackbot-proxy/src/entities/relation.ts

@@ -4,6 +4,10 @@ import {
 } from 'typeorm';
 } from 'typeorm';
 import { Installation } from './installation';
 import { Installation } from './installation';
 
 
+interface PermissionSettingsInterface {
+  [commandName: string]: boolean | string[],
+}
+
 @Entity()
 @Entity()
 @Index(['installation', 'growiUri'], { unique: true })
 @Index(['installation', 'growiUri'], { unique: true })
 export class Relation {
 export class Relation {
@@ -31,17 +35,16 @@ export class Relation {
   @Column()
   @Column()
   growiUri: string;
   growiUri: string;
 
 
-  @Column('simple-array')
-  supportedCommandsForBroadcastUse: string[];
+  @Column({ type: 'json' })
+  permissionsForBroadcastUseCommands: PermissionSettingsInterface;
 
 
-  @Column('simple-array')
-  supportedCommandsForSingleUse: string[];
+  @Column({ type: 'json' })
+  permissionsForSingleUseCommands: PermissionSettingsInterface;
 
 
   @Column({ type: 'timestamp' })
   @Column({ type: 'timestamp' })
   expiredAtCommands: Date;
   expiredAtCommands: Date;
 
 
   getDistanceInMillisecondsToExpiredAt(baseDate:Date):number {
   getDistanceInMillisecondsToExpiredAt(baseDate:Date):number {
-    // differenceInMilliseconds uses Date.prototype.getTime() internally
     return differenceInMilliseconds(this.expiredAtCommands, baseDate);
     return differenceInMilliseconds(this.expiredAtCommands, baseDate);
   }
   }
 
 

+ 6 - 6
packages/slackbot-proxy/src/middlewares/slack-to-growi/authorizer.ts

@@ -82,6 +82,12 @@ export class AuthorizeInteractionMiddleware implements IMiddleware {
     async use(@Req() req: SlackOauthReq, @Res() res:Res): Promise<void|Res> {
     async use(@Req() req: SlackOauthReq, @Res() res:Res): Promise<void|Res> {
       const { body } = req;
       const { body } = req;
 
 
+      if (body.payload == null) {
+        // do nothing
+        this.logger.info('body does not have payload');
+        return;
+      }
+
       const payload = JSON.parse(body.payload);
       const payload = JSON.parse(body.payload);
 
 
       // extract id from body.payload
       // extract id from body.payload
@@ -89,12 +95,6 @@ export class AuthorizeInteractionMiddleware implements IMiddleware {
       const enterpriseId = payload.enterprise?.id;
       const enterpriseId = payload.enterprise?.id;
       const isEnterpriseInstall = payload.is_enterprise_install === 'true';
       const isEnterpriseInstall = payload.is_enterprise_install === 'true';
 
 
-      if (body.payload == null) {
-      // do nothing
-        this.logger.info('body does not have payload');
-        return;
-      }
-
       const query: InstallationQuery<boolean> = {
       const query: InstallationQuery<boolean> = {
         teamId,
         teamId,
         enterpriseId,
         enterpriseId,

+ 0 - 10
packages/slackbot-proxy/src/repositories/relation-mock.ts

@@ -1,10 +0,0 @@
-import {
-  Repository, EntityRepository,
-} from 'typeorm';
-
-import { RelationMock } from '~/entities/relation-mock';
-
-@EntityRepository(RelationMock)
-export class RelationMockRepository extends Repository<RelationMock> {
-
-}

+ 10 - 20
packages/slackbot-proxy/src/services/RelationsService.ts

@@ -3,11 +3,9 @@ import { Inject, Service } from '@tsed/di';
 import axios from 'axios';
 import axios from 'axios';
 import { addHours } from 'date-fns';
 import { addHours } from 'date-fns';
 
 
-// import { Relation } from '~/entities/relation';
 import { REQUEST_TIMEOUT_FOR_PTOG } from '@growi/slack';
 import { REQUEST_TIMEOUT_FOR_PTOG } from '@growi/slack';
-import { RelationMock } from '~/entities/relation-mock';
-// import { RelationRepository } from '~/repositories/relation';
-import { RelationMockRepository } from '~/repositories/relation-mock';
+import { Relation } from '~/entities/relation';
+import { RelationRepository } from '~/repositories/relation';
 
 
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
@@ -17,11 +15,10 @@ const logger = loggerFactory('slackbot-proxy:services:RelationsService');
 export class RelationsService {
 export class RelationsService {
 
 
   @Inject()
   @Inject()
-  // relationRepository: RelationRepository;
 
 
-  relationMockRepository: RelationMockRepository;
+  relationRepository: RelationRepository;
 
 
-  async getSupportedGrowiCommands(relation:RelationMock):Promise<any> {
+  async getSupportedGrowiCommands(relation:Relation):Promise<any> {
     // generate API URL
     // generate API URL
     const url = new URL('/_api/v3/slack-integration/supported-commands', relation.growiUri);
     const url = new URL('/_api/v3/slack-integration/supported-commands', relation.growiUri);
     return axios.get(url.toString(), {
     return axios.get(url.toString(), {
@@ -32,26 +29,19 @@ export class RelationsService {
     });
     });
   }
   }
 
 
-  async syncSupportedGrowiCommands(relation:RelationMock): Promise<RelationMock> {
+  async syncSupportedGrowiCommands(relation:Relation): Promise<Relation> {
     const res = await this.getSupportedGrowiCommands(relation);
     const res = await this.getSupportedGrowiCommands(relation);
-
-    // MOCK DATA MODIFY THIS GW-6972 ---------------
-    /**
-     * this code represents the update of cache (Relation schema) using request from GROWI
-     */
     const { permissionsForBroadcastUseCommands, permissionsForSingleUseCommands } = res.data.data;
     const { permissionsForBroadcastUseCommands, permissionsForSingleUseCommands } = res.data.data;
     if (relation !== null) {
     if (relation !== null) {
       relation.permissionsForBroadcastUseCommands = permissionsForBroadcastUseCommands;
       relation.permissionsForBroadcastUseCommands = permissionsForBroadcastUseCommands;
       relation.permissionsForSingleUseCommands = permissionsForSingleUseCommands;
       relation.permissionsForSingleUseCommands = permissionsForSingleUseCommands;
       relation.expiredAtCommands = addHours(new Date(), 48);
       relation.expiredAtCommands = addHours(new Date(), 48);
-      return this.relationMockRepository.save(relation);
+      return this.relationRepository.save(relation);
     }
     }
     throw Error('No relation exists.');
     throw Error('No relation exists.');
-    // MOCK DATA MODIFY THIS GW-6972 ---------------
   }
   }
 
 
-  // MODIFY THIS METHOD USING ORIGINAL RELATION MODEL GW-6972
-  async syncRelation(relation:RelationMock, baseDate:Date):Promise<RelationMock|null> {
+  async syncRelation(relation:Relation, baseDate:Date):Promise<Relation|null> {
     if (relation == null) return null;
     if (relation == null) return null;
 
 
     const distanceMillisecondsToExpiredAt = relation.getDistanceInMillisecondsToExpiredAt(baseDate);
     const distanceMillisecondsToExpiredAt = relation.getDistanceInMillisecondsToExpiredAt(baseDate);
@@ -79,7 +69,7 @@ export class RelationsService {
     return relation;
     return relation;
   }
   }
 
 
-  async isPermissionsForSingleUseCommands(relation:RelationMock, growiCommandType:string, channelName:string, baseDate:Date):Promise<boolean> {
+  async isPermissionsForSingleUseCommands(relation:Relation, growiCommandType:string, channelName:string, baseDate:Date):Promise<boolean> {
     const syncedRelation = await this.syncRelation(relation, baseDate);
     const syncedRelation = await this.syncRelation(relation, baseDate);
     if (syncedRelation == null) {
     if (syncedRelation == null) {
       return false;
       return false;
@@ -98,7 +88,7 @@ export class RelationsService {
     return permission;
     return permission;
   }
   }
 
 
-  async isPermissionsUseBroadcastCommands(relation:RelationMock, growiCommandType:string, channelName:string, baseDate:Date):Promise<boolean> {
+  async isPermissionsUseBroadcastCommands(relation:Relation, growiCommandType:string, channelName:string, baseDate:Date):Promise<boolean> {
     const syncedRelation = await this.syncRelation(relation, baseDate);
     const syncedRelation = await this.syncRelation(relation, baseDate);
     if (syncedRelation == null) {
     if (syncedRelation == null) {
       return false;
       return false;
@@ -120,7 +110,7 @@ export class RelationsService {
 
 
   async checkPermissionForInteractions(
   async checkPermissionForInteractions(
       // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
       // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-      relation:RelationMock, channelName:string, callbackId:string, actionId:string,
+      relation:Relation, channelName:string, callbackId:string, actionId:string,
   ):Promise<{isPermittedForInteractions:boolean, commandName:string}> {
   ):Promise<{isPermittedForInteractions:boolean, commandName:string}> {
 
 
     let isPermittedForInteractions!:boolean;
     let isPermittedForInteractions!:boolean;

+ 5 - 10
packages/slackbot-proxy/src/services/SelectGrowiService.ts

@@ -5,14 +5,12 @@ import { AuthorizeResult } from '@slack/oauth';
 
 
 import { GrowiCommandProcessor } from '~/interfaces/slack-to-growi/growi-command-processor';
 import { GrowiCommandProcessor } from '~/interfaces/slack-to-growi/growi-command-processor';
 import { Installation } from '~/entities/installation';
 import { Installation } from '~/entities/installation';
-// import { Relation } from '~/entities/relation';
-import { RelationMock } from '~/entities/relation-mock';
-// import { RelationRepository } from '~/repositories/relation';
-import { RelationMockRepository } from '~/repositories/relation-mock';
+import { Relation } from '~/entities/relation';
+import { RelationRepository } from '~/repositories/relation';
 
 
 
 
 export type SelectedGrowiInformation = {
 export type SelectedGrowiInformation = {
-  relation: RelationMock,
+  relation: Relation,
   growiCommand: GrowiCommand,
   growiCommand: GrowiCommand,
   sendCommandBody: any,
   sendCommandBody: any,
 }
 }
@@ -20,11 +18,8 @@ export type SelectedGrowiInformation = {
 @Service()
 @Service()
 export class SelectGrowiService implements GrowiCommandProcessor {
 export class SelectGrowiService implements GrowiCommandProcessor {
 
 
-  // @Inject()
-  // relationRepository: RelationRepository;
-
   @Inject()
   @Inject()
-  relationMockRepository: RelationMockRepository;
+  relationRepository: RelationRepository;
 
 
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
   async process(growiCommand: GrowiCommand | string, authorizeResult: AuthorizeResult, body: {[key:string]:string } & {growiUrisForSingleUse:string[]}): Promise<void> {
   async process(growiCommand: GrowiCommand | string, authorizeResult: AuthorizeResult, body: {[key:string]:string } & {growiUrisForSingleUse:string[]}): Promise<void> {
@@ -99,7 +94,7 @@ export class SelectGrowiService implements GrowiCommandProcessor {
     // ovverride trigger_id
     // ovverride trigger_id
     sendCommandBody.trigger_id = triggerId;
     sendCommandBody.trigger_id = triggerId;
 
 
-    const relation = await this.relationMockRepository.createQueryBuilder('relation')
+    const relation = await this.relationRepository.createQueryBuilder('relation')
       .where('relation.growiUri =:growiUri', { growiUri })
       .where('relation.growiUri =:growiUri', { growiUri })
       .andWhere('relation.installationId = :id', { id: installation?.id })
       .andWhere('relation.installationId = :id', { id: installation?.id })
       .leftJoinAndSelect('relation.installation', 'installation')
       .leftJoinAndSelect('relation.installation', 'installation')