rodbot 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +1 -0
- data/CHANGELOG.md +14 -0
- data/LICENSE.txt +22 -0
- data/README.md +653 -0
- data/exe/rodbot +7 -0
- data/lib/roda/plugins/rodbot.rb +36 -0
- data/lib/rodbot/async.rb +45 -0
- data/lib/rodbot/cli/command.rb +25 -0
- data/lib/rodbot/cli/commands/console.rb +23 -0
- data/lib/rodbot/cli/commands/credentials.rb +19 -0
- data/lib/rodbot/cli/commands/deploy.rb +20 -0
- data/lib/rodbot/cli/commands/new.rb +21 -0
- data/lib/rodbot/cli/commands/simulator.rb +17 -0
- data/lib/rodbot/cli/commands/start.rb +26 -0
- data/lib/rodbot/cli/commands/stop.rb +15 -0
- data/lib/rodbot/cli/commands/version.rb +15 -0
- data/lib/rodbot/cli/commands.rb +18 -0
- data/lib/rodbot/cli.rb +9 -0
- data/lib/rodbot/config.rb +157 -0
- data/lib/rodbot/constants.rb +13 -0
- data/lib/rodbot/db/hash.rb +71 -0
- data/lib/rodbot/db/redis.rb +61 -0
- data/lib/rodbot/db.rb +91 -0
- data/lib/rodbot/dispatcher.rb +125 -0
- data/lib/rodbot/env.rb +48 -0
- data/lib/rodbot/error.rb +19 -0
- data/lib/rodbot/generator.rb +108 -0
- data/lib/rodbot/log.rb +67 -0
- data/lib/rodbot/memoize.rb +86 -0
- data/lib/rodbot/plugins/github_webhook/README.github_webhook.md +42 -0
- data/lib/rodbot/plugins/github_webhook/app.rb +46 -0
- data/lib/rodbot/plugins/gitlab_webhook/README.gitlab_webhook.md +40 -0
- data/lib/rodbot/plugins/gitlab_webhook/app.rb +40 -0
- data/lib/rodbot/plugins/hal/README.hal.md +15 -0
- data/lib/rodbot/plugins/hal/app.rb +22 -0
- data/lib/rodbot/plugins/matrix/README.matrix.md +42 -0
- data/lib/rodbot/plugins/matrix/relay.rb +113 -0
- data/lib/rodbot/plugins/otp/README.otp.md +82 -0
- data/lib/rodbot/plugins/otp/app.rb +47 -0
- data/lib/rodbot/plugins/word_of_the_day/README.word_of_the_day.md +13 -0
- data/lib/rodbot/plugins/word_of_the_day/schedule.rb +51 -0
- data/lib/rodbot/plugins.rb +81 -0
- data/lib/rodbot/rack.rb +50 -0
- data/lib/rodbot/refinements.rb +118 -0
- data/lib/rodbot/relay.rb +104 -0
- data/lib/rodbot/services/app.rb +72 -0
- data/lib/rodbot/services/relay.rb +37 -0
- data/lib/rodbot/services/schedule.rb +29 -0
- data/lib/rodbot/services.rb +32 -0
- data/lib/rodbot/simulator.rb +60 -0
- data/lib/rodbot/version.rb +5 -0
- data/lib/rodbot.rb +60 -0
- data/lib/templates/deploy/docker/compose.yaml.gerb +37 -0
- data/lib/templates/deploy/docker-split/compose.yaml.gerb +52 -0
- data/lib/templates/deploy/procfile/Procfile.gerb +1 -0
- data/lib/templates/deploy/procfile-split/Procfile.gerb +5 -0
- data/lib/templates/deploy/render/render.yaml.gerb +0 -0
- data/lib/templates/deploy/render-split/render.yaml.gerb +0 -0
- data/lib/templates/new/LICENSE.txt +22 -0
- data/lib/templates/new/README.md +4 -0
- data/lib/templates/new/app/app.rb +11 -0
- data/lib/templates/new/app/routes/help.rb +19 -0
- data/lib/templates/new/app/views/layout.erb +12 -0
- data/lib/templates/new/app/views/root.erb +5 -0
- data/lib/templates/new/config/rodbot.rb +8 -0
- data/lib/templates/new/config/schedule.rb +5 -0
- data/lib/templates/new/config.ru +3 -0
- data/lib/templates/new/gems.locked +104 -0
- data/lib/templates/new/gems.rb +15 -0
- data/lib/templates/new/guardfile.rb +9 -0
- data/lib/templates/new/public/assets/images/rodbot.avif +0 -0
- data/lib/templates/new/public/assets/stylesheets/base.css +18 -0
- data/lib/templates/new/rakefile.rb +28 -0
- data.tar.gz.sig +0 -0
- metadata +510 -0
- metadata.gz.sig +3 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
# Rodbot Plugin – Matrix
|
2
|
+
|
3
|
+
Relay with the Matrix communication network
|
4
|
+
|
5
|
+
## Preparation
|
6
|
+
|
7
|
+
To get an `access_token`, you have to create a new bot user. (Actually, Matrix does not know special bot users, just create a normal one instead.)
|
8
|
+
|
9
|
+
1. Create a regular Matrix user account with [Element](https://app.element.io)
|
10
|
+
2. In "All settings", set the display name, upload a user picture and disable all notifications. If the bot is supposed to join encrypted rooms as well, you should download the backup keys.
|
11
|
+
3. You find the access token in "All settings -> Help & About".
|
12
|
+
|
13
|
+
Invite this new bot user to the room of your choice and figure out the corresponding `room_id`. You can use the public room ID (e.g. `#myroom:matrix.org`) but since it may change you're better off using the internal room ID (e.g. `!kg7FkT64kGUgfk8R7a:matrix.org`) instead:
|
14
|
+
|
15
|
+
1. Right click on the room in question and select "Settings"
|
16
|
+
2. Click on "Advanced"
|
17
|
+
|
18
|
+
## Activation
|
19
|
+
|
20
|
+
Install the required gems via the corresponding Bundler group:
|
21
|
+
|
22
|
+
```
|
23
|
+
bundle config set --local with matrix
|
24
|
+
bundle install
|
25
|
+
```
|
26
|
+
|
27
|
+
Then activate and configure this plugin in `config/rodbot.rb`:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
plugin :matrix do
|
31
|
+
access_token '<TOKEN>'
|
32
|
+
room_id '<ID>'
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
You might want to use the credentials facilities of Rodbot to encrypt the token.
|
37
|
+
|
38
|
+
## Usage
|
39
|
+
|
40
|
+
Once Rodbot is restarted, the Matrix relay will automatically accept the invitation and start listening. To check whether the relay works fine, just say +!ping+ in the room, you should receive a "pong" in reply.
|
41
|
+
|
42
|
+
Any room message beginning with "!" is considered a bot command.
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'matrix_sdk'
|
4
|
+
|
5
|
+
using Rodbot::Refinements
|
6
|
+
|
7
|
+
module Rodbot
|
8
|
+
class Plugins
|
9
|
+
class Matrix
|
10
|
+
class Relay < Rodbot::Relay
|
11
|
+
include Rodbot::Memoize
|
12
|
+
|
13
|
+
def loops
|
14
|
+
client.on_invite_event.add_handler { on_invite(_1) }
|
15
|
+
client.sync(filter: filter(empty: true))
|
16
|
+
client.on_event.add_handler('m.room.message') { on_message(_1) }
|
17
|
+
[method(:read_loop), method(:write_loop)]
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def access_token
|
23
|
+
Rodbot.config(:plugin, :matrix, :access_token)
|
24
|
+
rescue => error
|
25
|
+
raise Rodbot::PluginError.new("invalid access_token", error.message)
|
26
|
+
end
|
27
|
+
|
28
|
+
def room_id
|
29
|
+
Rodbot.config(:plugin, :matrix, :room_id)
|
30
|
+
rescue => error
|
31
|
+
raise Rodbot::PluginError.new("invalid room_id", error.message)
|
32
|
+
end
|
33
|
+
|
34
|
+
def homeserver
|
35
|
+
room_id.split(':').last
|
36
|
+
rescue => error
|
37
|
+
raise Rodbot::PluginError.new("invalid room_id", error.message)
|
38
|
+
end
|
39
|
+
|
40
|
+
memoize def client
|
41
|
+
MatrixSdk::Client.new(homeserver, access_token: access_token, client_cache: :some)
|
42
|
+
end
|
43
|
+
|
44
|
+
memoize def room
|
45
|
+
client.ensure_room(room_id)
|
46
|
+
end
|
47
|
+
|
48
|
+
def read_loop
|
49
|
+
loop do
|
50
|
+
client.sync(filter: filter)
|
51
|
+
rescue StandardError
|
52
|
+
sleep 5
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def write_loop
|
57
|
+
server = TCPServer.new(*bind)
|
58
|
+
loop do
|
59
|
+
Thread.start(server.accept) do |remote|
|
60
|
+
body = remote.gets("\x04")
|
61
|
+
remote.close
|
62
|
+
body.force_encoding('UTF-8')
|
63
|
+
room.send_html body.md_to_html
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def on_invite(invite)
|
69
|
+
client.join_room(invite[:room_id]) if Rodbot.config(:plugin, :matrix, :room_id) == invite[:room_id]
|
70
|
+
end
|
71
|
+
|
72
|
+
def on_message(message)
|
73
|
+
if message.content[:msgtype] == 'm.text' && message.content[:body].start_with?('!')
|
74
|
+
html = 'pong' if message.content[:body] == '!ping'
|
75
|
+
html ||= reply_to(message)
|
76
|
+
room.send_html(html)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def reply_to(message)
|
81
|
+
command(*message.content[:body][1..].split(/\s+/, 2)).
|
82
|
+
md_to_html.
|
83
|
+
psub(placeholders(client.get_user(message.sender)))
|
84
|
+
end
|
85
|
+
|
86
|
+
def placeholders(sender)
|
87
|
+
{
|
88
|
+
sender: "https://matrix.to/#/#{sender.id}"
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
def filter(empty: false)
|
93
|
+
{
|
94
|
+
presence: { types: [] },
|
95
|
+
account_data: { types: [] },
|
96
|
+
room: {
|
97
|
+
ephemeral: { types: [] },
|
98
|
+
state: {
|
99
|
+
types: (empty ? [] : ['m.room.*']),
|
100
|
+
lazy_load_members: true
|
101
|
+
},
|
102
|
+
timeline: {
|
103
|
+
types: (empty ? [] : ['m.room.message'])
|
104
|
+
},
|
105
|
+
account_data: { types: [] }
|
106
|
+
}
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# Rodbot Plugin – OTP
|
2
|
+
|
3
|
+
Guard commands with one-time passwords
|
4
|
+
|
5
|
+
## Preparation
|
6
|
+
|
7
|
+
Create a secret key and add it to your authenticator app:
|
8
|
+
|
9
|
+
```
|
10
|
+
require 'rotp'
|
11
|
+
ROTP::Base32.random
|
12
|
+
```
|
13
|
+
|
14
|
+
## Activation
|
15
|
+
|
16
|
+
Activate and configure this plugin in `config/rodbot.rb` using the secret you've created in the previous section:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
plugin :otp do
|
20
|
+
secret '<SECRET>'
|
21
|
+
drift 10
|
22
|
+
end
|
23
|
+
```
|
24
|
+
|
25
|
+
The `drift` config is optional. In this example, one-time passwords are accepted up to 10 seconds beyond their expiration to compensate for slow networks. By default, no drift is granted.
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
To protect a command, just add a guard to the corresponding app route. Say, you have implemented a command `!reboot example.com` in `app/routes/reboot.rb`:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
module Routes
|
33
|
+
class Hello < App
|
34
|
+
|
35
|
+
route do |r|
|
36
|
+
|
37
|
+
# GET /reboot
|
38
|
+
r.root do |r|
|
39
|
+
response['Content-Type'] = 'text/plain; charset=utf-8'
|
40
|
+
ServerService.new(r.arguments).reboot!
|
41
|
+
'Done!'
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
(As a reminder: Up until this point, `r.arguments` is a mere shortcut for `r.params['arguments']`.)
|
51
|
+
|
52
|
+
In order to protect this rather dangerous command with a one-time password, you have to guard the route:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
r.root do |r|
|
56
|
+
r.halt [401, {}, ['Unauthorized']] unless r.valid_otp?
|
57
|
+
response['Content-Type'] = 'text/plain; charset=utf-8'
|
58
|
+
ServerService.new(r.arguments).reboot!
|
59
|
+
'Done!'
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
To execute the command now, you have to add the six digit one-time password to the end of it:
|
64
|
+
|
65
|
+
```
|
66
|
+
!reboot example.com 123456
|
67
|
+
```
|
68
|
+
|
69
|
+
The `r.valid_otp?` guard extracts the one-time password from the message and validates it. In this example, a validation result `true` causes the server to be rebooted. Please note that `r.argument` after the guard does not contain the one-time password anymore!
|
70
|
+
|
71
|
+
If halting with a 401 error is all you want, there's even a shorter alternative `r.require_valid_otp!`:
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
r.root do |r|
|
75
|
+
r.require_valid_otp!
|
76
|
+
response['Content-Type'] = 'text/plain; charset=utf-8'
|
77
|
+
ServerService.new(r.arguments).reboot!
|
78
|
+
'Done!'
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
This route does exactly the same as the more verbose one above.
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rotp'
|
4
|
+
|
5
|
+
module Rodbot
|
6
|
+
class Plugins
|
7
|
+
class Otp
|
8
|
+
module App
|
9
|
+
|
10
|
+
module RequestMethods
|
11
|
+
include Rodbot::Memoize
|
12
|
+
|
13
|
+
def valid_otp?
|
14
|
+
return false unless password
|
15
|
+
return false if Rodbot.db.get(:otp, password) # already used
|
16
|
+
valid = totp.verify(password, drift_behind: Rodbot.config(:otp, :drift).to_i)
|
17
|
+
!!if
|
18
|
+
Rodbot.db.set(:otp, password) { true }
|
19
|
+
true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def require_valid_otp!
|
24
|
+
halt [401, {}, ['Unauthorized']] unless valid_otp?
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
memoize def totp
|
30
|
+
secret = Rodbot.config(:plugin, :otp, :secret)
|
31
|
+
fail(Rodbot::PluginError, "OTP secret is not set") unless secret
|
32
|
+
ROTP::TOTP.new(secret, issuer: 'Rodbot')
|
33
|
+
end
|
34
|
+
|
35
|
+
# Extract (and remove) the password from arguments
|
36
|
+
#
|
37
|
+
# @return [String, nil] extracted password if any
|
38
|
+
memoize def password
|
39
|
+
params['arguments'] = params['arguments']&.sub(/\s*(\d{6})\s*\z/, '')
|
40
|
+
$1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'httparty'
|
4
|
+
|
5
|
+
module Rodbot
|
6
|
+
class Plugins
|
7
|
+
class WordOfTheDay
|
8
|
+
class Schedule
|
9
|
+
def initialize
|
10
|
+
Clockwork.every(1.day, -> { Rodbot.say message }, at: time)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def time
|
16
|
+
Rodbot.config(:plugin, :word_of_the_day, :time) || '12:00'
|
17
|
+
end
|
18
|
+
|
19
|
+
def message
|
20
|
+
Rodbot::Plugins::WordOfTheDay::Today.new.message
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
class Today
|
26
|
+
def initialize
|
27
|
+
@response = HTTParty.get('https://www.merriam-webster.com/word-of-the-day')
|
28
|
+
end
|
29
|
+
|
30
|
+
def message
|
31
|
+
if @response.success?
|
32
|
+
"Word of the day: [#{word}](#{url})"
|
33
|
+
else
|
34
|
+
"Sorry, there was a problem fetching the word of the day."
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def word
|
41
|
+
@response.body.match(/<h2 class="word-header-txt">(.+?)</).captures.first
|
42
|
+
end
|
43
|
+
|
44
|
+
def url
|
45
|
+
@response.body.match(/<meta property="og:url" content="(.+?)"/).captures.first
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
using Rodbot::Refinements
|
4
|
+
|
5
|
+
module Rodbot
|
6
|
+
|
7
|
+
# Interface for bundled and gemified plugins
|
8
|
+
class Plugins
|
9
|
+
|
10
|
+
# Required service extensions from plugins
|
11
|
+
#
|
12
|
+
# @return [Hash] map from extension name (Symbol) to module or class
|
13
|
+
# path (String)
|
14
|
+
attr_reader :extensions
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@extensions = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
# Extend app service with app components provided by all active plugins
|
21
|
+
def extend_app
|
22
|
+
require_extensions(:app) do |name, path|
|
23
|
+
begin
|
24
|
+
::App.run(name, "#{path}/routes".constantize)
|
25
|
+
rescue NameError
|
26
|
+
end
|
27
|
+
begin
|
28
|
+
Roda::RodaPlugins.register_plugin(name, path.constantize)
|
29
|
+
rescue NameError
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Extend relay service with relay components provided by all active plugins
|
35
|
+
def extend_relay
|
36
|
+
return if extensions.key? :relay
|
37
|
+
require_extensions(:relay)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Extend schedule service with schedule components provided by all active
|
41
|
+
# plugins
|
42
|
+
def extend_schedule
|
43
|
+
return if extensions.key? :schedule
|
44
|
+
require_extensions(:schedule) do |name, path|
|
45
|
+
path.constantize.new
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# Require (and log) the service extensions provided by all active plugins
|
52
|
+
#
|
53
|
+
# @param service [Symbol] any of {Rodbot::SERVICES}
|
54
|
+
# @yield additional code to execute after require
|
55
|
+
# @return [self]
|
56
|
+
def require_extensions(service)
|
57
|
+
Rodbot.config(:plugin).each_key do |name|
|
58
|
+
path = "rodbot/plugins/#{name}/#{service}"
|
59
|
+
if rescued_require(path)
|
60
|
+
Rodbot.log("#{path} required", level: Logger::DEBUG)
|
61
|
+
extensions[service] ||= {}
|
62
|
+
extensions[service][name] = path
|
63
|
+
yield(name, path) if block_given?
|
64
|
+
end
|
65
|
+
end
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
# Same as +require+ but never fail with +LoadError+
|
70
|
+
#
|
71
|
+
# @param path [String] path to require
|
72
|
+
# @return [Boolean] true if required (again) or false if load failed
|
73
|
+
def rescued_require(path)
|
74
|
+
require path
|
75
|
+
true
|
76
|
+
rescue LoadError
|
77
|
+
false
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
data/lib/rodbot/rack.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require 'httparty'
|
4
|
+
|
5
|
+
using Rodbot::Refinements
|
6
|
+
|
7
|
+
module Rodbot
|
8
|
+
module Rack
|
9
|
+
|
10
|
+
class << self
|
11
|
+
|
12
|
+
# Default +config.ru+
|
13
|
+
#
|
14
|
+
# In case you wish to do things differently, just copy the contents of
|
15
|
+
# this method into your +config.ru+ file and tweak it.
|
16
|
+
def boot(rack)
|
17
|
+
loader = Zeitwerk::Loader.new
|
18
|
+
loader.logger = Rodbot::Log.logger('loader')
|
19
|
+
loader.push_dir(Rodbot.env.root.join('lib'))
|
20
|
+
loader.push_dir(Rodbot.env.root.join('app'))
|
21
|
+
|
22
|
+
if Rodbot.env.development? || Rodbot.env.test?
|
23
|
+
loader.enable_reloading
|
24
|
+
loader.setup
|
25
|
+
rack.run ->(env) do
|
26
|
+
loader.reload
|
27
|
+
App.call(env)
|
28
|
+
end
|
29
|
+
else
|
30
|
+
loader.setup
|
31
|
+
Zeitwerk::Loader.eager_load_all
|
32
|
+
rack.run App.freeze.app
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Send request to the app service
|
37
|
+
#
|
38
|
+
# @param path [String] path e.g. +/help+
|
39
|
+
# @param query [Hash] query hash e.g. +{ search: 'foobar' }+
|
40
|
+
# @param method [Symbol, String] HTTP method
|
41
|
+
# @param timeout [Integer] max seconds to wait for response
|
42
|
+
# @return [HTTParty::Response]
|
43
|
+
def request(path, query: {}, method: :get, timeout: 10)
|
44
|
+
HTTParty.send(method, Rodbot::Services::App.url.uri_concat(path), query: query, timeout: timeout)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require 'kramdown'
|
5
|
+
|
6
|
+
module Rodbot
|
7
|
+
module Refinements
|
8
|
+
|
9
|
+
# @!method camelize
|
10
|
+
# Convert from under_scores to CamelCase
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# camelize('foo_bar') # => 'FooBar'
|
14
|
+
#
|
15
|
+
# @return [String] CamelCased string
|
16
|
+
refine String do
|
17
|
+
def camelize
|
18
|
+
Rodbot::Refinements.inflector.camelize(self, nil)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# @!method constantize
|
23
|
+
# Convert module or class path to module or class
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# 'foo/bar_baz'.constantize # => Foo::BarBaz
|
27
|
+
#
|
28
|
+
# @return [Class, Module] class or module
|
29
|
+
refine String do
|
30
|
+
def constantize
|
31
|
+
Module.const_get(self.split('/').map(&:camelize).join('::'))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# @!method uri_concat
|
36
|
+
# Safely concat path segments to a URI string
|
37
|
+
#
|
38
|
+
# {URI#join} is ultimately used to add the given segments which has a
|
39
|
+
# maybe counter-intuitive API at first. Check out the docs of {URI#join}
|
40
|
+
# and the examples below.
|
41
|
+
#
|
42
|
+
# @example
|
43
|
+
# s = 'http://example.com'
|
44
|
+
# s.uri_concat('foo') # => "http://example.com/foo"
|
45
|
+
# s.uri_concat('foo/') # => "http://example.com/foo/"
|
46
|
+
# s.uri_concat('foo', 'bar') # => "http://example.com/bar" <- sic!
|
47
|
+
# s.uri_concat('foo/, 'bar') # => "http://example.com/foo/bar"
|
48
|
+
# s.uri_concat('foo/, 'bar.html') # => "http://example.com/foo/bar.html"
|
49
|
+
# s.uri_concat('föö') # => "http://example.com/f%C3%B6%C3%B6"
|
50
|
+
#
|
51
|
+
# @param segments [Array<String>] path segments
|
52
|
+
# @return [String] concatted URI
|
53
|
+
refine String do
|
54
|
+
def uri_concat(*segments)
|
55
|
+
parser = URI::Parser.new
|
56
|
+
segments.inject(URI(self)) do |uri, segment|
|
57
|
+
uri + parser.escape(segment)
|
58
|
+
end.to_s
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# @!method md_to_html
|
63
|
+
# Converts Markdown in the string to HTML
|
64
|
+
#
|
65
|
+
# @example
|
66
|
+
# '**important**'.md_to_html # => '<strong>important</strong>'
|
67
|
+
#
|
68
|
+
# @return [String] HTML
|
69
|
+
refine String do
|
70
|
+
def md_to_html
|
71
|
+
Kramdown::Document.new(self, input: 'GFM').to_html.strip
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# @!method html_to_text
|
76
|
+
# Converts HTML to plain text by removing all tags
|
77
|
+
#
|
78
|
+
# @example
|
79
|
+
# '<strong>important</strong>'.html_to_text # => 'important'
|
80
|
+
#
|
81
|
+
# @return [String] text
|
82
|
+
refine String do
|
83
|
+
def html_to_text
|
84
|
+
self.gsub(/<.*?>/, '')
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# @!method psub
|
89
|
+
# Replace placeholders
|
90
|
+
#
|
91
|
+
# Placeholders are all UPCASE and wrapped in [[ and ]]. They must match
|
92
|
+
# keys in the placeholder hash, however, these keys are Symbols and all
|
93
|
+
# downcase.
|
94
|
+
#
|
95
|
+
# @example
|
96
|
+
# placeholders = { sender: 'Oggy' }
|
97
|
+
# 'Hi, [[SENDER]]!'.psub(placeholders) # => 'Hi, Oggy!'
|
98
|
+
#
|
99
|
+
# @return [String] string without placeholders
|
100
|
+
refine String do
|
101
|
+
def psub(placeholders)
|
102
|
+
self.gsub(/\[\[.*?\]\]/) { placeholders[_1[2..-3].downcase.to_sym] }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class << self
|
107
|
+
include Rodbot::Memoize
|
108
|
+
|
109
|
+
# Reusable inflector instance
|
110
|
+
#
|
111
|
+
# @return [Zeitwerk::Inflector]
|
112
|
+
memoize def inflector
|
113
|
+
Zeitwerk::Inflector.new
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
end
|
data/lib/rodbot/relay.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require 'digest'
|
4
|
+
require 'socket'
|
5
|
+
require 'httparty'
|
6
|
+
|
7
|
+
module Rodbot
|
8
|
+
|
9
|
+
# Base class for relay extensions
|
10
|
+
class Relay
|
11
|
+
include Rodbot::Memoize
|
12
|
+
|
13
|
+
class << self
|
14
|
+
|
15
|
+
# Post a message via one or more relay services
|
16
|
+
#
|
17
|
+
# By default, messages are posted via all relay services which have
|
18
|
+
# "say true" configured in their corresponding config blocks. To further
|
19
|
+
# narrow it to exactly one relay service, use the +on+ argument.
|
20
|
+
#
|
21
|
+
# @param message [String] message to post
|
22
|
+
# @param on [Symbol, nil] post via this relay service only
|
23
|
+
# @return [Boolean] +false+ if at least one relay refused the connection or
|
24
|
+
# +true+ otherwise
|
25
|
+
def say(message, on: nil)
|
26
|
+
Rodbot.config(:plugin).select do |extension, config|
|
27
|
+
config[:say] == true && (!on || extension == on)
|
28
|
+
end.keys.inject(true) do |success, extension|
|
29
|
+
write(message, extension) && success
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# Write a message to a relay service extension
|
36
|
+
#
|
37
|
+
# @param message [String] message to post
|
38
|
+
# @param extension [Symbol] post via this relay service
|
39
|
+
# @return [Boolean] +false+ if the connection was refused or +true+ otherwise
|
40
|
+
def write(message, extension)
|
41
|
+
uri = URI(Rodbot::Services::Relay.url(extension))
|
42
|
+
Socket.tcp(uri.host, uri.port, connect_timeout: 3) do |socket|
|
43
|
+
socket.write message
|
44
|
+
socket.write "\x04"
|
45
|
+
end
|
46
|
+
true
|
47
|
+
rescue Errno::ECONNREFUSED
|
48
|
+
warn "WARNING: say via relay #{extension} failed as connection was refused"
|
49
|
+
false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Loops which will be called by the relay service
|
54
|
+
#
|
55
|
+
# @abstract
|
56
|
+
# @return [Array<Proc>]
|
57
|
+
def loops
|
58
|
+
fail(Rodbot::RelayError, "loops method is not implemented")
|
59
|
+
end
|
60
|
+
|
61
|
+
# # @abstract
|
62
|
+
# def login
|
63
|
+
# fail(Rodbot::RelayError, "login not necessary")
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# # @abstract
|
67
|
+
# def logout
|
68
|
+
# fail(Rodbot::RelayError, "logout not possible")
|
69
|
+
# end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# @return [Symbol] name of the relay extension
|
74
|
+
memoize def name
|
75
|
+
self.class.to_s.split('::')[-2].downcase.to_sym
|
76
|
+
end
|
77
|
+
|
78
|
+
# @see {Rodbot::Relay.bind}
|
79
|
+
# @return [String] designated "IP:port"
|
80
|
+
memoize def bind
|
81
|
+
[
|
82
|
+
(ENV["RODBOT_RELAY_HOST"] || 'localhost'),
|
83
|
+
Rodbot.config(:port) + 1 + Rodbot.config(:plugin).keys.index(name)
|
84
|
+
]
|
85
|
+
end
|
86
|
+
|
87
|
+
# Perform the command on the app using a GET request
|
88
|
+
#
|
89
|
+
# @param command [String] command to perform
|
90
|
+
# @param argument [String, nil] optional arguments
|
91
|
+
# @return [String] response as Markdown
|
92
|
+
def command(command, argument=nil)
|
93
|
+
response = Rodbot.request(command, query: { argument: argument })
|
94
|
+
case response.code
|
95
|
+
when 200 then response.body
|
96
|
+
when 404 then "[[SENDER]] I don't know what do do with `!#{command}`. 🤔"
|
97
|
+
else fail
|
98
|
+
end
|
99
|
+
rescue
|
100
|
+
"[[SENDER]] I'm having trouble talking to the app. 💣"
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|