├── .gitignore ├── bolt-firebase-functions-js-template ├── .firebaserc ├── .gitignore ├── LICENSE ├── README.md ├── firebase.json └── functions │ ├── .gitignore │ ├── _config.js │ ├── index.js │ ├── package-lock.json │ └── package.json ├── bolt-gcp-js-template ├── .gcloudignore ├── .gitignore ├── README.md ├── _config.js ├── app.js ├── package-lock.json └── package.json ├── bolt-heroku-ts-template ├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── _env ├── app.json ├── deploy_to_heroku.png ├── hello.png ├── package-lock.json ├── package.json ├── src │ └── index.ts └── tsconfig.json ├── gas-clasp-typescript ├── .gitignore ├── README.md ├── _clasp.json ├── package-lock.json ├── package.json └── src │ ├── appsscript.json │ └── slack.ts ├── reacjilator-bolt-typescript ├── .gitignore ├── README.md ├── _env ├── app.ts ├── handler.ts ├── langcode.ts ├── package-lock.json ├── package.json ├── serverless.yml ├── tsconfig.json └── webpack.config.js ├── reacjilator-java-sdk ├── .gitignore ├── README.md ├── _env ├── deploy.sh ├── package-lock.json ├── package.json ├── pom.xml ├── serverless.yml └── src │ ├── main │ ├── java │ │ └── slackapp_backend │ │ │ ├── handler │ │ │ ├── EchoHandler.java │ │ │ ├── SlackEventsHandler.java │ │ │ └── WarmupHandler.java │ │ │ └── service │ │ │ ├── AmazonWebServices.java │ │ │ ├── GoogleTranslateApi.java │ │ │ ├── LangCodes.java │ │ │ ├── SlackEventsOperator.java │ │ │ └── SlackWebApi.java │ └── resources │ │ ├── langcode.json │ │ └── log4j.properties │ └── test │ └── java │ └── slackapp_backend │ └── service │ └── GoogleTranslateApiTest.java ├── reacjilator-python3 ├── .gitignore ├── README.md ├── _env ├── app.py ├── langcode.json └── requirements.txt ├── reacjilator-ruby ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.md ├── _env ├── deploy.sh ├── handler.rb ├── langcode.json ├── package-lock.json ├── package.json └── serverless.yml ├── reacjilator-typescript ├── .gitignore ├── README.md ├── _env ├── app.ts ├── handler.ts ├── langcode.ts ├── package-lock.json ├── package.json ├── scopes.png ├── serverless.yml ├── tsconfig.json └── webpack.config.js ├── serverless-bolt-template ├── aws-js │ ├── .gitignore │ ├── README.md │ ├── _env │ ├── app.js │ ├── handler.js │ ├── package-lock.json │ ├── package.json │ └── serverless.yml └── aws-ts │ ├── .gitignore │ ├── README.md │ ├── _env │ ├── app.ts │ ├── handler.ts │ ├── package-lock.json │ ├── package.json │ ├── serverless.yml │ ├── tsconfig.json │ └── webpack.config.js ├── slack-ruby-bot-examples ├── Gemfile ├── Gemfile.lock └── pongbot.rb └── slack-ruby-client-examples ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.md ├── channels.rb ├── conversations.rb ├── events.rb └── say_hello.rb /.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | 8 | # Webpack directories 9 | .webpack 10 | 11 | .env 12 | 13 | .DS_Store 14 | 15 | .vscode 16 | .metals/ 17 | -------------------------------------------------------------------------------- /bolt-firebase-functions-js-template/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "{your own project name}" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /bolt-firebase-functions-js-template/.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | 8 | # Webpack directories 9 | .webpack 10 | 11 | config.js 12 | -------------------------------------------------------------------------------- /bolt-firebase-functions-js-template/LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) Kazuhiro Sera 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | -------------------------------------------------------------------------------- /bolt-firebase-functions-js-template/README.md: -------------------------------------------------------------------------------- 1 | ## Bolt app running on Cloud Functions for Firebase 2 | 3 | This is a simple Bolt app which runs on Cloud Functions for Firebase. 4 | 5 | * https://slack.dev/bolt/ 6 | * https://firebase.google.com/docs/functions 7 | 8 | ## Setup 9 | 10 | Use node 10.x and its corresponding npm. 11 | 12 | ``` 13 | vi .firebaserc # set your own project 14 | 15 | npm install -g firebase-tools 16 | cd functions 17 | npm i 18 | cp -p _config.js config.js # and modify config.js 19 | cd - 20 | ``` 21 | 22 | ## How to run the app on your laptop 23 | 24 | ```bash 25 | firebase serve 26 | ``` 27 | 28 | ## How to deploy 29 | 30 | ```bash 31 | firebase deploy 32 | ``` 33 | 34 | ## How to configure Slack apps/GCP 35 | 36 | ### Slack App 37 | 38 | Set `https://{your domain}.cloudfunctions.net/slack/events` as the Request URL for event subscriptions. 39 | 40 | ### Cloud Functions for Firebase 41 | 42 | You have nothing to configure. Don't forget enabling billing info if it's your first time to use it. 43 | 44 | ## How to make sure if it works 45 | 46 | Post a message including `hello`. Then you'll receive a message saying `Hey there @yourname` from your bot user! 47 | 48 | ## LICENSE 49 | 50 | The MIT License 51 | -------------------------------------------------------------------------------- /bolt-firebase-functions-js-template/firebase.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /bolt-firebase-functions-js-template/functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /bolt-firebase-functions-js-template/functions/_config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "SLACK_API_TOKEN": "xoxp-xxx", 3 | "SLACK_BOT_TOKEN": "xoxb-xxx", 4 | "SLACK_SIGNING_SECRET": "a3exxx" 5 | }; -------------------------------------------------------------------------------- /bolt-firebase-functions-js-template/functions/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const config = require('./config.js'); 4 | const { App, ExpressReceiver } = require('@slack/bolt'); 5 | const expressReceiver = new ExpressReceiver({ 6 | signingSecret: config.SLACK_SIGNING_SECRET, 7 | endpoints: '/events', 8 | processBeforeResponse: true, 9 | }); 10 | const app = new App({ 11 | receiver: expressReceiver, 12 | token: config.SLACK_BOT_TOKEN, 13 | processBeforeResponse: true, 14 | }); 15 | app.message('hello', async ({ message, say }) => { 16 | await say({ "text": `Hey there <@${message.user}>!` }); 17 | }); 18 | app.error(console.log); 19 | 20 | const functions = require('firebase-functions'); 21 | // https://{your domain}.cloudfunctions.net/slack/events 22 | exports.slack = functions.https.onRequest(expressReceiver.app); 23 | -------------------------------------------------------------------------------- /bolt-firebase-functions-js-template/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": {}, 5 | "engines": { 6 | "node": "10" 7 | }, 8 | "dependencies": { 9 | "@slack/bolt": "^2.7.0", 10 | "firebase-admin": "^8.13.0", 11 | "firebase-functions": "^3.11.0" 12 | }, 13 | "devDependencies": { 14 | "firebase-functions-test": "^0.1.6" 15 | }, 16 | "private": true 17 | } 18 | -------------------------------------------------------------------------------- /bolt-gcp-js-template/.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | node_modules 17 | -------------------------------------------------------------------------------- /bolt-gcp-js-template/.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | 8 | # Webpack directories 9 | .webpack 10 | 11 | config.js 12 | -------------------------------------------------------------------------------- /bolt-gcp-js-template/README.md: -------------------------------------------------------------------------------- 1 | # Slack App backend built with Bolt on Google Cloud Functions 2 | 3 | ## Run the app on your local machine 4 | 5 | ### Configure necessary env variables 6 | 7 | ```bash 8 | cp _config.json config.json 9 | ``` 10 | 11 | ```json 12 | { 13 | "SLACK_API_TOKEN": "xoxp-xxxxxxxxx", 14 | "SLACK_BOT_TOKEN": "xoxb-xxxxxxxxx", 15 | "SLACK_SIGNING_SECRET": "xxx https://api.slack.com/docs/verifying-requests-from-slack", 16 | "SLACK_WEBHOOK_URL": "https://hooks.slack.com/xxx https://api.slack.com/apps/{apiAppId}/incoming-webhooks", 17 | "SLACK_APP_PORT": 3000 18 | } 19 | ``` 20 | 21 | ### Run ngrok proxy on your local machine 22 | 23 | https://ngrok.com/ 24 | 25 | ```bash 26 | ngrok http 3000 27 | ``` 28 | 29 | ### Create a Slack App 30 | 31 | https://api.slack.com/apps 32 | 33 | - Create a Slack App 34 | - Use these information in "App Credentials" (https://api.slack.com/apps/{apiAppId}/general) 35 | - Client ID, Client Secret 36 | - Signing Secret 37 | - Add an Incoming Webhook 38 | - Set the URL as the env variable: `SLACK_WEBHOOK_URL` 39 | - Add a Slash Command 40 | - `/echo` command 41 | - Request URL: `https://{ngrok domain}/slack/events` or `https://{gcp}/{stage}/slack/events` 42 | - Add a Bot user 43 | - Enable `bot` permission & add a bot user 44 | - Enable Interactive Components 45 | - Request URL: `https://{ngrok domain}/slack/events` or `https://{gcp}/{stage}/slack/events` 46 | - Message Actions 47 | - Add a random action 48 | - Message Menus 49 | - Unsupported in this example; If you're interested in this outmoded one, you can return it in any messages with attachments 50 | - Enable Event Subscriptions 51 | - Request URL: `https://{ngrok domain}/slack/events` or `https://{gcp}/{stage}/slack/events` 52 | - Enable `app_metion`, `message.channels` 53 | - Add Necessary Permissions 54 | - `bot` 55 | - `chat:write:bot` 56 | 57 | ```bash 58 | npm i 59 | node app.js 60 | ``` 61 | 62 | ## Deploy the app onto Google Cloud Functions 63 | 64 | ### Put ~/.gcloud/keyfile.json 65 | 66 | https://console.cloud.google.com/apis/credentials 67 | 68 | Create a "Service account key" and save it as `~/.gcloud/keyfile.json`. 69 | 70 | ### Run gcloud CLI 71 | 72 | https://cloud.google.com/sdk/gcloud/ 73 | 74 | ```bash 75 | gcloud functions deploy {NAME} --runtime nodejs10 --trigger-http --entry-point app 76 | ``` 77 | -------------------------------------------------------------------------------- /bolt-gcp-js-template/_config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "SLACK_API_TOKEN": "xoxp-xxxxxxxxx", 3 | "SLACK_BOT_TOKEN": "xoxb-xxxxxxxxx", 4 | "SLACK_SIGNING_SECRET": "xxx https://api.slack.com/docs/verifying-requests-from-slack", 5 | "SLACK_WEBHOOK_URL": "https://hooks.slack.com/xxx https://api.slack.com/apps/{apiAppId}/incoming-webhooks", 6 | "SLACK_APP_PORT": 3000 7 | }; -------------------------------------------------------------------------------- /bolt-gcp-js-template/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const config = require('./config.js'); 4 | 5 | // ------------------------------------------------------ 6 | // Bot app 7 | // https://slack.dev/bolt/ 8 | const { App, ExpressReceiver } = require('@slack/bolt'); 9 | const expressReceiver = new ExpressReceiver({ 10 | signingSecret: config.SLACK_SIGNING_SECRET, 11 | processBeforeResponse: true, 12 | }); 13 | const app = new App({ 14 | token: config.SLACK_BOT_TOKEN, 15 | receiver: expressReceiver, 16 | processBeforeResponse: true, 17 | }); 18 | const expressApp = expressReceiver.app; 19 | 20 | // ------------------------------------------------------ 21 | // If you need to use API methods that are not listed on https://api.slack.com/bot-users#methods 22 | // you need to use user api token instead like this: 23 | const { WebClient } = require('@slack/web-api'); 24 | app.client = new WebClient(config.SLACK_API_TOKEN); 25 | 26 | // ------------------------------------------------------ 27 | 28 | // React to "app_mention" events 29 | app.event('app_mention', async ({ client, event, say }) => { 30 | try { 31 | const res = await client.users.info({ user: event.user }) 32 | if (res.ok) { 33 | await say({ 34 | text: `Hi! <@${res.user.name}>` 35 | }); 36 | } else { 37 | console.error(`Failed because of ${res.error}`) 38 | } 39 | } catch (reason) { 40 | console.error(`Failed because ${reason}`) 41 | } 42 | }); 43 | 44 | // React to message.channels event 45 | app.message('hello', async ({ message, say }) => { 46 | // say() sends a message to the channel where the event was triggered 47 | await say({ 48 | blocks: [ 49 | { 50 | "type": "section", 51 | "text": { 52 | "type": "mrkdwn", 53 | "text": `Hey there <@${message.user}>!` 54 | }, 55 | "accessory": { 56 | "type": "button", 57 | "text": { 58 | "type": "plain_text", 59 | "text": "Click Me" 60 | }, 61 | "action_id": "button_click" 62 | } 63 | } 64 | ] 65 | }); 66 | }); 67 | 68 | // Handle the click event (action_id: button_click) on a message posted by the above hello handler 69 | app.action('button_click', async ({ body, ack, say }) => { 70 | // Acknowledge the action 71 | await ack(); 72 | await say(`<@${body.user.id}> clicked the button`); 73 | }); 74 | 75 | // Handle `/echo` command invocations 76 | app.command('/echo', async ({ command, ack, say }) => { 77 | // Acknowledge command request 78 | await ack(); 79 | await say(`You said "${command.text}"`); 80 | }); 81 | 82 | // A simple example to use WebApi client 83 | app.message('42', async ({ message, say }) => { 84 | console.log(`Got a message: ${JSON.stringify(message)}`); 85 | await say('The answer to life, the universe and everything'); 86 | }) 87 | 88 | // A simple example to use Webhook internally 89 | app.message('webhook', async ({ message }) => { 90 | const { IncomingWebhook } = require('@slack/webhook'); 91 | const url = config.SLACK_WEBHOOK_URL; 92 | const webhook = new IncomingWebhook(url); 93 | await webhook.send({ 94 | text: message.text.split("webhook")[1], 95 | unfurl_links: true 96 | }) 97 | .then((res) => { 98 | console.log(`Succeeded ${JSON.stringify(res)}`) 99 | }).catch(reason => { 100 | console.error(`Failed because ${reason}`) 101 | }) 102 | }) 103 | 104 | // Check the details of the error to handle cases where you should retry sending a message or stop the app 105 | app.error((error) => { 106 | console.error(error); 107 | }); 108 | 109 | // ------------------------------------------------------ 110 | 111 | function isOnGoogleCloud() { 112 | // https://cloud.google.com/functions/docs/env-var#nodejs_10_and_subsequent_runtimes 113 | return process.env.K_SERVICE && process.env.K_REVISION; 114 | } 115 | 116 | if (!isOnGoogleCloud()) { 117 | // Running on your local machine 118 | (async () => { 119 | // Start your app 120 | expressApp.listen(config.SLACK_APP_PORT || 3000); 121 | console.log('⚡️ Slack app is running!'); 122 | })(); 123 | } 124 | 125 | module.exports.app = function (req, res) { 126 | console.log(`Got a request: ${JSON.stringify(req.headers)}`) 127 | if (req.rawBody) { 128 | console.log(`Got raw request: ${req.rawBody}`) 129 | } 130 | expressApp(req, res); 131 | }; 132 | -------------------------------------------------------------------------------- /bolt-gcp-js-template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bot-serverless-gcp", 3 | "version": "1.0.0", 4 | "description": "Slack app built with Bolt", 5 | "main": "app.js", 6 | "dependencies": { 7 | "@slack/bolt": "^2.2.3", 8 | "@slack/web-api": "^5.11.0", 9 | "@types/express": "^4.17.8", 10 | "@types/node": "^14.6.4", 11 | "concat-stream": "^2.0.0", 12 | "please-upgrade-node": "^3.1.1", 13 | "raw-body": "^2.3.3", 14 | "tsscmp": "^1.0.6" 15 | }, 16 | "devDependencies": {}, 17 | "scripts": { 18 | "start": "node app.js", 19 | "test": "echo \"Error: no test specified\" && exit 1" 20 | }, 21 | "keywords": [ 22 | "Slack", 23 | "Serverless" 24 | ], 25 | "author": "Awesome developers", 26 | "license": "MIT" 27 | } 28 | -------------------------------------------------------------------------------- /bolt-heroku-ts-template/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | jspm_packages 3 | .serverless 4 | .webpack 5 | .env 6 | lib/ 7 | -------------------------------------------------------------------------------- /bolt-heroku-ts-template/LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) Kazuhiro Sera 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /bolt-heroku-ts-template/Procfile: -------------------------------------------------------------------------------- 1 | web: npm start -------------------------------------------------------------------------------- /bolt-heroku-ts-template/README.md: -------------------------------------------------------------------------------- 1 | # Slack Bolt app on Heroku 2 | 3 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/seratch/bolt-on-heroku/tree/master) 4 | 5 | ## Slack ⚡️ Bolt 6 | 7 | A framework to build Slack apps, fast. 8 | 9 | * https://slack.dev/bolt 10 | * https://github.com/SlackAPI/bolt 11 | 12 | ## How to build 13 | 14 | ### Create a Slack App 15 | 16 | https://api.slack.com/apps 17 | 18 | * Features > OAuth & Permissions: 19 | * Scopes: 20 | * "channels:history" 21 | * "chat:write:bot" 22 | * "bot" 23 | * Click "Save Changes" 24 | * Features > Bot User: 25 | * Click "Add a Bot User" 26 | * Click "Add Bot User" 27 | * Settings > Install App: 28 | * Complete "Install App" 29 | 30 | ### Deploy to Heroku 31 | 32 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/seratch/bolt-on-heroku/tree/master) 33 | 34 | * Set env variables on Heroku 35 | * (Slack) Settings > Basic Information > App Credentials > Siginging Secret 36 | * (Slack) Settings > Install App > Bot User OAuth Access Token 37 | 38 | [![Heroku deployment page](https://raw.githubusercontent.com/seratch/bolt-on-heroku/master/deploy_to_heroku.png)](https://heroku.com/deploy?template=https://github.com/seratch/bolt-on-heroku/tree/master) 39 | 40 | ### Enable Slack Events Subscription 41 | 42 | * Features > Event Subscriptions: 43 | * Enable Events: 44 | * Change from "Off" to "On" 45 | * Request URL: 46 | * Set "https://{your app name}.herokuapp.com/slack/events" 47 | * Subscribe to Workspace Events: 48 | * Add "message.channels" 49 | * Click "Save Changes" 50 | 51 | ### Try the Slack App 52 | 53 | * Invite your bot to a Slack channel 54 | * Post "hello" in the channel 55 | * You'll receive a response from the bot 56 | 57 | ![hello](https://raw.githubusercontent.com/seratch/bolt-on-heroku/master/hello.png) 58 | 59 | -------------------------------------------------------------------------------- /bolt-heroku-ts-template/_env: -------------------------------------------------------------------------------- 1 | export SLACK_SIGNING_SECRET=cda6e9cbf46a******************** 2 | export SLACK_BOT_TOKEN=xoxb-************-************-************************ 3 | -------------------------------------------------------------------------------- /bolt-heroku-ts-template/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Bolt app on Heroku", 3 | "description": "Bolt example app in TypeScript on Heroku", 4 | "repository": "https://github.com/seratch/bolt-on-heroku", 5 | "keywords": [ 6 | "Slack", 7 | "Slack API", 8 | "Heroku" 9 | ], 10 | "env": { 11 | "SLACK_SIGNING_SECRET": { 12 | "description": "Slack Signing Secret", 13 | "value": "cda6e9cbf46a********************" 14 | }, 15 | "SLACK_BOT_TOKEN": { 16 | "description": "Slack App Bot Token", 17 | "value": "xoxb-************-************-************************" 18 | } 19 | }, 20 | "image": "heroku/nodejs" 21 | } 22 | -------------------------------------------------------------------------------- /bolt-heroku-ts-template/deploy_to_heroku.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seratch/slack-app-examples/87f679e6856b3d0f1e86bd21c9a37d72ba52e9b0/bolt-heroku-ts-template/deploy_to_heroku.png -------------------------------------------------------------------------------- /bolt-heroku-ts-template/hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seratch/slack-app-examples/87f679e6856b3d0f1e86bd21c9a37d72ba52e9b0/bolt-heroku-ts-template/hello.png -------------------------------------------------------------------------------- /bolt-heroku-ts-template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bolt-on-heroku", 3 | "version": "1.0.0", 4 | "description": "Bolt app which runs on Heroku", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npm run build && node lib/index.js", 8 | "local": "npm run build:live", 9 | "build": "tsc -p .", 10 | "build:live": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/seratch/bolt-on-heroku.git" 15 | }, 16 | "keywords": [ 17 | "Slack", 18 | "Bolt", 19 | "Heroku" 20 | ], 21 | "author": "Kazuhiro Sera @seratch", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/seratch/bolt-on-heroku/issues" 25 | }, 26 | "homepage": "https://github.com/seratch/bolt-on-heroku#readme", 27 | "dependencies": { 28 | "@slack/bolt": "^2.2.3", 29 | "@types/node": "^12.0.10", 30 | "nodemon": "^2.0.20", 31 | "ts-node": "^9.0.0", 32 | "typescript": "^4.0.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /bolt-heroku-ts-template/src/index.ts: -------------------------------------------------------------------------------- 1 | import { App, ExpressReceiver } from '@slack/bolt'; 2 | import { ConsoleLogger, LogLevel } from '@slack/logger'; 3 | import { Request, Response } from 'express'; 4 | 5 | const logger = new ConsoleLogger(); 6 | logger.setLevel(LogLevel.DEBUG); 7 | const expressReceiver = new ExpressReceiver({ 8 | signingSecret: process.env.SLACK_SIGNING_SECRET!, 9 | logger: logger 10 | }); 11 | const app = new App({ 12 | token: process.env.SLACK_BOT_TOKEN, 13 | receiver: expressReceiver 14 | }); 15 | 16 | app.message('hello', async ({ message, say }) => { 17 | await say(`Hey there <@${message.user}>\!`); 18 | }); 19 | 20 | (async () => { 21 | // Start your app 22 | 23 | await app.start(process.env.PORT || 3000); 24 | // await expressApp.listen(process.env.PORT || 3000); 25 | 26 | // const wrapper = express(); 27 | // const appWrapper = wrapper((req: Request, res: Response) => { 28 | // return expressReceiver.app(req, res); 29 | // }); 30 | // expressReceiver.app = appWrapper; 31 | // await appWrapper.listen(process.env.PORT || 3000); 32 | // const express = require('express'); 33 | // const expressApp: Application = express(); 34 | // expressApp.use(function (req, _res, next) { 35 | // console.log('aaaaa'); 36 | // console.log(req.body); 37 | // next(); 38 | // }); 39 | // await expressApp.listen(process.env.PORT || 3000); 40 | console.log('⚡️ Bolt app is running!'); 41 | })(); 42 | 43 | app.message('block', async ({ say }) => { 44 | // say() sends a message to the channel where the event was triggered 45 | await say({ 46 | text: '', 47 | blocks: [ 48 | { 49 | "type": "section", 50 | "text": { 51 | "type": "mrkdwn", 52 | "text": "You can add a button alongside text in your message. " 53 | }, 54 | "accessory": { 55 | "type": "button", 56 | "text": { 57 | "type": "plain_text", 58 | "text": "Button", 59 | "emoji": true 60 | }, 61 | "value": "click_me_123", 62 | "action_id": "aid" 63 | } 64 | }, 65 | { 66 | "type": "section", 67 | "text": { 68 | "type": "mrkdwn", 69 | "text": "Pick an item from the dropdown list" 70 | }, 71 | "accessory": { 72 | "type": "static_select", 73 | "placeholder": { 74 | "type": "plain_text", 75 | "text": "Select an item", 76 | "emoji": true 77 | }, 78 | "options": [ 79 | { 80 | "text": { 81 | "type": "plain_text", 82 | "text": "Choice 1", 83 | "emoji": true 84 | }, 85 | "value": "value-0" 86 | }, 87 | { 88 | "text": { 89 | "type": "plain_text", 90 | "text": "Choice 2", 91 | "emoji": true 92 | }, 93 | "value": "value-1" 94 | }, 95 | { 96 | "text": { 97 | "type": "plain_text", 98 | "text": "Choice 3", 99 | "emoji": true 100 | }, 101 | "value": "value-2" 102 | } 103 | ] 104 | } 105 | } 106 | ] 107 | }); 108 | }); 109 | 110 | /* 111 | // 1) Use Block Kit 112 | * https://api.slack.com/block-kit 113 | * https://api.slack.com/tools/block-kit-builder 114 | 115 | app.message('hello', ({ message, say }) => { 116 | // say() sends a message to the channel where the event was triggered 117 | say({ 118 | text: '', 119 | blocks: [ 120 | { 121 | "type": "section", 122 | "text": { 123 | "type": "mrkdwn", 124 | "text": "You can add a button alongside text in your message. " 125 | }, 126 | "accessory": { 127 | "type": "button", 128 | "text": { 129 | "type": "plain_text", 130 | "text": "Button", 131 | "emoji": true 132 | }, 133 | "value": "click_me_123" 134 | } 135 | }, 136 | { 137 | "type": "section", 138 | "text": { 139 | "type": "mrkdwn", 140 | "text": "Pick an item from the dropdown list" 141 | }, 142 | "accessory": { 143 | "type": "static_select", 144 | "placeholder": { 145 | "type": "plain_text", 146 | "text": "Select an item", 147 | "emoji": true 148 | }, 149 | "options": [ 150 | { 151 | "text": { 152 | "type": "plain_text", 153 | "text": "Choice 1", 154 | "emoji": true 155 | }, 156 | "value": "value-0" 157 | }, 158 | { 159 | "text": { 160 | "type": "plain_text", 161 | "text": "Choice 2", 162 | "emoji": true 163 | }, 164 | "value": "value-1" 165 | }, 166 | { 167 | "text": { 168 | "type": "plain_text", 169 | "text": "Choice 3", 170 | "emoji": true 171 | }, 172 | "value": "value-2" 173 | } 174 | ] 175 | } 176 | } 177 | ] 178 | }); 179 | }); 180 | 181 | 182 | 183 | */ -------------------------------------------------------------------------------- /bolt-heroku-ts-template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "lib": ["es6","dom"], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | "outDir": "lib", /* Redirect output structure to the directory. */ 15 | "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "incremental": true, /* Enable incremental compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | 52 | /* Source Map Options */ 53 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 55 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 56 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 57 | 58 | /* Experimental Options */ 59 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 60 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 61 | 62 | /* Advanced Options */ 63 | "resolveJsonModule": true /* Include modules imported with '.json' extension */ 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /gas-clasp-typescript/.gitignore: -------------------------------------------------------------------------------- 1 | .clasp.json 2 | config -------------------------------------------------------------------------------- /gas-clasp-typescript/README.md: -------------------------------------------------------------------------------- 1 | # GAS (Google Apps Script) Slack Web API Examples 2 | 3 | ## Prerequisites 4 | 5 | * clasp - https://github.com/google/clasp 6 | * Slack App - https://api.slack.com/apps 7 | 8 | ## Development 9 | 10 | Run `npm i` then edit *.ts under `src`. 11 | 12 | ## Deployment 13 | 14 | ```bash 15 | clasp push && clasp open 16 | ``` 17 | 18 | Once the script has been created, set `SLACK_API_TOKEN` property in [ScriptProperties](https://developers.google.com/apps-script/reference/properties/properties-service#getScriptProperties()) -------------------------------------------------------------------------------- /gas-clasp-typescript/_clasp.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptId": "", 3 | "rootDir": "./src" 4 | } -------------------------------------------------------------------------------- /gas-clasp-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gas-clasp-typescript", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "Kazuhiro Sera", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "@google/clasp": "^2.4.2", 14 | "@slack/types": "^1.8.0", 15 | "@slack/web-api": "^5.11.0", 16 | "@types/google-apps-script": "0.0.46", 17 | "axios": "^0.21.2", 18 | "seratch-slack-types": "^0.2.7", 19 | "tslint": "^5.18.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /gas-clasp-typescript/src/appsscript.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeZone": "America/New_York", 3 | "dependencies": { 4 | }, 5 | "exceptionLogging": "STACKDRIVER" 6 | } -------------------------------------------------------------------------------- /gas-clasp-typescript/src/slack.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as A from '@slack/web-api'; 4 | import * as R from 'seratch-slack-types/web-api'; 5 | 6 | // --------------------------- 7 | // Examples 8 | 9 | const apiToken = PropertiesService.getScriptProperties().getProperty("SLACK_API_TOKEN"); 10 | 11 | // https://api.slack.com/methods/api.test 12 | function apiTest(): R.ApiTestResponse { 13 | const httpResponse = UrlFetchApp.fetch( 14 | url('api.test'), 15 | params(apiToken, { 16 | 'foo': 'bar' 17 | })); 18 | const response = as(httpResponse); 19 | print(response); 20 | return response; 21 | } 22 | 23 | // https://api.slack.com/methods/chat.postMessage 24 | function chatPostMessage(text: string): R.ChatPostMessageResponse { 25 | const httpResponse = UrlFetchApp.fetch( 26 | url('chat.postMessage'), 27 | params(apiToken, { 28 | channel: '#random', 29 | text: text || 'Hi!' 30 | })); 31 | const response = as(httpResponse); 32 | print(response); 33 | console.log(`Posted: ${json(response.message)}`); 34 | return response; 35 | } 36 | 37 | // https://api.slack.com/methods/channels.list 38 | function channelsList(): R.ChannelsListResponse { 39 | const response = as(UrlFetchApp.fetch( 40 | url('channels.list'), 41 | params(apiToken, { 42 | exclude_archived: true, 43 | exclude_members: true 44 | }) 45 | )); 46 | // `response.channels` can be a large array 47 | // print(response); 48 | if (response.error) console.error(`Got an error from Slack API: ${response.error}`) 49 | return response; 50 | } 51 | 52 | // https://api.slack.com/methods/conversations.history 53 | function conversationsHistory() { 54 | const channels = channelsList().channels; 55 | // #find method is not allowed on GAS 56 | const channelId: string = channels.reduce((prev, current) => { 57 | if (prev) return prev; 58 | else if (current.name == 'random') return current; 59 | else return null; 60 | }, null).id; 61 | console.log(`#random: ${channelId}`); 62 | 63 | const httpResponse = UrlFetchApp.fetch( 64 | url('conversations.history'), 65 | params(apiToken, { 66 | channel: channelId 67 | }) 68 | ); 69 | const response = as(httpResponse); 70 | // `response.messages` can be a large array 71 | // print(response); 72 | if (response.error) console.error(`Got an error from Slack API: ${response.error}`) 73 | 74 | response.messages.forEach(message => { 75 | const messagePart = `${message.text} by @${message.username} ts:${message.ts}`; 76 | const reactions = message.reactions 77 | ? message.reactions.map((r) => r.name + " * " + r.count) 78 | : []; 79 | const reactionsPart = reactions.length > 0 ? ` (reactions: ${reactions} )` : ''; 80 | console.log(`${messagePart}${reactionsPart}`) 81 | }); 82 | } 83 | 84 | // https://api.slack.com/methods/emoji.list 85 | function emojiList(): R.EmojiListResponse { 86 | const response = as(UrlFetchApp.fetch( 87 | url('emoji.list'), 88 | params(apiToken, { 89 | }) 90 | )); 91 | 92 | print(response); 93 | return response; 94 | } 95 | 96 | // https://api.slack.com/methods/users.lookupByEamil 97 | function usersLookupByEamil() { 98 | const url = 'https://slack.com/api/users.lookupByEmail'; 99 | const httpResponse = UrlFetchApp.fetch(url, params( 100 | apiToken, 101 | { email: '{your email address}' } 102 | )); 103 | const response = JSON.parse(httpResponse.getContentText()) as R.UsersLookupByEmailResponse; 104 | print(response); 105 | return response; 106 | } 107 | 108 | // --------------------------- 109 | // Common Functions 110 | 111 | function url(apiName: string): string { 112 | return `https://slack.com/api/${apiName}`; 113 | } 114 | 115 | function json(value: object): string { 116 | return JSON.stringify(value); 117 | } 118 | 119 | import { WebAPICallOptions } from '@slack/web-api/dist/WebClient'; 120 | 121 | function params( 122 | token: string, 123 | payload: A): GoogleAppsScript.URL_Fetch.URLFetchRequestOptions { 124 | return { 125 | method: 'post', 126 | contentType: 'application/x-www-form-urlencoded', 127 | headers: { 'Authorization': `Bearer ${token}` }, 128 | payload: payload, 129 | }; 130 | } 131 | 132 | function as(response: GoogleAppsScript.URL_Fetch.HTTPResponse): T { 133 | return JSON.parse(response.getContentText()) as T; 134 | } 135 | 136 | function print(response: any) { 137 | if (response.ok) { 138 | console.log(`Successfully got a response from Slack API: ${json(response)}`); 139 | } else { 140 | console.error(`Got an error from Slack API: ${response.error}`); 141 | } 142 | } -------------------------------------------------------------------------------- /reacjilator-bolt-typescript/.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | 8 | # Webpack directories 9 | .webpack -------------------------------------------------------------------------------- /reacjilator-bolt-typescript/README.md: -------------------------------------------------------------------------------- 1 | ## reacjilator in TypeScript 2 | 3 | Original: https://github.com/slackapi/reacjilator 4 | 5 | ## How to run the app 6 | 7 | ### Prerequisites 8 | 9 | * Google Translate API Token 10 | * https://console.cloud.google.com/apis/api/translate.googleapis.com/credentials 11 | 12 | * Slack App 13 | * Create a new one here: https://api.slack.com/apps 14 | * Bot Users: https://api.slack.com/apps/{apiAppId}/bots 15 | * Scopes: https://api.slack.com/apps/{apiAppId}/oauth 16 | 17 | ![Scopes](https://raw.githubusercontent.com/seratch/slack-app-examples/master/reacjilator-typescript/scopes.png "Scopes") 18 | 19 | * https://ngrok.com/ 20 | 21 | ### Launch the app from the Terminal 22 | 23 | ```bash 24 | npm i serveless -g 25 | npm i 26 | cp _env .env 27 | # edit .env 28 | source .env 29 | serverless offline --printOutput 30 | ``` 31 | -------------------------------------------------------------------------------- /reacjilator-bolt-typescript/_env: -------------------------------------------------------------------------------- 1 | # source .env 2 | export SLACK_BOT_TOKEN=xoxb-xxx 3 | export SLACK_API_TOKEN=xoxp-xxx 4 | export SLACK_SIGNING_SECRET=6c34xxxxxxxxxx 5 | export GOOGLE_PROJECT_ID=xxx 6 | export GOOGLE_KEY=xxx-xxx-xxx-xxx 7 | -------------------------------------------------------------------------------- /reacjilator-bolt-typescript/app.ts: -------------------------------------------------------------------------------- 1 | // 2 | // A TypeScript implementation of https://github.com/slackapi/reacjilator 3 | // 4 | // Author: Kazuhiro Sera @seratch 5 | // MIT License as with the original code 6 | // 7 | 8 | // ---------------- 9 | // Slack Web API Types 10 | import * as SlackWebApi from 'seratch-slack-types/web-api'; 11 | 12 | // Slack Web Client with sufficient scopes 13 | import * as SlackClient from '@slack/web-api'; 14 | const slackApiClient = new SlackClient.WebClient(process.env.SLACK_API_TOKEN); 15 | 16 | // ---------------- 17 | // Google Translate API 18 | import { Translate as GoogleTranslateApi } from '@google-cloud/translate'; 19 | // https://console.cloud.google.com/apis/api/translate.googleapis.com/credentials?project={Project ID} 20 | // $ export GOOGLE_PROJECT_ID={Project ID} 21 | // $ export GOOGLE_KEY={API Key} 22 | const googleApiCredentials = { 23 | projectId: process.env.GOOGLE_PROJECT_ID, 24 | key: process.env.GOOGLE_KEY 25 | } 26 | const googleApi: GoogleTranslateApi = new GoogleTranslateApi(googleApiCredentials); 27 | 28 | // ---------------- 29 | // Enable debug logging if true 30 | const debug: boolean = true; 31 | // lang code mapping data 32 | import { langcode } from './langcode'; 33 | 34 | // ---------------- 35 | // Slack Bolt App 36 | import { App, ExpressReceiver } from '@slack/bolt'; 37 | 38 | const expressReceiver = new ExpressReceiver({ 39 | signingSecret: process.env.SLACK_SIGNING_SECRET 40 | // Endpoints will be attached later by calling the declared methods in Bolt's App 41 | }); 42 | export const expressApp = expressReceiver.app; 43 | 44 | const app: App = new App({ 45 | token: process.env.SLACK_BOT_TOKEN, 46 | receiver: expressReceiver 47 | }); 48 | 49 | app.event('reaction_added', async ({ event }) => { 50 | try { 51 | if (debug) { 52 | console.log(event); 53 | } 54 | if (event.item['type'] !== 'message') { 55 | // Skip any events apart from reactions put on messages 56 | return; 57 | } 58 | const reactionName = event.reaction; 59 | let country: string = null; 60 | // Check the reaction name if it is a country flag 61 | if (reactionName.match(/flag-/)) { // when the name has flag- prefix 62 | country = reactionName.match(/(?!flag-\b)\b\w+/)[0]; 63 | } else { // jp, fr, etc. 64 | const flags = Object.keys(langcode.All); // array 65 | if (flags.includes(reactionName)) { 66 | country = reactionName; 67 | } else { 68 | return; 69 | } 70 | } 71 | // Finding a lang based on a country is not the best way but oh well 72 | // Matching ISO 639-1 language code 73 | const lang: string = langcode.All[country]; 74 | if (!lang) { 75 | return; 76 | } 77 | if (debug) { 78 | console.log(`Detected country: ${country}, lang: ${lang} from reaction: ${reactionName}`); 79 | } 80 | 81 | const channelId: string = event.item['channel']; 82 | const messageTs: string = event.item['ts']; 83 | 84 | // Fetch all the messages in the thread 85 | const repliesRes: SlackWebApi.ConversationsRepliesResponse = 86 | await slackApiClient.conversations.replies({ 87 | channel: channelId, 88 | ts: messageTs, 89 | inclusive: true 90 | }); 91 | if (debug) { 92 | console.log(repliesRes.messages); 93 | } 94 | const messages = repliesRes.messages; 95 | const message = messages[0]; 96 | if (message.text) { 97 | // Call Google Translate API to get a translated text 98 | googleApi.translate(message.text, lang) 99 | .then((array) => { 100 | const [translatedText, googleApiRes] = array; // [string, r.Response] 101 | if (debug) { 102 | console.log(`Response from Google Translate API: ${JSON.stringify(googleApiRes)}`); 103 | } 104 | 105 | // To avoid posting same messages several times, make sure if a same message in the thread doesn't exist 106 | let alreadyPosted: boolean = false; 107 | messages.forEach(messageInTheThread => { 108 | if (!alreadyPosted && messageInTheThread.text && messageInTheThread.text === translatedText) { 109 | alreadyPosted = true; 110 | } 111 | }); 112 | if (alreadyPosted) { 113 | return; 114 | } 115 | 116 | // Post the translated text as a following message in the thread 117 | slackApiClient.chat.postMessage({ 118 | channel: channelId, 119 | text: translatedText, 120 | as_user: false, 121 | username: "Reacjilator Bot", 122 | thread_ts: message.thread_ts ? message.thread_ts : message.ts 123 | }) 124 | .then((postRes: SlackWebApi.ChatPostMessageResponse) => { 125 | if (postRes.ok) { 126 | console.log(`Successfully posted a translated message (ts: ${postRes.ts})`); 127 | } else { 128 | if (debug) { 129 | console.error(postRes); 130 | } 131 | console.error(`Got an error from chat.postMessage (error: ${postRes.error})`); 132 | } 133 | }) 134 | .catch(reason => { 135 | console.error(`Failed to post a message because ${reason}`); 136 | }) 137 | 138 | }) 139 | .catch(reason => { 140 | console.error(`Failed to call Google Translate API because ${reason}`); 141 | }) 142 | 143 | } else { 144 | console.log(`Skipped the message because it doesn't have text property (ts: ${message.ts})`); 145 | } 146 | } 147 | catch (error) { 148 | console.error(error); 149 | } 150 | }); -------------------------------------------------------------------------------- /reacjilator-bolt-typescript/handler.ts: -------------------------------------------------------------------------------- 1 | import 'source-map-support/register'; 2 | import { expressApp } from './app'; 3 | export const dispatcher = require('serverless-http')(expressApp); -------------------------------------------------------------------------------- /reacjilator-bolt-typescript/langcode.ts: -------------------------------------------------------------------------------- 1 | export module langcode { 2 | export const All = { 3 | ac: 'en', 4 | ad: 'ca', 5 | ae: 'ar', 6 | af: 'ps', 7 | ag: 'en', 8 | ai: 'en', 9 | al: 'sq', 10 | am: 'hy', 11 | ao: 'pt', 12 | aq: '', 13 | ar: 'es', 14 | as: 'en', 15 | at: 'de', 16 | au: 'en', 17 | aw: 'nl', 18 | ax: 'sv', 19 | az: '', 20 | ba: 'bs', 21 | bb: 'en', 22 | bd: 'bn', 23 | be: 'nl', 24 | bf: 'fr', 25 | bg: 'bg', 26 | bh: 'ar', 27 | bi: 'fr', 28 | bj: 'fr', 29 | bl: 'fr', 30 | bn: 'en', 31 | bm: 'ms', 32 | bo: 'es', 33 | bq: 'nl', 34 | br: 'pt', 35 | bs: 'en', 36 | bt: '', 37 | bv: '', 38 | bw: 'en', 39 | by: 'be', 40 | bz: 'en', 41 | ca: 'en', 42 | cc: 'ms', 43 | cd: 'fr', 44 | cf: 'fr', 45 | cg: 'fr', 46 | ch: 'de', 47 | ci: 'fr', 48 | ck: 'en', 49 | cl: 'es', 50 | cm: 'fr', 51 | cn: 'zh-CN', 52 | co: 'es', 53 | cp: 'fr', 54 | cr: 'es', 55 | cu: 'es', 56 | cv: 'pt', 57 | cw: 'nl', 58 | cx: 'en', 59 | cy: 'el', 60 | cz: 'cs', 61 | de: 'de', 62 | dg: '', 63 | dj: 'fr', 64 | dk: 'da', 65 | dm: 'en', 66 | do: 'es', 67 | dz: 'ar', 68 | ea: 'es', 69 | ec: 'es', 70 | ee: 'et', 71 | eg: 'ar', 72 | eh: 'ar', 73 | er: '', 74 | es: 'es', 75 | et: '', 76 | eu: '', 77 | fi: 'fi', 78 | fj: 'en', 79 | fk: 'en', 80 | fm: 'en', 81 | fo: '', 82 | fr: 'fr', 83 | ga: 'fr', 84 | gb: 'en', 85 | gd: 'en', 86 | ge: 'ka', 87 | gf: 'fr', 88 | gg: 'en', 89 | gh: 'en', 90 | gi: 'en', 91 | gl: 'da', 92 | gm: 'en', 93 | gn: 'fr', 94 | gp: 'fr', 95 | gq: 'es', 96 | gr: 'el', 97 | gs: 'en', 98 | gt: 'es', 99 | gu: 'en', 100 | gw: 'pt', 101 | gy: 'en', 102 | hk: 'zh-TW', 103 | hm: '', 104 | hn: 'es', 105 | hr: 'hr', 106 | ht: 'ht', 107 | hu: 'hu', 108 | ic: 'es', 109 | id: 'id', 110 | ie: 'ga', 111 | il: 'iw', 112 | im: 'en', 113 | in: 'hi', 114 | io: 'en', 115 | iq: 'ar', 116 | ir: 'fa', 117 | is: 'is', 118 | it: 'it', 119 | je: 'en', 120 | jm: 'en', 121 | jo: 'ar', 122 | jp: 'ja', 123 | ke: 'en', 124 | kg: 'ky', 125 | kh: 'km', 126 | ki: 'en', 127 | km: '', 128 | kn: 'en', 129 | kp: 'ko', 130 | kr: 'ko', 131 | kw: 'ar', 132 | ky: 'en', 133 | kz: 'kk', 134 | la: 'lo', 135 | lb: 'ar', 136 | lc: 'en', 137 | li: 'de', 138 | lk: 'si', 139 | lr: 'en', 140 | ls: 'st', 141 | lt: 'lt', 142 | lu: 'lb', 143 | lv: 'lv', 144 | ly: 'ar', 145 | ma: 'ar', 146 | mc: 'fr', 147 | md: 'ro', 148 | me: '', 149 | mf: '', 150 | mg: 'mg', 151 | mh: '', 152 | mk: 'mk', 153 | ml: 'fr', 154 | mm: '', 155 | mn: 'mn', 156 | mo: 'zh-TW', 157 | mp: 'en', 158 | mq: 'fr', 159 | mr: 'ar', 160 | ms: 'en', 161 | mt: 'mt', 162 | mu: 'en', 163 | mv: '', 164 | mw: 'en', 165 | mx: 'es', 166 | my: 'ms', 167 | mz: 'pt', 168 | na: 'en', 169 | nc: 'fr', 170 | ne: 'fr', 171 | nf: 'en', 172 | ng: 'en', 173 | ni: 'es', 174 | nl: 'nl', 175 | no: 'no', 176 | np: 'ne', 177 | nr: '', 178 | nu: '', 179 | nz: 'en', 180 | om: 'ar', 181 | pa: 'es', 182 | pe: 'es', 183 | pf: 'fr', 184 | pg: '', 185 | ph: 'tl', 186 | pk: 'ur', 187 | pl: 'pl', 188 | pm: 'fr', 189 | pn: 'en', 190 | pr: 'es', 191 | ps: 'ar', 192 | pt: 'pt', 193 | pw: 'en', 194 | py: 'es', 195 | qa: 'ar', 196 | re: 'fr', 197 | ro: 'ro', 198 | rs: 'sr', 199 | ru: 'ru', 200 | rw: '', 201 | sa: 'ar', 202 | sb: 'en', 203 | sc: 'en', 204 | sd: 'ar', 205 | se: 'sv', 206 | sg: 'en', 207 | sh: 'en', 208 | si: 'sl', 209 | sj: 'no', 210 | sk: 'sk', 211 | sl: 'en', 212 | sm: 'it', 213 | sn: 'fr', 214 | so: 'so', 215 | sr: 'nl', 216 | ss: 'en', 217 | st: 'pt', 218 | sv: 'es', 219 | sx: 'nl', 220 | sw: 'ar', 221 | sz: '', 222 | ta: 'en', 223 | tc: 'en', 224 | td: 'fr', 225 | tf: 'fr', 226 | tg: 'fr', 227 | th: 'th', 228 | tj: 'tg', 229 | tk: '', 230 | tl: '', 231 | tm: '', 232 | tn: 'ar', 233 | to: '', 234 | tr: 'tr', 235 | tt: 'en', 236 | tv: '', 237 | tw: 'zh-TW', 238 | tz: 'sw', 239 | ua: 'uk', 240 | ug: 'en', 241 | um: 'en', 242 | us: 'en', 243 | uy: 'es', 244 | uz: 'uz', 245 | va: 'it', 246 | vc: 'en', 247 | ve: 'es', 248 | vg: 'en', 249 | vi: 'en', 250 | vn: 'vi', 251 | vu: '', 252 | wf: 'fr', 253 | ws: 'sm', 254 | xk: 'sq', 255 | ye: 'ar', 256 | yt: 'fr', 257 | za: 'af', 258 | zm: 'en', 259 | zw: 'en', 260 | }; 261 | } -------------------------------------------------------------------------------- /reacjilator-bolt-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foo-app", 3 | "version": "1.0.0", 4 | "description": "Serverless webpack example using Typescript", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "dependencies": { 10 | "@google-cloud/translate": "2.1.4", 11 | "@slack/bolt": "^2.2.3", 12 | "@slack/web-api": "^5.11.0", 13 | "serverless": "^3.25.1", 14 | "serverless-http": "^2.0.1", 15 | "source-map-support": "^0.5.10" 16 | }, 17 | "devDependencies": { 18 | "@types/aws-lambda": "^8.10.17", 19 | "@types/express": "^4.16.1", 20 | "@types/node": "^10.12.18", 21 | "seratch-slack-types": "^0.2.1", 22 | "serverless-offline": "^12.0.0", 23 | "serverless-webpack": "^5.3.4", 24 | "ts-loader": "^8.0.3", 25 | "typescript": "^4.0.2", 26 | "webpack": "^4.44.1" 27 | }, 28 | "author": "The serverless webpack authors (https://github.com/elastic-coders/serverless-webpack)", 29 | "license": "MIT" 30 | } 31 | -------------------------------------------------------------------------------- /reacjilator-bolt-typescript/serverless.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: awesome-app 3 | 4 | # Add the serverless-webpack plugin 5 | plugins: 6 | - serverless-offline 7 | - serverless-webpack 8 | 9 | provider: 10 | name: aws 11 | runtime: nodejs8.10 12 | 13 | functions: 14 | dispatcher: 15 | handler: handler.dispatcher 16 | events: 17 | - http: 18 | method: post 19 | path: /slack/events 20 | -------------------------------------------------------------------------------- /reacjilator-bolt-typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "es2017" 5 | ], 6 | "moduleResolution": "node", 7 | "noUnusedLocals": true, 8 | "noUnusedParameters": true, 9 | "sourceMap": true, 10 | "target": "es2017", 11 | "outDir": "lib", 12 | "allowSyntheticDefaultImports": true 13 | }, 14 | "exclude": [ 15 | "node_modules" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /reacjilator-bolt-typescript/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const slsw = require('serverless-webpack'); 3 | 4 | module.exports = { 5 | mode: slsw.lib.webpack.isLocal ? 'development' : 'production', 6 | entry: slsw.lib.entries, 7 | devtool: 'source-map', 8 | resolve: { 9 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 10 | }, 11 | output: { 12 | libraryTarget: 'commonjs', 13 | path: path.join(__dirname, '.webpack'), 14 | filename: '[name].js', 15 | }, 16 | target: 'node', 17 | externals: { 18 | // WARNING in ./node_modules/retry-request/index.js 19 | // Module not found: Error: Can't resolve 'request' in 'node_modules/retry-request' 20 | "request": "request", 21 | // WARNING in ./node_modules/express/lib/view.js 81:13-25 22 | // Critical dependency: the request of a dependency is an expression 23 | "express": "express" 24 | }, 25 | module: { 26 | rules: [ 27 | // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader` 28 | { 29 | test: /\.tsx?$/, 30 | loader: 'ts-loader' 31 | }, 32 | ], 33 | }, 34 | }; -------------------------------------------------------------------------------- /reacjilator-java-sdk/.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | target 3 | /bin/ 4 | /.settings/ 5 | .project 6 | .classpath 7 | 8 | # Package Files 9 | *.jar 10 | *.war 11 | *.ear 12 | 13 | # Serverless directories 14 | .serverless 15 | 16 | .env 17 | .gcp 18 | google-service-account.json 19 | 20 | .idea/ 21 | *.iml 22 | 23 | template.yml 24 | -------------------------------------------------------------------------------- /reacjilator-java-sdk/README.md: -------------------------------------------------------------------------------- 1 | ## reacjilator in Java + jSlack 2 | 3 | Original: https://github.com/slackapi/reacjilator 4 | 5 | ## How to run the app 6 | 7 | ### Prerequisites 8 | 9 | * Google Translate API Token 10 | * https://console.cloud.google.com/apis/api/translate.googleapis.com/credentials 11 | 12 | * Slack App 13 | * Create a new one here: https://api.slack.com/apps 14 | * Bot Users: https://api.slack.com/apps/{apiAppId}/bots 15 | * Scopes: https://api.slack.com/apps/{apiAppId}/oauth 16 | 17 | ![Scopes](https://raw.githubusercontent.com/seratch/slack-app-examples/master/reacjilator-typescript/scopes.png "Scopes") 18 | 19 | * https://ngrok.com/ 20 | 21 | ### Launch the app from the Terminal 22 | 23 | ```bash 24 | npm i serverless -g 25 | npm i 26 | mvn package \ 27 | && serverless sam export --output ./template.yml \ 28 | && sam local start-api 29 | ``` 30 | 31 | ### Deploy onto AWS 32 | 33 | ```bash 34 | # setup aws-cli + configure & prepare credentials 35 | curl -O https://bootstrap.pypa.io/get-pip.py 36 | python3 get-pip.py --user 37 | pip install awscli --upgrade --user 38 | aws configure 39 | 40 | ./deploy.sh 41 | ``` -------------------------------------------------------------------------------- /reacjilator-java-sdk/_env: -------------------------------------------------------------------------------- 1 | # source .env 2 | export SLACK_API_TOKEN=xoxp-xxx 3 | export SLACK_SIGNING_SECRET=6c34xxx 4 | # Place "google-service-account.json" under src/main/resources 5 | export GOOGLE_APPLICATION_CREDENTIALS=google-service-account.json 6 | -------------------------------------------------------------------------------- /reacjilator-java-sdk/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "${SERVERLESS_STAGE}" == "" ]; then 4 | export SERVERLESS_STAGE=dev 5 | fi 6 | 7 | export SLS_DEBUG=* 8 | 9 | mvn clean package && 10 | ./node_modules/serverless/bin/serverless deploy --stage ${SERVERLESS_STAGE} -v && 11 | ./node_modules/serverless/bin/serverless invoke --function warmup 12 | -------------------------------------------------------------------------------- /reacjilator-java-sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reacjilator-java", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": { 7 | "aws-sdk": "^2.814.0" 8 | }, 9 | "devDependencies": { 10 | "serverless": "^1.41.1", 11 | "serverless-sam": "^0.2.0" 12 | }, 13 | "scripts": { 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "author": "Kazuhiro Sera @seratch", 17 | "license": "MIT" 18 | } 19 | -------------------------------------------------------------------------------- /reacjilator-java-sdk/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | com.github.seratch 7 | reacjilator 8 | jar 9 | dev 10 | reacjilator 11 | 12 | 13 | 1.8 14 | 1.8 15 | UTF-8 16 | 17 | 18 | 26 | 27 | 28 | 29 | org.projectlombok 30 | lombok 31 | 1.18.8 32 | provided 33 | 34 | 35 | com.slack.api 36 | bolt-aws-lambda 37 | [1.1,) 38 | 39 | 40 | com.google.cloud 41 | google-cloud-translate 42 | 1.82.0 43 | 44 | 45 | org.slf4j 46 | jcl-over-slf4j 47 | 1.7.26 48 | 49 | 50 | org.slf4j 51 | slf4j-log4j12 52 | 1.7.26 53 | 54 | 55 | com.amazonaws 56 | aws-lambda-java-log4j 57 | 1.0.0 58 | 59 | 60 | com.amazonaws 61 | aws-java-sdk-lambda 62 | 1.11.592 63 | 64 | 65 | junit 66 | junit 67 | 4.13.1 68 | test 69 | 70 | 71 | com.squareup.okhttp3 72 | logging-interceptor 73 | 3.14.1 74 | test 75 | 76 | 77 | 78 | 79 | 80 | 89 | 90 | org.apache.maven.plugins 91 | maven-shade-plugin 92 | 2.3 93 | 94 | false 95 | 96 | 97 | 98 | package 99 | 100 | shade 101 | 102 | 103 | 104 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | com.github.edwgiz 114 | maven-shade-plugin.log4j2-cachefile-transformer 115 | 2.8.1 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /reacjilator-java-sdk/serverless.yml: -------------------------------------------------------------------------------- 1 | service: reacjilator 2 | 3 | provider: 4 | name: aws 5 | region: ap-northeast-1 6 | runtime: java8 7 | stage: ${opt:stage, 'dev'} 8 | # To warmup lambda functions 9 | iamRoleStatements: 10 | - Effect: Allow 11 | Action: 12 | - lambda:InvokeFunction 13 | - lambda:InvokeAsync 14 | Resource: "*" 15 | environment: 16 | SLACK_API_TOKEN: ${env:SLACK_API_TOKEN} 17 | SLACK_SIGNING_SECRET: ${env:SLACK_SIGNING_SECRET} 18 | GOOGLE_APPLICATION_CREDENTIALS: ${env:GOOGLE_APPLICATION_CREDENTIALS} 19 | SERVERLESS_STAGE: ${opt:stage, 'dev'} 20 | 21 | package: 22 | artifact: target/reacjilator-dev.jar 23 | 24 | plugins: 25 | # Enable to export the settings managed by serverless as SAM template.yml 26 | - serverless-sam 27 | 28 | functions: 29 | echo: 30 | handler: slackapp_backend.handler.EchoHandler 31 | events: 32 | - http: 33 | path: echo 34 | method: get 35 | 36 | events: 37 | handler: slackapp_backend.handler.SlackEventsHandler 38 | timeout: 30 39 | events: 40 | - http: 41 | path: slack/events 42 | method: post 43 | 44 | warmup: 45 | handler: slackapp_backend.handler.WarmupHandler 46 | timeout: 120 # optional, in seconds, default is 6 47 | events: 48 | - schedule: rate(5 minutes) 49 | -------------------------------------------------------------------------------- /reacjilator-java-sdk/src/main/java/slackapp_backend/handler/EchoHandler.java: -------------------------------------------------------------------------------- 1 | package slackapp_backend.handler; 2 | 3 | import com.amazonaws.services.lambda.runtime.Context; 4 | import com.amazonaws.services.lambda.runtime.RequestHandler; 5 | import com.slack.api.app_backend.SlackSignature; 6 | import com.slack.api.bolt.aws_lambda.request.ApiGatewayRequest; 7 | import com.slack.api.bolt.aws_lambda.response.ApiGatewayResponse; 8 | import lombok.extern.slf4j.Slf4j; 9 | import slackapp_backend.service.AmazonWebServices; 10 | 11 | import java.util.Collections; 12 | 13 | // This is just a minimal working example 14 | @Slf4j 15 | public class EchoHandler implements RequestHandler { 16 | 17 | private final SlackSignature.Verifier signatureVerifier = new SlackSignature.Verifier(new SlackSignature.Generator()); 18 | private final AmazonWebServices aws = new AmazonWebServices(); 19 | 20 | @Override 21 | public ApiGatewayResponse handleRequest(ApiGatewayRequest req, Context context) { 22 | log.info("request: {}", req); 23 | 24 | String timestamp = req.getHeaders().get(SlackSignature.HeaderNames.X_SLACK_REQUEST_TIMESTAMP); 25 | String body = req.getBody(); 26 | String signature = req.getHeaders().get(SlackSignature.HeaderNames.X_SLACK_SIGNATURE); 27 | if (!signatureVerifier.isValid(timestamp, body, signature) && !aws.isLocalDev(context)) { 28 | return ApiGatewayResponse.builder().statusCode(401).build(); 29 | } 30 | 31 | if (body != null && body.equals(WarmupHandler.PAYLOAD_STRING)) { 32 | return ApiGatewayResponse.builder().statusCode(200).build(); 33 | } else { 34 | return ApiGatewayResponse.builder() 35 | .statusCode(200) 36 | .headers(Collections.singletonMap("Content-Type", "application/json")) 37 | .objectBody(req.getQueryStringParameters()) 38 | .build(); 39 | } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /reacjilator-java-sdk/src/main/java/slackapp_backend/handler/SlackEventsHandler.java: -------------------------------------------------------------------------------- 1 | package slackapp_backend.handler; 2 | 3 | import com.amazonaws.services.lambda.model.InvokeResult; 4 | import com.amazonaws.services.lambda.runtime.Context; 5 | import com.amazonaws.services.lambda.runtime.RequestHandler; 6 | import com.slack.api.app_backend.SlackSignature; 7 | import com.slack.api.app_backend.events.payload.UrlVerificationPayload; 8 | import com.slack.api.bolt.aws_lambda.request.ApiGatewayRequest; 9 | import com.slack.api.bolt.aws_lambda.response.ApiGatewayResponse; 10 | import com.google.gson.Gson; 11 | import com.google.gson.JsonElement; 12 | import com.google.gson.JsonObject; 13 | import com.slack.api.util.json.GsonFactory; 14 | import lombok.extern.slf4j.Slf4j; 15 | import slackapp_backend.service.AmazonWebServices; 16 | import slackapp_backend.service.SlackEventsOperator; 17 | 18 | import java.util.Collections; 19 | 20 | @Slf4j 21 | public class SlackEventsHandler implements RequestHandler { 22 | 23 | private final AmazonWebServices aws = new AmazonWebServices(); 24 | private final SlackEventsOperator slackEventsOperator = SlackEventsOperator.getInstance(); 25 | private final SlackSignature.Verifier signatureVerifier = new SlackSignature.Verifier(new SlackSignature.Generator()); 26 | private final Gson gson = GsonFactory.createSnakeCase(); 27 | 28 | @Override 29 | public ApiGatewayResponse handleRequest(ApiGatewayRequest req, Context context) { 30 | logRequest(req, context); 31 | 32 | String timestamp = req.getHeaders().get(SlackSignature.HeaderNames.X_SLACK_REQUEST_TIMESTAMP); 33 | String body = req.getBody(); 34 | String signature = req.getHeaders().get(SlackSignature.HeaderNames.X_SLACK_SIGNATURE); 35 | if (!signatureVerifier.isValid(timestamp, body, signature) && !aws.isLocalDev(context)) { 36 | return ApiGatewayResponse.builder().statusCode(401).build(); 37 | } 38 | 39 | if (body == null || body.equals(WarmupHandler.PAYLOAD_STRING)) { 40 | // internal warmup request 41 | return OK_RESPONSE; 42 | } else { 43 | JsonObject payload = gson.fromJson(body, JsonElement.class).getAsJsonObject(); 44 | String eventType = payload.get("type").getAsString(); 45 | if (UrlVerificationPayload.TYPE.equals(eventType)) { 46 | // url_verification: https://api.slack.com/events/url_verification 47 | return handleUrlVerification(payload); 48 | } else { 49 | // events subscription 50 | if (aws.isLocalDev(context)) { 51 | // local dev 52 | slackEventsOperator.handleSynchronously(body); 53 | // may be timed out towards requests from Slack Platform 54 | return OK_RESPONSE; 55 | 56 | } else { 57 | // on aws 58 | if (req.getPath() == null) { // The null path value here means this is an internal request 59 | // do blocking here 60 | slackEventsOperator.handleSynchronously(body); 61 | // actually not in a hurry here 62 | return OK_RESPONSE; 63 | 64 | } else { 65 | // Kick this function asynchronously 66 | req.setPath(null); // The "path" can be modified only here 67 | InvokeResult invokeResult = aws.invokeLambdaFunction(context, req); 68 | if (invokeResult.getStatusCode() != 200) { 69 | log.error("Failed to invoke a function because {}", invokeResult.getFunctionError()); 70 | } 71 | // NOTE: You need to return 200 OK within 3 seconds 72 | return OK_RESPONSE; 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | private void logRequest(ApiGatewayRequest req, Context context) { 80 | if (log.isDebugEnabled()) { 81 | log.debug("request: {}, context: {}", req, gson.toJson(context)); 82 | } else { 83 | log.debug("request: {}", req); 84 | } 85 | } 86 | 87 | private static ApiGatewayResponse handleUrlVerification(JsonObject payload) { 88 | return ApiGatewayResponse.builder() 89 | .statusCode(200) 90 | .headers(Collections.singletonMap("Content-Type", "text/plain")) 91 | .rawBody(payload.get("challenge").getAsString()) 92 | .build(); 93 | } 94 | 95 | private static final ApiGatewayResponse OK_RESPONSE; 96 | 97 | static { 98 | OK_RESPONSE = ApiGatewayResponse.builder() 99 | .statusCode(200) 100 | .headers(Collections.singletonMap("Content-Type", "application/json")) 101 | .objectBody(Collections.singletonMap("ok", true)) 102 | .build(); 103 | } 104 | 105 | } -------------------------------------------------------------------------------- /reacjilator-java-sdk/src/main/java/slackapp_backend/handler/WarmupHandler.java: -------------------------------------------------------------------------------- 1 | package slackapp_backend.handler; 2 | 3 | import com.amazonaws.services.lambda.AWSLambda; 4 | import com.amazonaws.services.lambda.model.InvokeRequest; 5 | import com.amazonaws.services.lambda.model.InvokeResult; 6 | import com.amazonaws.services.lambda.runtime.Context; 7 | import com.amazonaws.services.lambda.runtime.RequestHandler; 8 | import com.slack.api.bolt.aws_lambda.request.ApiGatewayRequest; 9 | import com.slack.api.bolt.aws_lambda.response.ApiGatewayResponse; 10 | import com.slack.api.util.json.GsonFactory; 11 | import lombok.extern.slf4j.Slf4j; 12 | import slackapp_backend.service.AmazonWebServices; 13 | 14 | import java.util.Arrays; 15 | import java.util.HashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | /** 20 | * To address cold start problems 21 | */ 22 | @Slf4j 23 | public class WarmupHandler implements RequestHandler { 24 | 25 | private static final AmazonWebServices AWS = new AmazonWebServices(); 26 | 27 | private static final Map PAYLOAD; 28 | public static final String PAYLOAD_STRING; 29 | 30 | static { 31 | PAYLOAD = new HashMap<>(); 32 | PAYLOAD.put("warmup", true); 33 | PAYLOAD_STRING = GsonFactory.createSnakeCase().toJson(PAYLOAD); 34 | } 35 | 36 | @Override 37 | public ApiGatewayResponse handleRequest(ApiGatewayRequest req, Context context) { 38 | log.info("request: {}", req); 39 | 40 | AWSLambda lambda = AWS.createLambdaClient(); 41 | String serverlessStage = AWS.getServerlessStage(); 42 | List functionNames = Arrays.asList( 43 | "reacjilator-" + serverlessStage + "-echo", 44 | "reacjilator-" + serverlessStage + "-events" 45 | ); 46 | for (String functionName : functionNames) { 47 | InvokeRequest invokeReq = new InvokeRequest().withFunctionName(functionName).withPayload(PAYLOAD_STRING); 48 | InvokeResult result = lambda.invoke(invokeReq); 49 | if (result.getStatusCode() != 200) { 50 | log.error("Failed to warmup the function: {}, result: {}", functionName, result.getFunctionError()); 51 | } 52 | } 53 | 54 | return ApiGatewayResponse.builder() 55 | .statusCode(200) 56 | .objectBody("Done") 57 | .build(); 58 | } 59 | } -------------------------------------------------------------------------------- /reacjilator-java-sdk/src/main/java/slackapp_backend/service/AmazonWebServices.java: -------------------------------------------------------------------------------- 1 | package slackapp_backend.service; 2 | 3 | import com.amazonaws.regions.Regions; 4 | import com.amazonaws.services.lambda.AWSLambda; 5 | import com.amazonaws.services.lambda.AWSLambdaClient; 6 | import com.amazonaws.services.lambda.model.InvocationType; 7 | import com.amazonaws.services.lambda.model.InvokeRequest; 8 | import com.amazonaws.services.lambda.model.InvokeResult; 9 | import com.amazonaws.services.lambda.runtime.Context; 10 | import com.google.gson.GsonBuilder; 11 | import com.slack.api.bolt.aws_lambda.request.ApiGatewayRequest; 12 | 13 | public class AmazonWebServices { 14 | 15 | public Regions region() { 16 | // TODO: Use your own region here 17 | return Regions.AP_NORTHEAST_1; 18 | } 19 | 20 | public AWSLambda createLambdaClient() { 21 | return AWSLambdaClient.builder().withRegion(region().getName()).build(); 22 | } 23 | 24 | public InvokeResult invokeLambdaFunction(Context context, ApiGatewayRequest request) { 25 | InvokeResult result = createLambdaClient().invoke(new InvokeRequest() 26 | .withFunctionName(context.getFunctionName()) 27 | .withPayload(new GsonBuilder().create().toJson(request)) 28 | .withInvocationType(InvocationType.Event) 29 | ); 30 | return result; 31 | } 32 | 33 | public String getServerlessStage() { 34 | return System.getenv("SERVERLESS_STAGE"); 35 | } 36 | 37 | // when running by `sam local start-api` 38 | public boolean isLocalDev(Context context) { 39 | return context != null && context.getFunctionName().equals("test"); 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /reacjilator-java-sdk/src/main/java/slackapp_backend/service/GoogleTranslateApi.java: -------------------------------------------------------------------------------- 1 | package slackapp_backend.service; 2 | 3 | import com.google.auth.oauth2.GoogleCredentials; 4 | import com.google.cloud.translate.Translate; 5 | import com.google.cloud.translate.Translate.TranslateOption; 6 | import com.google.cloud.translate.TranslateOptions; 7 | import com.google.cloud.translate.Translation; 8 | 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | 12 | public class GoogleTranslateApi { 13 | 14 | private final Translate translateApi; 15 | 16 | // Replacing "google-service-account.json" under src/main/resources is expected here 17 | // To enable this service to run anywhere, we need to avoid using 18 | // the env variable "GOOGLE_APPLICATION_CREDENTIALS" pointing the filepath of a JSON file 19 | private static final String SERVICE_ACCOUNT_JSON_FILENAME = "google-service-account.json"; 20 | 21 | public GoogleTranslateApi() throws IOException { 22 | ClassLoader classLoader = GoogleTranslateApi.class.getClassLoader(); 23 | try (InputStream jsonFileResource = classLoader.getResourceAsStream(SERVICE_ACCOUNT_JSON_FILENAME)) { 24 | GoogleCredentials credentials = GoogleCredentials.fromStream(jsonFileResource); 25 | TranslateOptions options = TranslateOptions.newBuilder().setCredentials(credentials).build(); 26 | this.translateApi = options.getService(); 27 | } 28 | } 29 | 30 | public String translate(String text, String lang) { 31 | Translation translationResult = translateApi.translate(text, TranslateOption.targetLanguage(lang)); 32 | return translationResult.getTranslatedText(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /reacjilator-java-sdk/src/main/java/slackapp_backend/service/LangCodes.java: -------------------------------------------------------------------------------- 1 | package slackapp_backend.service; 2 | 3 | import com.amazonaws.util.IOUtils; 4 | import com.google.gson.GsonBuilder; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.util.Map; 9 | import java.util.Set; 10 | 11 | public class LangCodes { 12 | 13 | private static final String FILENAME = "langcode.json"; 14 | 15 | private final Map allData; 16 | private final Set allKeys; 17 | 18 | public LangCodes() throws IOException { 19 | ClassLoader classLoader = LangCodes.class.getClassLoader(); 20 | try (InputStream fileResource = classLoader.getResourceAsStream(FILENAME)) { 21 | String jsonStr = IOUtils.toString(fileResource); 22 | this.allData = new GsonBuilder().create().fromJson(jsonStr, Map.class); 23 | this.allKeys = this.allData.keySet(); 24 | } 25 | } 26 | 27 | public Map getAllData() { 28 | return allData; 29 | } 30 | 31 | public Set getAllKeys() { 32 | return allKeys; 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /reacjilator-java-sdk/src/main/java/slackapp_backend/service/SlackEventsOperator.java: -------------------------------------------------------------------------------- 1 | package slackapp_backend.service; 2 | 3 | import com.slack.api.app_backend.events.EventsDispatcher; 4 | import com.slack.api.app_backend.events.EventsDispatcherFactory; 5 | import com.slack.api.app_backend.events.handler.ReactionAddedHandler; 6 | import com.slack.api.app_backend.events.payload.ReactionAddedPayload; 7 | import com.slack.api.methods.SlackApiException; 8 | import com.slack.api.methods.response.chat.ChatPostMessageResponse; 9 | import com.slack.api.methods.response.conversations.ConversationsRepliesResponse; 10 | import com.slack.api.model.Message; 11 | import lombok.extern.slf4j.Slf4j; 12 | 13 | import java.io.IOException; 14 | import java.util.List; 15 | 16 | @Slf4j 17 | public class SlackEventsOperator { 18 | 19 | private final SlackWebApi slackWebApi; 20 | private final GoogleTranslateApi googleApi; 21 | private final EventsDispatcher dispatcher; 22 | private final LangCodes langCodes; 23 | 24 | private static final SlackEventsOperator INSTANCE; 25 | 26 | static { 27 | try { 28 | INSTANCE = new SlackEventsOperator( 29 | EventsDispatcherFactory.getInstance(), 30 | new SlackWebApi(), 31 | new GoogleTranslateApi(), 32 | new LangCodes() 33 | ); 34 | } catch (Exception e) { 35 | log.error("Failed to initialize SlackEventsOperator", e); 36 | throw new RuntimeException(e); 37 | } 38 | } 39 | 40 | public static SlackEventsOperator getInstance() { 41 | return INSTANCE; 42 | } 43 | 44 | public SlackEventsOperator( 45 | EventsDispatcher dispatcher, 46 | SlackWebApi slackWebApi, 47 | GoogleTranslateApi googleApi, 48 | LangCodes langCodes) { 49 | this.slackWebApi = slackWebApi; 50 | this.googleApi = googleApi; 51 | this.langCodes = langCodes; 52 | 53 | dispatcher.register(reactionAddedHandler); 54 | this.dispatcher = dispatcher; 55 | } 56 | 57 | public void handleSynchronously(String json) { 58 | this.dispatcher.dispatch(json); 59 | } 60 | 61 | private ReactionAddedHandler reactionAddedHandler = new ReactionAddedHandler() { 62 | @Override 63 | public void handle(ReactionAddedPayload payload) { 64 | log.info("payload: {}", payload); 65 | if (!payload.getEvent().getItem().getType().equals("message")) { 66 | log.info("Skipped: event type is not 'message' ({})", payload.getEvent().getType()); 67 | return; 68 | } 69 | String reactionName = payload.getEvent().getReaction(); 70 | String country; 71 | if (reactionName.startsWith("flag-")) { 72 | country = reactionName.substring(5, 7); 73 | } else { 74 | if (langCodes.getAllKeys().contains(reactionName)) { 75 | country = reactionName; 76 | } else { 77 | // unsupported 78 | log.info("Skipped: no country detected"); 79 | return; 80 | } 81 | } 82 | String lang = langCodes.getAllData().get(country); 83 | if (lang == null) { 84 | log.info("Skipped: no lang detected"); 85 | return; 86 | } 87 | 88 | try { 89 | String channelId = payload.getEvent().getItem().getChannel(); 90 | String threadTs = payload.getEvent().getItem().getTs(); 91 | 92 | ConversationsRepliesResponse repliesResponse = slackWebApi.fetchConversationsReplies(channelId, threadTs); 93 | if (repliesResponse.isOk()) { 94 | List messages = repliesResponse.getMessages(); 95 | Message firstMessage = messages.get(0); 96 | String translatedText = googleApi.translate(firstMessage.getText(), lang); 97 | for (Message message : messages) { 98 | if (message.getText() != null && message.getText().equals(translatedText)) { 99 | log.info("Skipped posting {} translation to the thread: already posted", lang); 100 | return; 101 | } 102 | } 103 | ChatPostMessageResponse postResponse = slackWebApi.postMessage(channelId, threadTs, translatedText); 104 | if (postResponse.isOk()) { 105 | log.info("The translation message (lang:{}) has been successfully posted (ts: {})", lang, postResponse.getTs()); 106 | } else { 107 | log.error("Failed to post a message because {}", postResponse.getError()); 108 | } 109 | } else { 110 | log.error("Failed to fetch replies because {}", repliesResponse.getError()); 111 | } 112 | } catch (IOException | SlackApiException e) { 113 | log.error("Failed to call APIs because {}", e.getMessage(), e); 114 | } 115 | } 116 | }; 117 | 118 | } 119 | -------------------------------------------------------------------------------- /reacjilator-java-sdk/src/main/java/slackapp_backend/service/SlackWebApi.java: -------------------------------------------------------------------------------- 1 | package slackapp_backend.service; 2 | 3 | import com.slack.api.Slack; 4 | import com.slack.api.methods.SlackApiException; 5 | import com.slack.api.methods.request.chat.ChatPostMessageRequest; 6 | import com.slack.api.methods.request.conversations.ConversationsRepliesRequest; 7 | import com.slack.api.methods.response.chat.ChatPostMessageResponse; 8 | import com.slack.api.methods.response.conversations.ConversationsRepliesResponse; 9 | 10 | import java.io.IOException; 11 | 12 | public class SlackWebApi { 13 | 14 | private final Slack slack = Slack.getInstance(); 15 | private final String token = System.getenv("SLACK_API_TOKEN"); 16 | 17 | public ConversationsRepliesResponse fetchConversationsReplies(String channelId, String threadTs) throws IOException, SlackApiException { 18 | return slack.methods().conversationsReplies(ConversationsRepliesRequest.builder() 19 | .token(token) 20 | .channel(channelId) 21 | .ts(threadTs) 22 | .build()); 23 | } 24 | 25 | public ChatPostMessageResponse postMessage(String channelId, String threadTs, String text) throws IOException, SlackApiException { 26 | return slack.methods().chatPostMessage(ChatPostMessageRequest.builder() 27 | .token(token) 28 | .channel(channelId) 29 | .threadTs(threadTs) 30 | .text(text) 31 | .username("Reacjilator Bot") 32 | .build()); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /reacjilator-java-sdk/src/main/resources/langcode.json: -------------------------------------------------------------------------------- 1 | { 2 | "ac": "en", 3 | "ad": "ca", 4 | "ae": "ar", 5 | "af": "ps", 6 | "ag": "en", 7 | "ai": "en", 8 | "al": "sq", 9 | "am": "hy", 10 | "ao": "pt", 11 | "aq": "", 12 | "ar": "es", 13 | "as": "en", 14 | "at": "de", 15 | "au": "en", 16 | "aw": "nl", 17 | "ax": "sv", 18 | "az": "", 19 | "ba": "bs", 20 | "bb": "en", 21 | "bd": "bn", 22 | "be": "nl", 23 | "bf": "fr", 24 | "bg": "bg", 25 | "bh": "ar", 26 | "bi": "fr", 27 | "bj": "fr", 28 | "bl": "fr", 29 | "bn": "en", 30 | "bm": "ms", 31 | "bo": "es", 32 | "bq": "nl", 33 | "br": "pt", 34 | "bs": "en", 35 | "bt": "", 36 | "bv": "", 37 | "bw": "en", 38 | "by": "be", 39 | "bz": "en", 40 | "ca": "en", 41 | "cc": "ms", 42 | "cd": "fr", 43 | "cf": "fr", 44 | "cg": "fr", 45 | "ch": "de", 46 | "ci": "fr", 47 | "ck": "en", 48 | "cl": "es", 49 | "cm": "fr", 50 | "cn": "zh-CN", 51 | "co": "es", 52 | "cp": "fr", 53 | "cr": "es", 54 | "cu": "es", 55 | "cv": "pt", 56 | "cw": "nl", 57 | "cx": "en", 58 | "cy": "el", 59 | "cz": "cs", 60 | "de": "de", 61 | "dg": "", 62 | "dj": "fr", 63 | "dk": "da", 64 | "dm": "en", 65 | "do": "es", 66 | "dz": "ar", 67 | "ea": "es", 68 | "ec": "es", 69 | "ee": "et", 70 | "eg": "ar", 71 | "eh": "ar", 72 | "er": "", 73 | "es": "es", 74 | "et": "", 75 | "eu": "", 76 | "fi": "fi", 77 | "fj": "en", 78 | "fk": "en", 79 | "fm": "en", 80 | "fo": "", 81 | "fr": "fr", 82 | "ga": "fr", 83 | "gb": "en", 84 | "gd": "en", 85 | "ge": "ka", 86 | "gf": "fr", 87 | "gg": "en", 88 | "gh": "en", 89 | "gi": "en", 90 | "gl": "da", 91 | "gm": "en", 92 | "gn": "fr", 93 | "gp": "fr", 94 | "gq": "es", 95 | "gr": "el", 96 | "gs": "en", 97 | "gt": "es", 98 | "gu": "en", 99 | "gw": "pt", 100 | "gy": "en", 101 | "hk": "zh-TW", 102 | "hm": "", 103 | "hn": "es", 104 | "hr": "hr", 105 | "ht": "ht", 106 | "hu": "hu", 107 | "ic": "es", 108 | "id": "id", 109 | "ie": "ga", 110 | "il": "iw", 111 | "im": "en", 112 | "in": "hi", 113 | "io": "en", 114 | "iq": "ar", 115 | "ir": "fa", 116 | "is": "is", 117 | "it": "it", 118 | "je": "en", 119 | "jm": "en", 120 | "jo": "ar", 121 | "jp": "ja", 122 | "ke": "en", 123 | "kg": "ky", 124 | "kh": "km", 125 | "ki": "en", 126 | "km": "", 127 | "kn": "en", 128 | "kp": "ko", 129 | "kr": "ko", 130 | "kw": "ar", 131 | "ky": "en", 132 | "kz": "kk", 133 | "la": "lo", 134 | "lb": "ar", 135 | "lc": "en", 136 | "li": "de", 137 | "lk": "si", 138 | "lr": "en", 139 | "ls": "st", 140 | "lt": "lt", 141 | "lu": "lb", 142 | "lv": "lv", 143 | "ly": "ar", 144 | "ma": "ar", 145 | "mc": "fr", 146 | "md": "ro", 147 | "me": "", 148 | "mf": "", 149 | "mg": "mg", 150 | "mh": "", 151 | "mk": "mk", 152 | "ml": "fr", 153 | "mm": "", 154 | "mn": "mn", 155 | "mo": "zh-TW", 156 | "mp": "en", 157 | "mq": "fr", 158 | "mr": "ar", 159 | "ms": "en", 160 | "mt": "mt", 161 | "mu": "en", 162 | "mv": "", 163 | "mw": "en", 164 | "mx": "es", 165 | "my": "ms", 166 | "mz": "pt", 167 | "na": "en", 168 | "nc": "fr", 169 | "ne": "fr", 170 | "nf": "en", 171 | "ng": "en", 172 | "ni": "es", 173 | "nl": "nl", 174 | "no": "no", 175 | "np": "ne", 176 | "nr": "", 177 | "nu": "", 178 | "nz": "en", 179 | "om": "ar", 180 | "pa": "es", 181 | "pe": "es", 182 | "pf": "fr", 183 | "pg": "", 184 | "ph": "tl", 185 | "pk": "ur", 186 | "pl": "pl", 187 | "pm": "fr", 188 | "pn": "en", 189 | "pr": "es", 190 | "ps": "ar", 191 | "pt": "pt", 192 | "pw": "en", 193 | "py": "es", 194 | "qa": "ar", 195 | "re": "fr", 196 | "ro": "ro", 197 | "rs": "sr", 198 | "ru": "ru", 199 | "rw": "", 200 | "sa": "ar", 201 | "sb": "en", 202 | "sc": "en", 203 | "sd": "ar", 204 | "se": "sv", 205 | "sg": "en", 206 | "sh": "en", 207 | "si": "sl", 208 | "sj": "no", 209 | "sk": "sk", 210 | "sl": "en", 211 | "sm": "it", 212 | "sn": "fr", 213 | "so": "so", 214 | "sr": "nl", 215 | "ss": "en", 216 | "st": "pt", 217 | "sv": "es", 218 | "sx": "nl", 219 | "sw": "ar", 220 | "sz": "", 221 | "ta": "en", 222 | "tc": "en", 223 | "td": "fr", 224 | "tf": "fr", 225 | "tg": "fr", 226 | "th": "th", 227 | "tj": "tg", 228 | "tk": "", 229 | "tl": "", 230 | "tm": "", 231 | "tn": "ar", 232 | "to": "", 233 | "tr": "tr", 234 | "tt": "en", 235 | "tv": "", 236 | "tw": "zh-TW", 237 | "tz": "sw", 238 | "ua": "uk", 239 | "ug": "en", 240 | "um": "en", 241 | "us": "en", 242 | "uy": "es", 243 | "uz": "uz", 244 | "va": "it", 245 | "vc": "en", 246 | "ve": "es", 247 | "vg": "en", 248 | "vi": "en", 249 | "vn": "vi", 250 | "vu": "", 251 | "wf": "fr", 252 | "ws": "sm", 253 | "xk": "sq", 254 | "ye": "ar", 255 | "yt": "fr", 256 | "za": "af", 257 | "zm": "en", 258 | "zw": "en" 259 | } -------------------------------------------------------------------------------- /reacjilator-java-sdk/src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # https://stackoverflow.com/questions/46080448/logging-in-aws-lambda-with-slf4j 2 | log=. 3 | log4j.rootLogger=INFO, LAMBDA 4 | #Define the LAMBDA appender 5 | log4j.appender.LAMBDA=com.amazonaws.services.lambda.runtime.log4j.LambdaAppender 6 | log4j.appender.LAMBDA.layout=org.apache.log4j.PatternLayout 7 | log4j.appender.LAMBDA.layout.conversionPattern=%d{yyyy-MM-dd HH:mm:ss} <%X{AWSRequestId}> %-5p %c{1}:%m%n -------------------------------------------------------------------------------- /reacjilator-java-sdk/src/test/java/slackapp_backend/service/GoogleTranslateApiTest.java: -------------------------------------------------------------------------------- 1 | package slackapp_backend.service; 2 | 3 | import org.junit.Test; 4 | 5 | import java.io.IOException; 6 | 7 | import static org.hamcrest.CoreMatchers.is; 8 | import static org.junit.Assert.*; 9 | 10 | public class GoogleTranslateApiTest { 11 | 12 | @Test 13 | public void test() throws IOException { 14 | GoogleTranslateApi service = new GoogleTranslateApi(); 15 | String result = service.translate("Hi there", "ja"); 16 | assertThat(result, is("こんにちは")); 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /reacjilator-python3/.gitignore: -------------------------------------------------------------------------------- 1 | # Distribution / packaging 2 | .Python 3 | env/ 4 | venv/ 5 | build/ 6 | develop-eggs/ 7 | dist/ 8 | downloads/ 9 | eggs/ 10 | .eggs/ 11 | lib/ 12 | lib64/ 13 | parts/ 14 | sdist/ 15 | var/ 16 | *.egg-info/ 17 | .installed.cfg 18 | *.egg 19 | __pycache__/ 20 | 21 | # npm 22 | node_modules/ 23 | 24 | .vscode/ 25 | 26 | .gcp/ 27 | 28 | # Serverless directories 29 | .serverless 30 | -------------------------------------------------------------------------------- /reacjilator-python3/README.md: -------------------------------------------------------------------------------- 1 | ## reacjilator in Python 2 | 3 | Original: https://github.com/slackapi/reacjilator 4 | 5 | ## How to run the app 6 | 7 | ### Prerequisites 8 | 9 | * Google Translate API Token 10 | * https://console.cloud.google.com/apis/api/translate.googleapis.com/credentials 11 | 12 | * Slack App 13 | * Create a new one here: https://api.slack.com/apps 14 | * Bot Users: https://api.slack.com/apps/{apiAppId}/bots 15 | * Scopes: https://api.slack.com/apps/{apiAppId}/oauth 16 | 17 | ![Scopes](https://raw.githubusercontent.com/seratch/slack-app-examples/master/reacjilator-typescript/scopes.png "Scopes") 18 | 19 | * https://ngrok.com/ 20 | 21 | ### Launch the app from the Terminal 22 | 23 | ```bash 24 | pip3 install --user virtualenv 25 | virtualenv venv --python=python3 26 | source venv/bin/activate 27 | pip3 install -r requirements.txt 28 | 29 | cp _env .env 30 | # edit .env 31 | source .env 32 | python app.py # POST http://127.0.0.1:3000/slack/events is available now 33 | ``` 34 | -------------------------------------------------------------------------------- /reacjilator-python3/_env: -------------------------------------------------------------------------------- 1 | # source .env 2 | export SLACK_API_TOKEN=xoxp-xxx 3 | export SLACK_SIGNING_SECRET=6c34xxx 4 | export GOOGLE_APPLICATION_CREDENTIALS=/path/to/{service-account-key}.json 5 | -------------------------------------------------------------------------------- /reacjilator-python3/app.py: -------------------------------------------------------------------------------- 1 | # 2 | # A Python implementation of https://github.com/slackapi/reacjilator 3 | # 4 | # Author: Kazuhiro Sera @seratch 5 | # MIT License as with the original code 6 | # 7 | 8 | import re 9 | import os 10 | import json 11 | 12 | # Google 13 | from google.cloud import translate 14 | import six 15 | import html 16 | 17 | # Slack 18 | from slack.web import WebClient 19 | from slackeventsapi import SlackEventAdapter 20 | 21 | # -------------------- 22 | # Slack API 23 | # Slack Web API Client with sufficient permissions 24 | slack_web_api_client = WebClient(token=os.environ['SLACK_API_TOKEN']) 25 | 26 | # In order for our application to verify the authenticity 27 | # of requests from Slack, we'll compare the request signature 28 | slack_signing_secret = os.environ["SLACK_SIGNING_SECRET"] 29 | 30 | # Create an instance of SlackEventAdapter, passing in our Flask server so it can bind the 31 | # Slack specific routes. The `endpoint` param specifies where to listen for Slack event traffic. 32 | slack_events_adapter = SlackEventAdapter( 33 | slack_signing_secret, 34 | endpoint="/slack/events" 35 | ) 36 | 37 | # -------------------- 38 | # Google Translate API 39 | 40 | # https://console.cloud.google.com/apis/credentials?project={project id} 41 | # export GOOGLE_APPLICATION_CREDENTIALS=/path/to/{service-account-key}.json 42 | translate_client = translate.Client() 43 | 44 | # -------------------- 45 | # Load language mapping data 46 | langcode: dict = {} 47 | with open('./langcode.json') as langcode_file: 48 | langcode = json.load(langcode_file) 49 | 50 | # -------------------- 51 | # reaction_added event handler 52 | 53 | 54 | @slack_events_adapter.on("reaction_added") 55 | def reaction_added(event_data): 56 | event = event_data["event"] 57 | 58 | # Get the reactji name from the event payload 59 | emoji_name = event["reaction"] 60 | 61 | country: str = None 62 | # Check the reaction name if it is a country flag 63 | if re.match(r"flag-", emoji_name): # when the name has flag- prefix 64 | country = re.search(r"flag-(\w{2})$", emoji_name).group(1) 65 | else: # jp, fr, etc. 66 | if emoji_name in langcode.keys(): 67 | country = emoji_name 68 | else: 69 | return 70 | # Finding a lang based on a country is not the best way but oh well 71 | # Matching ISO 639-1 language code 72 | lang: str = langcode[country] 73 | if lang is None: 74 | return 75 | print(u'country: {}, lang: {}'.format(country, lang)) 76 | 77 | channel = event["item"]["channel"] 78 | thread_ts = event['item']['ts'] 79 | 80 | # Fetch all the messages in the thread 81 | replies_response = slack_web_api_client.conversations_replies( 82 | channel=channel, 83 | ts=thread_ts 84 | ) 85 | if replies_response["ok"]: 86 | messages = replies_response["messages"] 87 | first_message = messages[0] 88 | if first_message["text"]: 89 | text: str = first_message["text"] 90 | # Call Google Translate API 91 | if isinstance(text, six.binary_type): 92 | text = text.decode('utf-8') 93 | translation_result = translate_client.translate( 94 | text, 95 | target_language=lang, 96 | model='nmt') 97 | translated_text = html.unescape( 98 | translation_result["translatedText"]) 99 | 100 | # Make sure if the translated text is not posted yet 101 | for msg in messages: 102 | if msg["text"] and msg["text"] == translated_text: 103 | print("The translation seems to be already posted") 104 | return 105 | 106 | # Post the translation as a new message in the thread 107 | post_response = slack_web_api_client.chat_postMessage( 108 | channel=channel, 109 | thread_ts=thread_ts, 110 | username="Reacjilator Bot", 111 | text=translated_text) 112 | if post_response["ok"]: 113 | print(u'The translated text for {} has been successfully posted: {}' 114 | .format(lang, post_response["ts"])) 115 | else: 116 | print(u'Failed to post a translated text: {}' 117 | .format(post_response["error"])) 118 | else: 119 | print(u'Skipped because the message doesn\'t contain text: {}' 120 | .format(first_message)) 121 | return 122 | else: 123 | print(u'Failed to fetch message replies: {}' 124 | .format(replies_response["error"])) 125 | 126 | 127 | # -------------------- 128 | # python app.py 129 | slack_events_adapter.start(port=3000) 130 | -------------------------------------------------------------------------------- /reacjilator-python3/langcode.json: -------------------------------------------------------------------------------- 1 | { 2 | "ac": "en", 3 | "ad": "ca", 4 | "ae": "ar", 5 | "af": "ps", 6 | "ag": "en", 7 | "ai": "en", 8 | "al": "sq", 9 | "am": "hy", 10 | "ao": "pt", 11 | "aq": "", 12 | "ar": "es", 13 | "as": "en", 14 | "at": "de", 15 | "au": "en", 16 | "aw": "nl", 17 | "ax": "sv", 18 | "az": "", 19 | "ba": "bs", 20 | "bb": "en", 21 | "bd": "bn", 22 | "be": "nl", 23 | "bf": "fr", 24 | "bg": "bg", 25 | "bh": "ar", 26 | "bi": "fr", 27 | "bj": "fr", 28 | "bl": "fr", 29 | "bn": "en", 30 | "bm": "ms", 31 | "bo": "es", 32 | "bq": "nl", 33 | "br": "pt", 34 | "bs": "en", 35 | "bt": "", 36 | "bv": "", 37 | "bw": "en", 38 | "by": "be", 39 | "bz": "en", 40 | "ca": "en", 41 | "cc": "ms", 42 | "cd": "fr", 43 | "cf": "fr", 44 | "cg": "fr", 45 | "ch": "de", 46 | "ci": "fr", 47 | "ck": "en", 48 | "cl": "es", 49 | "cm": "fr", 50 | "cn": "zh-CN", 51 | "co": "es", 52 | "cp": "fr", 53 | "cr": "es", 54 | "cu": "es", 55 | "cv": "pt", 56 | "cw": "nl", 57 | "cx": "en", 58 | "cy": "el", 59 | "cz": "cs", 60 | "de": "de", 61 | "dg": "", 62 | "dj": "fr", 63 | "dk": "da", 64 | "dm": "en", 65 | "do": "es", 66 | "dz": "ar", 67 | "ea": "es", 68 | "ec": "es", 69 | "ee": "et", 70 | "eg": "ar", 71 | "eh": "ar", 72 | "er": "", 73 | "es": "es", 74 | "et": "", 75 | "eu": "", 76 | "fi": "fi", 77 | "fj": "en", 78 | "fk": "en", 79 | "fm": "en", 80 | "fo": "", 81 | "fr": "fr", 82 | "ga": "fr", 83 | "gb": "en", 84 | "gd": "en", 85 | "ge": "ka", 86 | "gf": "fr", 87 | "gg": "en", 88 | "gh": "en", 89 | "gi": "en", 90 | "gl": "da", 91 | "gm": "en", 92 | "gn": "fr", 93 | "gp": "fr", 94 | "gq": "es", 95 | "gr": "el", 96 | "gs": "en", 97 | "gt": "es", 98 | "gu": "en", 99 | "gw": "pt", 100 | "gy": "en", 101 | "hk": "zh-TW", 102 | "hm": "", 103 | "hn": "es", 104 | "hr": "hr", 105 | "ht": "ht", 106 | "hu": "hu", 107 | "ic": "es", 108 | "id": "id", 109 | "ie": "ga", 110 | "il": "iw", 111 | "im": "en", 112 | "in": "hi", 113 | "io": "en", 114 | "iq": "ar", 115 | "ir": "fa", 116 | "is": "is", 117 | "it": "it", 118 | "je": "en", 119 | "jm": "en", 120 | "jo": "ar", 121 | "jp": "ja", 122 | "ke": "en", 123 | "kg": "ky", 124 | "kh": "km", 125 | "ki": "en", 126 | "km": "", 127 | "kn": "en", 128 | "kp": "ko", 129 | "kr": "ko", 130 | "kw": "ar", 131 | "ky": "en", 132 | "kz": "kk", 133 | "la": "lo", 134 | "lb": "ar", 135 | "lc": "en", 136 | "li": "de", 137 | "lk": "si", 138 | "lr": "en", 139 | "ls": "st", 140 | "lt": "lt", 141 | "lu": "lb", 142 | "lv": "lv", 143 | "ly": "ar", 144 | "ma": "ar", 145 | "mc": "fr", 146 | "md": "ro", 147 | "me": "", 148 | "mf": "", 149 | "mg": "mg", 150 | "mh": "", 151 | "mk": "mk", 152 | "ml": "fr", 153 | "mm": "", 154 | "mn": "mn", 155 | "mo": "zh-TW", 156 | "mp": "en", 157 | "mq": "fr", 158 | "mr": "ar", 159 | "ms": "en", 160 | "mt": "mt", 161 | "mu": "en", 162 | "mv": "", 163 | "mw": "en", 164 | "mx": "es", 165 | "my": "ms", 166 | "mz": "pt", 167 | "na": "en", 168 | "nc": "fr", 169 | "ne": "fr", 170 | "nf": "en", 171 | "ng": "en", 172 | "ni": "es", 173 | "nl": "nl", 174 | "no": "no", 175 | "np": "ne", 176 | "nr": "", 177 | "nu": "", 178 | "nz": "en", 179 | "om": "ar", 180 | "pa": "es", 181 | "pe": "es", 182 | "pf": "fr", 183 | "pg": "", 184 | "ph": "tl", 185 | "pk": "ur", 186 | "pl": "pl", 187 | "pm": "fr", 188 | "pn": "en", 189 | "pr": "es", 190 | "ps": "ar", 191 | "pt": "pt", 192 | "pw": "en", 193 | "py": "es", 194 | "qa": "ar", 195 | "re": "fr", 196 | "ro": "ro", 197 | "rs": "sr", 198 | "ru": "ru", 199 | "rw": "", 200 | "sa": "ar", 201 | "sb": "en", 202 | "sc": "en", 203 | "sd": "ar", 204 | "se": "sv", 205 | "sg": "en", 206 | "sh": "en", 207 | "si": "sl", 208 | "sj": "no", 209 | "sk": "sk", 210 | "sl": "en", 211 | "sm": "it", 212 | "sn": "fr", 213 | "so": "so", 214 | "sr": "nl", 215 | "ss": "en", 216 | "st": "pt", 217 | "sv": "es", 218 | "sx": "nl", 219 | "sw": "ar", 220 | "sz": "", 221 | "ta": "en", 222 | "tc": "en", 223 | "td": "fr", 224 | "tf": "fr", 225 | "tg": "fr", 226 | "th": "th", 227 | "tj": "tg", 228 | "tk": "", 229 | "tl": "", 230 | "tm": "", 231 | "tn": "ar", 232 | "to": "", 233 | "tr": "tr", 234 | "tt": "en", 235 | "tv": "", 236 | "tw": "zh-TW", 237 | "tz": "sw", 238 | "ua": "uk", 239 | "ug": "en", 240 | "um": "en", 241 | "us": "en", 242 | "uy": "es", 243 | "uz": "uz", 244 | "va": "it", 245 | "vc": "en", 246 | "ve": "es", 247 | "vg": "en", 248 | "vi": "en", 249 | "vn": "vi", 250 | "vu": "", 251 | "wf": "fr", 252 | "ws": "sm", 253 | "xk": "sq", 254 | "ye": "ar", 255 | "yt": "fr", 256 | "za": "af", 257 | "zm": "en", 258 | "zw": "en" 259 | } -------------------------------------------------------------------------------- /reacjilator-python3/requirements.txt: -------------------------------------------------------------------------------- 1 | google-cloud-translate==1.4.0 2 | slackclient>=2.8.1,<3 3 | slackeventsapi>=2.2.1,<3 4 | -------------------------------------------------------------------------------- /reacjilator-ruby/.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | # Used by dotenv library to load environment variables. 14 | # .env 15 | 16 | ## Specific to RubyMotion: 17 | .dat* 18 | .repl_history 19 | build/ 20 | *.bridgesupport 21 | build-iPhoneOS/ 22 | build-iPhoneSimulator/ 23 | 24 | ## Specific to RubyMotion (use of CocoaPods): 25 | # 26 | # We recommend against adding the Pods directory to your .gitignore. However 27 | # you should judge for yourself, the pros and cons are mentioned at: 28 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 29 | # 30 | # vendor/Pods/ 31 | 32 | ## Documentation cache and generated files: 33 | /.yardoc/ 34 | /_yardoc/ 35 | /doc/ 36 | /rdoc/ 37 | 38 | ## Environment normalization: 39 | /.bundle/ 40 | /vendor/bundle 41 | /lib/bundler/man/ 42 | 43 | # for a library or gem, you might want to ignore these files since the code is 44 | # intended to run in multiple environments; otherwise, check them in: 45 | # Gemfile.lock 46 | # .ruby-version 47 | # .ruby-gemset 48 | 49 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 50 | .rvmrc 51 | 52 | # Serverless directories 53 | .serverless 54 | 55 | # package directories 56 | node_modules 57 | 58 | .env 59 | google-service-account.json -------------------------------------------------------------------------------- /reacjilator-ruby/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | source "https://rubygems.org" 3 | 4 | gem 'slack-ruby-client' 5 | gem 'google-cloud-translate' 6 | -------------------------------------------------------------------------------- /reacjilator-ruby/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (7.0.3.1) 5 | concurrent-ruby (~> 1.0, >= 1.0.2) 6 | i18n (>= 1.6, < 2) 7 | minitest (>= 5.1) 8 | tzinfo (~> 2.0) 9 | addressable (2.8.0) 10 | public_suffix (>= 2.0.2, < 5.0) 11 | concurrent-ruby (1.1.10) 12 | faraday (0.15.4) 13 | multipart-post (>= 1.2, < 3) 14 | faraday_middleware (0.13.1) 15 | faraday (>= 0.7.4, < 1.0) 16 | gli (2.18.0) 17 | google-cloud-core (1.3.0) 18 | google-cloud-env (~> 1.0) 19 | google-cloud-env (1.0.5) 20 | faraday (~> 0.11) 21 | google-cloud-translate (1.3.0) 22 | faraday (~> 0.13) 23 | google-cloud-core (~> 1.2) 24 | googleauth (>= 0.6.2, < 0.10.0) 25 | googleauth (0.8.1) 26 | faraday (~> 0.12) 27 | jwt (>= 1.4, < 3.0) 28 | memoist (~> 0.16) 29 | multi_json (~> 1.11) 30 | os (>= 0.9, < 2.0) 31 | signet (~> 0.7) 32 | hashie (3.6.0) 33 | i18n (1.12.0) 34 | concurrent-ruby (~> 1.0) 35 | jwt (2.1.0) 36 | memoist (0.16.0) 37 | minitest (5.16.2) 38 | multi_json (1.13.1) 39 | multipart-post (2.0.0) 40 | os (1.0.1) 41 | public_suffix (4.0.6) 42 | signet (0.11.0) 43 | addressable (~> 2.3) 44 | faraday (~> 0.9) 45 | jwt (>= 1.5, < 3.0) 46 | multi_json (~> 1.10) 47 | slack-ruby-client (0.14.2) 48 | activesupport 49 | faraday (>= 0.9) 50 | faraday_middleware 51 | gli 52 | hashie 53 | websocket-driver 54 | tzinfo (2.0.5) 55 | concurrent-ruby (~> 1.0) 56 | websocket-driver (0.7.0) 57 | websocket-extensions (>= 0.1.0) 58 | websocket-extensions (0.1.5) 59 | 60 | PLATFORMS 61 | ruby 62 | 63 | DEPENDENCIES 64 | google-cloud-translate 65 | slack-ruby-client 66 | 67 | BUNDLED WITH 68 | 2.0.1 69 | -------------------------------------------------------------------------------- /reacjilator-ruby/README.md: -------------------------------------------------------------------------------- 1 | ## reacjilator in Ruby 2 | 3 | Original: https://github.com/slackapi/reacjilator 4 | 5 | ## How to run the app 6 | 7 | ### Prerequisites 8 | 9 | * Google Translate API Token 10 | * https://console.cloud.google.com/apis/api/translate.googleapis.com/credentials 11 | 12 | * Slack App 13 | * Create a new one here: https://api.slack.com/apps 14 | * Bot Users: https://api.slack.com/apps/{apiAppId}/bots 15 | * Scopes: https://api.slack.com/apps/{apiAppId}/oauth 16 | 17 | ![Scopes](https://raw.githubusercontent.com/seratch/slack-app-examples/master/reacjilator-typescript/scopes.png "Scopes") 18 | 19 | * https://ngrok.com/ 20 | 21 | ### Launch the app from the Terminal 22 | 23 | ```bash 24 | npm i serverless -g 25 | npm i 26 | bundle install # ruby 2.5 27 | serverless offline --printOutput 28 | ``` 29 | 30 | ### Deploy onto AWS 31 | 32 | ```bash 33 | # setup aws-cli + configure & prepare credentials 34 | curl -O https://bootstrap.pypa.io/get-pip.py 35 | python3 get-pip.py --user 36 | pip install awscli --upgrade --user 37 | aws configure 38 | 39 | ./deploy.sh 40 | ``` -------------------------------------------------------------------------------- /reacjilator-ruby/_env: -------------------------------------------------------------------------------- 1 | # source .env 2 | export SLACK_API_TOKEN=xoxp-xxx 3 | export SLACK_SIGNING_SECRET=6c34xxx 4 | export GOOGLE_APPLICATION_CREDENTIALS=/path/to/{service-account-key}.json 5 | -------------------------------------------------------------------------------- /reacjilator-ruby/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ "${SERVERLESS_STAGE}" == "" ]; then 3 | export SERVERLESS_STAGE=dev 4 | fi 5 | export SLS_DEBUG=* 6 | serverless deploy --stage ${SERVERLESS_STAGE} -v 7 | -------------------------------------------------------------------------------- /reacjilator-ruby/handler.rb: -------------------------------------------------------------------------------- 1 | # 2 | # A Ruby implementation of https://github.com/slackapi/reacjilator 3 | # 4 | # Author: Kazuhiro Sera @seratch 5 | # MIT License as with the original code 6 | # 7 | 8 | require 'json' 9 | require 'cgi' 10 | require 'slack-ruby-client' 11 | require "google/cloud/translate" 12 | 13 | def events(event:, context:) 14 | 15 | # Load language code master data 16 | all_lang_codes = LangCodesLoader.load() 17 | 18 | # Slack Web API 19 | init_slack_api() 20 | slack_api = Slack::Web::Client.new 21 | 22 | # Google Translate API 23 | google_translate_api = Google::Cloud::Translate.new 24 | 25 | body = JSON.parse(event['body']) 26 | puts "body: #{body}" 27 | 28 | # Verify "X-Slack-Signature" header 29 | begin 30 | Slack::Events::Request.new(HttpRequest.new(event)).verify! 31 | rescue => e 32 | puts "Invalid signature #{e.to_json}" 33 | return { statusCode: 401 } 34 | end 35 | 36 | # Handle "url_verification" requests 37 | if body['type'] == 'url_verification' 38 | return { 39 | statusCode: 200, 40 | headers: {'Content-Type': 'text/plain'}, 41 | body: body['challenge'] 42 | } 43 | end 44 | 45 | payload_event = body['event'] 46 | 47 | if payload_event.nil? || 48 | payload_event['type'] != 'reaction_added' || 49 | payload_event['item']['type'] != 'message' 50 | return ok_response 51 | end 52 | 53 | reaction_name = payload_event['reaction'] 54 | 55 | country = nil 56 | if /^flag-\w{2}$/.match?(reaction_name) 57 | country = /^flag-(\w{2})$/.match(reaction_name).captures.first 58 | else 59 | all_flags = all_lang_codes.keys 60 | if all_flags.include?(reaction_name) 61 | country = reaction_name 62 | else 63 | puts "Skipped: no country detected" 64 | return ok_response 65 | end 66 | end 67 | 68 | lang = all_lang_codes[country] 69 | if lang.nil? 70 | puts "Skipped: no lang detected" 71 | return ok_response 72 | end 73 | puts "country: #{country}, lang: #{lang}" 74 | 75 | channel_id = payload_event['item']['channel'] 76 | thread_ts = payload_event['item']['ts'] 77 | 78 | replies_resp = slack_api.conversations_replies( 79 | channel: channel_id, 80 | ts: thread_ts 81 | ) 82 | if replies_resp.ok 83 | messages = replies_resp['messages'] 84 | first_message = messages.first 85 | text = first_message['text'] 86 | if text 87 | translated_text = google_translate_api.translate(text, to: lang).try(:text) 88 | messages.each do |msg| 89 | existing_message = CGI.unescapeHTML(msg['text']) 90 | if translated_text == existing_message 91 | puts 'Skipped: already posted' 92 | return ok_response 93 | end 94 | end 95 | post_resp = slack_api.chat_postMessage( 96 | channel: channel_id, 97 | text: translated_text, 98 | thread_ts: thread_ts, 99 | username: 'Reacjilator Bot' 100 | ) 101 | if post_resp.ok 102 | puts "Successfully posted a translation (lang:#{lang}, ts: #{post_resp['ts']})" 103 | else 104 | puts "Failed to post a message because #{post_resp.error}" 105 | end 106 | else 107 | puts 'Skipped: no text' 108 | end 109 | else 110 | puts "Failed to fetch replies because #{replies_resp.error}" 111 | end 112 | 113 | ok_response 114 | end 115 | 116 | class HttpRequest 117 | attr_reader :headers, :body 118 | 119 | class Body 120 | def initialize(body) 121 | @body = body 122 | end 123 | 124 | def read 125 | @body 126 | end 127 | end 128 | 129 | def initialize(event) 130 | @headers = event['headers'] 131 | @body = HttpRequest::Body.new(event['body']) 132 | end 133 | end 134 | 135 | class LangCodesLoader 136 | def self.load(filename = 'langcode.json') 137 | all_lang_codes = {} 138 | File.open(filename) do |file| 139 | all_lang_codes = JSON.parse(file.read) 140 | end 141 | all_lang_codes 142 | end 143 | end 144 | 145 | def init_slack_api() 146 | Slack.configure do |config| 147 | config.token = ENV['SLACK_API_TOKEN'] 148 | end 149 | Slack::Events.configure do |config| 150 | config.signing_secret = ENV['SLACK_SIGNING_SECRET'] 151 | end 152 | end 153 | 154 | # Resuable 200 OK response 155 | def ok_response 156 | { 157 | statusCode: 200, 158 | headers: {'Content-Type': 'application/json'}, 159 | body: {ok: true}.to_json 160 | } 161 | end -------------------------------------------------------------------------------- /reacjilator-ruby/langcode.json: -------------------------------------------------------------------------------- 1 | { 2 | "ac": "en", 3 | "ad": "ca", 4 | "ae": "ar", 5 | "af": "ps", 6 | "ag": "en", 7 | "ai": "en", 8 | "al": "sq", 9 | "am": "hy", 10 | "ao": "pt", 11 | "aq": "", 12 | "ar": "es", 13 | "as": "en", 14 | "at": "de", 15 | "au": "en", 16 | "aw": "nl", 17 | "ax": "sv", 18 | "az": "", 19 | "ba": "bs", 20 | "bb": "en", 21 | "bd": "bn", 22 | "be": "nl", 23 | "bf": "fr", 24 | "bg": "bg", 25 | "bh": "ar", 26 | "bi": "fr", 27 | "bj": "fr", 28 | "bl": "fr", 29 | "bn": "en", 30 | "bm": "ms", 31 | "bo": "es", 32 | "bq": "nl", 33 | "br": "pt", 34 | "bs": "en", 35 | "bt": "", 36 | "bv": "", 37 | "bw": "en", 38 | "by": "be", 39 | "bz": "en", 40 | "ca": "en", 41 | "cc": "ms", 42 | "cd": "fr", 43 | "cf": "fr", 44 | "cg": "fr", 45 | "ch": "de", 46 | "ci": "fr", 47 | "ck": "en", 48 | "cl": "es", 49 | "cm": "fr", 50 | "cn": "zh-CN", 51 | "co": "es", 52 | "cp": "fr", 53 | "cr": "es", 54 | "cu": "es", 55 | "cv": "pt", 56 | "cw": "nl", 57 | "cx": "en", 58 | "cy": "el", 59 | "cz": "cs", 60 | "de": "de", 61 | "dg": "", 62 | "dj": "fr", 63 | "dk": "da", 64 | "dm": "en", 65 | "do": "es", 66 | "dz": "ar", 67 | "ea": "es", 68 | "ec": "es", 69 | "ee": "et", 70 | "eg": "ar", 71 | "eh": "ar", 72 | "er": "", 73 | "es": "es", 74 | "et": "", 75 | "eu": "", 76 | "fi": "fi", 77 | "fj": "en", 78 | "fk": "en", 79 | "fm": "en", 80 | "fo": "", 81 | "fr": "fr", 82 | "ga": "fr", 83 | "gb": "en", 84 | "gd": "en", 85 | "ge": "ka", 86 | "gf": "fr", 87 | "gg": "en", 88 | "gh": "en", 89 | "gi": "en", 90 | "gl": "da", 91 | "gm": "en", 92 | "gn": "fr", 93 | "gp": "fr", 94 | "gq": "es", 95 | "gr": "el", 96 | "gs": "en", 97 | "gt": "es", 98 | "gu": "en", 99 | "gw": "pt", 100 | "gy": "en", 101 | "hk": "zh-TW", 102 | "hm": "", 103 | "hn": "es", 104 | "hr": "hr", 105 | "ht": "ht", 106 | "hu": "hu", 107 | "ic": "es", 108 | "id": "id", 109 | "ie": "ga", 110 | "il": "iw", 111 | "im": "en", 112 | "in": "hi", 113 | "io": "en", 114 | "iq": "ar", 115 | "ir": "fa", 116 | "is": "is", 117 | "it": "it", 118 | "je": "en", 119 | "jm": "en", 120 | "jo": "ar", 121 | "jp": "ja", 122 | "ke": "en", 123 | "kg": "ky", 124 | "kh": "km", 125 | "ki": "en", 126 | "km": "", 127 | "kn": "en", 128 | "kp": "ko", 129 | "kr": "ko", 130 | "kw": "ar", 131 | "ky": "en", 132 | "kz": "kk", 133 | "la": "lo", 134 | "lb": "ar", 135 | "lc": "en", 136 | "li": "de", 137 | "lk": "si", 138 | "lr": "en", 139 | "ls": "st", 140 | "lt": "lt", 141 | "lu": "lb", 142 | "lv": "lv", 143 | "ly": "ar", 144 | "ma": "ar", 145 | "mc": "fr", 146 | "md": "ro", 147 | "me": "", 148 | "mf": "", 149 | "mg": "mg", 150 | "mh": "", 151 | "mk": "mk", 152 | "ml": "fr", 153 | "mm": "", 154 | "mn": "mn", 155 | "mo": "zh-TW", 156 | "mp": "en", 157 | "mq": "fr", 158 | "mr": "ar", 159 | "ms": "en", 160 | "mt": "mt", 161 | "mu": "en", 162 | "mv": "", 163 | "mw": "en", 164 | "mx": "es", 165 | "my": "ms", 166 | "mz": "pt", 167 | "na": "en", 168 | "nc": "fr", 169 | "ne": "fr", 170 | "nf": "en", 171 | "ng": "en", 172 | "ni": "es", 173 | "nl": "nl", 174 | "no": "no", 175 | "np": "ne", 176 | "nr": "", 177 | "nu": "", 178 | "nz": "en", 179 | "om": "ar", 180 | "pa": "es", 181 | "pe": "es", 182 | "pf": "fr", 183 | "pg": "", 184 | "ph": "tl", 185 | "pk": "ur", 186 | "pl": "pl", 187 | "pm": "fr", 188 | "pn": "en", 189 | "pr": "es", 190 | "ps": "ar", 191 | "pt": "pt", 192 | "pw": "en", 193 | "py": "es", 194 | "qa": "ar", 195 | "re": "fr", 196 | "ro": "ro", 197 | "rs": "sr", 198 | "ru": "ru", 199 | "rw": "", 200 | "sa": "ar", 201 | "sb": "en", 202 | "sc": "en", 203 | "sd": "ar", 204 | "se": "sv", 205 | "sg": "en", 206 | "sh": "en", 207 | "si": "sl", 208 | "sj": "no", 209 | "sk": "sk", 210 | "sl": "en", 211 | "sm": "it", 212 | "sn": "fr", 213 | "so": "so", 214 | "sr": "nl", 215 | "ss": "en", 216 | "st": "pt", 217 | "sv": "es", 218 | "sx": "nl", 219 | "sw": "ar", 220 | "sz": "", 221 | "ta": "en", 222 | "tc": "en", 223 | "td": "fr", 224 | "tf": "fr", 225 | "tg": "fr", 226 | "th": "th", 227 | "tj": "tg", 228 | "tk": "", 229 | "tl": "", 230 | "tm": "", 231 | "tn": "ar", 232 | "to": "", 233 | "tr": "tr", 234 | "tt": "en", 235 | "tv": "", 236 | "tw": "zh-TW", 237 | "tz": "sw", 238 | "ua": "uk", 239 | "ug": "en", 240 | "um": "en", 241 | "us": "en", 242 | "uy": "es", 243 | "uz": "uz", 244 | "va": "it", 245 | "vc": "en", 246 | "ve": "es", 247 | "vg": "en", 248 | "vi": "en", 249 | "vn": "vi", 250 | "vu": "", 251 | "wf": "fr", 252 | "ws": "sm", 253 | "xk": "sq", 254 | "ye": "ar", 255 | "yt": "fr", 256 | "za": "af", 257 | "zm": "en", 258 | "zw": "en" 259 | } -------------------------------------------------------------------------------- /reacjilator-ruby/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reacjilator-ruby", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accept": { 8 | "version": "2.1.4", 9 | "resolved": "https://registry.npmjs.org/accept/-/accept-2.1.4.tgz", 10 | "integrity": "sha1-iHr1TO7lx/RDBGGXHsQAxh0JrLs=", 11 | "dev": true, 12 | "requires": { 13 | "boom": "5.x.x", 14 | "hoek": "4.x.x" 15 | }, 16 | "dependencies": { 17 | "boom": { 18 | "version": "5.2.0", 19 | "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", 20 | "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", 21 | "dev": true, 22 | "requires": { 23 | "hoek": "4.x.x" 24 | } 25 | }, 26 | "hoek": { 27 | "version": "4.2.1", 28 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 29 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", 30 | "dev": true 31 | } 32 | } 33 | }, 34 | "ammo": { 35 | "version": "2.0.4", 36 | "resolved": "https://registry.npmjs.org/ammo/-/ammo-2.0.4.tgz", 37 | "integrity": "sha1-v4CqshFpjqePY+9efxE91dnokX8=", 38 | "dev": true, 39 | "requires": { 40 | "boom": "5.x.x", 41 | "hoek": "4.x.x" 42 | }, 43 | "dependencies": { 44 | "boom": { 45 | "version": "5.2.0", 46 | "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", 47 | "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", 48 | "dev": true, 49 | "requires": { 50 | "hoek": "4.x.x" 51 | } 52 | }, 53 | "hoek": { 54 | "version": "4.2.1", 55 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 56 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", 57 | "dev": true 58 | } 59 | } 60 | }, 61 | "b64": { 62 | "version": "3.0.3", 63 | "resolved": "https://registry.npmjs.org/b64/-/b64-3.0.3.tgz", 64 | "integrity": "sha512-Pbeh0i6OLubPJdIdCepn8ZQHwN2MWznZHbHABSTEfQ706ie+yuxNSaPdqX1xRatT6WanaS1EazMiSg0NUW2XxQ==", 65 | "dev": true 66 | }, 67 | "boom": { 68 | "version": "7.3.0", 69 | "resolved": "https://registry.npmjs.org/boom/-/boom-7.3.0.tgz", 70 | "integrity": "sha512-Swpoyi2t5+GhOEGw8rEsKvTxFLIDiiKoUc2gsoV6Lyr43LHBIzch3k2MvYUs8RTROrIkVJ3Al0TkaOGjnb+B6A==", 71 | "dev": true, 72 | "requires": { 73 | "hoek": "6.x.x" 74 | } 75 | }, 76 | "bourne": { 77 | "version": "1.1.2", 78 | "resolved": "https://registry.npmjs.org/bourne/-/bourne-1.1.2.tgz", 79 | "integrity": "sha512-b2dgVkTZhkQirNMohgC00rWfpVqEi9y5tKM1k3JvoNx05ODtfQoPPd4js9CYFQoY0IM8LAmnJulEuWv74zjUOg==", 80 | "dev": true 81 | }, 82 | "buffer-equal-constant-time": { 83 | "version": "1.0.1", 84 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 85 | "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=", 86 | "dev": true 87 | }, 88 | "call": { 89 | "version": "4.0.2", 90 | "resolved": "https://registry.npmjs.org/call/-/call-4.0.2.tgz", 91 | "integrity": "sha1-33b19R7o3Ui4VqyEAPfmnm1zmcQ=", 92 | "dev": true, 93 | "requires": { 94 | "boom": "5.x.x", 95 | "hoek": "4.x.x" 96 | }, 97 | "dependencies": { 98 | "boom": { 99 | "version": "5.2.0", 100 | "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", 101 | "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", 102 | "dev": true, 103 | "requires": { 104 | "hoek": "4.x.x" 105 | } 106 | }, 107 | "hoek": { 108 | "version": "4.2.1", 109 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 110 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", 111 | "dev": true 112 | } 113 | } 114 | }, 115 | "catbox": { 116 | "version": "7.1.5", 117 | "resolved": "https://registry.npmjs.org/catbox/-/catbox-7.1.5.tgz", 118 | "integrity": "sha512-4fui5lELzqZ+9cnaAP/BcqXTH6LvWLBRtFhJ0I4FfgfXiSaZcf6k9m9dqOyChiTxNYtvLk7ZMYSf7ahMq3bf5A==", 119 | "dev": true, 120 | "requires": { 121 | "boom": "5.x.x", 122 | "hoek": "4.x.x", 123 | "joi": "10.x.x" 124 | }, 125 | "dependencies": { 126 | "boom": { 127 | "version": "5.2.0", 128 | "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", 129 | "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", 130 | "dev": true, 131 | "requires": { 132 | "hoek": "4.x.x" 133 | } 134 | }, 135 | "hoek": { 136 | "version": "4.2.1", 137 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 138 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", 139 | "dev": true 140 | } 141 | } 142 | }, 143 | "catbox-memory": { 144 | "version": "2.0.4", 145 | "resolved": "https://registry.npmjs.org/catbox-memory/-/catbox-memory-2.0.4.tgz", 146 | "integrity": "sha1-Qz4lWQLK9UIz0ShkKcj03xToItU=", 147 | "dev": true, 148 | "requires": { 149 | "hoek": "4.x.x" 150 | }, 151 | "dependencies": { 152 | "hoek": { 153 | "version": "4.2.1", 154 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 155 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", 156 | "dev": true 157 | } 158 | } 159 | }, 160 | "content": { 161 | "version": "3.0.7", 162 | "resolved": "https://registry.npmjs.org/content/-/content-3.0.7.tgz", 163 | "integrity": "sha512-LXtnSnvE+Z1Cjpa3P9gh9kb396qV4MqpfwKy777BOSF8n6nw2vAi03tHNl0/XRqZUyzVzY/+nMXOZVnEapWzdg==", 164 | "dev": true, 165 | "requires": { 166 | "boom": "5.x.x" 167 | }, 168 | "dependencies": { 169 | "boom": { 170 | "version": "5.2.0", 171 | "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", 172 | "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", 173 | "dev": true, 174 | "requires": { 175 | "hoek": "4.x.x" 176 | } 177 | }, 178 | "hoek": { 179 | "version": "4.2.1", 180 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 181 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", 182 | "dev": true 183 | } 184 | } 185 | }, 186 | "cryptiles": { 187 | "version": "4.1.3", 188 | "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-4.1.3.tgz", 189 | "integrity": "sha512-gT9nyTMSUC1JnziQpPbxKGBbUg8VL7Zn2NB4E1cJYvuXdElHrwxrV9bmltZGDzet45zSDGyYceueke1TjynGzw==", 190 | "dev": true, 191 | "requires": { 192 | "boom": "7.x.x" 193 | } 194 | }, 195 | "ecdsa-sig-formatter": { 196 | "version": "1.0.11", 197 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 198 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 199 | "dev": true, 200 | "requires": { 201 | "safe-buffer": "^5.0.1" 202 | } 203 | }, 204 | "h2o2": { 205 | "version": "6.1.0", 206 | "resolved": "https://registry.npmjs.org/h2o2/-/h2o2-6.1.0.tgz", 207 | "integrity": "sha1-Ky5/zKDjZlyUl2ReMgOvme2QM/E=", 208 | "dev": true, 209 | "requires": { 210 | "boom": "5.x.x", 211 | "hoek": "4.x.x", 212 | "joi": "10.x.x", 213 | "wreck": "12.x.x" 214 | }, 215 | "dependencies": { 216 | "boom": { 217 | "version": "5.2.0", 218 | "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", 219 | "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", 220 | "dev": true, 221 | "requires": { 222 | "hoek": "4.x.x" 223 | } 224 | }, 225 | "hoek": { 226 | "version": "4.2.1", 227 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 228 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", 229 | "dev": true 230 | } 231 | } 232 | }, 233 | "hapi": { 234 | "version": "16.7.0", 235 | "resolved": "https://registry.npmjs.org/hapi/-/hapi-16.7.0.tgz", 236 | "integrity": "sha512-UeMX1LMWmHEIgMlwZGK/3lhI7X0VRvOioVply0Y9qF+/O5woGdQzNB8ZmDnLOBjnB6bdWWHyo5DEamuCsE1vmg==", 237 | "dev": true, 238 | "requires": { 239 | "accept": "2.x.x", 240 | "ammo": "2.x.x", 241 | "boom": "5.x.x", 242 | "call": "4.x.x", 243 | "catbox": "7.x.x", 244 | "catbox-memory": "2.x.x", 245 | "cryptiles": "3.x.x", 246 | "heavy": "4.x.x", 247 | "hoek": "4.x.x", 248 | "iron": "4.x.x", 249 | "items": "2.x.x", 250 | "joi": "11.x.x", 251 | "mimos": "3.x.x", 252 | "podium": "1.x.x", 253 | "shot": "3.x.x", 254 | "somever": "1.x.x", 255 | "statehood": "5.x.x", 256 | "subtext": "5.x.x", 257 | "topo": "2.x.x" 258 | }, 259 | "dependencies": { 260 | "boom": { 261 | "version": "5.2.0", 262 | "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", 263 | "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", 264 | "dev": true, 265 | "requires": { 266 | "hoek": "4.x.x" 267 | } 268 | }, 269 | "cryptiles": { 270 | "version": "3.1.4", 271 | "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.4.tgz", 272 | "integrity": "sha512-8I1sgZHfVwcSOY6mSGpVU3lw/GSIZvusg8dD2+OGehCJpOhQRLNcH0qb9upQnOH4XhgxxFJSg6E2kx95deb1Tw==", 273 | "dev": true, 274 | "requires": { 275 | "boom": "5.x.x" 276 | } 277 | }, 278 | "hoek": { 279 | "version": "4.2.1", 280 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 281 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", 282 | "dev": true 283 | }, 284 | "isemail": { 285 | "version": "3.2.0", 286 | "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz", 287 | "integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==", 288 | "dev": true, 289 | "requires": { 290 | "punycode": "2.x.x" 291 | } 292 | }, 293 | "joi": { 294 | "version": "11.4.0", 295 | "resolved": "https://registry.npmjs.org/joi/-/joi-11.4.0.tgz", 296 | "integrity": "sha512-O7Uw+w/zEWgbL6OcHbyACKSj0PkQeUgmehdoXVSxt92QFCq4+1390Rwh5moI2K/OgC7D8RHRZqHZxT2husMJHA==", 297 | "dev": true, 298 | "requires": { 299 | "hoek": "4.x.x", 300 | "isemail": "3.x.x", 301 | "topo": "2.x.x" 302 | } 303 | } 304 | } 305 | }, 306 | "hapi-cors-headers": { 307 | "version": "1.0.3", 308 | "resolved": "https://registry.npmjs.org/hapi-cors-headers/-/hapi-cors-headers-1.0.3.tgz", 309 | "integrity": "sha512-U/y+kpVLUJ0y86fEk8yleou9C1T5wFopcWQjuxKdMXzCcymTjfSqGz59waqvngUs1SbeXav/y8Ga9C0G0L1MGg==", 310 | "dev": true 311 | }, 312 | "heavy": { 313 | "version": "4.0.4", 314 | "resolved": "https://registry.npmjs.org/heavy/-/heavy-4.0.4.tgz", 315 | "integrity": "sha1-NskTNsAMz+hSyqTRUwhjNc0vAOk=", 316 | "dev": true, 317 | "requires": { 318 | "boom": "5.x.x", 319 | "hoek": "4.x.x", 320 | "joi": "10.x.x" 321 | }, 322 | "dependencies": { 323 | "boom": { 324 | "version": "5.2.0", 325 | "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", 326 | "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", 327 | "dev": true, 328 | "requires": { 329 | "hoek": "4.x.x" 330 | } 331 | }, 332 | "hoek": { 333 | "version": "4.2.1", 334 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 335 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", 336 | "dev": true 337 | } 338 | } 339 | }, 340 | "hoek": { 341 | "version": "6.1.3", 342 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.3.tgz", 343 | "integrity": "sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==", 344 | "dev": true 345 | }, 346 | "iron": { 347 | "version": "4.0.5", 348 | "resolved": "https://registry.npmjs.org/iron/-/iron-4.0.5.tgz", 349 | "integrity": "sha1-TwQszri5c480a1mqc0yDqJvDFCg=", 350 | "dev": true, 351 | "requires": { 352 | "boom": "5.x.x", 353 | "cryptiles": "3.x.x", 354 | "hoek": "4.x.x" 355 | }, 356 | "dependencies": { 357 | "boom": { 358 | "version": "5.2.0", 359 | "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", 360 | "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", 361 | "dev": true, 362 | "requires": { 363 | "hoek": "4.x.x" 364 | } 365 | }, 366 | "cryptiles": { 367 | "version": "3.1.4", 368 | "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.4.tgz", 369 | "integrity": "sha512-8I1sgZHfVwcSOY6mSGpVU3lw/GSIZvusg8dD2+OGehCJpOhQRLNcH0qb9upQnOH4XhgxxFJSg6E2kx95deb1Tw==", 370 | "dev": true, 371 | "requires": { 372 | "boom": "5.x.x" 373 | } 374 | }, 375 | "hoek": { 376 | "version": "4.2.1", 377 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 378 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", 379 | "dev": true 380 | } 381 | } 382 | }, 383 | "isemail": { 384 | "version": "2.2.1", 385 | "resolved": "https://registry.npmjs.org/isemail/-/isemail-2.2.1.tgz", 386 | "integrity": "sha1-A1PT2aYpUQgMJiwqoKQrjqjp4qY=", 387 | "dev": true 388 | }, 389 | "items": { 390 | "version": "2.1.2", 391 | "resolved": "https://registry.npmjs.org/items/-/items-2.1.2.tgz", 392 | "integrity": "sha512-kezcEqgB97BGeZZYtX/MA8AG410ptURstvnz5RAgyFZ8wQFPMxHY8GpTq+/ZHKT3frSlIthUq7EvLt9xn3TvXg==", 393 | "dev": true 394 | }, 395 | "joi": { 396 | "version": "10.6.0", 397 | "resolved": "https://registry.npmjs.org/joi/-/joi-10.6.0.tgz", 398 | "integrity": "sha512-hBF3LcqyAid+9X/pwg+eXjD2QBZI5eXnBFJYaAkH4SK3mp9QSRiiQnDYlmlz5pccMvnLcJRS4whhDOTCkmsAdQ==", 399 | "dev": true, 400 | "requires": { 401 | "hoek": "4.x.x", 402 | "isemail": "2.x.x", 403 | "items": "2.x.x", 404 | "topo": "2.x.x" 405 | }, 406 | "dependencies": { 407 | "hoek": { 408 | "version": "4.2.1", 409 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 410 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", 411 | "dev": true 412 | } 413 | } 414 | }, 415 | "js-string-escape": { 416 | "version": "1.0.1", 417 | "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", 418 | "integrity": "sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8=", 419 | "dev": true 420 | }, 421 | "jsonpath-plus": { 422 | "version": "0.16.0", 423 | "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.16.0.tgz", 424 | "integrity": "sha1-/kQbI/A+xpeaVgNROYjNPtt9tdw=", 425 | "dev": true 426 | }, 427 | "jsonschema": { 428 | "version": "1.2.6", 429 | "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.6.tgz", 430 | "integrity": "sha512-SqhURKZG07JyKKeo/ir24QnS4/BV7a6gQy93bUSe4lUdNp0QNpIz2c9elWJQ9dpc5cQYY6cvCzgRwy0MQCLyqA==", 431 | "dev": true 432 | }, 433 | "jsonwebtoken": { 434 | "version": "8.5.1", 435 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", 436 | "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", 437 | "dev": true, 438 | "requires": { 439 | "jws": "^3.2.2", 440 | "lodash.includes": "^4.3.0", 441 | "lodash.isboolean": "^3.0.3", 442 | "lodash.isinteger": "^4.0.4", 443 | "lodash.isnumber": "^3.0.3", 444 | "lodash.isplainobject": "^4.0.6", 445 | "lodash.isstring": "^4.0.1", 446 | "lodash.once": "^4.0.0", 447 | "ms": "^2.1.1", 448 | "semver": "^5.6.0" 449 | }, 450 | "dependencies": { 451 | "ms": { 452 | "version": "2.1.2", 453 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 454 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 455 | "dev": true 456 | }, 457 | "semver": { 458 | "version": "5.7.1", 459 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 460 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", 461 | "dev": true 462 | } 463 | } 464 | }, 465 | "jwa": { 466 | "version": "1.4.1", 467 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", 468 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", 469 | "dev": true, 470 | "requires": { 471 | "buffer-equal-constant-time": "1.0.1", 472 | "ecdsa-sig-formatter": "1.0.11", 473 | "safe-buffer": "^5.0.1" 474 | } 475 | }, 476 | "jws": { 477 | "version": "3.2.2", 478 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", 479 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", 480 | "dev": true, 481 | "requires": { 482 | "jwa": "^1.4.1", 483 | "safe-buffer": "^5.0.1" 484 | } 485 | }, 486 | "lodash.includes": { 487 | "version": "4.3.0", 488 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", 489 | "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=", 490 | "dev": true 491 | }, 492 | "lodash.isboolean": { 493 | "version": "3.0.3", 494 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 495 | "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=", 496 | "dev": true 497 | }, 498 | "lodash.isinteger": { 499 | "version": "4.0.4", 500 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", 501 | "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=", 502 | "dev": true 503 | }, 504 | "lodash.isnumber": { 505 | "version": "3.0.3", 506 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", 507 | "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=", 508 | "dev": true 509 | }, 510 | "lodash.isplainobject": { 511 | "version": "4.0.6", 512 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 513 | "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", 514 | "dev": true 515 | }, 516 | "lodash.isstring": { 517 | "version": "4.0.1", 518 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 519 | "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=", 520 | "dev": true 521 | }, 522 | "lodash.once": { 523 | "version": "4.1.1", 524 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 525 | "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=", 526 | "dev": true 527 | }, 528 | "mime-db": { 529 | "version": "1.44.0", 530 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", 531 | "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", 532 | "dev": true 533 | }, 534 | "mimos": { 535 | "version": "3.0.3", 536 | "resolved": "https://registry.npmjs.org/mimos/-/mimos-3.0.3.tgz", 537 | "integrity": "sha1-uRCQcq03jCty9qAQHEPd+ys2ZB8=", 538 | "dev": true, 539 | "requires": { 540 | "hoek": "4.x.x", 541 | "mime-db": "1.x.x" 542 | }, 543 | "dependencies": { 544 | "hoek": { 545 | "version": "4.2.1", 546 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 547 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", 548 | "dev": true 549 | } 550 | } 551 | }, 552 | "nigel": { 553 | "version": "2.0.2", 554 | "resolved": "https://registry.npmjs.org/nigel/-/nigel-2.0.2.tgz", 555 | "integrity": "sha1-k6GGb7DFLYc5CqdeKxYfS1x15bE=", 556 | "dev": true, 557 | "requires": { 558 | "hoek": "4.x.x", 559 | "vise": "2.x.x" 560 | }, 561 | "dependencies": { 562 | "hoek": { 563 | "version": "4.2.1", 564 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 565 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", 566 | "dev": true 567 | } 568 | } 569 | }, 570 | "pez": { 571 | "version": "2.1.5", 572 | "resolved": "https://registry.npmjs.org/pez/-/pez-2.1.5.tgz", 573 | "integrity": "sha1-XsLMYlAMw+tCNtSkFM9aF7XrUAc=", 574 | "dev": true, 575 | "requires": { 576 | "b64": "3.x.x", 577 | "boom": "5.x.x", 578 | "content": "3.x.x", 579 | "hoek": "4.x.x", 580 | "nigel": "2.x.x" 581 | }, 582 | "dependencies": { 583 | "boom": { 584 | "version": "5.2.0", 585 | "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", 586 | "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", 587 | "dev": true, 588 | "requires": { 589 | "hoek": "4.x.x" 590 | } 591 | }, 592 | "hoek": { 593 | "version": "4.2.1", 594 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 595 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", 596 | "dev": true 597 | } 598 | } 599 | }, 600 | "podium": { 601 | "version": "1.3.0", 602 | "resolved": "https://registry.npmjs.org/podium/-/podium-1.3.0.tgz", 603 | "integrity": "sha512-ZIujqk1pv8bRZNVxwwwq0BhXilZ2udycQT3Kp8ah3f3TcTmVg7ILJsv/oLf47gRa2qeiP584lNq+pfvS9U3aow==", 604 | "dev": true, 605 | "requires": { 606 | "hoek": "4.x.x", 607 | "items": "2.x.x", 608 | "joi": "10.x.x" 609 | }, 610 | "dependencies": { 611 | "hoek": { 612 | "version": "4.2.1", 613 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 614 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", 615 | "dev": true 616 | } 617 | } 618 | }, 619 | "punycode": { 620 | "version": "2.1.1", 621 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 622 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 623 | "dev": true 624 | }, 625 | "safe-buffer": { 626 | "version": "5.2.1", 627 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 628 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 629 | "dev": true 630 | }, 631 | "serverless-hooks-plugin": { 632 | "version": "1.1.0", 633 | "resolved": "https://registry.npmjs.org/serverless-hooks-plugin/-/serverless-hooks-plugin-1.1.0.tgz", 634 | "integrity": "sha1-UL6hSGhbEQls7noIfN3LSd+o75k=", 635 | "dev": true 636 | }, 637 | "serverless-offline": { 638 | "version": "4.10.6", 639 | "resolved": "https://registry.npmjs.org/serverless-offline/-/serverless-offline-4.10.6.tgz", 640 | "integrity": "sha512-4l8ngzsX90iq8SRm4wGzGv0688bKPCAXYm60dqaW1ipXVp1BvV/BcFQp+4UKPUnkzSqqvyisLc6oa3K7kkAAZw==", 641 | "dev": true, 642 | "requires": { 643 | "boom": "^7.3.0", 644 | "cryptiles": "^4.1.3", 645 | "h2o2": "^6.1.0", 646 | "hapi": "^16.7.0", 647 | "hapi-cors-headers": "^1.0.3", 648 | "js-string-escape": "^1.0.1", 649 | "jsonpath-plus": "^0.16.0", 650 | "jsonschema": "^1.2.4", 651 | "jsonwebtoken": "^8.5.1", 652 | "trim-newlines": "^2.0.0", 653 | "velocityjs": "^1.1.3" 654 | } 655 | }, 656 | "shot": { 657 | "version": "3.4.2", 658 | "resolved": "https://registry.npmjs.org/shot/-/shot-3.4.2.tgz", 659 | "integrity": "sha1-Hlw/bysmZJrcQvfrNQIUpaApHWc=", 660 | "dev": true, 661 | "requires": { 662 | "hoek": "4.x.x", 663 | "joi": "10.x.x" 664 | }, 665 | "dependencies": { 666 | "hoek": { 667 | "version": "4.2.1", 668 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 669 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", 670 | "dev": true 671 | } 672 | } 673 | }, 674 | "somever": { 675 | "version": "1.0.1", 676 | "resolved": "https://registry.npmjs.org/somever/-/somever-1.0.1.tgz", 677 | "integrity": "sha512-PCDMBcega4n7wuBUKmkiXidF3cOwtHHGg2qJYl0Rkw7StZqORoCgqce7HUuWNta/NAiQhwLDezNnTANxEWPCGA==", 678 | "dev": true, 679 | "requires": { 680 | "hoek": "4.x.x" 681 | }, 682 | "dependencies": { 683 | "hoek": { 684 | "version": "4.2.1", 685 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 686 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", 687 | "dev": true 688 | } 689 | } 690 | }, 691 | "statehood": { 692 | "version": "5.0.4", 693 | "resolved": "https://registry.npmjs.org/statehood/-/statehood-5.0.4.tgz", 694 | "integrity": "sha512-6/feFLqqHylvA/dHwJA0DgXvbEcKgbhRUeljsuu6+cIr8PO88nax7Wc+celZlPTncqT2arsxXL8P329Q1yfe9Q==", 695 | "dev": true, 696 | "requires": { 697 | "boom": "5.x.x", 698 | "bourne": "1.x.x", 699 | "cryptiles": "3.x.x", 700 | "hoek": "4.x.x", 701 | "iron": "4.x.x", 702 | "items": "2.x.x", 703 | "joi": "12.x.x" 704 | }, 705 | "dependencies": { 706 | "boom": { 707 | "version": "5.2.0", 708 | "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", 709 | "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", 710 | "dev": true, 711 | "requires": { 712 | "hoek": "4.x.x" 713 | } 714 | }, 715 | "cryptiles": { 716 | "version": "3.1.4", 717 | "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.4.tgz", 718 | "integrity": "sha512-8I1sgZHfVwcSOY6mSGpVU3lw/GSIZvusg8dD2+OGehCJpOhQRLNcH0qb9upQnOH4XhgxxFJSg6E2kx95deb1Tw==", 719 | "dev": true, 720 | "requires": { 721 | "boom": "5.x.x" 722 | } 723 | }, 724 | "hoek": { 725 | "version": "4.2.1", 726 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 727 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", 728 | "dev": true 729 | }, 730 | "isemail": { 731 | "version": "3.2.0", 732 | "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz", 733 | "integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==", 734 | "dev": true, 735 | "requires": { 736 | "punycode": "2.x.x" 737 | } 738 | }, 739 | "joi": { 740 | "version": "12.0.0", 741 | "resolved": "https://registry.npmjs.org/joi/-/joi-12.0.0.tgz", 742 | "integrity": "sha512-z0FNlV4NGgjQN1fdtHYXf5kmgludM65fG/JlXzU6+rwkt9U5UWuXVYnXa2FpK0u6+qBuCmrm5byPNuiiddAHvQ==", 743 | "dev": true, 744 | "requires": { 745 | "hoek": "4.x.x", 746 | "isemail": "3.x.x", 747 | "topo": "2.x.x" 748 | } 749 | } 750 | } 751 | }, 752 | "subtext": { 753 | "version": "5.0.1", 754 | "resolved": "https://registry.npmjs.org/subtext/-/subtext-5.0.1.tgz", 755 | "integrity": "sha512-zH/jaUKJ/bkrTpEe3zuTFIRnqAwv5xcGpXA2JaxEc30KRAT4k78jZnRqM45snjBSZAuvpI8chRUh1VZprcUVfw==", 756 | "dev": true, 757 | "requires": { 758 | "boom": "5.x.x", 759 | "bourne": "1.x.x", 760 | "content": "3.x.x", 761 | "hoek": "4.x.x", 762 | "pez": "2.x.x", 763 | "wreck": "12.x.x" 764 | }, 765 | "dependencies": { 766 | "boom": { 767 | "version": "5.2.0", 768 | "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", 769 | "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", 770 | "dev": true, 771 | "requires": { 772 | "hoek": "4.x.x" 773 | } 774 | }, 775 | "hoek": { 776 | "version": "4.2.1", 777 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 778 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", 779 | "dev": true 780 | } 781 | } 782 | }, 783 | "topo": { 784 | "version": "2.0.2", 785 | "resolved": "https://registry.npmjs.org/topo/-/topo-2.0.2.tgz", 786 | "integrity": "sha1-zVYVdSU5BXwNwEkaYhw7xvvh0YI=", 787 | "dev": true, 788 | "requires": { 789 | "hoek": "4.x.x" 790 | }, 791 | "dependencies": { 792 | "hoek": { 793 | "version": "4.2.1", 794 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 795 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", 796 | "dev": true 797 | } 798 | } 799 | }, 800 | "trim-newlines": { 801 | "version": "2.0.0", 802 | "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", 803 | "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", 804 | "dev": true 805 | }, 806 | "velocityjs": { 807 | "version": "1.1.5", 808 | "resolved": "https://registry.npmjs.org/velocityjs/-/velocityjs-1.1.5.tgz", 809 | "integrity": "sha512-U4ANK4MRYSczVZjOp9FkAQoPO9geKSy3CWrBShPxMoWyqDox8SW8AZYiKtlCrV21ucONUtlU0iF3+KKK9AGoyA==", 810 | "dev": true 811 | }, 812 | "vise": { 813 | "version": "2.0.2", 814 | "resolved": "https://registry.npmjs.org/vise/-/vise-2.0.2.tgz", 815 | "integrity": "sha1-awjo+0y3bjpQzW3Q7DczjoEaDTk=", 816 | "dev": true, 817 | "requires": { 818 | "hoek": "4.x.x" 819 | }, 820 | "dependencies": { 821 | "hoek": { 822 | "version": "4.2.1", 823 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 824 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", 825 | "dev": true 826 | } 827 | } 828 | }, 829 | "wreck": { 830 | "version": "12.5.1", 831 | "resolved": "https://registry.npmjs.org/wreck/-/wreck-12.5.1.tgz", 832 | "integrity": "sha512-l5DUGrc+yDyIflpty1x9XuMj1ehVjC/dTbF3/BasOO77xk0EdEa4M/DuOY8W88MQDAD0fEDqyjc8bkIMHd2E9A==", 833 | "dev": true, 834 | "requires": { 835 | "boom": "5.x.x", 836 | "hoek": "4.x.x" 837 | }, 838 | "dependencies": { 839 | "boom": { 840 | "version": "5.2.0", 841 | "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", 842 | "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", 843 | "dev": true, 844 | "requires": { 845 | "hoek": "4.x.x" 846 | } 847 | }, 848 | "hoek": { 849 | "version": "4.2.1", 850 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", 851 | "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", 852 | "dev": true 853 | } 854 | } 855 | } 856 | } 857 | } 858 | -------------------------------------------------------------------------------- /reacjilator-ruby/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reacjilator-ruby", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Kazuhiro Sera @seratch", 10 | "license": "MIT", 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "serverless-hooks-plugin": "^1.1.0", 14 | "serverless-offline": "^4.10.6" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /reacjilator-ruby/serverless.yml: -------------------------------------------------------------------------------- 1 | service: reacjilator-ruby 2 | 3 | provider: 4 | name: aws 5 | region: ap-northeast-1 6 | stage: ${opt:stage, 'dev'} 7 | runtime: ruby2.5 8 | environment: 9 | SLACK_API_TOKEN: ${env:SLACK_API_TOKEN} 10 | SLACK_SIGNING_SECRET: ${env:SLACK_SIGNING_SECRET} 11 | GOOGLE_APPLICATION_CREDENTIALS: ${env:GOOGLE_APPLICATION_CREDENTIALS} 12 | SERVERLESS_STAGE: ${opt:stage, 'dev'} 13 | 14 | plugins: 15 | - serverless-offline 16 | - serverless-hooks-plugin 17 | 18 | custom: 19 | hooks: 20 | package:initialize: 21 | - bundle install --deployment 22 | 23 | functions: 24 | events: 25 | handler: handler.events 26 | events: 27 | - http: 28 | path: /slack/events 29 | method: post -------------------------------------------------------------------------------- /reacjilator-typescript/.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | 8 | # Webpack directories 9 | .webpack -------------------------------------------------------------------------------- /reacjilator-typescript/README.md: -------------------------------------------------------------------------------- 1 | ## reacjilator in TypeScript 2 | 3 | Original: https://github.com/slackapi/reacjilator 4 | 5 | ## How to run the app 6 | 7 | ### Prerequisites 8 | 9 | * Google Translate API Token 10 | * https://console.cloud.google.com/apis/api/translate.googleapis.com/credentials 11 | 12 | * Slack App 13 | * Create a new one here: https://api.slack.com/apps 14 | * Bot Users: https://api.slack.com/apps/{apiAppId}/bots 15 | * Scopes: https://api.slack.com/apps/{apiAppId}/oauth 16 | 17 | ![Scopes](https://raw.githubusercontent.com/seratch/slack-app-examples/master/reacjilator-typescript/scopes.png "Scopes") 18 | 19 | * https://ngrok.com/ 20 | 21 | ### Launch the app from the Terminal 22 | 23 | ```bash 24 | npm i serveless -g 25 | npm i 26 | cp _env .env 27 | # edit .env 28 | source .env 29 | serverless offline --printOutput 30 | ``` 31 | -------------------------------------------------------------------------------- /reacjilator-typescript/_env: -------------------------------------------------------------------------------- 1 | # source .env 2 | export SLACK_API_TOKEN=xoxp-xxx 3 | export SLACK_SIGNING_SECRET=6c3xxx 4 | export GOOGLE_PROJECT_ID=xxx 5 | export GOOGLE_KEY=xxx-xxx-xxx-xxx 6 | -------------------------------------------------------------------------------- /reacjilator-typescript/app.ts: -------------------------------------------------------------------------------- 1 | // 2 | // A TypeScript implementation of https://github.com/slackapi/reacjilator 3 | // 4 | // Author: Kazuhiro Sera @seratch 5 | // MIT License as with the original code 6 | // 7 | 8 | // ---------------- 9 | // Express app 10 | import * as express from 'express'; 11 | import { Express, Request, Response } from 'express'; 12 | import * as bodyParser from 'body-parser'; 13 | 14 | export const app: Express = express(); 15 | // app.use(bodyParser.json()); 16 | app.use(bodyParser.urlencoded({ extended: true })); 17 | 18 | // ---------------- 19 | // Slack 20 | import * as Slack from '@slack/web-api'; 21 | const { verifyRequestSignature } = require('@slack/events-api/dist/'); 22 | 23 | import * as SlackWebApi from 'seratch-slack-types/web-api'; 24 | import * as SlackEventsApi from 'seratch-slack-types/events-api'; 25 | import * as SlackAppToolkit from 'seratch-slack-app-toolkit'; 26 | 27 | // shortened type names 28 | type ReactionAdded = SlackEventsApi.ReactionAddedPayload; 29 | const Op = SlackAppToolkit.EventsApi.EventsApiOperation; 30 | type OpArgs = SlackAppToolkit.EventsApi.EventsApiOperationArgs; 31 | 32 | // Slack Web Client with sufficient scopes 33 | export const slackApi = new Slack.WebClient(process.env.SLACK_API_TOKEN); 34 | // A framework to handle Slack Events crafted by @seratch 35 | // You should take a look at https://github.com/slackapi/bolt too 36 | const slackEventsOperator = new SlackAppToolkit.EventsApi.EventsApiOperator(); 37 | 38 | // ---------------- 39 | // Google Translate API 40 | import { Translate as GoogleTranslateApi } from '@google-cloud/translate'; 41 | // https://console.cloud.google.com/apis/api/translate.googleapis.com/credentials?project={Project ID} 42 | // $ export GOOGLE_PROJECT_ID={Project ID} 43 | // $ export GOOGLE_KEY={API Key} 44 | const googleApiCredentials = { 45 | projectId: process.env.GOOGLE_PROJECT_ID, 46 | key: process.env.GOOGLE_KEY 47 | } 48 | const googleApi: GoogleTranslateApi = new GoogleTranslateApi(googleApiCredentials); 49 | 50 | // ---------------- 51 | // Enable debug logging if true 52 | const debug: boolean = true; 53 | // lang code mapping data 54 | import { langcode } from './langcode'; 55 | 56 | // ---------------- 57 | // App 58 | 59 | slackEventsOperator.add('reaction_added', new Op( 60 | function (args: OpArgs) { 61 | const payload: ReactionAdded = args.payload; 62 | const res: Response = args.response; 63 | if (debug) { 64 | console.log(payload.event); 65 | } 66 | if (payload.event.item.type !== 'message') { 67 | // Skip any events apart from reactions put on messages 68 | return res.status(200); 69 | } 70 | const reactionName = payload.event.reaction; 71 | let country: string = null; 72 | // Check the reaction name if it is a country flag 73 | if (reactionName.match(/flag-/)) { // when the name has flag- prefix 74 | country = reactionName.match(/(?!flag-\b)\b\w+/)[0]; 75 | } else { // jp, fr, etc. 76 | const flags = Object.keys(langcode.All); // array 77 | if (flags.includes(reactionName)) { 78 | country = reactionName; 79 | } else { 80 | return res.status(200); 81 | } 82 | } 83 | // Finding a lang based on a country is not the best way but oh well 84 | // Matching ISO 639-1 language code 85 | const lang: string = langcode.All[country]; 86 | if (!lang) { 87 | return res.status(200); 88 | } 89 | if (debug) { 90 | console.log(`Detected country: ${country}, lang: ${lang} from reaction: ${reactionName}`); 91 | } 92 | 93 | const channelId: string = payload.event.item.channel; 94 | const messageTs: string = payload.event.item.ts; 95 | 96 | // Fetch all the messages in the thread 97 | slackApi.conversations.replies({ 98 | channel: channelId, 99 | ts: messageTs, 100 | inclusive: true 101 | }) // The returned value is a Promise - chaining operations started here 102 | .then((repliesRes: SlackWebApi.ConversationsRepliesResponse) => { 103 | if (debug) { 104 | console.log(repliesRes.messages); 105 | } 106 | const messages = repliesRes.messages; 107 | const message = messages[0]; 108 | if (message.text) { 109 | // Call Google Translate API to get a translated text 110 | googleApi.translate(message.text, lang) 111 | .then((array) => { 112 | const [translatedText, googleApiRes] = array; // [string, r.Response] 113 | if (debug) { 114 | console.log(`Response from Google Translate API: ${JSON.stringify(googleApiRes)}`); 115 | } 116 | 117 | // To avoid posting same messages several times, make sure if a same message in the thread doesn't exist 118 | let alreadyPosted: boolean = false; 119 | messages.forEach(messageInTheThread => { 120 | if (!alreadyPosted && messageInTheThread.text && messageInTheThread.text === translatedText) { 121 | alreadyPosted = true; 122 | } 123 | }); 124 | if (alreadyPosted) { 125 | return; 126 | } 127 | 128 | // Post the translated text as a following message in the thread 129 | slackApi.chat.postMessage({ 130 | channel: channelId, 131 | text: translatedText, 132 | as_user: false, 133 | username: "Reacjilator Bot", 134 | thread_ts: message.thread_ts ? message.thread_ts : message.ts 135 | }) 136 | .then((postRes: SlackWebApi.ChatPostMessageResponse) => { 137 | if (postRes.ok) { 138 | console.log(`Successfully posted a translated message (ts: ${postRes.ts})`); 139 | } else { 140 | if (debug) { 141 | console.error(postRes); 142 | } 143 | console.error(`Got an error from chat.postMessage (error: ${postRes.error})`); 144 | } 145 | }) 146 | .catch(reason => { 147 | console.error(`Failed to post a message because ${reason}`); 148 | }) 149 | 150 | }) 151 | .catch(reason => { 152 | console.error(`Failed to call Google Translate API because ${reason}`); 153 | }) 154 | 155 | } else { 156 | console.log(`Skipped the message because it doesn't have text property (ts: ${message.ts})`); 157 | } 158 | }) 159 | .catch(reason => { 160 | console.error(`Failed to fetch message replies because ${reason}`); 161 | }); 162 | 163 | // Return 200 OK right away 164 | return res.status(200).json({ ok: true }); 165 | } 166 | )); 167 | 168 | app.post('/slack/events', function (req: Request, res: Response) { 169 | if (debug) { 170 | console.log(req.body); 171 | } 172 | const requestBody = req.body.toString(); 173 | try { 174 | // https://github.com/slackapi/node-slack-events-api/blob/v2.2.0/src/http-handler.js#L22-L58 175 | verifyRequestSignature({ 176 | signingSecret: process.env.SLACK_SIGNING_SECRET, 177 | requestSignature: req.get('X-Slack-Signature'), 178 | requestTimestamp: req.get('X-Slack-Request-Timestamp'), 179 | body: requestBody 180 | }); 181 | } catch (verificationErr) { 182 | return res.status(401).json({ ok: false }); 183 | } 184 | slackEventsOperator.dispatch(JSON.parse(requestBody), req, res); 185 | }); -------------------------------------------------------------------------------- /reacjilator-typescript/handler.ts: -------------------------------------------------------------------------------- 1 | import 'source-map-support/register'; 2 | import { app } from './app'; 3 | export const dispatcher = require('serverless-http')(app); -------------------------------------------------------------------------------- /reacjilator-typescript/langcode.ts: -------------------------------------------------------------------------------- 1 | export module langcode { 2 | export const All = { 3 | ac: 'en', 4 | ad: 'ca', 5 | ae: 'ar', 6 | af: 'ps', 7 | ag: 'en', 8 | ai: 'en', 9 | al: 'sq', 10 | am: 'hy', 11 | ao: 'pt', 12 | aq: '', 13 | ar: 'es', 14 | as: 'en', 15 | at: 'de', 16 | au: 'en', 17 | aw: 'nl', 18 | ax: 'sv', 19 | az: '', 20 | ba: 'bs', 21 | bb: 'en', 22 | bd: 'bn', 23 | be: 'nl', 24 | bf: 'fr', 25 | bg: 'bg', 26 | bh: 'ar', 27 | bi: 'fr', 28 | bj: 'fr', 29 | bl: 'fr', 30 | bn: 'en', 31 | bm: 'ms', 32 | bo: 'es', 33 | bq: 'nl', 34 | br: 'pt', 35 | bs: 'en', 36 | bt: '', 37 | bv: '', 38 | bw: 'en', 39 | by: 'be', 40 | bz: 'en', 41 | ca: 'en', 42 | cc: 'ms', 43 | cd: 'fr', 44 | cf: 'fr', 45 | cg: 'fr', 46 | ch: 'de', 47 | ci: 'fr', 48 | ck: 'en', 49 | cl: 'es', 50 | cm: 'fr', 51 | cn: 'zh-CN', 52 | co: 'es', 53 | cp: 'fr', 54 | cr: 'es', 55 | cu: 'es', 56 | cv: 'pt', 57 | cw: 'nl', 58 | cx: 'en', 59 | cy: 'el', 60 | cz: 'cs', 61 | de: 'de', 62 | dg: '', 63 | dj: 'fr', 64 | dk: 'da', 65 | dm: 'en', 66 | do: 'es', 67 | dz: 'ar', 68 | ea: 'es', 69 | ec: 'es', 70 | ee: 'et', 71 | eg: 'ar', 72 | eh: 'ar', 73 | er: '', 74 | es: 'es', 75 | et: '', 76 | eu: '', 77 | fi: 'fi', 78 | fj: 'en', 79 | fk: 'en', 80 | fm: 'en', 81 | fo: '', 82 | fr: 'fr', 83 | ga: 'fr', 84 | gb: 'en', 85 | gd: 'en', 86 | ge: 'ka', 87 | gf: 'fr', 88 | gg: 'en', 89 | gh: 'en', 90 | gi: 'en', 91 | gl: 'da', 92 | gm: 'en', 93 | gn: 'fr', 94 | gp: 'fr', 95 | gq: 'es', 96 | gr: 'el', 97 | gs: 'en', 98 | gt: 'es', 99 | gu: 'en', 100 | gw: 'pt', 101 | gy: 'en', 102 | hk: 'zh-TW', 103 | hm: '', 104 | hn: 'es', 105 | hr: 'hr', 106 | ht: 'ht', 107 | hu: 'hu', 108 | ic: 'es', 109 | id: 'id', 110 | ie: 'ga', 111 | il: 'iw', 112 | im: 'en', 113 | in: 'hi', 114 | io: 'en', 115 | iq: 'ar', 116 | ir: 'fa', 117 | is: 'is', 118 | it: 'it', 119 | je: 'en', 120 | jm: 'en', 121 | jo: 'ar', 122 | jp: 'ja', 123 | ke: 'en', 124 | kg: 'ky', 125 | kh: 'km', 126 | ki: 'en', 127 | km: '', 128 | kn: 'en', 129 | kp: 'ko', 130 | kr: 'ko', 131 | kw: 'ar', 132 | ky: 'en', 133 | kz: 'kk', 134 | la: 'lo', 135 | lb: 'ar', 136 | lc: 'en', 137 | li: 'de', 138 | lk: 'si', 139 | lr: 'en', 140 | ls: 'st', 141 | lt: 'lt', 142 | lu: 'lb', 143 | lv: 'lv', 144 | ly: 'ar', 145 | ma: 'ar', 146 | mc: 'fr', 147 | md: 'ro', 148 | me: '', 149 | mf: '', 150 | mg: 'mg', 151 | mh: '', 152 | mk: 'mk', 153 | ml: 'fr', 154 | mm: '', 155 | mn: 'mn', 156 | mo: 'zh-TW', 157 | mp: 'en', 158 | mq: 'fr', 159 | mr: 'ar', 160 | ms: 'en', 161 | mt: 'mt', 162 | mu: 'en', 163 | mv: '', 164 | mw: 'en', 165 | mx: 'es', 166 | my: 'ms', 167 | mz: 'pt', 168 | na: 'en', 169 | nc: 'fr', 170 | ne: 'fr', 171 | nf: 'en', 172 | ng: 'en', 173 | ni: 'es', 174 | nl: 'nl', 175 | no: 'no', 176 | np: 'ne', 177 | nr: '', 178 | nu: '', 179 | nz: 'en', 180 | om: 'ar', 181 | pa: 'es', 182 | pe: 'es', 183 | pf: 'fr', 184 | pg: '', 185 | ph: 'tl', 186 | pk: 'ur', 187 | pl: 'pl', 188 | pm: 'fr', 189 | pn: 'en', 190 | pr: 'es', 191 | ps: 'ar', 192 | pt: 'pt', 193 | pw: 'en', 194 | py: 'es', 195 | qa: 'ar', 196 | re: 'fr', 197 | ro: 'ro', 198 | rs: 'sr', 199 | ru: 'ru', 200 | rw: '', 201 | sa: 'ar', 202 | sb: 'en', 203 | sc: 'en', 204 | sd: 'ar', 205 | se: 'sv', 206 | sg: 'en', 207 | sh: 'en', 208 | si: 'sl', 209 | sj: 'no', 210 | sk: 'sk', 211 | sl: 'en', 212 | sm: 'it', 213 | sn: 'fr', 214 | so: 'so', 215 | sr: 'nl', 216 | ss: 'en', 217 | st: 'pt', 218 | sv: 'es', 219 | sx: 'nl', 220 | sw: 'ar', 221 | sz: '', 222 | ta: 'en', 223 | tc: 'en', 224 | td: 'fr', 225 | tf: 'fr', 226 | tg: 'fr', 227 | th: 'th', 228 | tj: 'tg', 229 | tk: '', 230 | tl: '', 231 | tm: '', 232 | tn: 'ar', 233 | to: '', 234 | tr: 'tr', 235 | tt: 'en', 236 | tv: '', 237 | tw: 'zh-TW', 238 | tz: 'sw', 239 | ua: 'uk', 240 | ug: 'en', 241 | um: 'en', 242 | us: 'en', 243 | uy: 'es', 244 | uz: 'uz', 245 | va: 'it', 246 | vc: 'en', 247 | ve: 'es', 248 | vg: 'en', 249 | vi: 'en', 250 | vn: 'vi', 251 | vu: '', 252 | wf: 'fr', 253 | ws: 'sm', 254 | xk: 'sq', 255 | ye: 'ar', 256 | yt: 'fr', 257 | za: 'af', 258 | zm: 'en', 259 | zw: 'en', 260 | }; 261 | } -------------------------------------------------------------------------------- /reacjilator-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foo-app", 3 | "version": "1.0.0", 4 | "description": "Serverless webpack example using Typescript", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "dependencies": { 10 | "@google-cloud/translate": "2.1.4", 11 | "@slack/events-api": "^2.2.0", 12 | "@slack/web-api": "^5.0.1", 13 | "axios": "^0.21.2", 14 | "body-parser": "^1.19.0", 15 | "express": "^4.16.4", 16 | "seratch-slack-app-toolkit": "0.0.4", 17 | "serverless": "^3.25.1", 18 | "serverless-http": "^2.0.1", 19 | "source-map-support": "^0.5.10" 20 | }, 21 | "devDependencies": { 22 | "@types/aws-lambda": "^8.10.17", 23 | "@types/express": "^4.16.1", 24 | "@types/node": "^10.12.18", 25 | "seratch-slack-types": "^0.2.1", 26 | "serverless-offline": "^4.9.4", 27 | "serverless-webpack": "^5.2.0", 28 | "ts-loader": "^5.3.3", 29 | "typescript": "^3.2.4", 30 | "webpack": "^4.35.3" 31 | }, 32 | "author": "The serverless webpack authors (https://github.com/elastic-coders/serverless-webpack)", 33 | "license": "MIT" 34 | } 35 | -------------------------------------------------------------------------------- /reacjilator-typescript/scopes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seratch/slack-app-examples/87f679e6856b3d0f1e86bd21c9a37d72ba52e9b0/reacjilator-typescript/scopes.png -------------------------------------------------------------------------------- /reacjilator-typescript/serverless.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: awesome-app 3 | 4 | # Add the serverless-webpack plugin 5 | plugins: 6 | - serverless-offline 7 | - serverless-webpack 8 | 9 | provider: 10 | name: aws 11 | runtime: nodejs8.10 12 | 13 | functions: 14 | dispatcher: 15 | handler: handler.dispatcher 16 | events: 17 | - http: 18 | method: post 19 | path: /slack/events 20 | -------------------------------------------------------------------------------- /reacjilator-typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "es2017" 5 | ], 6 | "moduleResolution": "node", 7 | "noUnusedLocals": true, 8 | "noUnusedParameters": true, 9 | "sourceMap": true, 10 | "target": "es2017", 11 | "outDir": "lib", 12 | "allowSyntheticDefaultImports": true 13 | }, 14 | "exclude": [ 15 | "node_modules" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /reacjilator-typescript/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const slsw = require('serverless-webpack'); 3 | 4 | module.exports = { 5 | mode: slsw.lib.webpack.isLocal ? 'development' : 'production', 6 | entry: slsw.lib.entries, 7 | devtool: 'source-map', 8 | resolve: { 9 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 10 | }, 11 | output: { 12 | libraryTarget: 'commonjs', 13 | path: path.join(__dirname, '.webpack'), 14 | filename: '[name].js', 15 | }, 16 | target: 'node', 17 | externals: { 18 | // WARNING in ./node_modules/retry-request/index.js 19 | // Module not found: Error: Can't resolve 'request' in 'node_modules/retry-request' 20 | "request": "request", 21 | // WARNING in ./node_modules/express/lib/view.js 81:13-25 22 | // Critical dependency: the request of a dependency is an expression 23 | "express": "express" 24 | }, 25 | module: { 26 | rules: [ 27 | // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader` 28 | { 29 | test: /\.tsx?$/, 30 | loader: 'ts-loader' 31 | }, 32 | ], 33 | }, 34 | }; -------------------------------------------------------------------------------- /serverless-bolt-template/aws-js/.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | 8 | # Webpack directories 9 | .webpack -------------------------------------------------------------------------------- /serverless-bolt-template/aws-js/README.md: -------------------------------------------------------------------------------- 1 | # Slack App backend built with Bolt on AWS 2 | 3 | ## Run the app on your local machine 4 | 5 | ### Configure necessary env variables 6 | 7 | ```bash 8 | cp _env .env_prod 9 | cp _env .env_dev 10 | ``` 11 | 12 | ```bash 13 | # Slack Tokens https://api.slack.com/docs/oauth 14 | export SLACK_API_TOKEN=xoxp-xxxxxxxxx 15 | export SLACK_BOT_TOKEN=xoxb-xxxxxxxxx 16 | 17 | # X-Slack-Signature https://api.slack.com/docs/verifying-requests-from-slack 18 | export SLACK_SIGNING_SECRET=xxxxxxxxx 19 | 20 | # OAuth Credentials https://api.slack.com/apps/{apiAppId}/general 21 | export SLACK_CLIENT_ID=xxxxxxxxx.xxxxxxxxx 22 | export SLACK_CLIENT_SECRET=xxxxxxxxx 23 | export SLACK_REDIRECT_URI=https://www.example.com/... 24 | 25 | # Incoming Webhook https://api.slack.com/apps/{apiAppId}/incoming-webhooks 26 | export SLACK_WEBHOOK_URL=https://hooks.slack.com/... 27 | 28 | # Serverless Framework 29 | export SERVERLESS_STAGE=dev 30 | ``` 31 | 32 | ### Run ngrok proxy on your local machine 33 | 34 | https://ngrok.com/ 35 | 36 | ```bash 37 | ngrok http 3000 38 | ``` 39 | 40 | ### Create a Slack App 41 | 42 | https://api.slack.com/apps 43 | 44 | - Create a Slack App 45 | - Use these information in "App Credentials" (https://api.slack.com/apps/{apiAppId}/general) 46 | - Client ID, Client Secret 47 | - Signing Secret 48 | - Add an Incoming Webhook 49 | - Set the URL as the env variable: `SLACK_WEBHOOK_URL` 50 | - Add a Slash Command 51 | - `/echo` command 52 | - Request URL: `https://{ngrok domain}/slack/events` or `https://{aws}/{stage}/slack/events` 53 | - Add a Bot user 54 | - Enable `bot` permission & add a bot user 55 | - Enable Interactive Components 56 | - Request URL: `https://{ngrok domain}/slack/events` or `https://{aws}/{stage}/slack/events` 57 | - Message Actions 58 | - Add a random action 59 | - Message Menus 60 | - Unsupported in this example; If you're interested in this outmoded one, you can return it in any messages with attachments 61 | - Enable Event Subscriptions 62 | - Request URL: `https://{ngrok domain}/slack/events` or `https://{aws}/{stage}/slack/events` 63 | - Subscribe to Bot Events: `app_mention`, `message.channels` 64 | - Add Necessary Permissions 65 | - `bot` 66 | - `chat:write:bot` 67 | 68 | ```bash 69 | npm i 70 | npm i serverless -g 71 | serverless offline --printOutput 72 | ``` 73 | 74 | ## Deploy the app onto AWS 75 | 76 | ```bash 77 | serverless deploy --stage ${SERVERLESS_STAGE} -v 78 | ``` 79 | -------------------------------------------------------------------------------- /serverless-bolt-template/aws-js/_env: -------------------------------------------------------------------------------- 1 | # Slack Tokens https://api.slack.com/docs/oauth 2 | export SLACK_API_TOKEN=xoxp-xxxxxxxxx 3 | export SLACK_BOT_TOKEN=xoxb-xxxxxxxxx 4 | 5 | # X-Slack-Signature https://api.slack.com/docs/verifying-requests-from-slack 6 | export SLACK_SIGNING_SECRET=xxxxxxxxx 7 | 8 | # OAuth Credentials https://api.slack.com/apps/{apiAppId}/general 9 | export SLACK_CLIENT_ID=xxxxxxxxx.xxxxxxxxx 10 | export SLACK_CLIENT_SECRET=xxxxxxxxx 11 | export SLACK_REDIRECT_URI=https://www.example.com/... 12 | 13 | # Incoming Webhook https://api.slack.com/apps/{apiAppId}/incoming-webhooks 14 | export SLACK_WEBHOOK_URL=https://hooks.slack.com/... 15 | 16 | # Serverless Framework 17 | export SERVERLESS_STAGE=dev -------------------------------------------------------------------------------- /serverless-bolt-template/aws-js/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // ------------------------------------------------------ 4 | // Bot app 5 | // https://slack.dev/bolt/ 6 | const { App, ExpressReceiver } = require('@slack/bolt'); 7 | const expressReceiver = new ExpressReceiver({ 8 | signingSecret: process.env.SLACK_SIGNING_SECRET, 9 | processBeforeResponse: true, 10 | }); 11 | const app = new App({ 12 | token: process.env.SLACK_BOT_TOKEN, 13 | receiver: expressReceiver, 14 | processBeforeResponse: true, 15 | }); 16 | module.exports.expressApp = expressReceiver.app; 17 | 18 | // ------------------------------------------------------ 19 | // If you need to use API methods that are not listed on https://api.slack.com/bot-users#methods 20 | // you need to use user api token instead like this: 21 | const { WebClient } = require('@slack/web-api'); 22 | app.client = new WebClient(process.env.SLACK_API_TOKEN); 23 | 24 | // ------------------------------------------------------ 25 | 26 | // React to "app_mention" events 27 | app.event('app_mention', async ({ event, say }) => { 28 | const res = await app.client.users.info({ user: event.user }); 29 | if (res.ok) { 30 | say({ 31 | text: `Hi! <@${res.user.name}>` 32 | }); 33 | } else { 34 | console.error(`Failed because of ${res.error}`) 35 | } 36 | }); 37 | 38 | // React to message.channels event 39 | app.message('hello', async ({ message, say }) => { 40 | // say() sends a message to the channel where the event was triggered 41 | await say({ 42 | blocks: [ 43 | { 44 | "type": "section", 45 | "text": { 46 | "type": "mrkdwn", 47 | "text": `Hey there <@${message.user}>!` 48 | }, 49 | "accessory": { 50 | "type": "button", 51 | "text": { 52 | "type": "plain_text", 53 | "text": "Click Me" 54 | }, 55 | "action_id": "button_click" 56 | } 57 | } 58 | ] 59 | }); 60 | }); 61 | 62 | // Handle the click event (action_id: button_click) on a message posted by the above hello handler 63 | app.action('button_click', async ({ body, ack, say }) => { 64 | // Acknowledge the action 65 | await ack(); 66 | await say(`<@${body.user.id}> clicked the button`); 67 | }); 68 | 69 | // Handle `/echo` command invocations 70 | app.command('/echo', async ({ command, ack, say }) => { 71 | // Acknowledge command request 72 | await ack(); 73 | await say(`You said "${command.text}"`); 74 | }); 75 | 76 | // A simple example to use WebApi client 77 | app.message('42', async ({ message }) => { 78 | // use chat.postMessage over say method 79 | const res = await app.client.chat.postMessage({ 80 | channel: message.channel, 81 | text: 'The answer to life, the universe and everything', 82 | thread_ts: message.ts 83 | }); 84 | if (res.ok) { 85 | console.log(`Succeeded ${JSON.stringify(res.message)}`) 86 | } else { 87 | console.error(`Failed because of ${res.error}`) 88 | } 89 | }) 90 | 91 | // A simple example to use Webhook internally 92 | app.message('webhook', async ({ message }) => { 93 | const { IncomingWebhook } = require('@slack/webhook'); 94 | const url = process.env.SLACK_WEBHOOK_URL; 95 | const webhook = new IncomingWebhook(url); 96 | const res = await webhook.send({ 97 | text: message.text.split("webhook")[1], 98 | unfurl_links: true 99 | }); 100 | console.log(`Succeeded ${JSON.stringify(res)}`) 101 | }) 102 | 103 | // Check the details of the error to handle cases where you should retry sending a message or stop the app 104 | app.error(async (error) => { 105 | console.error(error); 106 | }); 107 | 108 | // As long as you run this app as a "Serverless Framework" app, you don't need to have the following code 109 | // (async () => { 110 | // // Start your app 111 | // await app.start(process.env.PORT || 3000); 112 | // console.log('⚡️ Bolt app is running!'); 113 | // })(); 114 | 115 | // ------------------------------------------------------ 116 | // OAuth flow 117 | module.exports.expressApp.get('/slack/installation', (req, res) => { 118 | const clientId = process.env.SLACK_CLIENT_ID; 119 | const scopesCsv = 'commands,users:read,users:read.email,team:read'; // TODO: modify 120 | const state = 'randomly-generated-string'; // TODO: implement the logic 121 | const url = `https://slack.com/oauth/authorize?client_id=${clientId}&scope=${scopesCsv}&state=${state}`; 122 | res.redirect(url); 123 | }); 124 | 125 | module.exports.expressApp.get('/slack/oauth', (req, res) => { 126 | // TODO: make sure if req.query.state is valid 127 | app.client.oauth.access({ 128 | code: req.query.code, 129 | client_id: process.env.SLACK_CLIENT_ID, 130 | client_secret: process.env.SLACK_CLIENT_SECRET, 131 | redirect_uri: process.env.SLACK_REDIRECT_URI 132 | }) 133 | .then(apiRes => { 134 | if (apiRes.ok) { 135 | console.log(`Succeeded! ${JSON.stringify(apiRes)}`) 136 | // TODO: show a complete webpage here 137 | res.status(200).send(`Thanks!`); 138 | } else { 139 | console.error(`Failed because of ${apiRes.error}`) 140 | // TODO: show a complete webpage here 141 | res.status(500).send(`Something went wrong! error: ${apiRes.error}`); 142 | } 143 | }).catch(reason => { 144 | console.error(`Failed because ${reason}`) 145 | // TODO: show a complete webpage here 146 | res.status(500).send(`Something went wrong! reason: ${reason}`); 147 | }); 148 | }); 149 | -------------------------------------------------------------------------------- /serverless-bolt-template/aws-js/handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const app = require('./app'); 4 | module.exports.app = require('serverless-http')(app.expressApp); 5 | -------------------------------------------------------------------------------- /serverless-bolt-template/aws-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bot-serverless-aws", 3 | "version": "1.0.0", 4 | "description": "Serverless Slack app built with Bolt, which runs on AWS", 5 | "main": "app.js", 6 | "dependencies": { 7 | "@slack/bolt": "^2.7.0", 8 | "serverless-http": "^2.5.0" 9 | }, 10 | "devDependencies": { 11 | "serverless-offline": "^11.6.0" 12 | }, 13 | "scripts": { 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "keywords": [ 17 | "Slack", 18 | "Serverless" 19 | ], 20 | "author": "Awesome developers", 21 | "license": "MIT" 22 | } 23 | -------------------------------------------------------------------------------- /serverless-bolt-template/aws-js/serverless.yml: -------------------------------------------------------------------------------- 1 | # Welcome to Serverless! 2 | # 3 | # This file is the main config file for your service. 4 | # It's very minimal at this point and uses default values. 5 | # You can always add more config options for more control. 6 | # We've included some commented out config examples here. 7 | # Just uncomment any of them to get that config option. 8 | # 9 | # For full config options, check the docs: 10 | # docs.serverless.com 11 | # 12 | # Happy Coding! 13 | 14 | plugins: 15 | - serverless-offline 16 | 17 | service: bolt-serverless-aws 18 | 19 | provider: 20 | name: aws 21 | runtime: nodejs8.10 22 | region: ap-northeast-1 # Tokyo Region 23 | environment: 24 | SERVERLESS_STAGE: ${env:SERVERLESS_STAGE, 'dev'} 25 | SLACK_WEBHOOK_URL: ${env:SLACK_WEBHOOK_URL} 26 | SLACK_API_TOKEN: ${env:SLACK_API_TOKEN} 27 | SLACK_BOT_TOKEN: ${env:SLACK_BOT_TOKEN} 28 | SLACK_APP_CLIENT_ID: ${env:SLACK_APP_CLIENT_ID} 29 | SLACK_APP_CLIENT_SECRET: ${env:SLACK_APP_CLIENT_SECRET} 30 | SLACK_SIGNING_SECRET: ${env:SLACK_SIGNING_SECRET} 31 | SLACK_APP_REDIRECT_URI: ${env:SLACK_APP_REDIRECT_URI} 32 | 33 | # you can add statements to the Lambda function's IAM Role here 34 | # iamRoleStatements: 35 | # - Effect: "Allow" 36 | # Action: 37 | # - "s3:ListBucket" 38 | # Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ] } 39 | # - Effect: "Allow" 40 | # Action: 41 | # - "s3:PutObject" 42 | # Resource: 43 | # Fn::Join: 44 | # - "" 45 | # - - "arn:aws:s3:::" 46 | # - "Ref" : "ServerlessDeploymentBucket" 47 | # - "/*" 48 | 49 | # you can add packaging information here 50 | #package: 51 | # include: 52 | # - include-me.js 53 | # - include-me-dir/** 54 | # exclude: 55 | # - exclude-me.js 56 | # - exclude-me-dir/** 57 | 58 | functions: 59 | app: 60 | handler: handler.app 61 | events: 62 | # Bolt App 63 | - http: 64 | method: post 65 | path: /slack/events 66 | # OAuth Flow 67 | - http: 68 | method: get 69 | path: /slack/installation 70 | - http: 71 | method: get 72 | path: /slack/oauth 73 | -------------------------------------------------------------------------------- /serverless-bolt-template/aws-ts/.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | 8 | # Webpack directories 9 | .webpack -------------------------------------------------------------------------------- /serverless-bolt-template/aws-ts/README.md: -------------------------------------------------------------------------------- 1 | # Slack App backend built with Bolt on AWS 2 | 3 | ## Run the app on your local machine 4 | 5 | ### Configure necessary env variables 6 | 7 | ```bash 8 | cp _env .env_prod 9 | cp _env .env_dev 10 | ``` 11 | 12 | ```bash 13 | # Slack Tokens https://api.slack.com/docs/oauth 14 | export SLACK_API_TOKEN=xoxp-xxxxxxxxx 15 | export SLACK_BOT_TOKEN=xoxb-xxxxxxxxx 16 | 17 | # X-Slack-Signature https://api.slack.com/docs/verifying-requests-from-slack 18 | export SLACK_SIGNING_SECRET=xxxxxxxxx 19 | 20 | # OAuth Credentials https://api.slack.com/apps/{apiAppId}/general 21 | export SLACK_CLIENT_ID=xxxxxxxxx.xxxxxxxxx 22 | export SLACK_CLIENT_SECRET=xxxxxxxxx 23 | export SLACK_REDIRECT_URI=https://www.example.com/... 24 | 25 | # Incoming Webhook https://api.slack.com/apps/{apiAppId}/incoming-webhooks 26 | export SLACK_WEBHOOK_URL=https://hooks.slack.com/... 27 | 28 | # Serverless Framework 29 | export SERVERLESS_STAGE=dev 30 | ``` 31 | 32 | ### Run ngrok proxy on your local machine 33 | 34 | https://ngrok.com/ 35 | 36 | ```bash 37 | ngrok http 3000 38 | ``` 39 | 40 | ### Create a Slack App 41 | 42 | https://api.slack.com/apps 43 | 44 | - Create a Slack App 45 | - Use these information in "App Credentials" (https://api.slack.com/apps/{apiAppId}/general) 46 | - Client ID, Client Secret 47 | - Signing Secret 48 | - Add an Incoming Webhook 49 | - Set the URL as the env variable: `SLACK_WEBHOOK_URL` 50 | - Add a Slash Command 51 | - `/echo` command 52 | - Request URL: `https://{ngrok domain}/slack/events` or `https://{aws}/{stage}/slack/events` 53 | - Add a Bot user 54 | - Enable `bot` permission & add a bot user 55 | - Enable Interactive Components 56 | - Request URL: `https://{ngrok domain}/slack/events` or `https://{aws}/{stage}/slack/events` 57 | - Message Actions 58 | - Add a random action 59 | - Message Menus 60 | - Unsupported in this example; If you're interested in this outmoded one, you can return it in any messages with attachments 61 | - Enable Event Subscriptions 62 | - Request URL: `https://{ngrok domain}/slack/events` or `https://{aws}/{stage}/slack/events` 63 | - Enable `app_metion`, `message.channels` 64 | - Add Necessary Permissions 65 | - `bot` 66 | - `chat:write:bot` 67 | 68 | ```bash 69 | npm i 70 | npm i serverless -g 71 | serverless offline --printOutput 72 | ``` 73 | 74 | ## Deploy the app onto AWS 75 | 76 | ```bash 77 | serverless deploy --stage ${SERVERLESS_STAGE} -v 78 | ``` 79 | -------------------------------------------------------------------------------- /serverless-bolt-template/aws-ts/_env: -------------------------------------------------------------------------------- 1 | # Slack Tokens https://api.slack.com/docs/oauth 2 | export SLACK_API_TOKEN=xoxp-xxxxxxxxx 3 | export SLACK_BOT_TOKEN=xoxb-xxxxxxxxx 4 | 5 | # X-Slack-Signature https://api.slack.com/docs/verifying-requests-from-slack 6 | export SLACK_SIGNING_SECRET=xxxxxxxxx 7 | 8 | # OAuth Credentials https://api.slack.com/apps/{apiAppId}/general 9 | export SLACK_CLIENT_ID=xxxxxxxxx.xxxxxxxxx 10 | export SLACK_CLIENT_SECRET=xxxxxxxxx 11 | export SLACK_REDIRECT_URI=https://www.example.com/... 12 | 13 | # Incoming Webhook https://api.slack.com/apps/{apiAppId}/incoming-webhooks 14 | export SLACK_WEBHOOK_URL=https://hooks.slack.com/... 15 | 16 | # Serverless Framework 17 | export SERVERLESS_STAGE=dev -------------------------------------------------------------------------------- /serverless-bolt-template/aws-ts/app.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // ------------------------------------------------------ 4 | // Type definitions in TypeScript 5 | import * as WebApi from 'seratch-slack-types/web-api'; 6 | import { Request, Response, Application } from 'express'; 7 | 8 | // ------------------------------------------------------ 9 | // Bot app 10 | // https://slack.dev/bolt/ 11 | import { App, ExpressReceiver } from '@slack/bolt'; 12 | 13 | const expressReceiver = new ExpressReceiver({ 14 | signingSecret: process.env.SLACK_SIGNING_SECRET, 15 | processBeforeResponse: true, 16 | }); 17 | export const expressApp: Application = expressReceiver.app; 18 | 19 | const app: App = new App({ 20 | token: process.env.SLACK_BOT_TOKEN, 21 | receiver: expressReceiver, 22 | processBeforeResponse: true, 23 | }); 24 | 25 | // ------------------------------------------------------ 26 | // If you need to use API methods that are not listed on https://api.slack.com/bot-users#methods 27 | // you need to use user api token instead like this: 28 | import { WebClient } from '@slack/web-api'; 29 | app.client = new WebClient(process.env.SLACK_API_TOKEN); 30 | 31 | // ------------------------------------------------------ 32 | 33 | // React to "app_mention" events 34 | app.event('app_mention', async ({ event, say }) => { 35 | const res: WebApi.UsersInfoResponse = await app.client.users.info({ user: event.user }); 36 | if (res.ok) { 37 | say({ 38 | text: `Hi! <@${res.user.name}>` 39 | }); 40 | } else { 41 | console.error(`Failed because of ${res.error}`) 42 | } 43 | }); 44 | 45 | // React to message.channels event 46 | app.message('hello', async ({ message, say }) => { 47 | // say() sends a message to the channel where the event was triggered 48 | await say({ 49 | "text": null, 50 | "blocks": [ 51 | { 52 | "type": "section", 53 | "text": { 54 | "type": "mrkdwn", 55 | "text": `Hey there <@${message.user}>!` 56 | }, 57 | "accessory": { 58 | "type": "button", 59 | "text": { 60 | "type": "plain_text", 61 | "text": "Click Me" 62 | }, 63 | "action_id": "button_click" 64 | } 65 | } 66 | ] 67 | }); 68 | }); 69 | 70 | // Handle the click event (action_id: button_click) on a message posted by the above hello handler 71 | app.action('button_click', async ({ body, ack, say }) => { 72 | // Acknowledge the action 73 | await ack(); 74 | await say(`<@${body.user.id}> clicked the button`); 75 | }); 76 | 77 | // Handle `/echo` command invocations 78 | app.command('/echo', async ({ command, ack, say }) => { 79 | // Acknowledge command request 80 | await ack(); 81 | await say(`You said "${command.text}"`); 82 | }); 83 | 84 | // A simple example to use WebApi client 85 | app.message('42', async ({ message }) => { 86 | // use chat.postMessage over say method 87 | const res: WebApi.ChatPostMessageResponse = await app.client.chat.postMessage({ 88 | channel: message.channel, 89 | text: 'The answer to life, the universe and everything', 90 | thread_ts: message.ts 91 | }); 92 | if (res.ok) { 93 | console.log(`Succeeded ${JSON.stringify(res.message)}`) 94 | } else { 95 | console.error(`Failed because of ${res.error}`) 96 | } 97 | }); 98 | 99 | // A simple example to use Webhook internally 100 | app.message('webhook', async ({ message }) => { 101 | const { IncomingWebhook } = require('@slack/webhook'); 102 | const url = process.env.SLACK_WEBHOOK_URL; 103 | const webhook = new IncomingWebhook(url); 104 | const res = await webhook.send({ 105 | text: message.text.split("webhook")[1], 106 | unfurl_links: true 107 | }); 108 | console.log(`Succeeded ${JSON.stringify(res)}`) 109 | }); 110 | 111 | // Check the details of the error to handle cases where you should retry sending a message or stop the app 112 | app.error(async (error) => { 113 | console.error(error); 114 | }); 115 | 116 | // As long as you run this app as a "Serverless Framework" app, you don't need to have the following code 117 | // (async () => { 118 | // // Start your app 119 | // await app.start(process.env.PORT || 3000); 120 | // console.log('⚡️ Bolt app is running!'); 121 | // })(); 122 | 123 | // ------------------------------------------------------ 124 | // OAuth flow 125 | expressApp.get('/slack/installation', (_req: Request, res: Response) => { 126 | const clientId = process.env.SLACK_CLIENT_ID; 127 | const scopesCsv = 'commands,users:read,users:read.email,team:read'; // TODO: modify 128 | const state = 'randomly-generated-string'; // TODO: implement the logic 129 | const url = `https://slack.com/oauth/authorize?client_id=${clientId}&scope=${scopesCsv}&state=${state}`; 130 | res.redirect(url); 131 | }); 132 | 133 | expressApp.get('/slack/oauth', (req: Request, res: Response) => { 134 | // TODO: make sure if req.query.state is valid 135 | app.client.oauth.access({ 136 | code: req.query.code, 137 | client_id: process.env.SLACK_CLIENT_ID, 138 | client_secret: process.env.SLACK_CLIENT_SECRET, 139 | redirect_uri: process.env.SLACK_REDIRECT_URI 140 | }) 141 | .then((apiRes: WebApi.OauthAccessResponse) => { 142 | if (apiRes.ok) { 143 | console.log(`Succeeded! ${JSON.stringify(apiRes)}`) 144 | // TODO: show a complete webpage here 145 | res.status(200).send(`Thanks!`); 146 | } else { 147 | console.error(`Failed because of ${apiRes.error}`) 148 | // TODO: show a complete webpage here 149 | res.status(500).send(`Something went wrong! error: ${apiRes.error}`); 150 | } 151 | }).catch(reason => { 152 | console.error(`Failed because ${reason}`) 153 | // TODO: show a complete webpage here 154 | res.status(500).send(`Something went wrong! reason: ${reason}`); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /serverless-bolt-template/aws-ts/handler.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { expressApp } from './app'; 4 | export const app = require('serverless-http')(expressApp); 5 | -------------------------------------------------------------------------------- /serverless-bolt-template/aws-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bot-serverless-aws", 3 | "version": "1.0.0", 4 | "description": "Serverless Slack app built with Bolt, which runs on AWS", 5 | "main": "app.js", 6 | "dependencies": { 7 | "@slack/bolt": "^2.7.0", 8 | "@slack/webhook": "^5.0.4", 9 | "seratch-slack-types": "^0.5.0", 10 | "serverless-http": "^2.5.0" 11 | }, 12 | "devDependencies": { 13 | "@types/aws-lambda": "^8.10.25", 14 | "@types/express": "^4.16.1", 15 | "@types/node": "^12.0.0", 16 | "serverless-offline": "^6.7.0", 17 | "serverless-webpack": "^5.3.4", 18 | "ts-loader": "^8.0.3", 19 | "typescript": "^4.0.2", 20 | "webpack": "^4.44.1" 21 | }, 22 | "scripts": { 23 | "test": "echo \"Error: no test specified\" && exit 1" 24 | }, 25 | "keywords": [ 26 | "Slack", 27 | "Serverless" 28 | ], 29 | "author": "Awesome developers", 30 | "license": "MIT" 31 | } 32 | -------------------------------------------------------------------------------- /serverless-bolt-template/aws-ts/serverless.yml: -------------------------------------------------------------------------------- 1 | # Welcome to Serverless! 2 | # 3 | # This file is the main config file for your service. 4 | # It's very minimal at this point and uses default values. 5 | # You can always add more config options for more control. 6 | # We've included some commented out config examples here. 7 | # Just uncomment any of them to get that config option. 8 | # 9 | # For full config options, check the docs: 10 | # docs.serverless.com 11 | # 12 | # Happy Coding! 13 | 14 | plugins: 15 | - serverless-offline 16 | - serverless-webpack 17 | 18 | service: bolt-serverless-aws 19 | 20 | provider: 21 | name: aws 22 | runtime: nodejs8.10 23 | region: ap-northeast-1 # Tokyo Region 24 | environment: 25 | SERVERLESS_STAGE: ${env:SERVERLESS_STAGE, 'dev'} 26 | SLACK_WEBHOOK_URL: ${env:SLACK_WEBHOOK_URL} 27 | SLACK_API_TOKEN: ${env:SLACK_API_TOKEN} 28 | SLACK_BOT_TOKEN: ${env:SLACK_BOT_TOKEN} 29 | SLACK_APP_CLIENT_ID: ${env:SLACK_APP_CLIENT_ID} 30 | SLACK_APP_CLIENT_SECRET: ${env:SLACK_APP_CLIENT_SECRET} 31 | SLACK_SIGNING_SECRET: ${env:SLACK_SIGNING_SECRET} 32 | SLACK_APP_REDIRECT_URI: ${env:SLACK_APP_REDIRECT_URI} 33 | 34 | # you can add statements to the Lambda function's IAM Role here 35 | # iamRoleStatements: 36 | # - Effect: "Allow" 37 | # Action: 38 | # - "s3:ListBucket" 39 | # Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ] } 40 | # - Effect: "Allow" 41 | # Action: 42 | # - "s3:PutObject" 43 | # Resource: 44 | # Fn::Join: 45 | # - "" 46 | # - - "arn:aws:s3:::" 47 | # - "Ref" : "ServerlessDeploymentBucket" 48 | # - "/*" 49 | 50 | # you can add packaging information here 51 | #package: 52 | # include: 53 | # - include-me.js 54 | # - include-me-dir/** 55 | # exclude: 56 | # - exclude-me.js 57 | # - exclude-me-dir/** 58 | 59 | functions: 60 | app: 61 | handler: handler.app 62 | events: 63 | # Bolt App 64 | - http: 65 | method: post 66 | path: /slack/events 67 | # OAuth Flow 68 | - http: 69 | method: get 70 | path: /slack/installation 71 | - http: 72 | method: get 73 | path: /slack/oauth 74 | -------------------------------------------------------------------------------- /serverless-bolt-template/aws-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "es2017" 5 | ], 6 | "moduleResolution": "node", 7 | "noUnusedLocals": true, 8 | "noUnusedParameters": true, 9 | "sourceMap": true, 10 | "target": "es2017", 11 | "outDir": "lib", 12 | "allowSyntheticDefaultImports": true 13 | }, 14 | "exclude": [ 15 | "node_modules" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /serverless-bolt-template/aws-ts/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const slsw = require('serverless-webpack'); 3 | 4 | module.exports = { 5 | mode: slsw.lib.webpack.isLocal ? 'development' : 'production', 6 | entry: slsw.lib.entries, 7 | devtool: 'source-map', 8 | resolve: { 9 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 10 | }, 11 | output: { 12 | libraryTarget: 'commonjs', 13 | path: path.join(__dirname, '.webpack'), 14 | filename: '[name].js', 15 | }, 16 | target: 'node', 17 | externals: { 18 | // WARNING in ./node_modules/retry-request/index.js 19 | // Module not found: Error: Can't resolve 'request' in 'node_modules/retry-request' 20 | "request": "request", 21 | // WARNING in ./node_modules/express/lib/view.js 81:13-25 22 | // Critical dependency: the request of a dependency is an expression 23 | "express": "express" 24 | }, 25 | module: { 26 | rules: [ 27 | // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader` 28 | { 29 | test: /\.tsx?$/, 30 | loader: 'ts-loader' 31 | }, 32 | ], 33 | }, 34 | }; -------------------------------------------------------------------------------- /slack-ruby-bot-examples/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | source 'https://rubygems.org' 3 | gem 'slack-ruby-bot' 4 | gem 'async-websocket', '~>0.8.0' 5 | -------------------------------------------------------------------------------- /slack-ruby-bot-examples/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (6.0.4.1) 5 | concurrent-ruby (~> 1.0, >= 1.0.2) 6 | i18n (>= 0.7, < 2) 7 | minitest (~> 5.1) 8 | tzinfo (~> 1.1) 9 | zeitwerk (~> 2.2, >= 2.2.2) 10 | async (1.20.1) 11 | console (~> 1.0) 12 | nio4r (~> 2.3) 13 | timers (~> 4.1) 14 | async-io (1.24.0) 15 | async (~> 1.14) 16 | async-websocket (0.8.0) 17 | async-io 18 | websocket-driver (~> 0.7.0) 19 | concurrent-ruby (1.1.9) 20 | console (1.4.0) 21 | faraday (0.15.4) 22 | multipart-post (>= 1.2, < 3) 23 | faraday_middleware (0.13.1) 24 | faraday (>= 0.7.4, < 1.0) 25 | gli (2.18.1) 26 | hashie (3.6.0) 27 | i18n (1.8.11) 28 | concurrent-ruby (~> 1.0) 29 | minitest (5.14.4) 30 | multipart-post (2.1.1) 31 | nio4r (2.4.0) 32 | slack-ruby-bot (0.12.0) 33 | hashie 34 | slack-ruby-client (>= 0.14.0) 35 | slack-ruby-client (0.14.2) 36 | activesupport 37 | faraday (>= 0.9) 38 | faraday_middleware 39 | gli 40 | hashie 41 | websocket-driver 42 | thread_safe (0.3.6) 43 | timers (4.3.0) 44 | tzinfo (1.2.10) 45 | thread_safe (~> 0.1) 46 | websocket-driver (0.7.1) 47 | websocket-extensions (>= 0.1.0) 48 | websocket-extensions (0.1.4) 49 | zeitwerk (2.5.1) 50 | 51 | PLATFORMS 52 | ruby 53 | 54 | DEPENDENCIES 55 | async-websocket (~> 0.8.0) 56 | slack-ruby-bot 57 | 58 | BUNDLED WITH 59 | 2.0.2 60 | -------------------------------------------------------------------------------- /slack-ruby-bot-examples/pongbot.rb: -------------------------------------------------------------------------------- 1 | # RTM API example 2 | # SLACK_API_TOKEN=xoxb-xxx ; bundle exec ruby pongbot.rb 3 | # Post a message "{bot name} ping" on a channel having the bot user as a member 4 | 5 | require 'slack-ruby-bot' 6 | 7 | class PongBot < SlackRubyBot::Bot 8 | command 'ping' do |client, data, match| 9 | client.say(text: 'pong', channel: data.channel) 10 | end 11 | end 12 | 13 | PongBot.run 14 | 15 | =begin 16 | 17 | $ SLACK_API_TOKEN=xoxb-xxx bundle exec ruby pongbot.rb 18 | I, [2019-07-15T17:30:07.646363 #39423] INFO -- request: POST https://slack.com/api/rtm.start 19 | D, [2019-07-15T17:30:07.646422 #39423] DEBUG -- request: Accept: "application/json; charset=utf-8" 20 | User-Agent: "Slack Ruby Client/0.14.2" 21 | Content-Type: "application/x-www-form-urlencoded" 22 | I, [2019-07-15T17:30:08.212685 #39423] INFO -- response: Status 200 23 | D, [2019-07-15T17:30:08.212775 #39423] DEBUG -- response: content-type: "application/json; charset=utf-8" 24 | transfer-encoding: "chunked" 25 | connection: "close" 26 | date: "Mon, 15 Jul 2019 08:30:07 GMT" 27 | server: "Apache" 28 | x-content-type-options: "nosniff" 29 | x-slack-req-id: "6baa23d9-daed-427a-8fe9-xxx" 30 | x-oauth-scopes: "identify,bot:basic" 31 | expires: "Mon, 26 Jul 1997 05:00:00 GMT" 32 | cache-control: "private, no-cache, no-store, must-revalidate" 33 | access-control-expose-headers: "x-slack-req-id, retry-after" 34 | x-xss-protection: "0" 35 | x-accepted-oauth-scopes: "rtm:stream,client" 36 | vary: "Accept-Encoding" 37 | pragma: "no-cache" 38 | access-control-allow-headers: "slack-route, x-slack-version-ts" 39 | strict-transport-security: "max-age=31536000; includeSubDomains; preload" 40 | referrer-policy: "no-referrer" 41 | access-control-allow-origin: "*" 42 | x-via: "haproxy-www-r4vb" 43 | x-cache: "Miss from cloudfront" 44 | via: "1.1 xxx.cloudfront.net (CloudFront)" 45 | x-amz-cf-pop: "NRT20-C2" 46 | x-amz-cf-id: "7zgE1TXats3_xxx" 47 | D, [2019-07-15T17:30:08.575278 #39423] DEBUG -- Slack::RealTime::Concurrency::Async::Socket#connect!: Slack::RealTime::Concurrency::Async::Client 48 | D, [2019-07-15T17:30:08.598564 #39423] DEBUG -- SlackRubyBot::Client#run_loop: WebSocket::Driver::OpenEvent 49 | D, [2019-07-15T17:30:08.933543 #39423] DEBUG -- SlackRubyBot::Client#run_loop: WebSocket::Driver::MessageEvent, {"type": "hello"} 50 | D, [2019-07-15T17:30:08.933637 #39423] DEBUG -- SlackRubyBot::Client#dispatch: type=hello 51 | I, [2019-07-15T17:30:08.933680 #39423] INFO -- : Successfully connected team xxx (T01234567) to https://xxx.slack.com. 52 | D, [2019-07-15T17:30:11.801694 #39423] DEBUG -- SlackRubyBot::Client#run_loop: WebSocket::Driver::MessageEvent, {"type":"user_typing","channel":"C01234567","user":"U01234567"} 53 | D, [2019-07-15T17:30:11.801882 #39423] DEBUG -- SlackRubyBot::Client#dispatch: channel=C01234567, type=user_typing, user=U01234567 54 | D, [2019-07-15T17:30:13.081899 #39423] DEBUG -- SlackRubyBot::Client#run_loop: WebSocket::Driver::MessageEvent, {"client_msg_id":"36357b6a-ff10-4251-884d-xxx","suppress_notification":false,"type":"message","text":"bot ping","user":"U01234567","team":"T01234567","user_team":"T01234567","source_team":"T01234567","channel":"C01234567","event_ts":"1563179412.003900","ts":"1563179412.003900"} 55 | D, [2019-07-15T17:30:13.082108 #39423] DEBUG -- SlackRubyBot::Client#dispatch: channel=C01234567, client_msg_id=36357b6a-ff10-4251-884d-de89011d7d82, event_ts=1563179412.003900, source_team=T01234567, suppress_notification=false, team=T01234567, text=bot ping, ts=1563179412.003900, type=message, user=U01234567, user_team=T01234567 56 | D, [2019-07-15T17:30:26.351723 #39423] DEBUG -- SlackRubyBot::Client#run_loop: WebSocket::Driver::MessageEvent, {"type":"user_typing","channel":"C01234567","user":"U01234567"} 57 | D, [2019-07-15T17:30:26.351803 #39423] DEBUG -- SlackRubyBot::Client#dispatch: channel=C01234567, type=user_typing, user=U01234567 58 | D, [2019-07-15T17:30:34.485296 #39423] DEBUG -- SlackRubyBot::Client#run_loop: WebSocket::Driver::MessageEvent, {"type":"user_typing","channel":"C01234567","user":"U01234567"} 59 | D, [2019-07-15T17:30:34.485489 #39423] DEBUG -- SlackRubyBot::Client#dispatch: channel=C01234567, type=user_typing, user=U01234567 60 | D, [2019-07-15T17:30:36.121611 #39423] DEBUG -- SlackRubyBot::Client#run_loop: WebSocket::Driver::MessageEvent, {"client_msg_id":"a808604e-5d63-4c9a-9484-xxx","suppress_notification":false,"type":"message","text":"jslacksample ping","user":"U01234567","team":"T01234567","user_team":"T01234567","source_team":"T01234567","channel":"C01234567","event_ts":"1563179435.004200","ts":"1563179435.004200"} 61 | D, [2019-07-15T17:30:36.121760 #39423] DEBUG -- SlackRubyBot::Client#dispatch: channel=C01234567, client_msg_id=a808604e-5d63-4c9a-9484-xxx, event_ts=1563179435.004200, source_team=T01234567, suppress_notification=false, team=T01234567, text=jslacksample ping, ts=1563179435.004200, type=message, user=U01234567, user_team=T01234567 62 | D, [2019-07-15T17:30:36.122345 #39423] DEBUG -- SlackRubyBot::Client#send_json: {:type=>"message", :id=>1, :text=>"pong", :channel=>"C01234567"} 63 | D, [2019-07-15T17:30:36.125273 #39423] DEBUG -- Slack::RealTime::Concurrency::Async::Socket#send_data: {"type":"message","id":1,"text":"pong","channel":"C01234567"} 64 | D, [2019-07-15T17:30:36.332184 #39423] DEBUG -- SlackRubyBot::Client#run_loop: WebSocket::Driver::MessageEvent, {"ok":true,"reply_to":1,"ts":"1563179436.004300","text":"pong"} 65 | =end 66 | -------------------------------------------------------------------------------- /slack-ruby-client-examples/.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | # Used by dotenv library to load environment variables. 14 | # .env 15 | 16 | ## Specific to RubyMotion: 17 | .dat* 18 | .repl_history 19 | build/ 20 | *.bridgesupport 21 | build-iPhoneOS/ 22 | build-iPhoneSimulator/ 23 | 24 | ## Specific to RubyMotion (use of CocoaPods): 25 | # 26 | # We recommend against adding the Pods directory to your .gitignore. However 27 | # you should judge for yourself, the pros and cons are mentioned at: 28 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 29 | # 30 | # vendor/Pods/ 31 | 32 | ## Documentation cache and generated files: 33 | /.yardoc/ 34 | /_yardoc/ 35 | /doc/ 36 | /rdoc/ 37 | 38 | ## Environment normalization: 39 | /.bundle/ 40 | /vendor/bundle 41 | /lib/bundler/man/ 42 | 43 | # for a library or gem, you might want to ignore these files since the code is 44 | # intended to run in multiple environments; otherwise, check them in: 45 | # Gemfile.lock 46 | # .ruby-version 47 | # .ruby-gemset 48 | 49 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 50 | .rvmrc 51 | 52 | # Serverless directories 53 | .serverless 54 | 55 | # package directories 56 | node_modules 57 | 58 | .env 59 | google-service-account.json -------------------------------------------------------------------------------- /slack-ruby-client-examples/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | source 'https://rubygems.org' 3 | gem 'slack-ruby-client' 4 | gem 'sinatra' 5 | gem 'sinatra-contrib' -------------------------------------------------------------------------------- /slack-ruby-client-examples/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (7.0.3.1) 5 | concurrent-ruby (~> 1.0, >= 1.0.2) 6 | i18n (>= 1.6, < 2) 7 | minitest (>= 5.1) 8 | tzinfo (~> 2.0) 9 | concurrent-ruby (1.1.10) 10 | faraday (0.15.4) 11 | multipart-post (>= 1.2, < 3) 12 | faraday_middleware (0.13.1) 13 | faraday (>= 0.7.4, < 1.0) 14 | gli (2.18.1) 15 | hashie (3.6.0) 16 | i18n (1.12.0) 17 | concurrent-ruby (~> 1.0) 18 | minitest (5.16.2) 19 | multi_json (1.15.0) 20 | multipart-post (2.1.1) 21 | mustermann (2.0.2) 22 | ruby2_keywords (~> 0.0.1) 23 | rack (2.2.4) 24 | rack-protection (2.2.3) 25 | rack 26 | ruby2_keywords (0.0.5) 27 | sinatra (2.2.3) 28 | mustermann (~> 2.0) 29 | rack (~> 2.2) 30 | rack-protection (= 2.2.3) 31 | tilt (~> 2.0) 32 | sinatra-contrib (2.2.3) 33 | multi_json 34 | mustermann (~> 2.0) 35 | rack-protection (= 2.2.3) 36 | sinatra (= 2.2.3) 37 | tilt (~> 2.0) 38 | slack-ruby-client (0.14.2) 39 | activesupport 40 | faraday (>= 0.9) 41 | faraday_middleware 42 | gli 43 | hashie 44 | websocket-driver 45 | tilt (2.0.11) 46 | tzinfo (2.0.5) 47 | concurrent-ruby (~> 1.0) 48 | websocket-driver (0.7.1) 49 | websocket-extensions (>= 0.1.0) 50 | websocket-extensions (0.1.4) 51 | 52 | PLATFORMS 53 | ruby 54 | 55 | DEPENDENCIES 56 | sinatra 57 | sinatra-contrib 58 | slack-ruby-client 59 | 60 | BUNDLED WITH 61 | 2.0.2 62 | -------------------------------------------------------------------------------- /slack-ruby-client-examples/README.md: -------------------------------------------------------------------------------- 1 | # Ruby examples 2 | 3 | ## Prerequisites 4 | 5 | ### Create your Slack App 6 | 7 | https://api.slack.com/apps 8 | 9 | ![Create a Slack App](https://camo.qiitausercontent.com/af01ba95b0e78b77765fca029c56b1eb6a878379/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3333393239332f66663433336433332d353364642d356461362d393036372d6565626137646166326363642e706e67) 10 | 11 | ```bash 12 | export SLACK_SIGNING_SECRET=xxx 13 | export SLACK_BOT_TOKEN=xoxb-xxx 14 | ``` 15 | 16 | ### Install Gems 17 | 18 | ```bash 19 | bundle i 20 | ``` 21 | 22 | ### ngrok account 23 | 24 | https://ngrok.com/ 25 | 26 | ## say_hello.rb 27 | 28 | ### How run 29 | 30 | ```bash 31 | SLACK_BOT_TOKEN=xoxb-xxx bundle exec ruby say_hello.rb 32 | ``` 33 | 34 | ## conversations.rb 35 | 36 | ### How run 37 | 38 | ```bash 39 | SLACK_BOT_TOKEN=xoxb-xxx bundle exec ruby conversations.rb 40 | ``` 41 | 42 | ## events.rb 43 | 44 | ### Event Subscriptions 45 | 46 | - `reaction_added` in workspace events or bot events 47 | - Set the ngrok URL for Request URL 48 | 49 | ### How run 50 | 51 | ```bash 52 | export SLACK_SIGNING_SECRET=xxx 53 | export SLACK_BOT_TOKEN=xoxb-xxx 54 | 55 | bundle exec ruby events.rb -p 3000 56 | ngrok http 3000 57 | ``` 58 | -------------------------------------------------------------------------------- /slack-ruby-client-examples/channels.rb: -------------------------------------------------------------------------------- 1 | # NOTE: using conversations APIs is recommended 2 | # SLACK_BOT_TOKEN=xoxb-xxx bundle exec ruby channels.rb 3 | require 'slack-ruby-client' 4 | require 'logger' 5 | 6 | Slack.configure do |config| 7 | config.token = ENV['SLACK_BOT_TOKEN'] 8 | end 9 | client = Slack::Web::Client.new 10 | client.logger.level = Logger::DEBUG 11 | 12 | require 'json' 13 | 14 | channels = client.channels_list.channels 15 | channels.take(10).each do |channel| 16 | # puts JSON.pretty_generate(channel) 17 | puts channel.name 18 | end 19 | -------------------------------------------------------------------------------- /slack-ruby-client-examples/conversations.rb: -------------------------------------------------------------------------------- 1 | # SLACK_BOT_TOKEN=xoxb-xxx bundle exec ruby converstaions.rb 2 | require 'slack-ruby-client' 3 | require 'logger' 4 | 5 | Slack.configure do |config| 6 | config.token = ENV['SLACK_BOT_TOKEN'] 7 | end 8 | client = Slack::Web::Client.new 9 | client.logger.level = Logger::DEBUG 10 | 11 | require 'json' 12 | 13 | channels = client.conversations_list.channels 14 | channels.take(10).each do |channel| 15 | # puts JSON.pretty_generate(channel) 16 | puts channel.name 17 | end 18 | -------------------------------------------------------------------------------- /slack-ruby-client-examples/events.rb: -------------------------------------------------------------------------------- 1 | # bundle exec ruby events.rb -p 3000 2 | # ngrok http 3000 3 | require 'sinatra' 4 | require 'sinatra/reloader' 5 | require 'json' 6 | 7 | require 'slack-ruby-client' 8 | require 'logger' 9 | 10 | # Configure Slack API client 11 | Slack::Events.configure do |config| 12 | config.signing_secret = ENV['SLACK_SIGNING_SECRET'] 13 | end 14 | Slack.configure do |config| 15 | config.token = ENV['SLACK_BOT_TOKEN'] 16 | end 17 | client = Slack::Web::Client.new 18 | client.logger.level = Logger::DEBUG 19 | 20 | # 'X-Slack-Signature' verification filter 21 | before do 22 | begin 23 | req = HttpRequest.new(request) 24 | Slack::Events::Request.new(req).verify! 25 | request.body.rewind 26 | rescue => e 27 | puts "Invalid signature #{e.to_json}" 28 | halt 401, 'Invalid signature!' 29 | end 30 | end 31 | 32 | # Endpoint to receive requests from Slack 33 | post '/slack/events' do 34 | body = request.body.read 35 | params = JSON.parse(body) 36 | if params['type'] == 'url_verification' 37 | return params['challenge'] 38 | else 39 | puts "Got #{params['event']}" 40 | end 41 | end 42 | 43 | # HTTP request wrapper for compatibility with slack-ruby-client 44 | class HttpRequest 45 | attr_reader :headers, :body 46 | def initialize(request) 47 | @headers = Headers.new(request.env) 48 | @body = request.body 49 | end 50 | 51 | class Headers 52 | def initialize(env) 53 | @env = env 54 | end 55 | def [](key) 56 | @env[to_env_key(key)] 57 | end 58 | 59 | private 60 | def to_env_key(str) 61 | 'HTTP_' + str.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') 62 | .gsub(/([a-z\d])([A-Z])/, '\1_\2') 63 | .tr("-", "_") 64 | .upcase 65 | end 66 | end 67 | end -------------------------------------------------------------------------------- /slack-ruby-client-examples/say_hello.rb: -------------------------------------------------------------------------------- 1 | # SLACK_BOT_TOKEN=xoxb-xxx bundle exec ruby say_hello.rb 2 | require 'slack-ruby-client' 3 | require 'logger' 4 | 5 | Slack.configure do |config| 6 | config.token = ENV['SLACK_BOT_TOKEN'] 7 | end 8 | client = Slack::Web::Client.new 9 | client.logger.level = Logger::DEBUG 10 | 11 | require 'json' 12 | 13 | auth_test_res = client.auth_test 14 | puts JSON.pretty_generate(auth_test_res) 15 | 16 | post_res = client.chat_postMessage( 17 | channel: '#random', 18 | text: 'Hello World!', 19 | as_user: true 20 | ) 21 | puts JSON.pretty_generate(post_res) --------------------------------------------------------------------------------