rodbot 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|