From 2dd6d3307d7bccc185afaf01ed973799f87fd227 Mon Sep 17 00:00:00 2001 From: pizzacat83 <17941141+pizzacat83@users.noreply.github.com> Date: Tue, 31 Mar 2020 18:48:45 +0900 Subject: [PATCH] implement channel-notifier --- .funcignore | 7 ++ .gitignore | 97 +++++++++++++++++++++ .vscode/extensions.json | 5 ++ .vscode/launch.json | 12 +++ .vscode/settings.json | 7 ++ .vscode/tasks.json | 31 +++++++ README.md | 1 + SlackEvents/function.json | 20 +++++ SlackEvents/handlers.ts | 39 +++++++++ SlackEvents/index.ts | 52 +++++++++++ SlackEvents/sample.dat | 3 + host.json | 7 ++ package-lock.json | 178 ++++++++++++++++++++++++++++++++++++++ package.json | 25 ++++++ proxies.json | 4 + tsconfig.json | 10 +++ 16 files changed, 498 insertions(+) create mode 100644 .funcignore create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 README.md create mode 100644 SlackEvents/function.json create mode 100644 SlackEvents/handlers.ts create mode 100644 SlackEvents/index.ts create mode 100644 SlackEvents/sample.dat create mode 100644 host.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 proxies.json create mode 100644 tsconfig.json diff --git a/.funcignore b/.funcignore new file mode 100644 index 00000000..51792224 --- /dev/null +++ b/.funcignore @@ -0,0 +1,7 @@ +*.js.map +*.ts +.git* +.vscode +local.settings.json +test +tsconfig.json \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6ffed946 --- /dev/null +++ b/.gitignore @@ -0,0 +1,97 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TypeScript output +dist +out + +# Azure Functions artifacts +bin +obj +appsettings.json +local.settings.json + +# Others +/tmp/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..26786f93 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "ms-azuretools.vscode-azurefunctions" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..9306c8ad --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to Node Functions", + "type": "node", + "request": "attach", + "port": 9229, + "preLaunchTask": "func: host start" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..58085fc6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "azureFunctions.deploySubpath": ".", + "azureFunctions.projectLanguage": "TypeScript", + "azureFunctions.projectRuntime": "~2", + "debug.internalConsoleOptions": "neverOpen", + "azureFunctions.preDeployTask": "npm prune" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..8994051e --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,31 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "func", + "command": "host start", + "problemMatcher": "$func-watch", + "isBackground": true, + "dependsOn": "npm build" + }, + { + "type": "shell", + "label": "npm build", + "command": "npm run build", + "dependsOn": "npm install", + "problemMatcher": "$tsc" + }, + { + "type": "shell", + "label": "npm install", + "command": "npm install" + }, + { + "type": "shell", + "label": "npm prune", + "command": "npm prune --production", + "dependsOn": "npm build", + "problemMatcher": [] + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 00000000..0aeeb099 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# ap2020bot diff --git a/SlackEvents/function.json b/SlackEvents/function.json new file mode 100644 index 00000000..5cf1b1a3 --- /dev/null +++ b/SlackEvents/function.json @@ -0,0 +1,20 @@ +{ + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ], + "scriptFile": "../dist/SlackEvents/index.js" +} diff --git a/SlackEvents/handlers.ts b/SlackEvents/handlers.ts new file mode 100644 index 00000000..e66bbecc --- /dev/null +++ b/SlackEvents/handlers.ts @@ -0,0 +1,39 @@ +import { Context } from "@azure/functions" +import { WebClient } from '@slack/web-api'; + +const webClient = new WebClient(process.env.SLACK_TOKEN_BOT); + +namespace Events { + export interface ChannelCreated { + type: "channel_created"; + channel: { + id: string; + name: string; + created: number; + creator: string; + } + } + + export interface ChannelUnarchive { + type: "channel_unarchive"; + channel: string; + user: string; + } +} + +// Handlers +// the name of the handler must be the type of the event + +export const channel_created = async ({channel}: Events.ChannelCreated, context: Context) => { + await webClient.chat.postMessage({ + channel: process.env.SLACK_CHANNEL_NOTIFICATIONS_OTHERS, + text: `:new: <@${channel.creator}> が <#${channel.id}> を作成しました :rocket:`, + }); +} + +export const channel_unarchive = async ({channel, user}: Events.ChannelUnarchive, context: Context) => { + await webClient.chat.postMessage({ + channel: process.env.SLACK_CHANNEL_NOTIFICATIONS_OTHERS, + text: `:recycle: <@${user}> が <#${channel}> をアーカイブから復元しました :rocket:`, + }); +}; \ No newline at end of file diff --git a/SlackEvents/index.ts b/SlackEvents/index.ts new file mode 100644 index 00000000..f8b3ba09 --- /dev/null +++ b/SlackEvents/index.ts @@ -0,0 +1,52 @@ +import { AzureFunction, Context, HttpRequest } from "@azure/functions" +import * as handlers from "./handlers"; +import * as crypto from "crypto"; +import * as timingSafeCompare from "tsscmp"; + +const verify = (req: HttpRequest) => { + const version = 'v0'; + const actual = req.headers['x-slack-signature']; + + const timestamp = req.headers['x-slack-request-timestamp']; + const hmac = crypto.createHmac('sha256', process.env.SLACK_SIGNING_SECRET); + hmac.update(`${version}:${timestamp}:${req.rawBody}`); + const expected = `${version}=${hmac.digest('hex')}`; + + return timingSafeCompare(expected, actual); +} + +const httpTrigger: AzureFunction = async (context: Context, req: HttpRequest): Promise => { + if (!verify(req)) { + context.log.error('unverified request from:', req.headers['x-forwarded-for']); + context.res = { + body: "", + }; + return; + } + switch (req.body.type) { + case "url_verification": + context.log('Challenge from Slack'); + context.res = { + body: req.body.challenge, + } + return; + case "event_callback": + let eventType = req.body.event.type + context.log('Incoming event: ', eventType); + context.res = { + body: "", + }; + context.done(); // easy return because of the 3sec rule + if (eventType in handlers) { + // TODO: handle errors + await handlers[eventType](req.body.event, context); + } else { + context.log.error('no handler for event type:', eventType); + } + break; + default: + context.log.error('not recognized top-level event: ', req.body.type); + } +}; + +export default httpTrigger; diff --git a/SlackEvents/sample.dat b/SlackEvents/sample.dat new file mode 100644 index 00000000..2e609439 --- /dev/null +++ b/SlackEvents/sample.dat @@ -0,0 +1,3 @@ +{ + "name": "Azure" +} \ No newline at end of file diff --git a/host.json b/host.json new file mode 100644 index 00000000..d342a8ea --- /dev/null +++ b/host.json @@ -0,0 +1,7 @@ +{ + "version": "2.0", + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[1.*, 2.0.0)" + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..c4f8d9a4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,178 @@ +{ + "name": "ap2020bot", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@azure/functions": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-1.2.0.tgz", + "integrity": "sha512-qkaQqTnr56xUnYNkKBM/2wsnf6imAJ3NF6Nbpk691Y6JYliA1YdZngsZsrpHS9tQ9/71MqARl8m50+EmEfLG3g==", + "dev": true + }, + "@slack/logger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-2.0.0.tgz", + "integrity": "sha512-OkIJpiU2fz6HOJujhlhfIGrc8hB4ibqtf7nnbJQDerG0BqwZCfmgtK5sWzZ0TkXVRBKD5MpLrTmCYyMxoMCgPw==", + "requires": { + "@types/node": ">=8.9.0" + } + }, + "@slack/types": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@slack/types/-/types-1.5.0.tgz", + "integrity": "sha512-oCYgatJYxHf9wE3tKXzOLeeTsF0ghX1TIcguNfVmO2V6NDe+cHAzZRglEOmJLdRINDS5gscAgSkeZpDhpKBeUA==" + }, + "@slack/web-api": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-5.8.0.tgz", + "integrity": "sha512-lUMFM9jtdn+9Q0kHLegf5RjIgQlmb1PGWwWdTsc9mQ8PJu2BogI5ZttG8/tA6r2bX/C4bgXhd0JJ4syUR0AAhQ==", + "requires": { + "@slack/logger": ">=1.0.0 <3.0.0", + "@slack/types": "^1.2.1", + "@types/is-stream": "^1.1.0", + "@types/node": ">=8.9.0", + "@types/p-queue": "^2.3.2", + "axios": "^0.19.0", + "eventemitter3": "^3.1.0", + "form-data": "^2.5.0", + "is-stream": "^1.1.0", + "p-queue": "^2.4.2", + "p-retry": "^4.0.0" + } + }, + "@types/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-jkZatu4QVbR60mpIzjINmtS1ZF4a/FqdTUTBeQDVOQ2PYyidtwFKr0B5G6ERukKwliq+7mIXvxyppwzG5EgRYg==", + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "13.9.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.8.tgz", + "integrity": "sha512-1WgO8hsyHynlx7nhP1kr0OFzsgKz5XDQL+Lfc3b1Q3qIln/n8cKD4m09NJ0+P1Rq7Zgnc7N0+SsMnoD1rEb0kA==" + }, + "@types/p-queue": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@types/p-queue/-/p-queue-2.3.2.tgz", + "integrity": "sha512-eKAv5Ql6k78dh3ULCsSBxX6bFNuGjTmof5Q/T6PiECDq0Yf8IIn46jCyp3RJvCi8owaEmm3DZH1PEImjBMd/vQ==" + }, + "@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + } + }, + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "mime-db": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", + "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" + }, + "mime-types": { + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", + "requires": { + "mime-db": "1.43.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "p-queue": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-2.4.2.tgz", + "integrity": "sha512-n8/y+yDJwBjoLQe1GSJbbaYQLTI7QHNZI2+rpmCDbe++WLf9HC3gf6iqj5yfPAV71W4UF3ql5W1+UBPXoXTxng==" + }, + "p-retry": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.2.0.tgz", + "integrity": "sha512-jPH38/MRh263KKcq0wBNOGFJbm+U6784RilTmHjB/HM9kH9V8WlCpVUcdOmip9cjXOh6MxZ5yk1z2SjDUJfWmA==", + "requires": { + "@types/retry": "^0.12.0", + "retry": "^0.12.0" + } + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" + }, + "tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==" + }, + "typescript": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", + "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..a65aa753 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "ap2020bot", + "version": "1.0.0", + "description": "bot for ap2020", + "keywords": ["bot", "slack"], + "author": "ap2020", + "repository": "https://github.com/ap2020/ap2020bot.git", + "homepage": "https://github.com/ap2020/ap2020bot", + "license": "MIT", + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "prestart": "npm run build", + "start": "func start", + "test": "echo \"No tests yet...\"" + }, + "dependencies": { + "@slack/web-api": "^5.8.0", + "tsscmp": "^1.0.6" + }, + "devDependencies": { + "@azure/functions": "^1.0.2-beta2", + "typescript": "^3.3.3" + } +} diff --git a/proxies.json b/proxies.json new file mode 100644 index 00000000..b385252f --- /dev/null +++ b/proxies.json @@ -0,0 +1,4 @@ +{ + "$schema": "http://json.schemastore.org/proxies", + "proxies": {} +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..77d91aa8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "dist", + "rootDir": ".", + "sourceMap": true, + "strict": false + } +}