telegem 1.0.6 → 2.0.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 +4 -4
- data/docs/Api.md +146 -354
- data/docs/Cookbook(copy_paste).md +644 -0
- data/docs/Getting_started.md +348 -0
- data/docs/webhook_setup.md +199 -0
- data/lib/api/client.rb +123 -49
- data/lib/api/types.rb +283 -67
- data/lib/core/bot.rb +91 -56
- data/lib/core/context.rb +96 -110
- data/lib/markup/.gitkeep +0 -0
- data/lib/markup/keyboard.rb +53 -38
- data/lib/telegem.rb +15 -5
- data/lib/webhook/server.rb +246 -112
- metadata +108 -17
- data/docs/Cookbook.md +0 -407
- data/docs/SETTING_WEBHOOK.md +0 -367
- data/docs/UNDERSTANDING-WEBHOOK-n-POLLING.md +0 -241
data/lib/core/bot.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
require 'concurrent'
|
|
2
|
+
require 'logger'
|
|
3
|
+
|
|
1
4
|
module Telegem
|
|
2
5
|
module Core
|
|
3
6
|
class Bot
|
|
4
|
-
attr_reader :token, :api, :handlers, :middleware, :logger, :scenes
|
|
5
|
-
|
|
7
|
+
attr_reader :token, :api, :handlers, :middleware, :logger, :scenes, :running
|
|
8
|
+
|
|
6
9
|
def initialize(token, **options)
|
|
7
10
|
@token = token
|
|
8
11
|
@api = API::Client.new(token, **options.slice(:logger, :timeout))
|
|
@@ -20,45 +23,52 @@ module Telegem
|
|
|
20
23
|
@logger = options[:logger] || Logger.new($stdout)
|
|
21
24
|
@error_handler = nil
|
|
22
25
|
@session_store = options[:session_store] || Session::MemoryStore.new
|
|
26
|
+
|
|
27
|
+
@thread_pool = Concurrent::FixedThreadPool.new(options[:max_threads] || 10)
|
|
28
|
+
@update_queue = Queue.new
|
|
29
|
+
@worker_threads = []
|
|
30
|
+
|
|
23
31
|
@polling_thread = nil
|
|
24
32
|
@running = false
|
|
25
33
|
@offset = nil
|
|
34
|
+
|
|
35
|
+
start_workers(options[:worker_count] || 5)
|
|
26
36
|
end
|
|
27
|
-
|
|
37
|
+
|
|
28
38
|
def command(name, **options, &block)
|
|
29
39
|
pattern = /^\/#{Regexp.escape(name)}(?:@\w+)?(?:\s+(.+))?$/i
|
|
30
|
-
|
|
40
|
+
|
|
31
41
|
on(:message, text: pattern) do |ctx|
|
|
32
42
|
ctx.match = ctx.message.text.match(pattern)
|
|
33
43
|
ctx.state[:command_args] = ctx.match[1] if ctx.match
|
|
34
44
|
block.call(ctx)
|
|
35
45
|
end
|
|
36
46
|
end
|
|
37
|
-
|
|
47
|
+
|
|
38
48
|
def hears(pattern, **options, &block)
|
|
39
49
|
on(:message, text: pattern) do |ctx|
|
|
40
50
|
ctx.match = ctx.message.text.match(pattern)
|
|
41
51
|
block.call(ctx)
|
|
42
52
|
end
|
|
43
53
|
end
|
|
44
|
-
|
|
54
|
+
|
|
45
55
|
def on(type, filters = {}, &block)
|
|
46
56
|
@handlers[type] << { filters: filters, handler: block }
|
|
47
57
|
end
|
|
48
|
-
|
|
58
|
+
|
|
49
59
|
def use(middleware, *args, &block)
|
|
50
60
|
@middleware << [middleware, args, block]
|
|
51
61
|
self
|
|
52
62
|
end
|
|
53
|
-
|
|
63
|
+
|
|
54
64
|
def error(&block)
|
|
55
65
|
@error_handler = block
|
|
56
66
|
end
|
|
57
|
-
|
|
67
|
+
|
|
58
68
|
def scene(id, &block)
|
|
59
69
|
@scenes[id] = Scene.new(id, &block)
|
|
60
70
|
end
|
|
61
|
-
|
|
71
|
+
|
|
62
72
|
def start_polling(**options)
|
|
63
73
|
return if @running
|
|
64
74
|
|
|
@@ -78,9 +88,9 @@ module Telegem
|
|
|
78
88
|
|
|
79
89
|
self
|
|
80
90
|
end
|
|
81
|
-
|
|
91
|
+
|
|
82
92
|
def webhook(app = nil, port: nil, host: '0.0.0.0', logger: nil, &block)
|
|
83
|
-
|
|
93
|
+
require_relative '../webhook/server'
|
|
84
94
|
|
|
85
95
|
if block_given?
|
|
86
96
|
Webhook::Server.new(self, &block)
|
|
@@ -90,64 +100,98 @@ module Telegem
|
|
|
90
100
|
Webhook::Server.new(self, port: port, host: host, logger: logger)
|
|
91
101
|
end
|
|
92
102
|
end
|
|
93
|
-
|
|
103
|
+
|
|
94
104
|
def set_webhook(url, **options)
|
|
95
|
-
@api.call('setWebhook', { url: url }.merge(options))
|
|
105
|
+
@api.call!('setWebhook', { url: url }.merge(options))
|
|
96
106
|
end
|
|
97
|
-
|
|
107
|
+
|
|
98
108
|
def delete_webhook
|
|
99
|
-
@api.call('deleteWebhook', {})
|
|
109
|
+
@api.call!('deleteWebhook', {})
|
|
100
110
|
end
|
|
101
|
-
|
|
111
|
+
|
|
102
112
|
def get_webhook_info
|
|
103
|
-
@api.call('getWebhookInfo', {})
|
|
113
|
+
@api.call!('getWebhookInfo', {})
|
|
104
114
|
end
|
|
105
|
-
|
|
115
|
+
|
|
106
116
|
def process(update_data)
|
|
107
117
|
update = Types::Update.new(update_data)
|
|
108
118
|
process_update(update)
|
|
109
119
|
end
|
|
110
|
-
|
|
120
|
+
|
|
111
121
|
def shutdown
|
|
112
122
|
@logger.info "🛑 Shutting down..."
|
|
113
123
|
|
|
114
124
|
@running = false
|
|
125
|
+
|
|
115
126
|
@polling_thread&.join(5) if @polling_thread&.alive?
|
|
127
|
+
|
|
128
|
+
stop_workers
|
|
129
|
+
|
|
116
130
|
@api.close
|
|
117
131
|
|
|
118
132
|
@logger.info "✅ Bot stopped"
|
|
119
133
|
end
|
|
120
|
-
|
|
134
|
+
|
|
121
135
|
def running?
|
|
122
136
|
@running
|
|
123
137
|
end
|
|
124
|
-
|
|
138
|
+
|
|
125
139
|
private
|
|
126
|
-
|
|
140
|
+
|
|
141
|
+
def start_workers(count)
|
|
142
|
+
count.times do |i|
|
|
143
|
+
@worker_threads << Thread.new do
|
|
144
|
+
worker_loop(i)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def stop_workers
|
|
150
|
+
@worker_threads.size.times { @update_queue << :shutdown }
|
|
151
|
+
@worker_threads.each do |thread|
|
|
152
|
+
thread.join(2) if thread.alive?
|
|
153
|
+
end
|
|
154
|
+
@worker_threads.clear
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def worker_loop(id)
|
|
158
|
+
@logger.debug "Worker #{id} started" if @logger
|
|
159
|
+
while @running
|
|
160
|
+
begin
|
|
161
|
+
task = @update_queue.pop
|
|
162
|
+
break if task == :shutdown
|
|
163
|
+
|
|
164
|
+
update_data, callback = task
|
|
165
|
+
update = Types::Update.new(update_data)
|
|
166
|
+
process_update(update)
|
|
167
|
+
callback&.call(update) if callback.respond_to?(:call)
|
|
168
|
+
rescue => e
|
|
169
|
+
@logger.error "Worker #{id} error: #{e.message}"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
@logger.debug "Worker #{id} stopped" if @logger
|
|
173
|
+
end
|
|
174
|
+
|
|
127
175
|
def poll_loop
|
|
128
176
|
while @running
|
|
129
177
|
begin
|
|
130
|
-
# Get updates - returns HTTPX request immediately
|
|
131
178
|
updates_request = fetch_updates
|
|
132
179
|
|
|
133
|
-
# Wait for this poll to complete (with timeout)
|
|
134
180
|
response = updates_request.wait(@polling_options[:timeout] + 5)
|
|
135
181
|
|
|
136
182
|
if response && response.status == 200 && response.json
|
|
137
183
|
handle_updates_response(response.json)
|
|
138
184
|
end
|
|
139
185
|
|
|
140
|
-
# Small delay to prevent tight loop on errors
|
|
141
186
|
sleep 0.1 unless @offset.nil?
|
|
142
187
|
|
|
143
188
|
rescue => e
|
|
144
189
|
handle_error(e)
|
|
145
|
-
# Longer delay on error
|
|
146
190
|
sleep 5
|
|
147
191
|
end
|
|
148
192
|
end
|
|
149
193
|
end
|
|
150
|
-
|
|
194
|
+
|
|
151
195
|
def fetch_updates
|
|
152
196
|
params = {
|
|
153
197
|
timeout: @polling_options[:timeout],
|
|
@@ -158,30 +202,21 @@ module Telegem
|
|
|
158
202
|
|
|
159
203
|
@api.call('getUpdates', params)
|
|
160
204
|
end
|
|
161
|
-
|
|
205
|
+
|
|
162
206
|
def handle_updates_response(api_response)
|
|
163
207
|
return unless api_response['ok']
|
|
164
208
|
|
|
165
209
|
updates_data = api_response['result'] || []
|
|
166
210
|
|
|
167
|
-
# Process each update in its own thread for concurrency
|
|
168
211
|
updates_data.each do |update_data|
|
|
169
|
-
|
|
170
|
-
begin
|
|
171
|
-
update = Types::Update.new(update_data)
|
|
172
|
-
process_update(update)
|
|
173
|
-
rescue => e
|
|
174
|
-
@logger.error("Error in update thread: #{e.message}")
|
|
175
|
-
end
|
|
176
|
-
end
|
|
212
|
+
@update_queue << [update_data, nil]
|
|
177
213
|
end
|
|
178
214
|
|
|
179
|
-
# Update offset for next poll
|
|
180
215
|
if updates_data.any?
|
|
181
216
|
@offset = updates_data.last['update_id'] + 1
|
|
182
217
|
end
|
|
183
218
|
end
|
|
184
|
-
|
|
219
|
+
|
|
185
220
|
def process_update(update)
|
|
186
221
|
ctx = Context.new(update, self)
|
|
187
222
|
|
|
@@ -193,15 +228,15 @@ module Telegem
|
|
|
193
228
|
handle_error(e, ctx)
|
|
194
229
|
end
|
|
195
230
|
end
|
|
196
|
-
|
|
231
|
+
|
|
197
232
|
def run_middleware_chain(ctx, &final)
|
|
198
233
|
chain = build_middleware_chain
|
|
199
234
|
chain.call(ctx, &final)
|
|
200
235
|
end
|
|
201
|
-
|
|
236
|
+
|
|
202
237
|
def build_middleware_chain
|
|
203
238
|
chain = Composer.new
|
|
204
|
-
|
|
239
|
+
|
|
205
240
|
@middleware.each do |middleware_class, args, block|
|
|
206
241
|
if middleware_class.respond_to?(:new)
|
|
207
242
|
middleware = middleware_class.new(*args, &block)
|
|
@@ -210,26 +245,26 @@ module Telegem
|
|
|
210
245
|
chain.use(middleware_class)
|
|
211
246
|
end
|
|
212
247
|
end
|
|
213
|
-
|
|
248
|
+
|
|
214
249
|
unless @middleware.any? { |m, _, _| m.is_a?(Session::Middleware) }
|
|
215
250
|
chain.use(Session::Middleware.new(@session_store))
|
|
216
251
|
end
|
|
217
|
-
|
|
252
|
+
|
|
218
253
|
chain
|
|
219
254
|
end
|
|
220
|
-
|
|
255
|
+
|
|
221
256
|
def dispatch_to_handlers(ctx)
|
|
222
257
|
update_type = detect_update_type(ctx.update)
|
|
223
258
|
handlers = @handlers[update_type] || []
|
|
224
|
-
|
|
259
|
+
|
|
225
260
|
handlers.each do |handler|
|
|
226
261
|
if matches_filters?(ctx, handler[:filters])
|
|
227
262
|
handler[:handler].call(ctx)
|
|
228
|
-
break
|
|
263
|
+
break
|
|
229
264
|
end
|
|
230
265
|
end
|
|
231
266
|
end
|
|
232
|
-
|
|
267
|
+
|
|
233
268
|
def detect_update_type(update)
|
|
234
269
|
return :message if update.message
|
|
235
270
|
return :callback_query if update.callback_query
|
|
@@ -240,10 +275,10 @@ module Telegem
|
|
|
240
275
|
return :shipping_query if update.shipping_query
|
|
241
276
|
:unknown
|
|
242
277
|
end
|
|
243
|
-
|
|
278
|
+
|
|
244
279
|
def matches_filters?(ctx, filters)
|
|
245
280
|
return true if filters.empty?
|
|
246
|
-
|
|
281
|
+
|
|
247
282
|
filters.all? do |key, value|
|
|
248
283
|
case key
|
|
249
284
|
when :text
|
|
@@ -257,27 +292,27 @@ module Telegem
|
|
|
257
292
|
end
|
|
258
293
|
end
|
|
259
294
|
end
|
|
260
|
-
|
|
295
|
+
|
|
261
296
|
def matches_text_filter(ctx, pattern)
|
|
262
297
|
return false unless ctx.message&.text
|
|
263
|
-
|
|
298
|
+
|
|
264
299
|
if pattern.is_a?(Regexp)
|
|
265
300
|
ctx.message.text.match?(pattern)
|
|
266
301
|
else
|
|
267
302
|
ctx.message.text.include?(pattern.to_s)
|
|
268
303
|
end
|
|
269
304
|
end
|
|
270
|
-
|
|
305
|
+
|
|
271
306
|
def matches_chat_type_filter(ctx, type)
|
|
272
307
|
return false unless ctx.chat
|
|
273
308
|
ctx.chat.type == type.to_s
|
|
274
309
|
end
|
|
275
|
-
|
|
310
|
+
|
|
276
311
|
def matches_command_filter(ctx, command_name)
|
|
277
312
|
return false unless ctx.message&.command?
|
|
278
313
|
ctx.message.command_name == command_name.to_s
|
|
279
314
|
end
|
|
280
|
-
|
|
315
|
+
|
|
281
316
|
def handle_error(error, ctx = nil)
|
|
282
317
|
if @error_handler
|
|
283
318
|
@error_handler.call(error, ctx)
|