├── .gitignore ├── LICENSE ├── README.md └── samples ├── bitly ├── README.md └── command.js ├── coin ├── README.md └── command.js ├── dataQuery ├── README.md ├── command.js └── package.json ├── gif ├── README.md └── command.js ├── githubDispatch ├── README.md ├── command.js └── package.json ├── kudos ├── README.md └── command.js ├── reddit ├── README.md └── command.js ├── status ├── README.md └── command.js └── stocks ├── README.md ├── command.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Fusebit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Fusebot! 2 | 3 | Fusebot - Integrate & automate anything on Slack/Discord in seconds | Product Hunt 4 | 5 | [Fusebot](https://fusebot.io?utm_source=github.com&utm_medium=referral&utm_campaign=fusebot-readme&utm_content=intro) is the simplest way to quickly develop custom Slash Commands for Discord and Slack. Fusebot gives you a Node.js and npm development environment and takes care of running, scaling, and securing your Slash Commands. All you need to bring is your code and imagination. 6 | 7 | ![Fusebot for Slack and Discord](https://user-images.githubusercontent.com/822369/126600208-ec9c129c-9bc1-4ed4-92b2-141951b16e34.png) 8 | 9 | You interact with Fusebot using the `/fusebot` Slash Command from Discord or Slack. It enables you to open a browser-based Node.js and npm development environment to implement any number of custom commands. Once created, the commands can be invoked by anyone in your Slack workspace or Discord guild. 10 | 11 | Fusebot was created for developers by developers at [fusebit.io](https://fusebit.io/?utm_source=github.com&utm_medium=referral&utm_campaign=fusebot-readme&utm_content=company) and is free to use. We hope you enjoy it! 12 | 13 | ## Table of contents 14 | 15 | [Installing Fusebot](#installing-fusebot) 16 | [Code your first command](#code-your-first-command) 17 | [Interacting with Fusebot](#interacting-with-fusebot) 18 | [Command code samples](#command-samples) 19 | [Programming model](#programming-model) 20 |   [Receiving data from Slack or Discord](#ctxbody) 21 |   [Managing secrets and configuration](#ctxconfiguration) 22 |   [Sending messages back to Slack or Discord](#ctxclient) 23 |   [Using storage](#ctxstorage) 24 | [Support](#support) 25 | 26 | ## Installing Fusebot 27 | 28 | 1. [Download Fusebot on Discord or Slack](https://fusebot.io?utm_source=github.com&utm_medium=referral&utm_campaign=fusebot-readme&utm_content=install#lp-pom-block-135) 29 | 2. Run ```/fusebot coin DOGE``` to test command 30 | 31 | If you see the current value of dogecoin, then Fusebot is successfully installed! Let's move on to coding our first command. 32 | 33 | ## Code your first command 34 | 35 | Now that you have Fusebot installed and you tested the ```/fusebot coin``` command, let's get started on building your first custom command. Let's build a command to search reddit. It is best to do development on a desktop browser as you will have more room to work with tabs and copy/paste code. 36 | 37 | 1. Type ```/fusebot edit reddit``` and hit enter 38 | 39 | ![image](https://user-images.githubusercontent.com/751491/126013119-033caee3-d6bd-4dcf-be6b-8025fe655261.png) 40 | 41 | 2. Click the "click here" link 42 | 43 | ![image](https://user-images.githubusercontent.com/751491/126013074-30368c7a-e5e2-4c86-a83d-979c2453c906.png) 44 | 45 | Your browser will open a window like this: 46 | 47 | ![image](https://user-images.githubusercontent.com/751491/126013397-720bfe76-1909-4f9e-8a3a-2ccd554e835f.png) 48 | 49 | You are looking at the Fusebot command editor that allows you to implement the logic behind the reddit command or any command you build. You can write any Node.js code here and use all public npm modules by declaring them in package.json. After you save the code, you can invoke it from Slack or Discord using the ```/fusebot reddit``` Slash Command. 50 | 51 | 3. Open this [command.js file](https://github.com/fusebit/fusebot/blob/main/samples/reddit/command.js) in another tab 52 | 53 | ![image](https://user-images.githubusercontent.com/751491/126013219-d0f5169b-ae65-4a63-b8d0-afda9dc04369.png) 54 | 55 | 4. Copy and paste the entire [command.js file](https://github.com/fusebit/fusebot/blob/main/samples/reddit/command.js) file content into the command.js file in the web editor. 56 | 57 | Copy: 58 | 59 | ![image](https://user-images.githubusercontent.com/751491/126013286-b83c9435-9341-472b-8f74-4cd156f0d5c7.png) 60 | 61 | Paste: 62 | 63 | ![image](https://user-images.githubusercontent.com/751491/126013734-9bce2ed1-deea-4ab1-bc12-4cca12f703e1.png) 64 | 65 | 66 | 5. Click the save icon in the top left of the web editor 67 | 68 | ![image](https://user-images.githubusercontent.com/751491/126013817-19d128d4-c271-4380-a821-fa67699e1631.png) 69 | 70 | 71 | 6. Return back to Slack or Discord to test out your new reddit command 72 | 73 | ```/fusebot reddit node API``` 74 | 75 | This command will search for "API" in the node subreddit. 76 | 77 | ![image](https://user-images.githubusercontent.com/751491/126014245-03fcebcb-038c-4329-830f-42d14b261c40.png) 78 | 79 | BOOM! You've built your first custom command with Fusebot. Check out other examples below and start coding today. 80 | 81 | ## Interacting with Fusebot 82 | 83 | If you are wondering about all of the ways to interact with manage your custom slash commands, just run: 84 | 85 | ```/fusebot help``` 86 | 87 | This is what you can do with Fusebot: 88 | 89 | ```/fusebot ls``` list custom commands in your workspace 90 | 91 | ```/fusebot edit {command}``` create or edit a custom command 92 | 93 | ```/fusebot rm {command}``` remove a command 94 | 95 | ```/fusebot feedback {your-feedback}``` tell us what you think (leave contact if expecting response) 96 | 97 | ```/fusebot {command} [arguments]``` run a custom command 98 | 99 | Note: replace {command} and/or [arguments] with the specific command you are creating or editing. { } denotes an argument you decide. 100 | 101 | ## Command samples 102 | 103 | With the power of Node.js and npm, the sky is the limit. What are you going to build today? 104 | 105 | - [/fusebot kudos](samples/kudos) - leave kudos for your team members, check ranking, and more 106 | - [/fusebot status](samples/status) - check and report the status of your systems 107 | - [/fusebot bitly](samples/bitly) - shorten URLs using bit.ly 108 | - [/fusebot coin](samples/coin) - get current value of a digital currency 109 | - [/fusebot github-dispatch](samples/githubDispatch) - trigger a github action 110 | - [/fusebot run-query](samples/dataQuery) - run SQL queries against MySQL compatible databases 111 | - [/fusebot stocks](samples/stocks) - get current value of a stock 112 | - [/fusebot gif](samples/gif) - display your favorite meme by name 113 | - [/fusebot reddit](samples/reddit) - search a subreddit for posts 114 | 115 | ## Programming model 116 | 117 | The function exported from `command.js` is the entry point to your Slash Command implementation. 118 | The `ctx` parameter contains the details of the event generated by Slack or Discord and a few useful methods 119 | described below. 120 | 121 | At the time your code executes, we have already done the Slack or Discord signature validation for you, so you can 122 | focus on the task at hand. Also, we have responded to Slack, so you are not subject to the 123 | 3 second execution limit those platforms impose. 124 | 125 | ```javascript 126 | module.export = async (ctx) => { 127 | await ctx.client.send(`Hello ${ctx.body.userName}!`); 128 | }; 129 | ``` 130 | 131 | Using `package.json` you can specify a dependency on any public npm module and then use 132 | it code. 133 | 134 | ```json 135 | { 136 | "engines": { 137 | "node": ">= 14" 138 | }, 139 | "dependencies": { 140 | "superagent": "6.1.0" 141 | } 142 | } 143 | ``` 144 | 145 | ### ctx.body 146 | 147 | The `ctx.body` contains the payload describing the event received from Slack or Discord. It is normalized 148 | between the two systems to facilite reusing the same code. 149 | 150 | ```javascript 151 | { 152 | "teamId": "TDFBLCJV9", 153 | "channelId": "DFNE1FG2E", 154 | "userId": "UFN96HN1J", 155 | "userName": "tomek", 156 | "text": "hello arg1 arg2 arg3", 157 | "args": ["arg1", "arg2", "arg3"], 158 | "native": { 159 | /* native event received from Slack or Discord */ 160 | } 161 | } 162 | ``` 163 | 164 | The `ctx.body.args` contains tokenized arguments the caller passed to your Slash Command. For example, 165 | if the command name is `hello`, calling `/fusebot hello arg1 arg2 arg3` would result in the `ctx.body.args` 166 | value shown above. 167 | 168 | The `ctx.body.native` represents the native event received from Slack or Discord. Check the [Slack event schema](https://api.slack.com/interactivity/slash-commands#command_payload_descriptions) or the [Discord event schema](https://discord.com/developers/docs/interactions/slash-commands#data-models-and-types) for details. 169 | 170 | ### ctx.configuration 171 | 172 | The `ctx.configuration` property provides access to the key/value pairs you define in the _Configuration_ section of the editor. These values are stored encrypted at rest and are a convenient way of providing any secrets or API keys to your code. 173 | 174 | ```javascript 175 | // In Configuration: 176 | // MY_KEY=SOME_VALUE 177 | const key = ctx.configuration.MY_KEY; 178 | // key is "SOME_VALUE" 179 | ``` 180 | 181 | ### ctx.client 182 | 183 | The `ctx.client` provides a way to send messages back to Slack or Discord. You can send multiple messages during 184 | a single invocation of your Slash Command. 185 | 186 | #### ctx.client.send 187 | 188 | The `ctx.client.send` asynchronous method sends a text message back to Slack or Discord. The message 189 | is visible to anyone in the channel the Slash Command was invoked in. You can use [Slack markup](https://api.slack.com/messaging/composing#text_formatting), or [Discord markup](https://support.discord.com/hc/en-us/articles/210298617-Markdown-Text-101-Chat-Formatting-Bold-Italic-Underline-) to format the message, respectively: 190 | 191 | Remember to wait for the asynchronous method to complete: 192 | 193 | ```javascript 194 | await ctx.client.send(`Hello, ${ctx.body.userName}!`); 195 | ``` 196 | 197 | #### ctx.client.sendEphemeral 198 | 199 | Works similarly to `ctx.client.send`, but the message will only be visible to the person who invoked 200 | the Slash Command. 201 | 202 | ### ctx.storage 203 | 204 | The `ctx.storage` offers access to a simple persistent storage solution. Think about it as local storage, 205 | only in the cloud. 206 | 207 | #### ctx.storage.get 208 | 209 | Returns a named document or undefined if no document is stored under the specified name: 210 | 211 | ```javascript 212 | const item = await ctx.storage.get("my_data"); 213 | // item is: 214 | // { 215 | // data: { ...object you previously stored... }, 216 | // etag: "{etag}" 217 | // } 218 | ``` 219 | 220 | The `etag` property describes the version of the item in storage which can be used in the call to `ctx.storage.put` 221 | to detect and prevent conflicting, concurrent writes. 222 | 223 | You can omit the document name to read the default document. 224 | 225 | #### ctx.storage.put 226 | 227 | Upserts a named document: 228 | 229 | ```javascript 230 | const item = { 231 | data: { ...whatever object you want to store... }, 232 | etag: "{etag}" // optional 233 | }; 234 | await ctx.storage.put(item, "my_data") 235 | ``` 236 | 237 | If you specify the `etag` property (usually by obtaining it from a prior `ctx.storage.get` call), and the 238 | document in storage has changed since then, the call will fail with an exception indicating a conflict. 239 | If you don't specify the `etag` property, any data in storage will be forcefuly overwritten. 240 | 241 | You can omit the document name to write to the default document. 242 | 243 | #### ctx.storage.list 244 | 245 | The `ctx.storage.list` returns the list of existing documents: 246 | 247 | ```javascript 248 | const result = await ctx.storage.list(); 249 | // result is 250 | // { 251 | // items: [ { storageKey: "{storageKey1}" }, ... ], 252 | // next: "{continuation-token}" // optional 253 | // } 254 | ``` 255 | 256 | If you organize your documents in a hierarchy using the `/` character as segment delimiter, you can also 257 | list only the documents underneath a particular segment of the hierarchy: 258 | 259 | ```javascript 260 | const result = await ctx.storage.list("/a/b"); 261 | ``` 262 | 263 | If the `result.next` property is present, there are more results which can be obtained with a subsequent call 264 | to the API: 265 | 266 | ```javascript 267 | const next; 268 | const result = await ctx.storage.list("/a/b", { next }); 269 | ``` 270 | 271 | You can limit the maximum number of results returned using the `limit` parameter: 272 | 273 | ```javascript 274 | const result = await ctx.storage.list("/a/b", { limit: 10 }); 275 | ``` 276 | 277 | #### ctx.storage.delete 278 | 279 | The `ctx.storage.delete` deletes an item from storage: 280 | 281 | ```javascript 282 | await ctx.storage.delete("my_data"); 283 | ``` 284 | 285 | If you organize your documents in a hierarchy using the `/` character as segment delimiter, you can also 286 | delete an entire branch of documents with a single call: 287 | 288 | ```javascript 289 | // Existing documents: 290 | // /a/b/c/d 291 | // /a/b/e/f 292 | // /a/b/ 293 | // /a/g/h/i 294 | await ctx.storage.delete("/a/b", true, true); 295 | // Documents after delete: 296 | // /a/b/ 297 | // /a/g/h/i 298 | ``` 299 | 300 | **NOTE** Deleteing `/a/b` recursively deletes all documents _underneath_ `/a/b` but not `/a/b` itself. 301 | 302 | **NOTE** The two `true` parameters are not a typo - they are intended to prevent accidental deletion of the entire storage. 303 | 304 | ## Support 305 | 306 | You can share your thoughts or ask questions using `/fusebot feedback` command, [contacting the Fusebit team](https://fusebit.io/contact?utm_source=github.com&utm_medium=referral&utm_campaign=fusebot-readme&utm_content=support). 307 | -------------------------------------------------------------------------------- /samples/bitly/README.md: -------------------------------------------------------------------------------- 1 | # /fusebot bitly {long-url} 2 | 3 | This command shortens links using bitly. 4 | 5 | 1. Type `/fusebot edit bitly`. 6 | 2. Click the _edit_ link. 7 | 3. Copy and paste the [command.js](command.js) file content into the command.js file in the web editor. 8 | 4. Go to the Configuration section of the editor, and set the `ACCESS_TOKEN={token}` configuration property. You can get your access token for bitly from [here](https://bitly.is/accesstoken). 9 | 5. Save. 10 | 6. Call `/fusebot bitly {long-url}` from Slack/Discord. 11 | 7. Modify and tinker! 12 | -------------------------------------------------------------------------------- /samples/bitly/command.js: -------------------------------------------------------------------------------- 1 | const Superagent = require("superagent"); 2 | 3 | /** 4 | * This is the implementation of your Fusebot command. 5 | * You can run it from Slack with `/fusebot bitly ...`. 6 | * Fusebot is powered by the Fusebit integration platform, https://fusebit.io/ 7 | * 8 | * Docs and samples are at https://github.com/fusebit/fusebot 9 | * You can talk to a fellow developer on Slack at https://fusebit.io/contact 10 | * Or tell us what you think with `/fusebot feedback {your-comments}` 11 | * 12 | * Now let's build something already. 13 | 14 | * @param ctx {FusebotContext} 15 | */ 16 | module.exports = async (ctx) => { 17 | let [url] = ctx.body.args; 18 | 19 | if (!url || url === "help") { 20 | return await help(ctx); 21 | } 22 | 23 | if (!ctx.configuration.ACCESS_TOKEN) { 24 | return await ctx.client.send( 25 | "Please edit the command and set the ACCESS_TOKEN configuration property. See https://github.com/fusebit/fusebot/tree/main/samples/bitly for details." 26 | ); 27 | } 28 | 29 | const result = await Superagent.post("https://api-ssl.bitly.com/v4/shorten") 30 | .set("Authorization", `Bearer ${ctx.configuration.ACCESS_TOKEN}`) 31 | .send({ long_url: url }); 32 | 33 | await ctx.client.send(`Short link:\n${result.body.link}`); 34 | }; 35 | 36 | const help = async (ctx) => { 37 | await ctx.client.send(`This command shortens URLs using bitly. Usage: 38 | - /fusebot bitly {url} - shorten the URL 39 | - /fusebot bitly help - display this help 40 | e.g. 41 | /fusebot bitly https://fusebit.io/careers`); 42 | }; 43 | -------------------------------------------------------------------------------- /samples/coin/README.md: -------------------------------------------------------------------------------- 1 | # /fusebot coin {coin-symbol} 2 | 3 | This command gets the current USD value of the specified digital currency on multiple exchanges. 4 | 5 | 1. Type `/fusebot edit coin`. 6 | 2. Click the _edit_ link. 7 | 3. Copy and paste the [command.js](command.js) file content into the command.js file in the web editor. 8 | 4. Save. 9 | 5. Call `/fusebot coin bty` from Slack/Discord. 10 | 6. Modify and tinker! 11 | -------------------------------------------------------------------------------- /samples/coin/command.js: -------------------------------------------------------------------------------- 1 | const Superagent = require("superagent"); 2 | 3 | /** 4 | * This is the implementation of your Fusebot command. 5 | * You can run it from Slack with `/fusebot coin ...`. 6 | * Fusebot is powered by the Fusebit integration platform, https://fusebit.io/ 7 | * 8 | * Docs and samples are at https://github.com/fusebit/fusebot 9 | * You can talk to a fellow developer on Slack at https://fusebit.io/contact 10 | * Or tell us what you think with `/fusebot feedback {your-comments}` 11 | * 12 | * Now let's build something already. 13 | 14 | * @param ctx {FusebotContext} 15 | */ 16 | module.exports = async (ctx) => { 17 | // Get the coin symbol provided by the caller 18 | let [coin] = ctx.body.args; 19 | 20 | // If no coin symbol was provided by the caller, show help 21 | if (!coin) { 22 | return await help(ctx); 23 | } 24 | 25 | coin = coin.toUpperCase(); 26 | 27 | // Get current prices. See sochain.com API docs at https://sochain.com/api#get-prices 28 | const r = await Superagent.get( 29 | `https://sochain.com/api/v2/get_price/${coin}/USD` 30 | ); 31 | 32 | // Format response 33 | const message = r.body.data.prices 34 | .map( 35 | (p) => 36 | `:moneybag: ${coin} price is ${p.price} USD on the ${p.exchange} exchange.` 37 | ) 38 | .join("\n"); 39 | 40 | // And send it 41 | await ctx.client.send(message); 42 | }; 43 | 44 | const help = async (ctx) => { 45 | await ctx.client 46 | .send(`:information_source: This command gets the current trading value of a digital currency on multiple exchanges. 47 | Usage: \`/fusebot coin {coin-symbol}\` 48 | e.g. 49 | \`/fusebot coin DOGE\` 50 | \`/fusebot coin BTY\` 51 | Enjoy!`); 52 | }; 53 | -------------------------------------------------------------------------------- /samples/dataQuery/README.md: -------------------------------------------------------------------------------- 1 | # /fusebot run-query SELECT samples FROM fusebot WHERE sample_name="dataQuery"; 2 | 3 | This command triggers a SQL query against a MySQL compatible database. 4 | 5 | 1. Type `/fusebot edit run-query`. 6 | 2. Click the _edit_ link. 7 | 3. Copy and paste the [command.js](command.js) file content into the command.js file in the web editor. 8 | 4. Add `"promise-mysql": "5.0.3"` into package.json as a dependency. 9 | 5. In the Configuration section of the editor, set the HOST, PORT, USERNAME, PASSWORD property. 10 | 6. Save. 11 | 7. Call `/fusebot run-query` from Slack/Discord. 12 | 8. Modify and tinker! 13 | -------------------------------------------------------------------------------- /samples/dataQuery/command.js: -------------------------------------------------------------------------------- 1 | const Superagent = require('superagent'); 2 | const mysql = require('promise-mysql'); 3 | /** 4 | * This is the implementation of your Fusebot command. 5 | * You can run it from Slack with `/fusebot run-query ...`. 6 | * Fusebot is powered by the Fusebit integration platform, https://fusebit.io/ 7 | * 8 | * Docs and samples are at https://github.com/fusebit/fusebot 9 | * You can talk to a fellow developer on Slack at https://fusebit.io/contact 10 | * Or tell us what you think with `/fusebot feedback {your-comments}` 11 | * 12 | * Now let's build something already. 13 | 14 | * @param ctx {FusebotContext} 15 | */ 16 | module.exports = async (ctx) => { 17 | if (ctx.body.args.length === 0 || ctx.body.args[0] === "help") { 18 | return help(ctx) 19 | } 20 | if (!ctx.configuration.HOST || !ctx.configuration.PORT || !ctx.configuration.USERNAME || !ctx.configuration.PASSWORD) { 21 | return ctx.client.sendEphemeral("Unable to detect SQL connection settings from configuration. For more information, please check https://github.com/fusebit/fusebot/tree/main/samples/dataQuery") 22 | } 23 | const sqlStatement = ctx.body.args.join(" ") 24 | try { 25 | const conn = await mysql.createConnection({ 26 | host: ctx.configuration.HOST, 27 | port: ctx.configuration.PORT, 28 | user: ctx.configuration.USERNAME, 29 | password: ctx.configuration.PASSWORD 30 | }) 31 | const results = await conn.query(sqlStatement) 32 | const length = results.length; 33 | results.splice(20); 34 | await ctx.client.send(results.map(row => JSON.stringify(row) + '\n').join()); 35 | if (length > 20) { 36 | await ctx.client.send(`:red_circle: truncated. Query returned ${length} results, showing only first 20.`); 37 | } 38 | } catch (e) { 39 | await ctx.client.send("Something went wrong when trying to execute the queries.") 40 | await ctx.client.sendEphemeral(e.message) 41 | } 42 | }; 43 | 44 | const help = async (ctx, optionalMessages) => { 45 | let messagesToSend = []; 46 | if (optionalMessages) { 47 | messagesToSend = messagesToSend.concat(optionalMessages) 48 | } 49 | messagesToSend.push(` 50 | - /fusebot run-query 51 | - /fusebot run-query help - display this help 52 | e.g. 53 | /fusebot run-query SELECT * FROM fusebot WHERE sample_name="dataQuery";`) 54 | await ctx.client.send(messagesToSend.join("\n")) 55 | } 56 | -------------------------------------------------------------------------------- /samples/dataQuery/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "engines": { 3 | "node": ">= 14" 4 | }, 5 | "dependencies": { 6 | "superagent": "6.1.0", 7 | "promise-mysql": "5.0.3" 8 | } 9 | } -------------------------------------------------------------------------------- /samples/gif/README.md: -------------------------------------------------------------------------------- 1 | # /fusebot gif {name} 2 | 3 | This command lets you easily display your favorite gifs. Register whatever image you want with the name you prefer, and then invoke it by that name from anywhere 4 | 5 | 1. Type `/fusebot edit gif`. 6 | 2. Click the _edit_ link. 7 | 3. Copy and paste the [command.js](command.js) file content into the command.js file in the web editor. 8 | 4. Save. 9 | 5. Call `/fusebot gif add magic https://media.giphy.com/media/12NUbkX6p4xOO4/giphy.gif` from Slack/Discord as an example 10 | 6. Display the gif anywhere by typing `/fusebot gif magic` 11 | 7. Modify and tinker! 12 | -------------------------------------------------------------------------------- /samples/gif/command.js: -------------------------------------------------------------------------------- 1 | module.exports = async (ctx) => { 2 | const [cmd, ...rest] = ctx.body.args; 3 | 4 | if (cmd === "add" && rest && rest[0] && rest[1]) { 5 | // add image 6 | const name = rest[0]; 7 | const image = rest[1]; 8 | const item = (await ctx.storage.get()) || { data: { images: {} } }; 9 | item.data.images[name.toLowerCase()] = image; 10 | await ctx.storage.put(item); 11 | await ctx.client.send(`Image ${name} added`); 12 | return await ctx.client.send(image); 13 | } 14 | if (cmd === "ls") { 15 | // get image list 16 | const item = await ctx.storage.get(); 17 | let message; 18 | if (!item || !item.data || !item.data.images) { 19 | message = `No images registered yet`; 20 | } else { 21 | message = Object.keys(item.data.images) 22 | .map((k) => `:frame_with_picture: ${k} => \`${item.data.images[k]}\``) 23 | .join("\n"); 24 | } 25 | return await ctx.client.send( 26 | `${message}\n\nCall \`/fusebot gif\` for help` 27 | ); 28 | } 29 | if (cmd === "rm" && rest && rest[0]) { 30 | // remove image 31 | const name = rest[0].toLowerCase(); 32 | const item = await ctx.storage.get(); 33 | if (!item || !item.data || !item.data.images || !item.data.images[name]) { 34 | return await ctx.client.send(`Image ${name} doesn't exist`); 35 | } 36 | delete item.data.images[name]; 37 | await ctx.storage.put(item); 38 | return await ctx.client.send(`Image ${name} removed`); 39 | } 40 | if (cmd && rest.length === 0) { 41 | // display an image 42 | const name = cmd.toLowerCase(); 43 | const item = await ctx.storage.get(); 44 | if (!item || !item.data || !item.data.images || !item.data.images[name]) { 45 | return await ctx.client.send(`Image ${name} doesn't exist`); 46 | } 47 | return await ctx.client.send(`${item.data.images[name]}`); 48 | } 49 | return await help(ctx); 50 | }; 51 | 52 | const help = async (ctx) => { 53 | await ctx.client.send(`Usage: 54 | - \`/fusebot gif {name}\` - display that image 55 | - \`/fusebot gif add {name} {url}\` - add a new named image 56 | - \`/fusebot gif rm {name}\` - remove an image 57 | - \`/fusebot gif ls\` - list all images`); 58 | }; 59 | -------------------------------------------------------------------------------- /samples/githubDispatch/README.md: -------------------------------------------------------------------------------- 1 | # /fusebot github-dispatch 2 | 3 | This command triggers a github action through a workflow_trigger. 4 | 5 | 1. Type `/fusebot edit github-dispatch`. 6 | 2. Click the _edit_ link. 7 | 3. Copy and paste the [command.js](command.js) file content into the command.js file in the web editor. 8 | 4. Add `"octokit": "1.0.5"` into package.json as a dependency. 9 | 5. In the Configuration section of the editor, set the PAT={github-personal-access-token} property. You can obtain your GitHub personal access token from [github_pat_tutorial](https://docs.github.com/en/github/authenticating-to-github/keeping-your-account-and-data-secure/creating-a-personal-access-token). 10 | 6. Save. 11 | 7. Call `/fusebot github-dispatch` from Slack/Discord. 12 | 8. Modify and tinker! 13 | 14 | ## Note: 15 | 16 | The Github personal access token for this function requires the permission of `repo:*` and `action:write` to function. -------------------------------------------------------------------------------- /samples/githubDispatch/command.js: -------------------------------------------------------------------------------- 1 | const { Octokit } = require("octokit"); 2 | /** 3 | * This is the implementation of your Fusebot command. 4 | * You can run it from Slack with `/fusebot github-dispatch ...`. 5 | * Fusebot is powered by the Fusebit integration platform, https://fusebit.io/ 6 | * 7 | * Docs and samples are at https://github.com/fusebit/fusebot 8 | * You can talk to a fellow developer on Slack at https://fusebit.io/contact 9 | * Or tell us what you think with `/fusebot feedback {your-comments}` 10 | * 11 | * Now let's build something already. 12 | 13 | * @param ctx {FusebotContext} 14 | */ 15 | module.exports = async (ctx) => { 16 | if (ctx.body.args.length === 0 || ctx.body.args[0] === "help") { 17 | return help(ctx); 18 | } 19 | if (!ctx.configuration.PAT) { 20 | await ctx.client.send( 21 | "Unable to find github PAT in configuration, please refer to https://github.com/fusebit/fusebot/blob/main/samples/githubDispatch/README.md for detail." 22 | ); 23 | } 24 | const octokit = new Octokit({ auth: ctx.configuration.PAT }); 25 | const [userRepo, workflowId, branchTagName] = ctx.body.args; 26 | let errorMessages = [] 27 | if (!userRepo) { 28 | isError = true; 29 | errorMessages.push("Username/repository was not found."); 30 | } 31 | if (!workflowId) { 32 | errorMessages.push("WorkflowId/workflowFileName was not found."); 33 | } 34 | if (!branchTagName) { 35 | errorMessages.push("Branch name or tag name was not found."); 36 | } 37 | if (errorMessages.length !== 0) { 38 | await help(ctx, errorMessages); 39 | return; 40 | } 41 | await octokit.request( 42 | "POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches", 43 | { 44 | owner: userRepo.split("/")[0], 45 | repo: userRepo.split("/")[1], 46 | workflow_id: workflowId, 47 | ref: branchTagName, 48 | } 49 | ); 50 | await ctx.client.send( 51 | `Workflow ${workflowId} on ${userRepo} triggered with branch/tag ${branchTagName}!` 52 | ); 53 | }; 54 | 55 | const help = async (ctx, optionalMessage) => { 56 | let messageToSend = [] 57 | if (optionalMessage) { 58 | messageToSend = messageToSend.concat(optionalMessage) 59 | } 60 | messageToSend.push(` 61 | - /fusebot github-dispatch / or or 62 | - /fusebot github-dispatch help - display this help 63 | e.g. 64 | /fusebot github-dispatch fusebit/fusebot publish.yaml master`); 65 | await ctx.client.send(messageToSend.join('\n')) 66 | }; 67 | -------------------------------------------------------------------------------- /samples/githubDispatch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "engines": { 3 | "node": ">= 14" 4 | }, 5 | "dependencies": { 6 | "superagent": "6.1.0", 7 | "octokit": "1.0.5" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /samples/kudos/README.md: -------------------------------------------------------------------------------- 1 | # /fusebot kudos 2 | 3 | This command allows you to leave kudos for team members, check the ranking, and individual comments. 4 | 5 | 1. Type `/fusebot edit kudos`. 6 | 2. Click the _edit_ link. 7 | 3. Copy and paste the [command.js](command.js) file content into the command.js file in the web editor. 8 | 4. Save. 9 | 5. Call `/fusebot kudos help` from Slack/Discord for instructions. 10 | 6. Modify and tinker! 11 | -------------------------------------------------------------------------------- /samples/kudos/command.js: -------------------------------------------------------------------------------- 1 | const Superagent = require("superagent"); 2 | 3 | /** 4 | * This is the implementation of your Fusebot command. 5 | * You can run it from Slack with `/fusebot kudos ...`. 6 | * Fusebot is powered by the Fusebit integration platform, https://fusebit.io/ 7 | * 8 | * Docs and samples are at https://github.com/fusebit/fusebot 9 | * You can talk to a fellow developer on Slack at https://fusebit.io/contact 10 | * Or tell us what you think with `/fusebot feedback {your-comments}` 11 | * 12 | * Now let's build something already. 13 | * 14 | * @param ctx {FusebotContext} 15 | */ 16 | module.exports = async (ctx) => { 17 | const [cmd, ...rest] = ctx.body.args; 18 | if (cmd === "help") { 19 | return await help(ctx); 20 | } 21 | if (cmd && cmd[0] === "@") { 22 | // leave feedback 23 | const username = cmd; 24 | const feedback = rest.join(" ").trim() || undefined; 25 | const item = (await ctx.storage.get()) || { data: { users: {} } }; 26 | item.data.users[username] = item.data.users[username] || { kudos: 0 }; 27 | item.data.users[username].kudos++; 28 | if (feedback) { 29 | item.data.users[username].feedback = 30 | item.data.users[username].feedback || []; 31 | item.data.users[username].feedback.unshift(feedback); 32 | item.data.users[username].feedback.splice(20); 33 | } 34 | await ctx.storage.put(item); 35 | return await ctx.client.send( 36 | `Hey ${ctx.body.userName}, thank you for leaving kudos for ${username}!` 37 | ); 38 | } 39 | if (cmd === undefined) { 40 | // get ranking 41 | const item = await ctx.storage.get(); 42 | let message; 43 | if (!item) { 44 | message = `Nobody left kudos yet, be the first!`; 45 | } else { 46 | const ranking = []; 47 | for (var username in item.data.users) { 48 | ranking.push({ username, ...item.data.users[username] }); 49 | } 50 | ranking.sort((a, b) => 51 | a.kudos > b.kudos ? -1 : a.kudos < b.kudos ? 1 : 0 52 | ); 53 | message = ranking 54 | .map((u) => `:trophy: ${u.username} (${u.kudos})`) 55 | .join("\n"); 56 | } 57 | return await ctx.client.send( 58 | `${message}\n\nCall \`/fusebot kudos help\` for help` 59 | ); 60 | } 61 | if (cmd === "get") { 62 | // get feedback for user 63 | const [username] = rest; 64 | if (username && username[0] === "@") { 65 | const item = await ctx.storage.get(); 66 | if (!item || !item.data.users[username]) { 67 | return await ctx.client.send( 68 | `User ${username} has not received any kudos yet. Leave them some!` 69 | ); 70 | } else { 71 | const u = item.data.users[username]; 72 | return await ctx.client.send( 73 | `User ${username} has ${u.kudos} kudos!${ 74 | u.feedback 75 | ? `\nMost recent comments:\n${u.feedback 76 | .map((f) => `:trophy: ${f}`) 77 | .join("\n")}` 78 | : "" 79 | }` 80 | ); 81 | } 82 | } 83 | } 84 | return await help(ctx); 85 | }; 86 | 87 | const help = async (ctx) => { 88 | await ctx.client.send(`Usage: 89 | - /fusebot kudos @{username} [{feedback}] - to give kudos 90 | - /fusebot kudos - get kudos ranking for everybody 91 | - /fusebot kudos get @{username} - get kudos feedback for a person 92 | - /fusebot kudos help - display this help 93 | 94 | e.g. 95 | 96 | /fusebot kudos @mark You did a great job! 97 | /fusebit kudos 98 | /fusebot kudos @jane`); 99 | }; 100 | -------------------------------------------------------------------------------- /samples/reddit/README.md: -------------------------------------------------------------------------------- 1 | # /fusebot reddit <subreddit> <term> [<term> ]+ 2 | 3 | This command searches a specified subreddit for new posts with titles containing the supplied terms. 4 | 5 | 1. Type `/fusebot edit reddit`. 6 | 2. Click the _edit_ link. 7 | 3. Copy and paste the [command.js](command.js) file content into the command.js file in the web editor. 8 | 4. Save. 9 | 5. Call `/fusebot reddit buildapcsales monitor gaming ` from Slack/Discord. 10 | 6. Modify and tinker! -------------------------------------------------------------------------------- /samples/reddit/command.js: -------------------------------------------------------------------------------- 1 | const Superagent = require('superagent'); 2 | 3 | const REDDIT_TIMEFRAME = 'hour'; // hour, day, week, month, year, all 4 | const REDDIT_LIMIT = 100 // Number of posts to retrieve 5 | const REDDIT_LISTING = 'new' // controversial, best, hot, new, random, rising, top 6 | const MAX_RESULTS = 5; // To prevent going over the Discord 2000 character limit for a single message. 7 | 8 | /** 9 | * This is the implementation of your Fusebot command. 10 | * You can run it from Discord with `/fusebot reddit ...`. 11 | * Fusebot is powered by the Fusebit integration platform, https://fusebit.io/ 12 | * 13 | * Docs and samples are at https://github.com/fusebit/fusebot 14 | * You can talk to a fellow developer on Discord at https://fusebit.io/contact 15 | * Or tell us what you think with `/fusebot feedback {your-comments}` 16 | * 17 | * Now let's build something already. 18 | * 19 | * @param ctx {FusebotContext} 20 | */ 21 | module.exports = async (ctx) => { 22 | if (ctx.body.args.length < 2) { 23 | return await help(ctx); 24 | } 25 | 26 | // The first arg is the subreddit, and the remaining args are terms that all must be in the post's title for a match. 27 | const [subreddit] = ctx.body.args; 28 | const terms = ctx.body.args.slice(1).map(term => term.toLowerCase()); // Convert all to lowercase for case-insensitive search. 29 | 30 | // See: https://www.jcchouinard.com/reddit-api-without-api-credentials/ 31 | const url = `https://www.reddit.com/r/${subreddit}/${REDDIT_LISTING}.json?limit=${REDDIT_LIMIT}&t=${REDDIT_TIMEFRAME}`; 32 | 33 | const result = await Superagent.get(url); 34 | const posts = result.body.data.children; 35 | 36 | let message = 'Results: '; 37 | let numFound = 0; 38 | for (const post of posts) { 39 | let title = post.data.title.toLowerCase(); 40 | let match = true; 41 | // Check that each term is in the title. (Case insensitive match) 42 | for (const term of terms) { 43 | if (!title.includes(term)) { 44 | match = false; 45 | break; 46 | } 47 | } 48 | 49 | if (match) { 50 | numFound++; 51 | message += `\n${numFound}. ${post.data.title}: ${post.data.url}`; 52 | } 53 | 54 | if (numFound >= MAX_RESULTS) { 55 | break; 56 | } 57 | } 58 | 59 | if (numFound == 0) { 60 | message += '\nNo results found.' 61 | } 62 | 63 | await ctx.client.send(message); 64 | }; 65 | 66 | const help = async (ctx) => { 67 | await ctx.client.send(`This command searches a specified subreddit for new posts with titles containing the supplied terms. Usage: 68 | \`\`\` 69 | /fusebot reddit [ ]+ 70 | Example: 71 | /fusebot reddit buildapcsales monitor gaming 72 | \`\`\` 73 | `) 74 | }; 75 | -------------------------------------------------------------------------------- /samples/status/README.md: -------------------------------------------------------------------------------- 1 | # /fusebot status 2 | 3 | This command allows you to check status of your systems and report health. 4 | 5 | 1. Type `/fusebot edit status`. 6 | 2. Click the _edit_ link. 7 | 3. Copy and paste the [command.js](command.js) file content into the command.js file in the web editor. 8 | 4. Save. 9 | 5. Call `/fusebot status` from Slack/Discord. 10 | 6. Modify and tinker! 11 | -------------------------------------------------------------------------------- /samples/status/command.js: -------------------------------------------------------------------------------- 1 | const Superagent = require("superagent"); 2 | 3 | module.exports = async (ctx) => { 4 | const systems = [ 5 | { name: "google", url: "https://google.com" }, 6 | { name: "amazon", url: "https://amazon.com" }, 7 | { name: "apple", url: "https://apple.com" }, 8 | { name: "fusebot", url: "https://go.us-west-2.fusebot.io/v1/health" }, 9 | ]; 10 | 11 | const status = await Promise.all( 12 | systems.map((s) => Superagent.get(s.url).ok((r) => true)) 13 | ); 14 | const response = status.map( 15 | (s, i) => 16 | `${s.status === 200 ? ":white_check_mark:" : ":red_circle:"} ${ 17 | s.status 18 | } ${systems[i].name}` 19 | ); 20 | 21 | await ctx.client.send(response.join("\n")); 22 | }; 23 | -------------------------------------------------------------------------------- /samples/stocks/README.md: -------------------------------------------------------------------------------- 1 | # /fusebot stocks <ticker> 2 | 3 | This command gets the current USD value of the specified stock. 4 | 5 | 1. Type `/fusebot edit stocks`. 6 | 2. Click the _edit_ link. 7 | 3. Copy and paste the [command.js](command.js) file content into the command.js file in the web editor. 8 | 5. In the Configuration section of the editor, set the API_KEY={alphavantage.co API KEY} property. You can obtain your Alphavantage.co API key at (alphavantageco-website)[https://www.alphavantage.co/]. 9 | 6. Save. 10 | 7. Call `/fusebot stocks` from Slack/Discord. 11 | 8. Modify and tinker! 12 | -------------------------------------------------------------------------------- /samples/stocks/command.js: -------------------------------------------------------------------------------- 1 | const Superagent = require('superagent'); 2 | 3 | /** 4 | * This is the implementation of your Fusebot command. 5 | * You can run it from Slack with `/fusebot stocks ...`. 6 | * Fusebot is powered by the Fusebit integration platform, https://fusebit.io/ 7 | * 8 | * Docs and samples are at https://github.com/fusebit/fusebot 9 | * You can talk to a fellow developer on Slack at https://fusebit.io/contact 10 | * Or tell us what you think with `/fusebot feedback {your-comments}` 11 | * 12 | * Now let's build something already. 13 | 14 | * @param ctx {FusebotContext} 15 | */ 16 | module.exports = async (ctx) => { 17 | if (ctx.body.args.length === 0 || ctx.body.args[0] === 'help') { 18 | return help(ctx) 19 | } 20 | if (!ctx.configuration.API_KEY) { 21 | await ctx.client.send("API_KEY not found, please refer to https://github.com/fusebit/fusebot/blob/main/samples/stocks/README.md for more information.") 22 | return 23 | } 24 | const stockSymbol = ctx.body.args[0] 25 | // Api docs: https://www.alphavantage.co/documentation/#latestprice 26 | const results = await Superagent.get(`https://www.alphavantage.co/query\?function\=GLOBAL_QUOTE\&symbol\=${stockSymbol}\&apikey\=${ctx.configuration.API_KEY}`) 27 | await ctx.client.send(`${stockSymbol}'s current price is ${results.body['Global Quote']['05. price']}.`) 28 | }; 29 | 30 | const help = async (ctx) => { 31 | await ctx.client.send(` 32 | - /fusebot stocks 33 | - /fusebot stocks help - display this help 34 | e.g. 35 | /fusebot stocks AMC`) 36 | } 37 | -------------------------------------------------------------------------------- /samples/stocks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "engines": { 3 | "node": ">= 14" 4 | }, 5 | "dependencies": { 6 | "superagent": "6.1.0" 7 | } 8 | } 9 | --------------------------------------------------------------------------------