├── .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 |
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 | 
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 | 
40 |
41 | 2. Click the "click here" link
42 |
43 | 
44 |
45 | Your browser will open a window like this:
46 |
47 | 
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 | 
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 | 
60 |
61 | Paste:
62 |
63 | 
64 |
65 |
66 | 5. Click the save icon in the top left of the web editor
67 |
68 | 
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 | 
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 |
--------------------------------------------------------------------------------