kybus-bot 0.11.3 → 0.11.5
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 +4 -4
- data/lib/kybus/bot/adapters/base.rb +2 -2
- data/lib/kybus/bot/adapters/debug.rb +16 -5
- data/lib/kybus/bot/adapters/discord.rb +32 -11
- data/lib/kybus/bot/adapters/telegram.rb +108 -10
- data/lib/kybus/bot/adapters/telegram_file.rb +1 -0
- data/lib/kybus/bot/adapters/telegram_message.rb +10 -2
- data/lib/kybus/bot/base.rb +64 -0
- data/lib/kybus/bot/command/command.rb +3 -2
- data/lib/kybus/bot/command/command_definition.rb +2 -1
- data/lib/kybus/bot/command/command_handler.rb +1 -0
- data/lib/kybus/bot/command/command_state.rb +5 -1
- data/lib/kybus/bot/command/command_state_factory.rb +1 -0
- data/lib/kybus/bot/command/execution_context.rb +4 -0
- data/lib/kybus/bot/command/inline_command_matcher.rb +1 -0
- data/lib/kybus/bot/command/regular_command_matcher.rb +1 -0
- data/lib/kybus/bot/command_executor.rb +8 -0
- data/lib/kybus/bot/command_help.rb +110 -0
- data/lib/kybus/bot/dsl_methods.rb +36 -4
- data/lib/kybus/bot/exceptions.rb +1 -0
- data/lib/kybus/bot/forkers/base.rb +4 -0
- data/lib/kybus/bot/forkers/thread_forker.rb +3 -1
- data/lib/kybus/bot/message.rb +2 -1
- data/lib/kybus/bot/migrator.rb +2 -0
- data/lib/kybus/bot/serialized_message.rb +5 -1
- data/lib/kybus/bot/sidekiq_command_executor.rb +4 -0
- data/lib/kybus/bot/test.rb +4 -0
- data/lib/kybus/bot/ux/base.rb +57 -0
- data/lib/kybus/bot/ux/telegram.rb +111 -0
- data/lib/kybus/bot/ux.rb +21 -0
- data/lib/kybus/bot/version.rb +1 -1
- data/lib/kybus/bot.rb +2 -0
- metadata +7 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 688379f443d3679dba44474a96c6068755df45f81c8972cb3e715a0561722fd5
|
|
4
|
+
data.tar.gz: b5a21e91866dc4cd1dfd79f73b4556acf11a436d0ec4ae8127a67661b673bbc9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3b19d61d37bd6a50b214747c258880719e094ab538d77d86a6a1f7a85327c8d046826e7c2213857e270b93419f76806f00897fe428c5baf16b8d996fe28a66b7
|
|
7
|
+
data.tar.gz: '058300edd89b02221df1e293a320d5be930f2859420e16d7a36eef21a5fceda06f919a360594190c12660ea79aa837db214622a2b1a038e569c502c521c1eddf'
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module Kybus
|
|
4
4
|
module Bot
|
|
5
|
-
#
|
|
5
|
+
# Factory for building bot adapters from config.
|
|
6
6
|
module Adapter
|
|
7
7
|
extend Kybus::DRY::ResourceInjector
|
|
8
8
|
|
|
9
|
-
#
|
|
9
|
+
# Builds the adapter instance for the given config.
|
|
10
10
|
def self.from_config(configs)
|
|
11
11
|
require_relative configs['name']
|
|
12
12
|
resource(configs['name']).new(configs)
|
|
@@ -8,7 +8,7 @@ module Kybus
|
|
|
8
8
|
# :nodoc: #
|
|
9
9
|
module Adapter
|
|
10
10
|
# :nodoc: #
|
|
11
|
-
#
|
|
11
|
+
# Debug message for test and development adapters.
|
|
12
12
|
class DebugMessage < Kybus::Bot::Message
|
|
13
13
|
# It receives a string with the raw text and the id of the channel
|
|
14
14
|
attr_accessor :replied_message
|
|
@@ -80,7 +80,7 @@ module Kybus
|
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
-
#
|
|
83
|
+
# Simulates a message channel for the debug adapter.
|
|
84
84
|
class Channel
|
|
85
85
|
# It is build from
|
|
86
86
|
# an array of raw messages, the name of the channel and the config
|
|
@@ -128,8 +128,9 @@ module Kybus
|
|
|
128
128
|
end
|
|
129
129
|
|
|
130
130
|
##
|
|
131
|
-
#
|
|
131
|
+
# Debug adapter for tests and local development.
|
|
132
132
|
class Debug
|
|
133
|
+
include Kybus::Logger
|
|
133
134
|
# Exception for stoping the loop of messages
|
|
134
135
|
class NoMoreMessageException < Kybus::Exceptions::KybusError
|
|
135
136
|
def initialize
|
|
@@ -171,26 +172,36 @@ module Kybus
|
|
|
171
172
|
# interface for sending messages
|
|
172
173
|
def send_message(contents, channel_name, attachment = nil)
|
|
173
174
|
channel(channel_name).answer(contents, attachment)
|
|
175
|
+
rescue StandardError => e
|
|
176
|
+
log_error('Debug send_message failed', error: e.class, msg: e.message)
|
|
174
177
|
end
|
|
175
178
|
|
|
176
179
|
# interface for sending video
|
|
177
180
|
def send_video(channel_name, video_url, _caption = nil)
|
|
178
181
|
channel(channel_name).answer("VIDEO: #{video_url}")
|
|
182
|
+
rescue StandardError => e
|
|
183
|
+
log_error('Debug send_video failed', error: e.class, msg: e.message)
|
|
179
184
|
end
|
|
180
185
|
|
|
181
186
|
# interface for sending uadio
|
|
182
|
-
def send_audio(channel_name, audio_url)
|
|
187
|
+
def send_audio(channel_name, audio_url, _caption = nil)
|
|
183
188
|
channel(channel_name).answer("AUDIO: #{audio_url}")
|
|
189
|
+
rescue StandardError => e
|
|
190
|
+
log_error('Debug send_audio failed', error: e.class, msg: e.message)
|
|
184
191
|
end
|
|
185
192
|
|
|
186
193
|
# interface for sending image
|
|
187
194
|
def send_image(channel_name, image_url, _caption = nil)
|
|
188
195
|
channel(channel_name).answer("IMG: #{image_url}")
|
|
196
|
+
rescue StandardError => e
|
|
197
|
+
log_error('Debug send_image failed', error: e.class, msg: e.message)
|
|
189
198
|
end
|
|
190
199
|
|
|
191
200
|
# interface for sending image
|
|
192
|
-
def send_document(channel_name, doc_url)
|
|
201
|
+
def send_document(channel_name, doc_url, _caption = nil)
|
|
193
202
|
channel(channel_name).answer("DOC: #{doc_url}")
|
|
203
|
+
rescue StandardError => e
|
|
204
|
+
log_error('Debug send_document failed', error: e.class, msg: e.message)
|
|
194
205
|
end
|
|
195
206
|
|
|
196
207
|
def file_builder(data)
|
|
@@ -7,7 +7,7 @@ module Kybus
|
|
|
7
7
|
# :nodoc: #
|
|
8
8
|
module Adapter
|
|
9
9
|
# :nodoc: #
|
|
10
|
-
# Wraps a
|
|
10
|
+
# Wraps a Discord message and exposes Kybus::Bot::Message API.
|
|
11
11
|
class DiscordMessage < Kybus::Bot::Message
|
|
12
12
|
# It receives a string with the raw text and the id of the channel
|
|
13
13
|
def initialize(msg)
|
|
@@ -21,7 +21,7 @@ module Kybus
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def message_id
|
|
24
|
-
|
|
24
|
+
@message.id if @message.respond_to?(:id)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def has_attachment?
|
|
@@ -46,16 +46,20 @@ module Kybus
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def reply?
|
|
49
|
-
@message.
|
|
49
|
+
return false unless @message.respond_to?(:referenced_message)
|
|
50
|
+
|
|
51
|
+
!@message.referenced_message.nil?
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
def replied_message
|
|
53
|
-
|
|
55
|
+
return unless reply?
|
|
56
|
+
|
|
57
|
+
DiscordMessage.new(@message.referenced_message)
|
|
54
58
|
end
|
|
55
59
|
end
|
|
56
60
|
|
|
57
61
|
##
|
|
58
|
-
#
|
|
62
|
+
# Discord adapter for polling and sending messages.
|
|
59
63
|
class Discord
|
|
60
64
|
include ::Kybus::Logger
|
|
61
65
|
|
|
@@ -69,7 +73,7 @@ module Kybus
|
|
|
69
73
|
def initialize(configs)
|
|
70
74
|
@config = configs
|
|
71
75
|
@client = Discordrb::Bot.new(token: @config['token'])
|
|
72
|
-
@pool =
|
|
76
|
+
@pool = Queue.new
|
|
73
77
|
@client.message do |msg|
|
|
74
78
|
@pool << msg
|
|
75
79
|
end
|
|
@@ -82,13 +86,14 @@ module Kybus
|
|
|
82
86
|
|
|
83
87
|
# Interface for receiving message
|
|
84
88
|
def read_message
|
|
85
|
-
# take the first message from the first open message,
|
|
86
89
|
loop do
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
begin
|
|
91
|
+
msg = @pool.pop(true)
|
|
92
|
+
return @last_message = DiscordMessage.new(msg)
|
|
93
|
+
rescue ThreadError
|
|
94
|
+
sleep(0.1)
|
|
95
|
+
end
|
|
90
96
|
end
|
|
91
|
-
@last_message = DiscordMessage.new(@pool.shift)
|
|
92
97
|
end
|
|
93
98
|
|
|
94
99
|
# interface for sending messages
|
|
@@ -100,6 +105,8 @@ module Kybus
|
|
|
100
105
|
else
|
|
101
106
|
@client.user(channel_name).pm(contents)
|
|
102
107
|
end
|
|
108
|
+
rescue StandardError => e
|
|
109
|
+
log_error('Discord send_message failed', error: e.class, msg: e.message)
|
|
103
110
|
end
|
|
104
111
|
|
|
105
112
|
def message_builder(raw_message)
|
|
@@ -108,20 +115,34 @@ module Kybus
|
|
|
108
115
|
|
|
109
116
|
def send_file(channel_name, file, _caption = nil)
|
|
110
117
|
@client.send_file(channel_name, File.open(file, 'r'))
|
|
118
|
+
rescue StandardError => e
|
|
119
|
+
log_error('Discord send_file failed', error: e.class, msg: e.message)
|
|
111
120
|
end
|
|
112
121
|
|
|
113
122
|
def send_video(channel_name, file, _caption = nil)
|
|
114
123
|
@client.send_file(channel_name, File.open(file, 'r'))
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
log_error('Discord send_video failed', error: e.class, msg: e.message)
|
|
115
126
|
end
|
|
116
127
|
|
|
117
128
|
# interface for sending uadio
|
|
118
129
|
def send_audio(channel_name, file, _caption = nil)
|
|
119
130
|
@client.send_file(channel_name, File.open(file, 'r'))
|
|
131
|
+
rescue StandardError => e
|
|
132
|
+
log_error('Discord send_audio failed', error: e.class, msg: e.message)
|
|
120
133
|
end
|
|
121
134
|
|
|
122
135
|
# interface for sending image
|
|
123
136
|
def send_image(channel_name, file, _caption = nil)
|
|
124
137
|
@client.send_file(channel_name, File.open(file, 'r'))
|
|
138
|
+
rescue StandardError => e
|
|
139
|
+
log_error('Discord send_image failed', error: e.class, msg: e.message)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def send_document(channel_name, file, _caption = nil)
|
|
143
|
+
@client.send_file(channel_name, File.open(file, 'r'))
|
|
144
|
+
rescue StandardError => e
|
|
145
|
+
log_error('Discord send_document failed', error: e.class, msg: e.message)
|
|
125
146
|
end
|
|
126
147
|
end
|
|
127
148
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'telegram/bot'
|
|
4
|
+
require 'timeout'
|
|
4
5
|
require 'faraday'
|
|
5
6
|
require_relative 'telegram_file'
|
|
6
7
|
require_relative 'telegram_message'
|
|
@@ -8,6 +9,7 @@ require_relative 'telegram_message'
|
|
|
8
9
|
module Kybus
|
|
9
10
|
module Bot
|
|
10
11
|
module Adapter
|
|
12
|
+
# Telegram adapter for polling and sending messages.
|
|
11
13
|
class Telegram
|
|
12
14
|
include ::Kybus::Logger
|
|
13
15
|
|
|
@@ -19,18 +21,46 @@ module Kybus
|
|
|
19
21
|
TelegramFile.register(:cli, @client)
|
|
20
22
|
end
|
|
21
23
|
|
|
24
|
+
# Blocking read from Telegram long-polling.
|
|
22
25
|
def read_message
|
|
23
26
|
loop do
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
Timeout.timeout(30) do
|
|
28
|
+
@client.listen do |message|
|
|
29
|
+
if message.respond_to?(:data) && message.respond_to?(:message)
|
|
30
|
+
return @last_message = serialize_callback_query(message)
|
|
31
|
+
end
|
|
32
|
+
message_hash = message.respond_to?(:to_h) ? message.to_h : {}
|
|
33
|
+
has_message_id = message.respond_to?(:message_id)
|
|
34
|
+
payload = message_hash.is_a?(Hash) ? (message_hash['result'] || message_hash[:result]) : nil
|
|
35
|
+
has_result_message_id = payload.is_a?(Hash) && payload['message_id']
|
|
36
|
+
|
|
37
|
+
unless has_message_id || has_result_message_id
|
|
38
|
+
log_info('Skipping non-message update', type: message.class.name)
|
|
39
|
+
next
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
from_hash = message.respond_to?(:from) && message.from ? message.from.to_h : nil
|
|
43
|
+
log_info('Received message', message: message_hash, from: from_hash)
|
|
44
|
+
return @last_message = TelegramMessage.new(message)
|
|
45
|
+
end
|
|
27
46
|
end
|
|
28
47
|
rescue ::Telegram::Bot::Exceptions::ResponseError => e
|
|
29
48
|
log_error('An error occurred while calling to Telegram API', e)
|
|
49
|
+
sleep(5)
|
|
50
|
+
rescue Timeout::Error
|
|
51
|
+
log_error('Telegram read timeout, retrying')
|
|
52
|
+
sleep(2)
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
log_error('Error while reading Telegram message', error: e.class, msg: e.message)
|
|
55
|
+
sleep(5)
|
|
30
56
|
end
|
|
31
57
|
end
|
|
32
58
|
|
|
59
|
+
# Parse a webhook-style payload into a SerializedMessage.
|
|
33
60
|
def handle_message(body)
|
|
61
|
+
if body['callback_query']
|
|
62
|
+
return serialize_callback_payload(body['callback_query'])
|
|
63
|
+
end
|
|
34
64
|
chat_id = body.dig('message', 'chat', 'id')
|
|
35
65
|
message_id = body.dig('message', 'message_id')
|
|
36
66
|
user = extract_user(body.dig('message', 'from'))
|
|
@@ -48,6 +78,7 @@ module Kybus
|
|
|
48
78
|
"[user](tg://user?id=#{id})"
|
|
49
79
|
end
|
|
50
80
|
|
|
81
|
+
# Send a text message.
|
|
51
82
|
def send_message(contents, channel_name)
|
|
52
83
|
log_debug('Sending message', channel_name:, message: contents)
|
|
53
84
|
@client.api.send_message(chat_id: channel_name.to_i, text: contents, parse_mode: @config['parse_mode'])
|
|
@@ -55,24 +86,49 @@ module Kybus
|
|
|
55
86
|
nil if e.error_code == '403'
|
|
56
87
|
end
|
|
57
88
|
|
|
89
|
+
# Send a text message with reply markup.
|
|
90
|
+
def send_message_with_markup(contents, channel_name, reply_markup: nil)
|
|
91
|
+
log_debug('Sending message', channel_name:, message: contents)
|
|
92
|
+
@client.api.send_message(chat_id: channel_name.to_i, text: contents, parse_mode: @config['parse_mode'],
|
|
93
|
+
reply_markup: reply_markup)
|
|
94
|
+
rescue ::Telegram::Bot::Exceptions::ResponseError => e
|
|
95
|
+
nil if e.error_code == '403'
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Edit an existing message.
|
|
99
|
+
def edit_message_text(channel_name, message_id, contents, reply_markup: nil)
|
|
100
|
+
@client.api.edit_message_text(chat_id: channel_name.to_i, message_id: message_id, text: contents,
|
|
101
|
+
parse_mode: @config['parse_mode'], reply_markup: reply_markup)
|
|
102
|
+
rescue ::Telegram::Bot::Exceptions::ResponseError => e
|
|
103
|
+
nil if e.error_code == '403'
|
|
104
|
+
end
|
|
105
|
+
|
|
58
106
|
def send_video(channel_name, video_url, comment = nil)
|
|
59
107
|
file = Faraday::FilePart.new(video_url, 'video/mp4')
|
|
60
|
-
@client.api.send_video(chat_id: channel_name, video: file, caption: comment)
|
|
108
|
+
@client.api.send_video(chat_id: channel_name, video: file, caption: comment, parse_mode: @config['parse_mode'])
|
|
109
|
+
rescue ::Telegram::Bot::Exceptions::ResponseError => e
|
|
110
|
+
nil if e.error_code == '403'
|
|
61
111
|
end
|
|
62
112
|
|
|
63
|
-
def send_audio(channel_name, audio_url)
|
|
113
|
+
def send_audio(channel_name, audio_url, comment = nil)
|
|
64
114
|
file = Faraday::FilePart.new(audio_url, 'audio/mp3')
|
|
65
|
-
@client.api.send_audio(chat_id: channel_name, audio: file)
|
|
115
|
+
@client.api.send_audio(chat_id: channel_name, audio: file, caption: comment, parse_mode: @config['parse_mode'])
|
|
116
|
+
rescue ::Telegram::Bot::Exceptions::ResponseError => e
|
|
117
|
+
nil if e.error_code == '403'
|
|
66
118
|
end
|
|
67
119
|
|
|
68
120
|
def send_image(channel_name, image_url, comment = nil)
|
|
69
121
|
file = Faraday::FilePart.new(image_url, 'image/jpeg')
|
|
70
|
-
@client.api.send_photo(chat_id: channel_name, photo: file, caption: comment)
|
|
122
|
+
@client.api.send_photo(chat_id: channel_name, photo: file, caption: comment, parse_mode: @config['parse_mode'])
|
|
123
|
+
rescue ::Telegram::Bot::Exceptions::ResponseError => e
|
|
124
|
+
nil if e.error_code == '403'
|
|
71
125
|
end
|
|
72
126
|
|
|
73
|
-
def send_document(channel_name, image_url)
|
|
127
|
+
def send_document(channel_name, image_url, comment = nil)
|
|
74
128
|
file = Faraday::FilePart.new(image_url, 'application/octet-stream')
|
|
75
|
-
@client.api.send_document(chat_id: channel_name, document: file)
|
|
129
|
+
@client.api.send_document(chat_id: channel_name, document: file, caption: comment, parse_mode: @config['parse_mode'])
|
|
130
|
+
rescue ::Telegram::Bot::Exceptions::ResponseError => e
|
|
131
|
+
nil if e.error_code == '403'
|
|
76
132
|
end
|
|
77
133
|
|
|
78
134
|
def message_builder(raw_message)
|
|
@@ -86,7 +142,18 @@ module Kybus
|
|
|
86
142
|
private
|
|
87
143
|
|
|
88
144
|
def extract_user(from)
|
|
89
|
-
|
|
145
|
+
return if from.nil?
|
|
146
|
+
|
|
147
|
+
if from.respond_to?(:username) || from.respond_to?(:first_name)
|
|
148
|
+
username = from.respond_to?(:username) ? from.username : nil
|
|
149
|
+
return username if username && !username.to_s.empty?
|
|
150
|
+
|
|
151
|
+
return from.first_name if from.respond_to?(:first_name)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
return from[:username] || from[:first_name] if from.respond_to?(:[])
|
|
155
|
+
|
|
156
|
+
nil
|
|
90
157
|
end
|
|
91
158
|
|
|
92
159
|
def extract_attachment(message)
|
|
@@ -111,6 +178,37 @@ module Kybus
|
|
|
111
178
|
is_private?: replied_message.dig('chat', 'type') == 'private'
|
|
112
179
|
).serialize
|
|
113
180
|
end
|
|
181
|
+
|
|
182
|
+
def serialize_callback_query(callback)
|
|
183
|
+
msg = callback.message
|
|
184
|
+
SerializedMessage.new(
|
|
185
|
+
provider: 'telegram',
|
|
186
|
+
channel_id: msg.chat.id,
|
|
187
|
+
message_id: msg.message_id,
|
|
188
|
+
user: extract_user(callback.from),
|
|
189
|
+
replied_message: nil,
|
|
190
|
+
raw_message: callback.data,
|
|
191
|
+
is_private?: msg.chat.type == 'private',
|
|
192
|
+
callback: true,
|
|
193
|
+
attachment: nil
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def serialize_callback_payload(callback)
|
|
198
|
+
msg = callback['message'] || {}
|
|
199
|
+
chat = msg['chat'] || {}
|
|
200
|
+
SerializedMessage.new(
|
|
201
|
+
provider: 'telegram',
|
|
202
|
+
channel_id: chat['id'],
|
|
203
|
+
message_id: msg['message_id'],
|
|
204
|
+
user: extract_user(callback['from'] || {}),
|
|
205
|
+
replied_message: nil,
|
|
206
|
+
raw_message: callback['data'],
|
|
207
|
+
is_private?: chat['type'] == 'private',
|
|
208
|
+
callback: true,
|
|
209
|
+
attachment: nil
|
|
210
|
+
)
|
|
211
|
+
end
|
|
114
212
|
end
|
|
115
213
|
|
|
116
214
|
register('telegram', Telegram)
|
|
@@ -8,7 +8,7 @@ module Kybus
|
|
|
8
8
|
# :nodoc: #
|
|
9
9
|
module Adapter
|
|
10
10
|
# :nodoc: #
|
|
11
|
-
# Wraps a
|
|
11
|
+
# Wraps a Telegram message and exposes Kybus::Bot::Message API.
|
|
12
12
|
class TelegramMessage < Kybus::Bot::Message
|
|
13
13
|
# It receives a string with the raw text and the id of the channel
|
|
14
14
|
def initialize(message)
|
|
@@ -30,7 +30,15 @@ module Kybus
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def message_id
|
|
33
|
-
@message.
|
|
33
|
+
return @message.message_id if @message.respond_to?(:message_id)
|
|
34
|
+
|
|
35
|
+
if @message.respond_to?(:to_h)
|
|
36
|
+
result = @message.to_h
|
|
37
|
+
payload = result.is_a?(Hash) ? (result['result'] || result[:result]) : nil
|
|
38
|
+
return payload['message_id'] if payload.is_a?(Hash)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
nil
|
|
34
42
|
end
|
|
35
43
|
|
|
36
44
|
# Returns the message contents
|
data/lib/kybus/bot/base.rb
CHANGED
|
@@ -18,6 +18,7 @@ require 'forwardable'
|
|
|
18
18
|
|
|
19
19
|
module Kybus
|
|
20
20
|
module Bot
|
|
21
|
+
# Main bot runtime: command registry, provider IO, and state management.
|
|
21
22
|
class Base # rubocop: disable Metrics/ClassLength
|
|
22
23
|
extend Forwardable
|
|
23
24
|
include Kybus::Logger
|
|
@@ -51,24 +52,33 @@ module Kybus
|
|
|
51
52
|
build_pool
|
|
52
53
|
end
|
|
53
54
|
|
|
55
|
+
# Extend DSL methods available inside command blocks.
|
|
54
56
|
def self.helpers(mod = nil, &)
|
|
55
57
|
DSLMethods.include(mod) if mod
|
|
56
58
|
DSLMethods.class_eval(&) if block_given?
|
|
57
59
|
end
|
|
58
60
|
|
|
61
|
+
# Enable automatic help and hints injection for commands.
|
|
62
|
+
def self.enable_command_help!
|
|
63
|
+
Kybus::Bot::CommandHelp.apply!(self)
|
|
64
|
+
end
|
|
65
|
+
|
|
59
66
|
def extend(*)
|
|
60
67
|
DSLMethods.include(*)
|
|
61
68
|
end
|
|
62
69
|
|
|
70
|
+
# Returns the DSL context used to execute commands.
|
|
63
71
|
def dsl
|
|
64
72
|
@executor.dsl
|
|
65
73
|
end
|
|
66
74
|
|
|
75
|
+
# Process an incoming provider message (webhook mode).
|
|
67
76
|
def handle_message(msg)
|
|
68
77
|
parsed = @provider.handle_message(msg)
|
|
69
78
|
@executor.process_message(parsed)
|
|
70
79
|
end
|
|
71
80
|
|
|
81
|
+
# Execute a background job (used by async forkers).
|
|
72
82
|
def handle_job(job, args, channel_id)
|
|
73
83
|
@executor.load_state!(channel_id)
|
|
74
84
|
@forker.handle_job(job, args)
|
|
@@ -80,27 +90,67 @@ module Kybus
|
|
|
80
90
|
pool.each(&:await)
|
|
81
91
|
end
|
|
82
92
|
|
|
93
|
+
# Redirect execution to another command with params.
|
|
83
94
|
def redirect(command, *params)
|
|
84
95
|
@executor.redirect(command, params)
|
|
85
96
|
end
|
|
86
97
|
|
|
98
|
+
# Send a message through the provider.
|
|
87
99
|
def send_message(contents, channel)
|
|
88
100
|
log_debug('Sending message', contents:, channel:)
|
|
89
101
|
provider.message_builder(@provider.send_message(contents, channel))
|
|
90
102
|
end
|
|
91
103
|
|
|
104
|
+
# Send a message with buttons using the active UX renderer.
|
|
105
|
+
def send_message_with_buttons(contents, buttons, channel)
|
|
106
|
+
ux.render_buttons(dsl, text: contents, buttons: buttons, channel: channel)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Register a paginated query command with enhanced UX rendering.
|
|
110
|
+
def define_paginated_query(command, params: [], hint: nil, per_page: 10, &block)
|
|
111
|
+
register_command(command, params, hint: hint) do
|
|
112
|
+
@bot.run_paginated_query(self, command, params, per_page, &block)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Render a paginated response using the active UX renderer.
|
|
117
|
+
def run_paginated_query(dsl, command, params, per_page, &block)
|
|
118
|
+
args = Array(params).map { |param| dsl.params[param] }
|
|
119
|
+
last_arg = args.last
|
|
120
|
+
value, page = split_value_page(last_arg || dsl.last_message.raw_message)
|
|
121
|
+
args[-1] = value if args.any?
|
|
122
|
+
|
|
123
|
+
result = block.call(dsl, *args, page, per_page)
|
|
124
|
+
total_pages = result[:total_pages] || 1
|
|
125
|
+
text = result[:text] || [result[:header], result[:body], result[:nav]].compact.join("\n")
|
|
126
|
+
key = result[:key] || "#{command}:#{args.compact.join(':')}"
|
|
127
|
+
prev_cmd = result[:prev_cmd] || (page > 1 ? build_paginated_command(command, args, page - 1) : nil)
|
|
128
|
+
next_cmd = result[:next_cmd] || (page < total_pages ? build_paginated_command(command, args, page + 1) : nil)
|
|
129
|
+
|
|
130
|
+
ux.render_paginated(dsl, key:, text:, prev_cmd:, next_cmd:)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Returns the UX renderer for the current provider.
|
|
134
|
+
def ux
|
|
135
|
+
@ux ||= Kybus::Bot::UX.for(provider)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Register a command and its params.
|
|
92
139
|
def register_command(klass, params = [], &)
|
|
93
140
|
definitions.register_command(klass, params, &)
|
|
94
141
|
end
|
|
95
142
|
|
|
143
|
+
# Register a background job handler.
|
|
96
144
|
def register_job(name, args = {}, &)
|
|
97
145
|
@forker.register_command(name, args, &)
|
|
98
146
|
end
|
|
99
147
|
|
|
148
|
+
# Enqueue a background job.
|
|
100
149
|
def invoke_job(name, args)
|
|
101
150
|
@forker.fork(name, args, dsl)
|
|
102
151
|
end
|
|
103
152
|
|
|
153
|
+
# Enqueue a background job with delay.
|
|
104
154
|
def invoke_job_with_delay(name, delay, args)
|
|
105
155
|
@forker.fork(name, args, dsl, delay:)
|
|
106
156
|
end
|
|
@@ -123,6 +173,20 @@ module Kybus
|
|
|
123
173
|
Kybus::Storage::Repository.from_config(nil, repository_config, {})
|
|
124
174
|
end
|
|
125
175
|
|
|
176
|
+
def split_value_page(raw)
|
|
177
|
+
token = raw.to_s
|
|
178
|
+
value, page_str = token.split(/__|\s+/, 2)
|
|
179
|
+
page = page_str.to_i
|
|
180
|
+
page = 1 if page <= 0
|
|
181
|
+
[value, page]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def build_paginated_command(command, args, page)
|
|
185
|
+
base = command.to_s
|
|
186
|
+
base += args.first.to_s if args.any? && !args.first.to_s.empty?
|
|
187
|
+
"#{base}__#{page}"
|
|
188
|
+
end
|
|
189
|
+
|
|
126
190
|
def create_executor(configs, command_factory)
|
|
127
191
|
@executor = if configs['sidekiq']
|
|
128
192
|
require_relative 'sidekiq_command_executor'
|
|
@@ -5,6 +5,7 @@ module Kybus
|
|
|
5
5
|
# Object that wraps a command, it is analogus to a route definition.
|
|
6
6
|
# it currently only gets a param list, but it will be extended to a more
|
|
7
7
|
# complex DSL.
|
|
8
|
+
# Command definition with params and executable block.
|
|
8
9
|
class Command
|
|
9
10
|
attr_accessor :name
|
|
10
11
|
attr_reader :block, :params
|
|
@@ -23,12 +24,12 @@ module Kybus
|
|
|
23
24
|
end
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
# Checks if the params object given contains all the needed values
|
|
27
|
+
# Checks if the params object given contains all the needed values.
|
|
27
28
|
def ready?(current_params)
|
|
28
29
|
params.all? { |key| current_params.key?(key) }
|
|
29
30
|
end
|
|
30
31
|
|
|
31
|
-
# Finds the first
|
|
32
|
+
# Finds the first missing param from the given parameter set.
|
|
32
33
|
def next_missing_param(current_params)
|
|
33
34
|
params.find { |key| !current_params.key?(key) }.to_s
|
|
34
35
|
end
|
|
@@ -4,12 +4,13 @@ require_relative 'command'
|
|
|
4
4
|
|
|
5
5
|
module Kybus
|
|
6
6
|
module Bot
|
|
7
|
+
# Registry for commands keyed by name/pattern.
|
|
7
8
|
class CommandDefinition
|
|
8
9
|
def initialize
|
|
9
10
|
@commands = {}
|
|
10
11
|
end
|
|
11
12
|
|
|
12
|
-
# Stores an operation definition
|
|
13
|
+
# Stores an operation definition.
|
|
13
14
|
def register_command(name, params, &)
|
|
14
15
|
@commands[name] = Command.new(name, params, &)
|
|
15
16
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Kybus
|
|
4
4
|
module Bot
|
|
5
|
+
# Persisted state for a channel (command, params, files, metadata).
|
|
5
6
|
class CommandState
|
|
6
7
|
attr_reader :command
|
|
7
8
|
|
|
@@ -35,6 +36,7 @@ module Kybus
|
|
|
35
36
|
command.next_missing_param(params)
|
|
36
37
|
end
|
|
37
38
|
|
|
39
|
+
# Store the last message for reply context.
|
|
38
40
|
def last_message=(msg)
|
|
39
41
|
@data[:last_message] = msg
|
|
40
42
|
end
|
|
@@ -83,12 +85,14 @@ module Kybus
|
|
|
83
85
|
@data[:params][param] = value
|
|
84
86
|
end
|
|
85
87
|
|
|
88
|
+
# Metadata hash persisted with the state.
|
|
86
89
|
def metadata
|
|
87
90
|
@data[:metadata] = parse_json(@data[:metadata]) if @data[:metadata].is_a?(String)
|
|
88
|
-
@data[:metadata]
|
|
91
|
+
@data[:metadata] ||= {}
|
|
89
92
|
end
|
|
90
93
|
|
|
91
94
|
include Kybus::Logger
|
|
95
|
+
# Persist the current state into the repository.
|
|
92
96
|
def save!
|
|
93
97
|
backup = @data.clone
|
|
94
98
|
serialize_data!
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Kybus
|
|
4
4
|
module Bot
|
|
5
|
+
# Execution wrapper for a command and its state.
|
|
5
6
|
class ExecutionContest
|
|
6
7
|
include Kybus::Logger
|
|
7
8
|
extend Forwardable
|
|
@@ -15,6 +16,7 @@ module Kybus
|
|
|
15
16
|
state.command.block
|
|
16
17
|
end
|
|
17
18
|
|
|
19
|
+
# Execute the command block with a DSL context.
|
|
18
20
|
def call!(context)
|
|
19
21
|
context.state = state
|
|
20
22
|
statement = context.instance_eval(&block)
|
|
@@ -28,6 +30,7 @@ module Kybus
|
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
# Stores a parameter into the status
|
|
33
|
+
# Adds a parameter value to the current state.
|
|
31
34
|
def add_param(value)
|
|
32
35
|
param = state.requested_param
|
|
33
36
|
return unless param
|
|
@@ -40,6 +43,7 @@ module Kybus
|
|
|
40
43
|
state.command.nil?
|
|
41
44
|
end
|
|
42
45
|
|
|
46
|
+
# Adds an uploaded file to the current state.
|
|
43
47
|
def add_file(file)
|
|
44
48
|
param = state.requested_param
|
|
45
49
|
return unless param
|