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

Merge pull request #3604 from weseek/feat/growi-bot-proxy

Feat/growi bot proxy
Yuki Takei 5 лет назад
Родитель
Сommit
a1198c6593
41 измененных файлов с 1744 добавлено и 87 удалено
  1. 1 0
      .eslintignore
  2. 240 0
      .github/workflows/ci-slackbot-proxy.yml
  3. 20 10
      .github/workflows/ci.yml
  4. 2 2
      .gitignore
  5. 2 6
      .vscode/settings.json
  6. 4 1
      CHANGES.md
  7. 2 2
      docker/Dockerfile
  8. 1 0
      docker/Dockerfile.dockerignore
  9. 10 1
      package.json
  10. 2 0
      packages/slack/.eslintignore
  11. 31 0
      packages/slack/.eslintrc.js
  12. 1 0
      packages/slack/.gitignore
  13. 61 0
      packages/slack/jest.config.js
  14. 38 0
      packages/slack/package.json
  15. 8 0
      packages/slack/src/index.ts
  16. 5 0
      packages/slack/src/interfaces/growi-command.ts
  17. 9 0
      packages/slack/src/models/errors.ts
  18. 80 0
      packages/slack/src/utils/slash-command-parser.test.ts
  19. 17 0
      packages/slack/src/utils/slash-command-parser.ts
  20. 12 0
      packages/slack/tsconfig.build.json
  21. 46 0
      packages/slack/tsconfig.json
  22. 1 0
      packages/slackbot-proxy/.dockerignore
  23. 1 0
      packages/slackbot-proxy/.env
  24. 5 0
      packages/slackbot-proxy/.env.development
  25. 2 0
      packages/slackbot-proxy/.eslintignore
  26. 31 0
      packages/slackbot-proxy/.eslintrc.js
  27. 1 0
      packages/slackbot-proxy/.gitignore
  28. 3 0
      packages/slackbot-proxy/config/ci/.env.local.for-ci
  29. 32 0
      packages/slackbot-proxy/docker-compose.dev.yml
  30. 48 0
      packages/slackbot-proxy/package.json
  31. 94 0
      packages/slackbot-proxy/src/Server.ts
  32. 93 0
      packages/slackbot-proxy/src/controllers/slack.ts
  33. 43 0
      packages/slackbot-proxy/src/entities/installation.ts
  34. 28 0
      packages/slackbot-proxy/src/index.ts
  35. 14 0
      packages/slackbot-proxy/src/repositories/installation.ts
  36. 60 0
      packages/slackbot-proxy/src/services/InstallerService.ts
  37. 16 0
      packages/slackbot-proxy/src/services/RecieveService.ts
  38. 12 0
      packages/slackbot-proxy/tsconfig.build.json
  39. 46 0
      packages/slackbot-proxy/tsconfig.json
  40. 1 1
      src/linter-checker/test.scss
  41. 621 64
      yarn.lock

+ 1 - 0
.eslintignore

@@ -1,6 +1,7 @@
 /.github/**
 /.vscode/**
 /node_modules/**
+/packages/**
 /public/**
 /src/client/js/legacy/thirdparty-js/**
 /src/client/js/util/reveal/plugins/markdown.js

+ 240 - 0
.github/workflows/ci-slackbot-proxy.yml

@@ -0,0 +1,240 @@
+name: Node CI for slackbot-proxy
+
+on:
+  push:
+    branches-ignore:
+      - release/**
+      - tmp/**
+    paths:
+      - .github/workflows/ci-slackbot-proxy.yml
+      - packages/slackbot-proxy/*
+      - package.json
+      - yarn.lock
+
+defaults:
+  run:
+    working-directory: packages/slackbot-proxy
+
+jobs:
+
+  test:
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        node-version: [14.x]
+
+    steps:
+    - uses: actions/checkout@v2
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - name: Cache/Restore node_modules
+      id: cache-dependencies
+      uses: actions/cache@v2
+      with:
+        path: node_modules
+        key: ${{ runner.OS }}-node_modules-${{ matrix.node-version }}-${{ hashFiles('./yarn.lock') }}
+    - name: Get yarn cache dir
+      if: steps.cache-dependencies.outputs.cache-hit != 'true'
+      id: cache-yarn
+      run: echo "::set-output name=dir::$(yarn cache dir)"
+    - name: Cache/Restore yarn cache
+      if: steps.cache-dependencies.outputs.cache-hit != 'true'
+      uses: actions/cache@v2
+      with:
+        path: ${{ steps.cache-yarn.outputs.dir }}
+        key: ${{ runner.os }}-yarn-${{ matrix.node-version }}-${{ hashFiles('./yarn.lock') }}
+        restore-keys: |
+          ${{ runner.os }}-yarn-${{ matrix.node-version }}-
+    - name: Install dependencies
+      if: steps.cache-dependencies.outputs.cache-hit != 'true'
+      run: |
+        yarn --frozen-lockfile
+    - name: Print dependencies
+      run: |
+        echo -n "node " && node -v
+        echo -n "npm " && npm -v
+        yarn list --depth=0
+    - name: yarn lint
+      run: |
+        yarn test
+
+    - name: Slack Notification
+      uses: weseek/ghaction-slack-notification@master
+      if: failure()
+      with:
+        type: ${{ job.status }}
+        job_name: '*Node CI for growi-bot-proxy - test (${{ matrix.node-version }})*'
+        channel: '#ci'
+        isCompactMode: true
+        url: ${{ secrets.SLACK_WEBHOOK_URL }}
+
+
+  launch-dev:
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        node-version: [14.x]
+
+    services:
+      mysql:
+        image: mysql:8.0
+        ports:
+          - 3306
+        options: --health-cmd "mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 3
+        env:
+          MYSQL_ALLOW_EMPTY_PASSWORD: yes
+          MYSQL_DATABASE: growi-slackbot-proxy
+
+    steps:
+    - uses: actions/checkout@v2
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - name: Cache/Restore node_modules
+      id: cache-dependencies
+      uses: actions/cache@v2
+      with:
+        path: node_modules
+        key: ${{ runner.OS }}-node_modules_dev-${{ matrix.node-version }}-${{ hashFiles('./yarn.lock') }}
+    - name: Get yarn cache dir
+      if: steps.cache-dependencies.outputs.cache-hit != 'true'
+      id: cache-yarn
+      run: echo "::set-output name=dir::$(yarn cache dir)"
+    - name: Cache/Restore yarn cache
+      if: steps.cache-dependencies.outputs.cache-hit != 'true'
+      uses: actions/cache@v2
+      with:
+        path: ${{ steps.cache-yarn.outputs.dir }}
+        key: ${{ runner.os }}-yarn-${{ hashFiles('./yarn.lock') }}
+        restore-keys: |
+          ${{ runner.os }}-yarn-
+    - name: Install dependencies
+      if: steps.cache-dependencies.outputs.cache-hit != 'true'
+      run: |
+        yarn --frozen-lockfile
+    - name: Print dependencies
+      run: |
+        echo -n "node " && node -v
+        echo -n "npm " && npm -v
+        yarn list --depth=0
+    - name: yarn dev:ci
+      run: |
+        cp config/ci/.env.local.for-ci .env.local
+        yarn dev:ci
+      env:
+        TYPEORM_CONNECTION: mysql
+        TYPEORM_HOST: localhost
+        TYPEORM_PORT: ${{ job.services.mysql.ports[3306] }}
+        TYPEORM_DATABASE: growi-slackbot-proxy
+        TYPEORM_USERNAME: root
+        TYPEORM_PASSWORD:
+    - name: Slack Notification
+      uses: weseek/ghaction-slack-notification@master
+      if: failure()
+      with:
+        type: ${{ job.status }}
+        job_name: '*Node CI for growi-bot-proxy - launch-dev (${{ matrix.node-version }})*'
+        channel: '#ci'
+        isCompactMode: true
+        url: ${{ secrets.SLACK_WEBHOOK_URL }}
+
+
+  launch-prod:
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        node-version: [14.x]
+
+    services:
+      mysql:
+        image: mysql:8.0
+        ports:
+          - 3306
+        options: --health-cmd "mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 3
+        env:
+          MYSQL_ALLOW_EMPTY_PASSWORD: yes
+          MYSQL_DATABASE: growi-slackbot-proxy
+
+    steps:
+    - uses: actions/checkout@v2
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - name: Get Date
+      id: date
+      run: |
+        echo ::set-output name=YmdH::$(date '+%Y%m%d%H')
+        echo ::set-output name=Ymd::$(date '+%Y%m%d')
+        echo ::set-output name=Ym::$(date '+%Y%m')
+        echo ::set-output name=Y::$(date '+%Y')
+    - name: Cache/Restore node_modules
+      uses: actions/cache@v2
+      with:
+        path: node_modules
+        key: ${{ runner.OS }}-node_modules_prod-${{ matrix.node-version }}-${{ steps.date.outputs.YmdH }}
+        restore-keys: |
+          ${{ runner.os }}-node_modules_prod-${{ matrix.node-version }}-${{ steps.date.outputs.Ymd }}
+          ${{ runner.os }}-node_modules_prod-${{ matrix.node-version }}-${{ steps.date.outputs.Ym }}
+          ${{ runner.os }}-node_modules_prod-${{ matrix.node-version }}-${{ steps.date.outputs.Y }}
+    - name: Get yarn cache dir
+      id: cache-yarn
+      run: echo "::set-output name=dir::$(yarn cache dir)"
+    - name: Cache/Restore yarn cache
+      uses: actions/cache@v2
+      with:
+        path: ${{ steps.cache-yarn.outputs.dir }}
+        key: ${{ runner.os }}-yarn-${{ matrix.node-version }}-${{ hashFiles('./yarn.lock') }}
+        restore-keys: |
+          ${{ runner.os }}-yarn-
+    - name: Install dependencies
+      run: |
+        yarn --frozen-lockfile
+    - name: Print dependencies
+      run: |
+        echo -n "node " && node -v
+        echo -n "npm " && npm -v
+        yarn list --depth=0
+    - name: yarn build
+      run: |
+        yarn build
+    - name: yarn install --production
+      run: |
+        yarn install --production
+    - name: Print dependencies
+      run: |
+        echo -n "node " && node -v
+        echo -n "npm " && npm -v
+        yarn list --production --depth=0
+    - name: yarn start:prod:ci
+      run: |
+        cp config/ci/.env.local.for-ci .env.local
+        yarn start:prod:ci
+      env:
+        TYPEORM_CONNECTION: mysql
+        TYPEORM_HOST: localhost
+        TYPEORM_PORT: ${{ job.services.mysql.ports[3306] }}
+        TYPEORM_DATABASE: growi-slackbot-proxy
+        TYPEORM_USERNAME: root
+        TYPEORM_PASSWORD:
+    - name: Upload report as artifact
+      uses: actions/upload-artifact@v2
+      with:
+        name: Report
+        path: report
+
+    - name: Slack Notification
+      uses: weseek/ghaction-slack-notification@master
+      if: failure()
+      with:
+        type: ${{ job.status }}
+        job_name: '*Node CI for growi-bot-proxy - launch-prod (${{ matrix.node-version }})*'
+        channel: '#ci'
+        isCompactMode: true
+        url: ${{ secrets.SLACK_WEBHOOK_URL }}

+ 20 - 10
.github/workflows/ci.yml

@@ -1,10 +1,20 @@
-name: Node CI
+name: Node CI for growi
 
 on:
   push:
     branches-ignore:
       - release/**
       - tmp/**
+    paths:
+      - .github/workflows/ci.yml
+      - .eslint*
+      - .prettier*
+      - .stylelint*
+      - config
+      - resource
+      - src
+      - package.json
+      - yarn.lock
 
 jobs:
 
@@ -42,7 +52,7 @@ jobs:
     - name: Install dependencies
       if: steps.cache-dependencies.outputs.cache-hit != 'true'
       run: |
-        yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images@1.0.0 react-motion
+        yarn add -W growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images@1.0.0 react-motion
     - name: Print dependencies
       run: |
         echo -n "node " && node -v
@@ -57,7 +67,7 @@ jobs:
       if: failure()
       with:
         type: ${{ job.status }}
-        job_name: '*test (${{ matrix.node-version }})*'
+        job_name: '*Node CI for growi - lint (${{ matrix.node-version }})*'
         channel: '#ci'
         isCompactMode: true
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
@@ -107,7 +117,7 @@ jobs:
     - name: Install dependencies
       if: steps.cache-dependencies.outputs.cache-hit != 'true'
       run: |
-        yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images@1.0.0 react-motion
+        yarn add -W growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images@1.0.0 react-motion
     - name: Print dependencies
       run: |
         echo -n "node " && node -v
@@ -129,7 +139,7 @@ jobs:
       if: failure()
       with:
         type: ${{ job.status }}
-        job_name: '*test (${{ matrix.node-version }})*'
+        job_name: '*Node CI for growi - test (${{ matrix.node-version }})*'
         channel: '#ci'
         isCompactMode: true
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
@@ -185,7 +195,7 @@ jobs:
     - name: Install dependencies
       if: steps.cache-dependencies.outputs.cache-hit != 'true'
       run: |
-        yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images@1.0.0 react-motion
+        yarn add -W growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images@1.0.0 react-motion
     - name: Print dependencies
       run: |
         echo -n "node " && node -v
@@ -200,7 +210,7 @@ jobs:
       if: failure()
       with:
         type: ${{ job.status }}
-        job_name: '*build-dev (${{ matrix.node-version }})*'
+        job_name: '*Node CI for growi - build-dev (${{ matrix.node-version }})*'
         channel: '#ci'
         isCompactMode: true
         url: ${{ secrets.SLACK_WEBHOOK_URL }}
@@ -257,8 +267,8 @@ jobs:
           ${{ runner.os }}-yarn-
     - name: Install dependencies
       run: |
-        yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs
-        yarn add -D react-images@1.0.0 react-motion
+        yarn add -W growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs
+        yarn add -W -D react-images@1.0.0 react-motion
     - name: Print dependencies
       run: |
         echo -n "node " && node -v
@@ -300,7 +310,7 @@ jobs:
       if: failure()
       with:
         type: ${{ job.status }}
-        job_name: '*build-prod (${{ matrix.node-version }})*'
+        job_name: '*Node CI for growi - build-prod (${{ matrix.node-version }})*'
         channel: '#ci'
         isCompactMode: true
         url: ${{ secrets.SLACK_WEBHOOK_URL }}

+ 2 - 2
.gitignore

@@ -1,12 +1,12 @@
 # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 
 # dependencies
-/node_modules
+node_modules
 /.pnp
 .pnp.js
 
 # testing
-/coverage
+coverage
 
 # next.js
 /.next/

+ 2 - 6
.vscode/settings.json

@@ -1,10 +1,7 @@
 {
-  // 既定の改行文字。LF の場合には \n を CRLF の場合には \r\n を使用してください。
   "files.eol": "\n",
-  // 指定した構文に対してプロファイルを定義するか、特定の規則がある独自のプロファイルをご使用ください。
-  "emmet.syntaxProfiles": {
-    "javascript": "jsx"
-  },
+
+  "eslint.workingDirectories": [{ "mode": "auto" }],
 
   // use stylelint-plus
   // see https://qiita.com/y-w/items/bd7f11013fe34b69f0df#vs-code%E3%81%A8%E7%B5%84%E3%81%BF%E5%90%88%E3%82%8F%E3%81%9B%E3%82%8B
@@ -16,7 +13,6 @@
   "[scss]": {
     "editor.formatOnSave": true
   },
-  "stylelint.autoFixOnSave": true,
 
   // for vscode-eslint
   "[javascript]": {

+ 4 - 1
CHANGES.md

@@ -2,7 +2,10 @@
 
 ## v4.2.14-RC
 
-* 
+* Support: Update libs
+    * bunyan
+    * browser-bunyan
+    * eslint-config-weseek
 
 ## v4.2.13
 

+ 2 - 2
docker/Dockerfile

@@ -20,9 +20,9 @@ COPY ./yarn.lock ./
 RUN yarn config set network-timeout 300000
 RUN yarn
 # install official plugins
-RUN yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs
+RUN yarn add -W growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs
 # install peerDependencies
-RUN yarn add -D react-images@1.0.0 react-motion
+RUN yarn add -W -D react-images@1.0.0 react-motion
 
 
 

+ 1 - 0
docker/Dockerfile.dockerignore

@@ -2,6 +2,7 @@
 .github
 .vscode
 node_modules
+packages
 src/linter-checker
 src/test
 .editorconfig

+ 10 - 1
package.json

@@ -19,6 +19,11 @@
   "bugs": {
     "url": "https://github.com/weseek/growi/issues"
   },
+  "private": true,
+  "workspaces": {
+    "packages": ["packages/*"],
+    "nohoist": []
+  },
   "scripts": {
     "build:api:jsdoc": "swagger-jsdoc -o tmp/swagger.json -d config/swagger-definition.js",
     "build:apiv3:jsdoc": "cross-env API_VERSION=3 npm run build:api:jsdoc -- \"src/server/routes/apiv3/**/*.js\" \"src/server/models/**/*.js\"",
@@ -177,6 +182,10 @@
     "@babel/preset-env": "^7.4.5",
     "@babel/preset-react": "^7.0.0",
     "@handsontable/react": "=2.1.0",
+    "@types/compression": "^1.7.0",
+    "@types/express": "^4.17.11",
+    "@types/multer": "^1.4.5",
+    "@types/node": "^14.14.35",
     "autoprefixer": "^9.0.0",
     "babel-eslint": "^10.0.1",
     "babel-loader": "^8.0.6",
@@ -196,7 +205,7 @@
     "diff2html": "^3.1.2",
     "eazy-logger": "^3.0.2",
     "eslint": "^6.0.1",
-    "eslint-config-weseek": "^1.0.4",
+    "eslint-config-weseek": "^1.0.8",
     "eslint-plugin-import": "^2.18.0",
     "eslint-plugin-jest": "^23.0.3",
     "eslint-plugin-react": "^7.14.2",

+ 2 - 0
packages/slack/.eslintignore

@@ -0,0 +1,2 @@
+/dist/**
+/node_modules/**

+ 31 - 0
packages/slack/.eslintrc.js

@@ -0,0 +1,31 @@
+module.exports = {
+  parser: '@typescript-eslint/parser',
+  extends: [
+    'weseek',
+    'weseek/typescript',
+    'plugin:jest/recommended',
+  ],
+  env: {
+    jquery: true,
+    'jest/globals': true,
+  },
+  globals: {
+  },
+  plugins: [
+    'jest',
+  ],
+  rules: {
+    'import/prefer-default-export': 'off',
+    '@typescript-eslint/no-explicit-any': 'off',
+    indent: [
+      'error',
+      2,
+      {
+        SwitchCase: 1,
+        ArrayExpression: 'first',
+        FunctionDeclaration: { body: 1, parameters: 2 },
+        FunctionExpression: { body: 1, parameters: 2 },
+      },
+    ],
+  },
+};

+ 1 - 0
packages/slack/.gitignore

@@ -0,0 +1 @@
+/dist/

+ 61 - 0
packages/slack/jest.config.js

@@ -0,0 +1,61 @@
+// For a detailed explanation regarding each configuration property, visit:
+// https://jestjs.io/docs/en/configuration.html
+
+const MODULE_NAME_MAPPING = {
+  '^\\^/(.+)$': '<rootDir>/$1',
+  '^~/(.+)$': '<rootDir>/src/$1',
+};
+
+module.exports = {
+
+  preset: 'ts-jest/presets/js-with-ts',
+
+  moduleNameMapper: MODULE_NAME_MAPPING,
+
+  // Automatically clear mock calls and instances between every test
+  clearMocks: true,
+
+  // Indicates whether the coverage information should be collected while executing the test
+  collectCoverage: true,
+
+  // An array of glob patterns indicating a set of files for which coverage information should be collected
+  // collectCoverageFrom: undefined,
+
+  // The directory where Jest should output its coverage files
+  coverageDirectory: 'coverage',
+
+  // An array of regexp pattern strings used to skip coverage collection
+  coveragePathIgnorePatterns: [
+    '/node_modules/',
+  ],
+
+  // An object that configures minimum threshold enforcement for coverage results
+  // TODO: activate -- 2020.03.24 Yuki Takei
+  // coverageThreshold: {
+  //   global: {
+  //     branches: 70,
+  //     functions: 70,
+  //     lines: 70,
+  //     statements: 70,
+  //   },
+  // },
+
+  // An array of file extensions your modules use
+  moduleFileExtensions: [
+    'js',
+    'json',
+    'jsx',
+    'ts',
+    'tsx',
+    'node',
+  ],
+
+  // The test environment that will be used for testing
+  testEnvironment: 'node',
+
+  // The glob patterns Jest uses to detect test files
+  testMatch: [
+    '**/src/**/__tests__/**/*.[jt]s?(x)',
+    '**/src/**/?(*.)+(spec|test).[tj]s?(x)',
+  ],
+};

+ 38 - 0
packages/slack/package.json

@@ -0,0 +1,38 @@
+{
+  "name": "@growi/slack",
+  "version": "0.9.0-RC",
+  "license": "MIT",
+  "main": "dist/index.js",
+  "files": ["dist"],
+  "scripts": {
+    "build": "yarn tsc",
+    "tsc": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
+    "tsc:w": "tsc -w",
+    "test": "yarn test:lint && yarn test:coverage",
+    "test:unit": "cross-env NODE_ENV=test jest --passWithNoTests",
+    "test:coverage": "yarn test:unit",
+    "test:lint": "eslint src --ext .ts",
+    "test:lint:fix": "eslint src --ext .ts --fix"
+  },
+  "dependencies": {
+    "dotenv-flow": "^3.2.0"
+  },
+  "devDependencies": {
+    "@slack/bolt": "^3.3.0",
+    "@tsed/core": "^6.34.3",
+    "@tsed/exceptions": "^6.34.3",
+    "@tsed/json-mapper": "^6.34.3",
+    "@tsed/schema": "^6.34.3",
+    "@types/jest": "^26.0.22",
+    "@typescript-eslint/eslint-plugin": "^4.18.0",
+    "@typescript-eslint/parser": "^4.18.0",
+    "eslint-import-resolver-typescript": "^2.4.0",
+    "eslint-plugin-jest": "^24.3.2",
+    "ts-jest": "^26.5.4",
+    "ts-node": "^9.1.1",
+    "ts-node-dev": "^1.1.6",
+    "tsc-alias": "1.2.6",
+    "tsconfig-paths": "^3.9.0",
+    "typescript": "^4.2.3"
+  }
+}

+ 8 - 0
packages/slack/src/index.ts

@@ -0,0 +1,8 @@
+export const supportedSlackCommands: string[] = [
+  '/growi',
+];
+
+export const supportedGrowiCommands: string[] = [
+  'search',
+  'create',
+];

+ 5 - 0
packages/slack/src/interfaces/growi-command.ts

@@ -0,0 +1,5 @@
+export type GrowiCommand = {
+  text: string,
+  growiCommandType: string,
+  growiCommandArgs: string[],
+};

+ 9 - 0
packages/slack/src/models/errors.ts

@@ -0,0 +1,9 @@
+export class InvalidGrowiCommandError extends Error {
+
+  constructor(e?: string) {
+    super(e);
+    this.name = new.target.name;
+    Object.setPrototypeOf(this, new.target.prototype);
+  }
+
+}

+ 80 - 0
packages/slack/src/utils/slash-command-parser.test.ts

@@ -0,0 +1,80 @@
+import { SlashCommand } from '@slack/bolt';
+import { InvalidGrowiCommandError } from '~/models/errors';
+
+import { parse } from './slash-command-parser';
+
+const SlashCommandMock = jest.fn<SlashCommand, [string]>().mockImplementation((text) => {
+  return { text } as SlashCommand;
+});
+
+describe('parse SlashCommand', () => {
+
+  describe('without growiCommandType', () => {
+    test('throws InvalidGrowiCommandError', () => {
+      // setup
+      const slashCommandText = '';
+      const slashCommand = new SlashCommandMock(slashCommandText);
+
+      // when/then
+      expect(() => {
+        parse(slashCommand);
+      }).toThrowError(InvalidGrowiCommandError);
+    });
+  });
+
+  test('returns a GrowiCommand instance with empty growiCommandArgs', () => {
+    // setup
+    const slashCommandText = 'search';
+    const slashCommand = new SlashCommandMock(slashCommandText);
+
+    // when
+    const result = parse(slashCommand);
+
+    // then
+    expect(result.text).toBe(slashCommandText);
+    expect(result.growiCommandType).toBe('search');
+    expect(result.growiCommandArgs).toStrictEqual([]);
+  });
+
+  test('returns a GrowiCommand instance with space growiCommandType', () => {
+    // setup
+    const slashCommandText = '   search   ';
+    const slashCommand = new SlashCommandMock(slashCommandText);
+
+    // when
+    const result = parse(slashCommand);
+
+    // then
+    expect(result.text).toBe(slashCommandText);
+    expect(result.growiCommandType).toBe('search');
+    expect(result.growiCommandArgs).toStrictEqual([]);
+  });
+
+  test('returns a GrowiCommand instance with space growiCommandArgs', () => {
+    // setup
+    const slashCommandText = '   search hoge   ';
+    const slashCommand = new SlashCommandMock(slashCommandText);
+
+    // when
+    const result = parse(slashCommand);
+
+    // then
+    expect(result.text).toBe(slashCommandText);
+    expect(result.growiCommandType).toBe('search');
+    expect(result.growiCommandArgs).toStrictEqual(['hoge']);
+  });
+
+  test('returns a GrowiCommand instance', () => {
+    // setup
+    const slashCommandText = 'search keyword1 keyword2';
+    const slashCommand = new SlashCommandMock(slashCommandText);
+
+    // when
+    const result = parse(slashCommand);
+
+    // then
+    expect(result.text).toBe(slashCommandText);
+    expect(result.growiCommandType).toBe('search');
+    expect(result.growiCommandArgs).toStrictEqual(['keyword1', 'keyword2']);
+  });
+});

+ 17 - 0
packages/slack/src/utils/slash-command-parser.ts

@@ -0,0 +1,17 @@
+import { GrowiCommand } from '../interfaces/growi-command';
+import { InvalidGrowiCommandError } from '../models/errors';
+
+export const parse = (slashCommand:{[key:string]:string}): GrowiCommand => {
+  const trimmedText = slashCommand.text.trim();
+  const splitted = trimmedText.split(' ');
+
+  if (splitted[0] === '') {
+    throw new InvalidGrowiCommandError('The SlashCommand.text does not specify GrowiCommand type');
+  }
+
+  return {
+    text: slashCommand.text,
+    growiCommandType: splitted[0],
+    growiCommandArgs: splitted.slice(1),
+  };
+};

+ 12 - 0
packages/slack/tsconfig.build.json

@@ -0,0 +1,12 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "dist",
+    "declaration": true,
+    "noResolve": false,
+    "preserveConstEnums": true,
+    "sourceMap": true,
+    "noEmit": false,
+    "inlineSources": true
+  }
+}

+ 46 - 0
packages/slack/tsconfig.json

@@ -0,0 +1,46 @@
+{
+  "compilerOptions": {
+    "target": "es2019",
+    "module": "commonjs",
+    "lib": ["dom", "dom.iterable", "esnext"],
+    "sourceMap": true,
+    "noEmit": true,
+    "removeComments": false,
+    "importHelpers": true,
+    "isolatedModules": true,
+
+    /* Strict Type-Checking Options */
+    // "strict": true,
+    "strictNullChecks": true,
+    "noImplicitAny": false,
+
+    /* Additional Checks */
+    "noUnusedLocals": false,
+    "noUnusedParameters": false,
+
+    /* Module Resolution Options */
+    "moduleResolution": "node",
+    "baseUrl": ".",
+    "paths": {
+      "~/*": ["src/*"],
+      "^/*": ["./*"],
+    },
+    "typeRoots": [
+      "../../node_modules/@types",
+      "./node_modules/@types"
+    ],
+    "allowSyntheticDefaultImports": true,
+    "esModuleInterop": true,
+
+    /* Misc */
+    "preserveConstEnums": true,
+    "forceConsistentCasingInFileNames": true,
+    "resolveJsonModule": true,
+
+    /* Experimental Options */
+    "experimentalDecorators": true,
+    "emitDecoratorMetadata": true
+  },
+  "exclude": ["node_modules", "./public", "dist", "test"],
+  "include": ["./src/**/*.ts"]
+}

+ 1 - 0
packages/slackbot-proxy/.dockerignore

@@ -0,0 +1 @@
+node_modules

+ 1 - 0
packages/slackbot-proxy/.env

@@ -0,0 +1 @@
+SLACK_INSTALLPROVIDER_STATE_SECRET=change-it

+ 5 - 0
packages/slackbot-proxy/.env.development

@@ -0,0 +1,5 @@
+TYPEORM_CONNECTION=mysql
+TYPEORM_HOST=mysql
+TYPEORM_DATABASE=growi-slackbot-proxy
+TYPEORM_USERNAME=growi-slackbot-proxy
+TYPEORM_PASSWORD=YrkUi7rCW46Z2N6EXSFUBwaQTUR8biCU

+ 2 - 0
packages/slackbot-proxy/.eslintignore

@@ -0,0 +1,2 @@
+/dist/**
+/node_modules/**

+ 31 - 0
packages/slackbot-proxy/.eslintrc.js

@@ -0,0 +1,31 @@
+module.exports = {
+  parser: '@typescript-eslint/parser',
+  extends: [
+    'weseek',
+    'weseek/typescript',
+    'plugin:jest/recommended',
+  ],
+  env: {
+    jquery: true,
+    'jest/globals': true,
+  },
+  globals: {
+  },
+  plugins: [
+    'jest',
+  ],
+  rules: {
+    'import/prefer-default-export': 'off',
+    '@typescript-eslint/no-explicit-any': 'off',
+    indent: [
+      'error',
+      2,
+      {
+        SwitchCase: 1,
+        ArrayExpression: 'first',
+        FunctionDeclaration: { body: 1, parameters: 2 },
+        FunctionExpression: { body: 1, parameters: 2 },
+      },
+    ],
+  },
+};

+ 1 - 0
packages/slackbot-proxy/.gitignore

@@ -0,0 +1 @@
+/dist/

+ 3 - 0
packages/slackbot-proxy/config/ci/.env.local.for-ci

@@ -0,0 +1,3 @@
+SLACK_CLIENT_ID=dummy
+SLACK_CLIENT_SECRET=dummy
+SLACK_SIGNING_SECRET=dummy

+ 32 - 0
packages/slackbot-proxy/docker-compose.dev.yml

@@ -0,0 +1,32 @@
+version: '3'
+services:
+
+  mysql:
+    image: mysql:8.0
+    restart: unless-stopped
+    ports:
+      - 3306:3306
+    environment:
+      - MYSQL_ALLOW_EMPTY_PASSWORD=yes
+      - MYSQL_USER=growi-slackbot-proxy
+      - MYSQL_PASSWORD=YrkUi7rCW46Z2N6EXSFUBwaQTUR8biCU
+      - MYSQL_DATABASE=growi-slackbot-proxy
+    volumes:
+      - /data/db
+
+  phpmyadmin:
+    depends_on:
+      - mysql
+    image: phpmyadmin
+    restart: unless-stopped
+    ports:
+      - 13306:80
+    environment:
+      - PMA_HOST=mysql
+      - PMA_USER=growi-slackbot-proxy
+      - PMA_PASSWORD=YrkUi7rCW46Z2N6EXSFUBwaQTUR8biCU
+
+networks:
+  default:
+    external:
+      name: growi_devcontainer_default

+ 48 - 0
packages/slackbot-proxy/package.json

@@ -0,0 +1,48 @@
+{
+  "name": "@growi/slackbot-proxy",
+  "version": "0.9.0-RC",
+  "license": "MIT",
+  "scripts": {
+    "build": "yarn tsc",
+    "tsc": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
+    "tsc:w": "tsc -w",
+    "dev:ci": "yarn dev --ci",
+    "dev": "cross-env NODE_ENV=development ts-node-dev -r tsconfig-paths/register -r dotenv-flow/config src/index.ts",
+    "start:prod:ci": "yarn start:prod --ci",
+    "start:prod": "cross-env NODE_ENV=production node -r dotenv-flow/config dist/index.js",
+    "test": "yarn test:lint && yarn test:coverage",
+    "test:unit": "cross-env NODE_ENV=test jest --passWithNoTests",
+    "test:coverage": "yarn test:unit",
+    "test:lint": "eslint src --ext .ts",
+    "test:lint:fix": "eslint src --ext .ts --fix"
+  },
+  "dependencies": {
+    "@growi/slack": "0.9.0-RC",
+    "@slack/oauth": "^2.0.1",
+    "@slack/web-api": "^6.1.0",
+    "@tsed/common": "^6.34.3",
+    "@tsed/di": "^6.34.3",
+    "@tsed/platform-express": "^6.34.3",
+    "@tsed/swagger": "^6.34.3",
+    "@tsed/typeorm": "^6.34.3",
+    "compression": "^1.7.4",
+    "dotenv-flow": "^3.2.0",
+    "mysql2": "^2.2.5",
+    "typeorm": "^0.2.31"
+  },
+  "devDependencies": {
+    "@tsed/core": "^6.34.3",
+    "@tsed/exceptions": "^6.34.3",
+    "@tsed/json-mapper": "^6.34.3",
+    "@tsed/schema": "^6.34.3",
+    "@typescript-eslint/eslint-plugin": "^4.18.0",
+    "@typescript-eslint/parser": "^4.18.0",
+    "eslint-import-resolver-typescript": "^2.4.0",
+    "ts-jest": "^26.5.4",
+    "ts-node": "^9.1.1",
+    "ts-node-dev": "^1.1.6",
+    "tsc-alias": "1.2.6",
+    "tsconfig-paths": "^3.9.0",
+    "typescript": "^4.2.3"
+  }
+}

+ 94 - 0
packages/slackbot-proxy/src/Server.ts

@@ -0,0 +1,94 @@
+import { Configuration, Inject, InjectorService } from '@tsed/di';
+import { PlatformApplication } from '@tsed/common';
+import '@tsed/platform-express'; // /!\ keep this import
+import bodyParser from 'body-parser';
+import compress from 'compression';
+import cookieParser from 'cookie-parser';
+import methodOverride from 'method-override';
+import '@tsed/swagger';
+import { TypeORMService } from '@tsed/typeorm';
+import { ConnectionOptions } from 'typeorm';
+
+
+export const rootDir = __dirname;
+
+const connectionOptions: ConnectionOptions = {
+  // The 'name' property must be set. Otherwise, the 'name' will be '0' and won't work well. -- 2021.04.05 Yuki Takei
+  // see: https://github.com/TypedProject/tsed/blob/7630cda20a1f6fa3a692ecc3e6cd51d37bc3c45f/packages/typeorm/src/utils/createConnection.ts#L10
+  name: 'default',
+  type: process.env.TYPEORM_CONNECTION,
+  host: process.env.TYPEORM_HOST,
+  port: process.env.TYPEORM_PORT,
+  database: process.env.TYPEORM_DATABASE,
+  username: process.env.TYPEORM_USERNAME,
+  password: process.env.TYPEORM_PASSWORD,
+  synchronize: true,
+} as ConnectionOptions;
+
+
+@Configuration({
+  rootDir,
+  acceptMimes: ['application/json'],
+  httpPort: process.env.PORT || 8080,
+  httpsPort: false, // CHANGE
+  mount: {
+    '/': [
+      `${rootDir}/controllers/*.ts`,
+      `${rootDir}/middlewares/*.ts`,
+    ],
+  },
+  componentsScan: [
+    `${rootDir}/services/*.ts`,
+  ],
+  typeorm: [
+    {
+      ...connectionOptions,
+      entities: [
+        `${rootDir}/entities/*{.ts,.js}`,
+      ],
+      migrations: [
+        `${rootDir}/migrations/*{.ts,.js}`,
+      ],
+      subscribers: [
+        `${rootDir}/subscribers/*{.ts,.js}`,
+      ],
+    } as ConnectionOptions,
+  ],
+  swagger: [
+    {
+      path: '/docs',
+      specVersion: '3.0.1',
+    },
+  ],
+  exclude: [
+    '**/*.spec.ts',
+  ],
+})
+export class Server {
+
+  @Inject()
+  app: PlatformApplication;
+
+  @Configuration()
+  settings: Configuration;
+
+  @Inject()
+  injector: InjectorService;
+
+  $beforeRoutesInit(): void {
+    this.app
+      .use(cookieParser())
+      .use(compress({}))
+      .use(methodOverride())
+      .use(bodyParser.json())
+      .use(bodyParser.urlencoded({
+        extended: true,
+      }));
+  }
+
+  async $onReady(): Promise<void> {
+    // for synchromizing when boot
+    this.injector.get<TypeORMService>(TypeORMService);
+  }
+
+}

+ 93 - 0
packages/slackbot-proxy/src/controllers/slack.ts

@@ -0,0 +1,93 @@
+import {
+  BodyParams, Controller, Get, Inject, Post, Req, Res,
+} from '@tsed/common';
+
+import { Installation } from '~/entities/installation';
+import { InstallationRepository } from '~/repositories/installation';
+import { InstallerService } from '~/services/InstallerService';
+import { ReceiveService } from '~/services/RecieveService';
+
+
+@Controller('/slack')
+export class SlackCtrl {
+
+  @Inject()
+  installerService: InstallerService;
+
+  @Inject()
+  installationRepository: InstallationRepository;
+
+  @Inject()
+  receiveService: ReceiveService;
+
+  @Get('/testsave')
+  testsave(): void {
+    const installation = new Installation();
+    installation.data = {
+      team: undefined,
+      enterprise: undefined,
+      user: {
+        id: '',
+        token: undefined,
+        scopes: undefined,
+      },
+    };
+
+    // const installationRepository = getRepository(Installation);
+
+    this.installationRepository.save(installation);
+  }
+
+
+  @Get('/install')
+  async install(): Promise<string> {
+    const url = await this.installerService.installer.generateInstallUrl({
+      // Add the scopes your app needs
+      scopes: [
+        'channels:history',
+        'commands',
+        'groups:history',
+        'im:history',
+        'mpim:history',
+        'chat:write',
+      ],
+    });
+
+    return `<a href="${url}">`
+      // eslint-disable-next-line max-len
+      + '<img alt="Add to Slack" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcSet="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" />'
+      + '</a>';
+  }
+
+  @Post('/events')
+  handleEvent(@BodyParams() body:{[key:string]:string}, @Res() res: Res): string {
+    // Send response immediately to avoid opelation_timeout error
+    // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
+
+    const slackInput = this.receiveService.receiveContentsFromSlack(body);
+    console.log('Controller/events', slackInput);
+    res.send();
+
+    console.log('body', body);
+
+    return 'This action will be handled by bolt service.';
+  }
+
+  @Get('/oauth_redirect')
+  async handleOauthRedirect(@Req() req: Req, @Res() res: Res): Promise<void> {
+    // illegal state
+    // TODO: https://youtrack.weseek.co.jp/issue/GW-5543
+    if (req.query.state === '') {
+      throw new Error('illegal state');
+    }
+
+    return this.installerService.installer.handleCallback(req, res);
+
+    // TODO: https://youtrack.weseek.co.jp/issue/GW-5543
+    // this.installer.handleCallback(req, res, {
+    //   success: (installation, metadata, req, res) => {},
+    //   failure: (error, installOptions, req, res) => {},
+    // });
+  }
+
+}

+ 43 - 0
packages/slackbot-proxy/src/entities/installation.ts

@@ -0,0 +1,43 @@
+import {
+  Required,
+} from '@tsed/schema';
+import {
+  Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn,
+} from 'typeorm';
+
+import { Installation as SlackInstallation } from '@slack/oauth';
+
+@Entity()
+export class Installation {
+
+  @PrimaryGeneratedColumn()
+  readonly id: number;
+
+  @Column({ type: 'json' })
+  @Required()
+  data: SlackInstallation;
+
+  @CreateDateColumn()
+  readonly createdAt: Date;
+
+  @UpdateDateColumn()
+  readonly updatedAt: Date;
+
+  @Column({ nullable: true })
+  isEnterpriseInstall?: boolean;
+
+  @Column({ nullable: true, unique: true })
+  teamId?: string;
+
+  @Column({ nullable: true, unique: true })
+  enterpriseId?: string;
+
+  setData(slackInstallation: SlackInstallation): void {
+    this.data = slackInstallation;
+
+    this.isEnterpriseInstall = slackInstallation.isEnterpriseInstall;
+    this.teamId = slackInstallation.team?.id;
+    this.enterpriseId = slackInstallation.enterprise?.id;
+  }
+
+}

+ 28 - 0
packages/slackbot-proxy/src/index.ts

@@ -0,0 +1,28 @@
+import { $log } from '@tsed/common';
+import { PlatformExpress } from '@tsed/platform-express';
+
+import { Server } from './Server';
+
+function hasProcessFlag(flag: string): boolean {
+  return process.argv.join('').indexOf(flag) > -1;
+}
+
+async function bootstrap() {
+  try {
+    $log.debug('Start server...');
+    const platform = await PlatformExpress.bootstrap(Server);
+
+    await platform.listen();
+    $log.debug('Server initialized');
+
+    if (hasProcessFlag('ci')) {
+      $log.info('"--ci" flag is detected. Exit process.');
+      process.exit();
+    }
+  }
+  catch (er) {
+    $log.error(er);
+  }
+}
+
+bootstrap();

+ 14 - 0
packages/slackbot-proxy/src/repositories/installation.ts

@@ -0,0 +1,14 @@
+import {
+  Repository, EntityRepository,
+} from 'typeorm';
+
+import { Installation } from '~/entities/installation';
+
+@EntityRepository(Installation)
+export class InstallationRepository extends Repository<Installation> {
+
+  findByID(id: string): Promise<Installation | undefined> {
+    return this.findOne(id);
+  }
+
+}

+ 60 - 0
packages/slackbot-proxy/src/services/InstallerService.ts

@@ -0,0 +1,60 @@
+import {
+  Installation as SlackInstallation, InstallationQuery, InstallProvider,
+} from '@slack/oauth';
+import { Inject, Service } from '@tsed/di';
+
+import { Installation } from '~/entities/installation';
+import { InstallationRepository } from '~/repositories/installation';
+
+@Service()
+export class InstallerService {
+
+  installer: InstallProvider;
+
+  @Inject()
+  private readonly repository: InstallationRepository;
+
+  $onInit(): Promise<any> | void {
+    const clientId = process.env.SLACK_CLIENT_ID;
+    const clientSecret = process.env.SLACK_CLIENT_SECRET;
+    const stateSecret = process.env.SLACK_INSTALLPROVIDER_STATE_SECRET;
+
+    if (clientId === undefined) {
+      throw new Error('The environment variable \'SLACK_CLIENT_ID\' must be defined.');
+    }
+    if (clientSecret === undefined) {
+      throw new Error('The environment variable \'SLACK_CLIENT_SECRET\' must be defined.');
+    }
+
+    const { repository } = this;
+
+    this.installer = new InstallProvider({
+      clientId,
+      clientSecret,
+      stateSecret,
+      installationStore: {
+        storeInstallation: async(slackInstallation: SlackInstallation<'v1' | 'v2', boolean>) => {
+          const installation = new Installation();
+          installation.setData(slackInstallation);
+
+          await repository.save(installation);
+
+          return;
+        },
+        fetchInstallation: async(installQuery: InstallationQuery<boolean>) => {
+          const installation: SlackInstallation<'v1' | 'v2', boolean> = {
+            team: undefined,
+            enterprise: undefined,
+            user: {
+              id: '',
+              token: undefined,
+              scopes: undefined,
+            },
+          };
+          return installation;
+        },
+      },
+    });
+  }
+
+}

+ 16 - 0
packages/slackbot-proxy/src/services/RecieveService.ts

@@ -0,0 +1,16 @@
+import { Service } from '@tsed/di';
+// import { parse } from '@growi/slack/src/utils/slash-command-parser';
+
+@Service()
+export class ReceiveService {
+
+  receiveContentsFromSlack(body:{[key:string]:string}) : string {
+    // const parseBody = parse(body);
+    // if (parseBody.growiCommandType === 'register') {
+    //   console.log('register action occured');
+    //   return 'register action occurd';
+    // }
+    return 'return receiveContentsFromSlack';
+  }
+
+}

+ 12 - 0
packages/slackbot-proxy/tsconfig.build.json

@@ -0,0 +1,12 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "dist",
+    "declaration": true,
+    "noResolve": false,
+    "preserveConstEnums": true,
+    "sourceMap": true,
+    "noEmit": false,
+    "inlineSources": true
+  }
+}

+ 46 - 0
packages/slackbot-proxy/tsconfig.json

@@ -0,0 +1,46 @@
+{
+  "compilerOptions": {
+    "target": "es2019",
+    "module": "commonjs",
+    "lib": ["dom", "dom.iterable", "esnext"],
+    "sourceMap": true,
+    "noEmit": true,
+    "removeComments": false,
+    "importHelpers": true,
+    "isolatedModules": true,
+
+    /* Strict Type-Checking Options */
+    // "strict": true,
+    "strictNullChecks": true,
+    "noImplicitAny": false,
+
+    /* Additional Checks */
+    "noUnusedLocals": false,
+    "noUnusedParameters": false,
+
+    /* Module Resolution Options */
+    "moduleResolution": "node",
+    "baseUrl": "src",
+    "paths": {
+      "~/*": ["./*"],
+      "^/*": ["../*"],
+    },
+    "typeRoots": [
+      "../../node_modules/@types",
+      "./node_modules/@types"
+    ],
+    "allowSyntheticDefaultImports": true,
+    "esModuleInterop": true,
+
+    /* Misc */
+    "preserveConstEnums": true,
+    "forceConsistentCasingInFileNames": true,
+    "resolveJsonModule": true,
+
+    /* Experimental Options */
+    "experimentalDecorators": true,
+    "emitDecoratorMetadata": true
+  },
+  "exclude": ["node_modules", "./public", "dist", "test"],
+  "include": ["./src/**/*.ts"]
+}

+ 1 - 1
src/linter-checker/test.scss

@@ -19,7 +19,7 @@
   background: $gray-300;
   color: $gray-900;
 
-  ul > li {
+  ul>li {
     margin-left: 0;
   }
 }

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


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