├── .gitignore ├── .vscode └── settings.json ├── README.md ├── assets └── icon.png ├── deno.jsonc ├── env.sample.ts ├── import_map.json ├── manifest.ts ├── slack.json └── src ├── datastore.ts ├── functions ├── extract.test.ts ├── extract.ts ├── karma.ts └── send.ts ├── triggers └── mention.ts └── workflow.ts /.gitignore: -------------------------------------------------------------------------------- 1 | env.ts 2 | 3 | dist 4 | package 5 | .DS_Store 6 | .slack/apps.dev.json 7 | .slack/apps.json 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.suggest.imports.hosts": { 5 | "https://deno.land": false 6 | }, 7 | "[typescript]": { 8 | "editor.formatOnSave": true, 9 | "editor.defaultFormatter": "denoland.vscode-deno" 10 | }, 11 | "editor.tabSize": 2 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Karma Bot for Slack 2 | 3 | An example for "[new Slack platform](https://api.slack.com/future)" with Deno. 4 | 5 | ## Screenshots 6 | 7 | ![SS](https://user-images.githubusercontent.com/10682/192105268-ded58e21-3b82-4625-a24c-819ad5c96fc6.png) 8 | 9 | ## Usage 10 | 11 | Develop: 12 | 13 | ```shell 14 | slack run 15 | ``` 16 | 17 | Test: 18 | 19 | ```shell 20 | deno test 21 | ``` 22 | 23 | Deploy 24 | 25 | ```shell 26 | slack deploy 27 | ``` 28 | 29 | Add a trigger: 30 | 31 | ```shell 32 | slack trigger create --trigger-def ./src/triggers/mention.ts 33 | ``` 34 | 35 | ## Author 36 | 37 | Yusuke Wada 38 | 39 | ## License 40 | 41 | MIT 42 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yusukebe/slack-deno-karma/cd847e94bb45965a29b965c0ec46a0443dd8fff9/assets/icon.png -------------------------------------------------------------------------------- /deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "importMap": "import_map.json" 3 | } 4 | -------------------------------------------------------------------------------- /env.sample.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | "CHANNEL_ID": "", 3 | }; 4 | -------------------------------------------------------------------------------- /import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "deno-slack-sdk/": "https://deno.land/x/deno_slack_sdk@1.1.2/", 4 | "deno-slack-api/": "https://deno.land/x/deno_slack_api@1.0.1/" 5 | } 6 | } -------------------------------------------------------------------------------- /manifest.ts: -------------------------------------------------------------------------------- 1 | import { Workflow } from "./src/workflow.ts"; 2 | import { Manifest } from "deno-slack-sdk/mod.ts"; 3 | import { Datastore } from "./src/datastore.ts"; 4 | 5 | export default Manifest({ 6 | name: "karma", 7 | description: "Karma Bot", 8 | icon: "assets/icon.png", 9 | workflows: [Workflow], 10 | outgoingDomains: [], 11 | datastores: [Datastore], 12 | botScopes: [ 13 | "app_mentions:read", 14 | "chat:write", 15 | "chat:write.public", 16 | "datastore:read", 17 | "datastore:write", 18 | ], 19 | }); 20 | -------------------------------------------------------------------------------- /slack.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "get-hooks": "deno run -q --allow-read --allow-net https://deno.land/x/deno_slack_hooks@0.3.0/mod.ts" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/datastore.ts: -------------------------------------------------------------------------------- 1 | import { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts"; 2 | 3 | export const DATASTORE_NAME = "karma"; 4 | 5 | export const Datastore = DefineDatastore({ 6 | name: DATASTORE_NAME, 7 | primary_key: "id", 8 | attributes: { 9 | id: { 10 | type: Schema.types.string, 11 | }, 12 | target: { 13 | type: Schema.types.string, 14 | }, 15 | karma: { 16 | type: Schema.types.integer, 17 | }, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/functions/extract.test.ts: -------------------------------------------------------------------------------- 1 | import { SlackFunctionTester } from "deno-slack-sdk/mod.ts"; 2 | import { assertEquals } from "https://deno.land/std@0.153.0/testing/asserts.ts"; 3 | import ExtractFunction from "./extract.ts"; 4 | 5 | const { createContext } = SlackFunctionTester("extract"); 6 | 7 | Deno.test("Extract function test", async () => { 8 | let inputs = { 9 | body: "<@U0LAN0Z89> foo++", 10 | }; 11 | let res = await ExtractFunction(createContext({ inputs })); 12 | assertEquals(res.outputs?.target, "foo"); 13 | assertEquals(res.outputs?.plus, true); 14 | 15 | inputs = { 16 | body: "<@U0LAN0Z89> bar--", 17 | }; 18 | res = await ExtractFunction(createContext({ inputs })); 19 | assertEquals(res.outputs?.target, "bar"); 20 | assertEquals(res.outputs?.plus, false); 21 | 22 | inputs = { 23 | body: "<@U0LAN0Z89> bar", 24 | }; 25 | res = await ExtractFunction(createContext({ inputs })); 26 | assertEquals(res.outputs, {}); 27 | }); 28 | -------------------------------------------------------------------------------- /src/functions/extract.ts: -------------------------------------------------------------------------------- 1 | import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts"; 2 | 3 | export const ExtractFunction = DefineFunction({ 4 | callback_id: "extract", 5 | title: "Extract", 6 | description: "Extract message from body", 7 | source_file: "src/functions/extract.ts", 8 | input_parameters: { 9 | properties: { 10 | body: { 11 | type: Schema.types.string, 12 | }, 13 | }, 14 | required: ["body"], 15 | }, 16 | output_parameters: { 17 | properties: { 18 | plus: { 19 | type: Schema.types.boolean, 20 | default: true, 21 | }, 22 | target: { 23 | type: Schema.types.string, 24 | }, 25 | }, 26 | required: [], 27 | }, 28 | }); 29 | 30 | export default SlackFunction(ExtractFunction, ({ inputs }) => { 31 | const regExp = /\<\@.+?\>\s?(.+)(\+\+|\-\-)$/; 32 | const match = inputs.body.match(regExp); 33 | if (match) { 34 | const target = match[1]; 35 | const karma = match[2]; 36 | const plus = karma === "++" ? true : false; 37 | return { 38 | outputs: { 39 | plus, 40 | target, 41 | }, 42 | }; 43 | } 44 | return { 45 | outputs: {}, 46 | }; 47 | }); 48 | -------------------------------------------------------------------------------- /src/functions/karma.ts: -------------------------------------------------------------------------------- 1 | import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts"; 2 | import { SlackAPI } from "deno-slack-api/mod.ts"; 3 | import { Datastore, DATASTORE_NAME } from "../datastore.ts"; 4 | 5 | export const KarmaFunction = DefineFunction({ 6 | callback_id: "karma-function", 7 | title: "Karma", 8 | source_file: "src/functions/karma.ts", 9 | input_parameters: { 10 | properties: { 11 | target: { 12 | type: Schema.types.string, 13 | }, 14 | plus: { 15 | type: Schema.types.boolean, 16 | }, 17 | }, 18 | required: ["target", "plus"], 19 | }, 20 | output_parameters: { 21 | properties: { 22 | karma: { 23 | type: Schema.types.number, 24 | }, 25 | }, 26 | required: [], 27 | }, 28 | }); 29 | 30 | export default SlackFunction(KarmaFunction, async ({ inputs, token }) => { 31 | if (!inputs.target) { 32 | return { outputs: {} }; 33 | } 34 | 35 | const client = SlackAPI(token, {}); 36 | 37 | const result = await client.apps.datastore.query( 38 | { 39 | datastore: DATASTORE_NAME, 40 | expression: "#target = :target", 41 | expression_attributes: { "#target": "target" }, 42 | expression_values: { ":target": inputs.target }, 43 | }, 44 | ); 45 | 46 | let karma = 0; 47 | 48 | if (result.items.length > 0) { 49 | const item = result.items[0]; 50 | karma = getKarma(item.karma, inputs.plus); 51 | await client.apps.datastore.put({ 52 | datastore: DATASTORE_NAME, 53 | item: { 54 | id: item.id, 55 | karma: karma, 56 | }, 57 | }); 58 | } else { 59 | const uuid = crypto.randomUUID(); 60 | karma = getKarma(karma, inputs.plus); 61 | await client.apps.datastore.put({ 62 | datastore: DATASTORE_NAME, 63 | item: { 64 | id: uuid, 65 | target: inputs.target, 66 | karma: karma, 67 | }, 68 | }); 69 | } 70 | 71 | return { 72 | outputs: { 73 | karma, 74 | }, 75 | }; 76 | }); 77 | 78 | const getKarma = (karma: number, plus: boolean) => { 79 | return plus ? karma + 1 : karma - 1; 80 | }; 81 | -------------------------------------------------------------------------------- /src/functions/send.ts: -------------------------------------------------------------------------------- 1 | import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts"; 2 | import { SlackAPI } from "deno-slack-api/mod.ts"; 3 | 4 | export const SendFunction = DefineFunction({ 5 | callback_id: "send", 6 | title: "Send", 7 | description: "Send message", 8 | source_file: "src/functions/send.ts", 9 | input_parameters: { 10 | properties: { 11 | target: { 12 | type: Schema.types.string, 13 | }, 14 | karma: { 15 | type: Schema.types.number, 16 | }, 17 | channelId: { 18 | type: Schema.slack.types.channel_id, 19 | }, 20 | }, 21 | required: ["channelId"], 22 | }, 23 | }); 24 | 25 | export default SlackFunction(SendFunction, ({ inputs, token }) => { 26 | let message = ""; 27 | if (inputs.target === undefined || inputs.karma === undefined) { 28 | message = "Usage: @karma {name}++"; 29 | } else { 30 | message = `${inputs.target} : ${inputs.karma}`; 31 | } 32 | 33 | const client = SlackAPI(token, {}); 34 | client.chat.postMessage({ 35 | channel: inputs.channelId, 36 | text: message, 37 | }); 38 | return { 39 | outputs: {}, 40 | }; 41 | }); 42 | -------------------------------------------------------------------------------- /src/triggers/mention.ts: -------------------------------------------------------------------------------- 1 | import { Trigger } from "deno-slack-api/types.ts"; 2 | import { Workflow } from "../workflow.ts"; 3 | import env from "../../env.ts"; 4 | 5 | const trigger: Trigger = { 6 | type: "event", 7 | event: { 8 | event_type: "slack#/events/app_mentioned", 9 | channel_ids: [`${env.CHANNEL_ID}`], // TODO: Should use environment variables etc. 10 | }, 11 | name: "Mention trigger", 12 | workflow: "#/workflows/karma", 13 | "inputs": { 14 | "text": { 15 | value: "{{data.text}}", 16 | }, 17 | "userId": { 18 | value: "{{data.user_id}}", 19 | }, 20 | "channelId": { 21 | value: "{{data.channel_id}}", 22 | }, 23 | }, 24 | }; 25 | 26 | export default trigger; 27 | -------------------------------------------------------------------------------- /src/workflow.ts: -------------------------------------------------------------------------------- 1 | import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts"; 2 | import { ExtractFunction } from "./functions/extract.ts"; 3 | import { KarmaFunction } from "./functions/karma.ts"; 4 | import { SendFunction } from "./functions/send.ts"; 5 | 6 | export const Workflow = DefineWorkflow({ 7 | callback_id: "karma", 8 | title: "Karma Workflow", 9 | input_parameters: { 10 | properties: { 11 | text: { 12 | type: Schema.types.string, 13 | }, 14 | userId: { 15 | type: Schema.slack.types.user_id, 16 | }, 17 | channelId: { 18 | type: Schema.slack.types.channel_id, 19 | }, 20 | }, 21 | required: ["text", "userId", "channelId"], 22 | }, 23 | }); 24 | 25 | const extractStep = Workflow.addStep(ExtractFunction, { 26 | body: Workflow.inputs.text, 27 | }); 28 | 29 | const karmaStep = Workflow.addStep(KarmaFunction, { 30 | target: extractStep.outputs.target, 31 | plus: extractStep.outputs.plus, 32 | }); 33 | 34 | Workflow.addStep(SendFunction, { 35 | channelId: Workflow.inputs.channelId, 36 | target: extractStep.outputs.target, 37 | karma: karmaStep.outputs.karma, 38 | }); 39 | 40 | export default Workflow; 41 | --------------------------------------------------------------------------------