rodbot 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +1 -0
  3. data/CHANGELOG.md +14 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +653 -0
  6. data/exe/rodbot +7 -0
  7. data/lib/roda/plugins/rodbot.rb +36 -0
  8. data/lib/rodbot/async.rb +45 -0
  9. data/lib/rodbot/cli/command.rb +25 -0
  10. data/lib/rodbot/cli/commands/console.rb +23 -0
  11. data/lib/rodbot/cli/commands/credentials.rb +19 -0
  12. data/lib/rodbot/cli/commands/deploy.rb +20 -0
  13. data/lib/rodbot/cli/commands/new.rb +21 -0
  14. data/lib/rodbot/cli/commands/simulator.rb +17 -0
  15. data/lib/rodbot/cli/commands/start.rb +26 -0
  16. data/lib/rodbot/cli/commands/stop.rb +15 -0
  17. data/lib/rodbot/cli/commands/version.rb +15 -0
  18. data/lib/rodbot/cli/commands.rb +18 -0
  19. data/lib/rodbot/cli.rb +9 -0
  20. data/lib/rodbot/config.rb +157 -0
  21. data/lib/rodbot/constants.rb +13 -0
  22. data/lib/rodbot/db/hash.rb +71 -0
  23. data/lib/rodbot/db/redis.rb +61 -0
  24. data/lib/rodbot/db.rb +91 -0
  25. data/lib/rodbot/dispatcher.rb +125 -0
  26. data/lib/rodbot/env.rb +48 -0
  27. data/lib/rodbot/error.rb +19 -0
  28. data/lib/rodbot/generator.rb +108 -0
  29. data/lib/rodbot/log.rb +67 -0
  30. data/lib/rodbot/memoize.rb +86 -0
  31. data/lib/rodbot/plugins/github_webhook/README.github_webhook.md +42 -0
  32. data/lib/rodbot/plugins/github_webhook/app.rb +46 -0
  33. data/lib/rodbot/plugins/gitlab_webhook/README.gitlab_webhook.md +40 -0
  34. data/lib/rodbot/plugins/gitlab_webhook/app.rb +40 -0
  35. data/lib/rodbot/plugins/hal/README.hal.md +15 -0
  36. data/lib/rodbot/plugins/hal/app.rb +22 -0
  37. data/lib/rodbot/plugins/matrix/README.matrix.md +42 -0
  38. data/lib/rodbot/plugins/matrix/relay.rb +113 -0
  39. data/lib/rodbot/plugins/otp/README.otp.md +82 -0
  40. data/lib/rodbot/plugins/otp/app.rb +47 -0
  41. data/lib/rodbot/plugins/word_of_the_day/README.word_of_the_day.md +13 -0
  42. data/lib/rodbot/plugins/word_of_the_day/schedule.rb +51 -0
  43. data/lib/rodbot/plugins.rb +81 -0
  44. data/lib/rodbot/rack.rb +50 -0
  45. data/lib/rodbot/refinements.rb +118 -0
  46. data/lib/rodbot/relay.rb +104 -0
  47. data/lib/rodbot/services/app.rb +72 -0
  48. data/lib/rodbot/services/relay.rb +37 -0
  49. data/lib/rodbot/services/schedule.rb +29 -0
  50. data/lib/rodbot/services.rb +32 -0
  51. data/lib/rodbot/simulator.rb +60 -0
  52. data/lib/rodbot/version.rb +5 -0
  53. data/lib/rodbot.rb +60 -0
  54. data/lib/templates/deploy/docker/compose.yaml.gerb +37 -0
  55. data/lib/templates/deploy/docker-split/compose.yaml.gerb +52 -0
  56. data/lib/templates/deploy/procfile/Procfile.gerb +1 -0
  57. data/lib/templates/deploy/procfile-split/Procfile.gerb +5 -0
  58. data/lib/templates/deploy/render/render.yaml.gerb +0 -0
  59. data/lib/templates/deploy/render-split/render.yaml.gerb +0 -0
  60. data/lib/templates/new/LICENSE.txt +22 -0
  61. data/lib/templates/new/README.md +4 -0
  62. data/lib/templates/new/app/app.rb +11 -0
  63. data/lib/templates/new/app/routes/help.rb +19 -0
  64. data/lib/templates/new/app/views/layout.erb +12 -0
  65. data/lib/templates/new/app/views/root.erb +5 -0
  66. data/lib/templates/new/config/rodbot.rb +8 -0
  67. data/lib/templates/new/config/schedule.rb +5 -0
  68. data/lib/templates/new/config.ru +3 -0
  69. data/lib/templates/new/gems.locked +104 -0
  70. data/lib/templates/new/gems.rb +15 -0
  71. data/lib/templates/new/guardfile.rb +9 -0
  72. data/lib/templates/new/public/assets/images/rodbot.avif +0 -0
  73. data/lib/templates/new/public/assets/stylesheets/base.css +18 -0
  74. data/lib/templates/new/rakefile.rb +28 -0
  75. data.tar.gz.sig +0 -0
  76. metadata +510 -0
  77. metadata.gz.sig +3 -0
data/README.md ADDED
@@ -0,0 +1,653 @@
1
+ [![Version](https://img.shields.io/gem/v/rodbot.svg?style=flat)](https://rubygems.org/gems/rodbot)
2
+ [![Tests](https://img.shields.io/github/actions/workflow/status/svoop/rodbot/test.yml?style=flat&label=tests)](https://github.com/svoop/rodbot/actions?workflow=Test)
3
+ [![Code Climate](https://img.shields.io/codeclimate/maintainability/svoop/rodbot.svg?style=flat)](https://codeclimate.com/github/svoop/rodbot/)
4
+ [![Donorbox](https://img.shields.io/badge/donate-on_donorbox-yellow.svg)](https://donorbox.org/bitcetera)
5
+
6
+ <img src="https://github.com/svoop/rodbot/raw/main/doc/rodbot.avif" alt="Rodbot" height="125" align="left">
7
+
8
+ # Rodbot
9
+
10
+ Minimalistic yet polyglot framework to build chat bots on top of a Roda backend for chatops and fun.
11
+
12
+ <br clear="all">
13
+
14
+ * [Homepage](https://github.com/svoop/rodbot)
15
+ * [API](https://rubydoc.info/gems/rodbot)
16
+ * Author: [Sven Schwyn - Bitcetera](https://bitcetera.com)
17
+
18
+ ## Table of Contents
19
+
20
+ [Install](#label-Install) <br>
21
+ [Anatomy](#label-Anatomy) <br>
22
+ &emsp;&emsp;&emsp;[App Service](#label-App-Service) <br>
23
+ &emsp;&emsp;&emsp;[Relay Services](#label-Relay-Services) <br>
24
+ &emsp;&emsp;&emsp;[Schedule Service](#label-Schedule-Service) <br>
25
+ [CLI](#label-CLI) <br>
26
+ [Request](#request)<br>
27
+ [Say](#say)<br>
28
+ [Routes and Commands](#label-Routes-and-Commands) <br>
29
+ [Database](#label-Database) <br>
30
+ [Environments](#environments) <br>
31
+ [Credentials](#credentials) <br>
32
+ [Plugins](#label-Plugins) <br>
33
+ [Environment Variables](#label-Environment-Variables) <br>
34
+ [Development](#label-Development) <br>
35
+
36
+ ## Install
37
+
38
+ ### Security
39
+
40
+ This gem is [cryptographically signed](https://guides.rubygems.org/security/#using-gems) in order to assure it hasn't been tampered with. Unless already done, please add the author's public key as a trusted certificate now:
41
+
42
+ ```
43
+ gem cert --add <(curl -Ls https://raw.github.com/svoop/rodbot/main/certs/svoop.pem)
44
+ ```
45
+
46
+ ### Generate New Bot
47
+
48
+ Similar to other frameworks, generate the files for your new bot as follows:
49
+
50
+ ```
51
+ gem install rodbot --trust-policy MediumSecurity
52
+ rodbot new my_bot
53
+ cd my_bot
54
+ ```
55
+
56
+ For the bot to be useful at all, you should choose one of the supported [relay service plugins](#label-Plugins). Say, you'd like to interact via Matrix:
57
+
58
+ ```
59
+ bundle config set --local with matrix
60
+ bundle install
61
+ ```
62
+
63
+ Please refer to the [Matrix plugin README](https://rubydoc.info/github/svoop/rodbot/file/lib/rodbot/plugins/matrix/README.matrix.md) for more on how to configure and authorise this relay service.
64
+
65
+ Time to add Git to the mix. Both `gems.locked` and `.bundle` are included in order to use the same gems and versions both for local development and deployment to production:
66
+
67
+ ```
68
+ git init
69
+ git add .
70
+ git commit -m "Bootstrap Rodbot"
71
+ ```
72
+
73
+ You're all set, let's have a look at what Rodbot can do for you:
74
+
75
+ ```
76
+ bundle exec rodbot --help
77
+ ```
78
+
79
+ ## Anatomy
80
+
81
+ The bot consists of three kinds of services interacting with one another:
82
+
83
+ ```
84
+ RODBOT EXTERNAL
85
+ ╭╴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╶╮
86
+ ╷ ╭──────────────────╮ <─────> ╭──────────────────╮ ╷
87
+ ╷ │ APP │ <───╮ │ RELAY - Matrix ├╮ <──────> [1] Matrix
88
+ ╷ ╰──────────────────╯ <─╮ │ ╰┬─────────────────╯├╮ <──┼──> [1] simulator
89
+ ╷ │ │ ╰┬─────────────────╯│ <────> [1] ...
90
+ ╷ │ │ ╰──────────────────╯ ╵
91
+ ╷ │ │ ╵
92
+ ╷ │ │ ╭──────────────────╮ ╵
93
+ ╷ │ ╰──> │ SCHEDULE │ <───┼─── [2] clock
94
+ ╷ │ ╰──────────────────╯ ╷
95
+ ╷ │ ╷
96
+ ╷ ╰───────────────────────────────────── [3] webhook caller
97
+ ╰╴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╶╯
98
+ ```
99
+
100
+ ### App Service
101
+
102
+ The **app service** is a [Roda app](https://roda.jeremyevans.net) where the real action happens. It acts on and responds to HTTP requests from:
103
+
104
+ * commands forwarded by **relay services**
105
+ * timed events triggered by the **schedule service**
106
+ * third party webhook calls e.g. from GitLab, GitHub etc
107
+
108
+ See [Rodbot::Config::DEFAULTS](https://github.com/svoop/rodbot/blob/main/lib/rodbot/config.rb) for available config settings and their defaults.
109
+
110
+ #### Roda
111
+
112
+ The Roda app is located in the `app` directory. It contains:
113
+
114
+ * `app.rb` – Roda app class where new routes are added using `run` statements
115
+ * `routes\` – Directory which contains one route file for every `run` statement
116
+ * `views\` – Directory which contains layouts and views called with `view` in route files
117
+
118
+ For an example, take a look at `app/routes/help.rb` generated as part of every new Rodbot app.
119
+
120
+ The `app.rb` loads the Rodbot plugin with `plugin :rodbot`. This Roda plugin is a necessary dependency for many Rodbot plugins and does two things.
121
+
122
+ It loads the following Roda plugins:
123
+
124
+ * [multi_run](http://roda.jeremyevans.net/rdoc/classes/Roda/RodaPlugins/MultiRun.html)
125
+ * [environments](http://roda.jeremyevans.net/rdoc/classes/Roda/RodaPlugins/Environments.html)
126
+ * [heartbeat](http://roda.jeremyevans.net/rdoc/classes/Roda/RodaPlugins/Heartbeat.html)
127
+ * [public](http://roda.jeremyevans.net/rdoc/classes/Roda/RodaPlugins/Public.html)
128
+ * [run_append_slash](http://roda.jeremyevans.net/rdoc/classes/Roda/RodaPlugins/RunAppendSlash.html)
129
+ * [halt](http://roda.jeremyevans.net/rdoc/classes/Roda/RodaPlugins/Halt.html)
130
+ * [unescape_path](http://roda.jeremyevans.net/rdoc/classes/Roda/RodaPlugins/UnescapePath.html)
131
+ * [render](http://roda.jeremyevans.net/rdoc/classes/Roda/RodaPlugins/Render.html).
132
+
133
+ It loads the following Roda extensions provided by Rodbot:
134
+
135
+ * Shortcut `r.arguments` for `r.params['arguments']`
136
+
137
+ #### Host
138
+
139
+ The **app service** binds to `localhost` by default and therefore isolates it from the internet. In case you want to make it publicly reachable, you have to set the `RODBOT_APP_HOST` environment variable to a public IP. Or to bind to all IPs of all interfaces:
140
+
141
+ ```
142
+ export RODBOT_APP_HOST=0.0.0.0
143
+ ```
144
+
145
+ #### Ports
146
+
147
+ The **app service** binds to the base port 7200 by default. However, each **relay service** needs a predictable port to bind to as well, which is why the next few following ports must not be in use already. If you have to, you can change the base port in `config/rodbot.rb`:
148
+
149
+ ```ruby
150
+ port 12345
151
+ ```
152
+
153
+ #### Commands
154
+
155
+ All top level GET requests such as `GET /foobar` are commands and therefore are accessible by relays, for instance using `!foobar` on Matrix.
156
+
157
+ Responses have to be either of the following content types:
158
+
159
+ * `text/plain; charset=utf-8`
160
+ * `text/markdown; charset=utf-8`
161
+
162
+ Please note that the Markdown might get stripped on communication networks which feature only limited or no support for Markdown.
163
+
164
+ The response may contain special tags which have to be replace appropriately by the corresponding **relay service**:
165
+
166
+ Tag | Replaced with
167
+ ----|--------------
168
+ `[[SENDER]]` | Mention the sender of the command.
169
+
170
+ #### Other Routes
171
+
172
+ All higher level requests such as `GET /foo/bar` are not accessible by relays. Use them to implement other aspects of your bot such as webhooks or schedule tasks.
173
+
174
+ ### Relay Services
175
+
176
+ The **relay service** act as glue between the **app service** and external communication networks such as Matrix.
177
+
178
+ Each relay service does three things:
179
+
180
+ * **Proactive:** It creates and listens to a local TCP socket. Plain text or Markdown sent to this socket is forwarded as a message to the corresponding communication network. This text may have multiple lines, use the EOT character (`\x04` alias Ctrl-D) to mark the end.
181
+ * **Reactive:** It reads messages, detects commands usually beginning with a `!`, forwards them to the **app service** and writes the HTTP response back as a message to the communication network.
182
+ * **Test:** It detects the `!ping` command and replies with "pong" *without* hitting the **app service**.
183
+
184
+ You can simulate such a communication network locally:
185
+
186
+ ```
187
+ rodbot simulator
188
+ ```
189
+
190
+ Enter the command `!pay EUR 123` and you see the request `GET /pay?argument=EUR+123` hitting the **app service**.
191
+
192
+ ### Schedule Service
193
+
194
+ The **schedule service** is a [Clockwork process](https://github.com/Rykian/clockwork) which triggers Ruby code asynchronously as configured in `config/schedule.rb`.
195
+
196
+ It's a good idea to have the **app service** do the heavy lifting while the schedule simply fires the corresponding HTTP request.
197
+
198
+ ## CLI
199
+
200
+ The `rodbot` CLI is the main tool to manage your bot. For a full list of functions:
201
+
202
+ ```
203
+ rodbot --help
204
+ ```
205
+
206
+ ### Starting and Stopping Services
207
+
208
+ While working on the app service, you certainly want to try routes:
209
+
210
+ ```
211
+ rodbot start app
212
+ ```
213
+
214
+ This starts the server in the current terminal. You can set breakpoints with `binding.irb`, however, if you prefer a real debugger:
215
+
216
+ ```
217
+ rodbot start app --debugger
218
+ ```
219
+
220
+ This requires the [debug gem](https://rubygems.org/gems/debug) and adds the ability to set breakpoints with `debugger`.
221
+
222
+ Here's how to start single services in the background:
223
+
224
+ ```
225
+ rodbot start app --daemonize
226
+ ```
227
+
228
+ You can also start all services at once in which case the services must run in the background and therefore the `--daemonize` is implied and may be omitted:
229
+
230
+
231
+ ```
232
+ rodbot start
233
+ ```
234
+
235
+ Finally, to stop all running Rodbot services:
236
+
237
+ ```
238
+ rodbot stop
239
+ ```
240
+
241
+ ### Deployment
242
+
243
+ While controlling Rodbot as mentioned in the previous section is okay for local development, deploying the bot to production comes in a gazillion scenarios. Rodbot helps you with scaffolds for some of them. To get the list of all deploy scaffolds:
244
+
245
+ ```
246
+ rodbot deploy --help
247
+ ```
248
+
249
+ :warning: It's near impossible to include such deployment scenarios in the test suite. If you find an error or have an improvement, please [submit an issue](https://github.com/svoop/rodbot/issues)!
250
+
251
+ Let's take a quick look at the two most common scenarios:
252
+
253
+ #### Docker
254
+
255
+ To run all of Rodbot in one single Docker service:
256
+
257
+ ```
258
+ rodbot deploy docker
259
+ ```
260
+
261
+ In case you prefer to split each service into its own container:
262
+
263
+ ```
264
+ rodbot deploy docker --split
265
+ ```
266
+
267
+ The Docker deployment is a `compose.yml` file, so you might want to write it to disk:
268
+
269
+ ```
270
+ rodbot deploy docker >compose.yml
271
+ ```
272
+
273
+ #### Procfile
274
+
275
+ The `Procfile` was introduced by Heroku and is nowadays supported many cloud providers as well as tools for local development.
276
+
277
+ While a monolith approach is certainly possible, it makes more sense to split each service into its own process:
278
+
279
+ ```
280
+ rodbot deploy procfile --split
281
+ ```
282
+
283
+ As per convention, the `Procfile` should be placed in the root of the project:
284
+
285
+ ```
286
+ rodbot deploy procfile --split >Procfile
287
+ ```
288
+
289
+ It's easy to test drive using a process manager such as [Foreman](https://rubygems.org/gems/foreman):
290
+
291
+ ```
292
+ gem install foreman
293
+ foreman start
294
+ ```
295
+
296
+ For more control and debug features, you might want to try [Overmind](https://github.com/DarthSim/overmind) instead e.g. installed via [Homebrew](https://brew.sh):
297
+
298
+ ```
299
+ brew install overmind
300
+ overmind start
301
+ ```
302
+
303
+ ## Request
304
+
305
+ To query the **app service**, you can either use the bundled [HTTParty](https://rubygems.org/gems/httparty) gem or the following convenience wrapper:
306
+
307
+ ```ruby
308
+ response = Rodbot.request('/time', query: { zone: 'UTC' })
309
+ ```
310
+
311
+ This uses the default `method: :get` and the default `timeout: 10` seconds, it returns an instance of [HTTParty::Response](https://www.rubydoc.info/gems/httparty/HTTParty/Response):
312
+
313
+ ```ruby
314
+ response.code # => 200
315
+ response.body # => '2023-09-06 22:51:50.231703 UTC'
316
+ ```
317
+
318
+ ## Say
319
+
320
+ You can send proactive messages to communication networks with `Rodbot.say`.
321
+
322
+ Since you're not limited to just one relay plugin, you have to configure which of them shall post messages submitted with `Rodbot.say` by adding `say true` in `config/rodbot.rb`. Here's an example for the Matrix relay plugin:
323
+
324
+ ```ruby
325
+ plugin :matrix do
326
+ say true
327
+ (...)
328
+ end
329
+ ```
330
+
331
+ With this in place, you can now submit messages from just about anywhere, most notably **app service** routes and **schedule service** jobs.
332
+
333
+ ```ruby
334
+ say("Hello, World!")
335
+ ```
336
+
337
+ You can further narrow where to post the message if you specify the relay plugin explicitly:
338
+
339
+ ```ruby
340
+ say("Hello, Slack!", on: :slack)
341
+ ```
342
+
343
+ ## Routes and Commands
344
+
345
+ Adding new tricks to your bot boils down to adding routes to the app service which is powered by Roda, a simple yet very powerful framework for web applications: Easy to learn (like Sinatra) but really fast and efficient. Take a minute and [get familiar with the basics of Roda](http://roda.jeremyevans.net/).
346
+
347
+ Rodbot relies on [MultiRun](https://roda.jeremyevans.net/rdoc/classes/Roda/RodaPlugins/MultiRun.html) to spread routes over more than one routing file. This is necessary for Rodbot plugins but is entirely optional for your own routes.
348
+
349
+ ⚠️ At this point, keep in mind that any routes at the root level like `/pay` or `/calculate` can be accessed via chat commands such as `!pay` and `!calculate`. Routes which are nested further down, say, `/myapi/users` are off limits and should be used to trigger schedule events and such. Make sure you don't accidentally add routes to the root level you don't want people to access via chat commands, not even by accident.
350
+
351
+ To add a simple "Hello, World!" command, all you have to do is add a route `/hello`. A good place to do so is `app/routes/hello.rb`:
352
+
353
+ ```ruby
354
+ module Routes
355
+ class Hello < App
356
+
357
+ route do |r|
358
+
359
+ # GET /hello
360
+ r.root do
361
+ response['Content-Type'] = 'text/plain; charset=utf-8'
362
+ 'Hello, World!'
363
+ end
364
+
365
+ end
366
+
367
+ end
368
+ end
369
+ ```
370
+
371
+ To try, start the app service with `rodbot start app` and fire up the simulator with `rodbot simulator`:
372
+
373
+ ```
374
+ rodbot> !hello
375
+ Hello, World!
376
+ ```
377
+
378
+ Try to keep these route files thin and extract the heavy lifting into service classes. Put those into the `lib` directory where they will be autoloaded by Zeitwerk.
379
+
380
+ ## Database
381
+
382
+ Your bot might be happy dealing with every command as an isolated event. However, some implementations require data to be persisted between requests. A good example is the [OTP plugin](https://rubydoc.info/github/svoop/rodbot/file/lib/rodbot/plugins/otp/README.otp.md) which needs a database to assure each one-time password is accepted once only.
383
+
384
+ Rodbot implements a very simple key/value database which is completely optional and supports a few different backends.
385
+
386
+ ### Redis
387
+
388
+ For the Redis backend to work, you have to install the corresponding Bundler group:
389
+
390
+ ```
391
+ bundle config set --local with redis
392
+ bundle install
393
+ ```
394
+
395
+ Then set the connection URL in `config/rodbot.rb`:
396
+
397
+ ```ruby
398
+ db 'redis://localhost:6379/10'
399
+ ```
400
+
401
+ ### Hash
402
+
403
+ The Hash backend is not thread-safe and therefore shouldn't be used in production. To use it, simply add the following to `config/rodbot.rb`:
404
+
405
+ ```ruby
406
+ db 'hash'
407
+ ```
408
+
409
+ ### Write and Read Data
410
+
411
+ With this in place, you can access the database with `Rodbot.db`:
412
+
413
+ ```ruby
414
+ Rodbot.db.flush # => Rodbot::Db
415
+
416
+ Rodbot.db.set('foo') { 'bar' } # => 'bar'
417
+ Rodbot.db.get('foo') # => 'bar'
418
+ Rodbot.db.scan('*') # => ['foo']
419
+ Rodbot.db.delete('foo') # => 'bar'
420
+ Rodbot.db.get('foo') # => nil
421
+
422
+ Rodbot.db.set('lifetime', expires_in: 1) { 'short' } # => 'short'
423
+ Rodbot.db.get('lifetime') # => 'short'
424
+ sleep 1
425
+ Rodbot.db.get('lifetime') # => nil
426
+ ```
427
+
428
+ For a few more tricks, see the [Rodbot::Db docs](https://www.rubydoc.info/gems/rodbot/Rodbot/Db.html).
429
+
430
+ ## Environments
431
+
432
+ Similar to other frameworks, Rodbot features different environments which affect the way certain processes work. Use the environment variable `RODBOT_ENV` to set control this:
433
+
434
+ Value | Meaning
435
+ ------|--------
436
+ development | This is the default environment used for local develoment.
437
+ production | Use this environment when you deploy Rodbot.
438
+ test | This environment is set for the automated tests of Rodbot.
439
+
440
+ The current environment can be programmatically queried:
441
+
442
+ ```ruby
443
+ ENV['RODBOT_ENV'] = "production"
444
+ Rodbot.env.current # => "production"
445
+ Rodbot.env.production? # => true
446
+ Rodbot.env.development? # => false
447
+ ```
448
+
449
+ ## Credentials
450
+
451
+ In order not to commit secrets to repositories or environment variables, Rodbot bundles the [dry-credentials](https://rubygems.org/gems/dry-credentials) gem and exposes it via the `rodbot credentials` CLI command. The secrets are then available in your code like `Rodbot.credentials.my_secret` and the encrypted files are written to `config/credentials`.
452
+
453
+ ## Plugins
454
+
455
+ Rodbot aims to keep its core small and add features via plugins, either built-in or provided by gems.
456
+
457
+ ### Built-In Plugins
458
+
459
+ Name | Dependencies | Description
460
+ -----|--------------|------------
461
+ [:matrix](https://rubydoc.info/github/svoop/rodbot/file/lib/rodbot/plugins/matrix/README.matrix.md) | yes | relay service for the [Matrix communication network](https://matrix.org)
462
+ [:otp](https://rubydoc.info/github/svoop/rodbot/file/lib/rodbot/plugins/otp/README.otp.md) | yes | guard commands with one-time passwords
463
+ [:gitlab_webhook](ttps://rubydoc.info/github/svoop/rodbot/file/lib/rodbot/plugins/gitlab_webhook/README.gitlab_webhook.md) | no | event announcements from [GitLab](https://gitlab.com)
464
+ [:github_webhook](ttps://rubydoc.info/github/svoop/rodbot/file/lib/rodbot/plugins/github_webhook/README.github_webhook.md) | no | event announcements from [GitHub](https://github.com)
465
+ [:hal](https://rubydoc.info/github/svoop/rodbot/file/lib/rodbot/plugins/hal/README.hal.md) | no | feel like Dave (demo)
466
+ [:word_of_the_day](ttps://rubydoc.info/github/svoop/rodbot/file/lib/rodbot/plugins/word_of_the_day/README.word_of_the_day.md) | no | word of the day announcements (demo)
467
+
468
+ You have to install the corresponding Bundler group in case the plugin depends on extra gems. Here's an example for the `:otp` plugin listed above:
469
+
470
+ ```
471
+ bundle config set --local with otp
472
+ bundle install
473
+ ```
474
+
475
+ ### How Plugins Work
476
+
477
+ Given the following `config/rodbot.rb`:
478
+
479
+ ```ruby
480
+ plugin :my_plugin do
481
+ color 'red'
482
+ end
483
+ ```
484
+
485
+ Plugins provide one or more extensions each of which extends one of the services. In order only to spin things up when needed, the plugin may contain the following files:
486
+
487
+ * `rodbot/plugins/my_plugin/app.rb` – add routes and/or extend Roda
488
+ * `rodbot/plugins/my_plugin/relay.rb` – add a relay
489
+ * `rodbot/plugins/my_plugin/schedule.rb` – add schedules to Clockwork
490
+
491
+ Whenever a service boots, the corresponding file is required.
492
+
493
+ In order to keep these plugin files slim, you should extract functionality into service classes. Just put them into `rodbot/plugins/my_plugin/lib/` and use `require_relative` where you need them.
494
+
495
+ ### Create Plugins
496
+
497
+ You can create plugins in any of the following places:
498
+
499
+ * inside your Rodbot instance:<br>`/lib/rodbot/plugins/my_plugin`
500
+ * in a vendored gem "rodbot-my_plugin":<br>`/lib/rodbot/vendor/gems/rodbot-my_plugin/lib/rodbot/my_plugin`
501
+ * in a published gem "rodbot-my_plugin":<br>`/lib/rodbot/plugins/my_plugin`
502
+
503
+ Please adhere to common naming conventions and use the dashed prefix `rodbot-` (and Module `Rodbot`), however, underscores in case the remaining gem name consists of several words.
504
+
505
+ #### App Extension
506
+
507
+ An app extension `rodbot/plugins/my_plugin/app.rb` defines the module `App` and looks something like this:
508
+
509
+ ```ruby
510
+ module Rodbot
511
+ class Plugins
512
+ module MyPlugin
513
+ module App
514
+
515
+ module Routes < ::App
516
+ route do |r|
517
+ # GET /my_plugin
518
+ r.root do
519
+ # called by command !my_plugin
520
+ end
521
+
522
+ # GET /my_plugin/whatever
523
+ r.get('whatever') do
524
+ # not reachable by any command
525
+ end
526
+ end
527
+ end
528
+
529
+ module ResponseMethods
530
+ # (...)
531
+ end
532
+
533
+ end
534
+ end
535
+ end
536
+ end
537
+ ```
538
+
539
+ The `Routes` module contains all the routes you would like to inject.
540
+
541
+ The `App` module can be used to [extend all aspects of Roda](https://github.com/jeremyevans/roda#plugins-).
542
+
543
+ For an example, take a look at the [:hal plugin](https://github.com/svoop/rodbot/tree/main/lib/rodbot/plugins/hal).
544
+
545
+ #### Relay Extension
546
+
547
+ A relay extension `rodbot/plugins/my_plugin/relay.rb` defines the class `Relay` and looks something like this:
548
+
549
+ ```ruby
550
+ module Rodbot
551
+ class Plugins
552
+ module MyPlugin
553
+ class Relay < Rodbot::Relay
554
+
555
+ def loops
556
+ SomeAwesomeCommunicationNetwork.connect
557
+ [method(:read_loop), method(:write_loop)]
558
+ end
559
+
560
+ private
561
+
562
+ def read_loop
563
+ loop do
564
+ # Listen in on the communication network
565
+ end
566
+ end
567
+
568
+ def write_loop
569
+ loop do
570
+ # Post something to the communication network
571
+ end
572
+ end
573
+
574
+ end
575
+ end
576
+ end
577
+ end
578
+ ```
579
+
580
+ The `loops` method must returns an array of callables (e.g. a Proc or Method) which will be called when this relay service is started. The loops must trap the `INT` signal.
581
+
582
+ Proactive messsages require other parts of Rodbot to forward a message directly. To do so, the relay has to implement a TCP socket. This socket must bind to the IP and port you get from the `bind` method which returns an array like `["localhost", 7201]`.
583
+
584
+ For an example, take a look at the [:matrix plugin](https://github.com/svoop/rodbot/tree/main/lib/rodbot/plugins/matrix).
585
+
586
+ #### Schedule Extension
587
+
588
+ A schedule extension `rodbot/plugins/my_plugin/schedule.rb` defines the class `Schedule` and looks something like this:
589
+
590
+ ```ruby
591
+ module Rodbot
592
+ class Plugins
593
+ module MyPlugin
594
+ class Schedule
595
+
596
+ def initialize
597
+ Clockwork.every(1.day, -> { tea }, at: '16:00')
598
+ end
599
+
600
+ private
601
+
602
+ def tea
603
+ Rodbot.say "Time for a cup of tea!"
604
+ end
605
+
606
+ end
607
+ end
608
+ end
609
+ end
610
+ ```
611
+
612
+ The initializer must set at least one schedule using `Clockwork.every` – see the [Clockwork docs](https://www.rubydoc.info/gems/clockwork).
613
+
614
+ For an example, take a look at the [:word_of_the_day plugin](https://github.com/svoop/rodbot/tree/main/lib/rodbot/plugins/word_of_the_day).
615
+
616
+ #### Toolbox
617
+
618
+ Before you write a plugin, familiarize yourself with the following bundled helpers:
619
+
620
+ * [Rodbot::Refinements](https://www.rubydoc.info/gems/rodbot/Rodbot/Refinements.html) – just a few handy extensions to Ruby core classes
621
+ * [Rodbot::Memoize](https://www.rubydoc.info/gems/rodbot/Rodbot/Memoize.html) – environment-aware memoization for method return values
622
+
623
+ ## Environment Variables
624
+
625
+ Environment variables are used for the configuration bits which cannot or should not be part of `config/rodbot.rb` mainly because they have to be set on the infrastructure level.
626
+
627
+ Variable | Description | Default
628
+ ---------|-------------|--------
629
+ `RODBOT_ENV` | Environment | development
630
+ `RODBOT_CREDENTIALS_DIR` | Override the directory containing encrypted credentials files | config/credentials/
631
+ `RODBOT_APP_HOST` | Override where to locally bind the app service | localhost
632
+ `RODBOT_APP_URL` | Override where to locally reach the app service | http://localhost
633
+ `RODBOT_RELAY_HOST` | Override where to bind the relay services | localhost
634
+ `RODBOT_RELAY_URL_XXX` | Override where to locally reach the given relay service `XXX` (e.g. `MATRIX`) | tcp://localhost
635
+
636
+ ## Development
637
+
638
+ To install the development dependencies and then run the test suite:
639
+
640
+ ```
641
+ bundle install
642
+ bundle exec rake # run tests once
643
+ bundle exec guard # run tests whenever files are modified
644
+ ```
645
+
646
+ Some tests require Redis and will be skipped by default. You can enable them by setting the following environment variable along the lines of:
647
+
648
+ ```
649
+ export RODBOT_SPEC_REDIS_URL=redis://localhost:6379/10
650
+ ```
651
+
652
+ You're welcome to join the [discussion forum](https://github.com/svoop/rodbot/discussions) to ask questions or drop feature ideas, [submit issues](https://github.com/svoop/rodbot/issues) you may encounter or contribute code by [forking this project and submitting pull requests](https://docs.github.com/en/get-started/quickstart/fork-a-repo).
653
+
data/exe/rodbot ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "rodbot"
4
+
5
+ Rodbot.boot
6
+ Rodbot::CLI.new(Rodbot::CLI::Commands).call
7
+
@@ -0,0 +1,36 @@
1
+ class Roda
2
+ module RodaPlugins
3
+
4
+ module Rodbot
5
+ class << self
6
+ def load_dependencies(app)
7
+ app.plugin :multi_run
8
+ app.plugin :environments
9
+ app.plugin :heartbeat
10
+ app.plugin :public
11
+ app.plugin :run_append_slash
12
+ app.plugin :halt
13
+ app.plugin :unescape_path
14
+ app.plugin :render, layout: './layout', views: 'app/views'
15
+ load_rodbot_dependencies(app)
16
+ end
17
+
18
+ private
19
+
20
+ def load_rodbot_dependencies(app)
21
+ ::Rodbot.plugins.extend_app
22
+ ::Rodbot.plugins.extensions[:app].keys.each { app.plugin _1 }
23
+ end
24
+ end
25
+
26
+ module RequestMethods
27
+ def arguments
28
+ params['arguments']
29
+ end
30
+ end
31
+ end
32
+
33
+ register_plugin :rodbot, Rodbot
34
+
35
+ end
36
+ end