telegem 2.0.4 → 2.0.6
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/core/bot.rb +160 -83
- data/lib/telegem.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a800a5e96af8c42cd15dd4448e6b157beb99c6a1e5e29de786b9af0464903093
|
|
4
|
+
data.tar.gz: 25829f45ed0434bd0a8bd6f09a98296292d96037b21dea52323596f42cdec7a4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5cb72e7a6d9dc453808e617033f6e5d52229dc72299779834e4ecacf67d4cc25510f2ca97dbbbca199c0adb7e2ee1c5f18a4c77d576198a891f8b98bbfd75c94
|
|
7
|
+
data.tar.gz: a490e086730633ccf1e8ba3cc4d018033fbbe0ddc3b300104f9aff1a91d542766e7053a2c81cefcedc8b4555c0415368a7df42ef47c59ff9d5b284a0e413defc
|
data/lib/core/bot.rb
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
+
# lib/core/bot.rb - Telegem v2.0.0 (Stable)
|
|
1
2
|
require 'concurrent'
|
|
2
3
|
require 'logger'
|
|
3
4
|
|
|
4
5
|
module Telegem
|
|
5
6
|
module Core
|
|
6
7
|
class Bot
|
|
7
|
-
|
|
8
|
+
# Public accessors for bot state and components
|
|
9
|
+
attr_reader :token, :api, :handlers, :middleware, :logger, :scenes,
|
|
10
|
+
:running, :polling_thread, :session_store
|
|
8
11
|
|
|
9
12
|
def initialize(token, **options)
|
|
10
13
|
@token = token
|
|
11
14
|
@api = API::Client.new(token, **options.slice(:logger, :timeout))
|
|
15
|
+
|
|
16
|
+
# Initialize handler registries for different update types
|
|
12
17
|
@handlers = {
|
|
13
18
|
message: [],
|
|
14
19
|
callback_query: [],
|
|
@@ -18,23 +23,81 @@ module Telegem
|
|
|
18
23
|
pre_checkout_query: [],
|
|
19
24
|
shipping_query: []
|
|
20
25
|
}
|
|
26
|
+
|
|
21
27
|
@middleware = []
|
|
22
28
|
@scenes = {}
|
|
23
29
|
@logger = options[:logger] || Logger.new($stdout)
|
|
24
30
|
@error_handler = nil
|
|
25
31
|
@session_store = options[:session_store] || Session::MemoryStore.new
|
|
26
32
|
|
|
33
|
+
# Thread pool and worker management
|
|
27
34
|
@thread_pool = Concurrent::FixedThreadPool.new(options[:max_threads] || 10)
|
|
28
35
|
@update_queue = Queue.new
|
|
29
36
|
@worker_threads = []
|
|
30
37
|
|
|
38
|
+
# Polling state
|
|
31
39
|
@polling_thread = nil
|
|
32
40
|
@running = false
|
|
33
|
-
@offset = nil
|
|
41
|
+
@offset = nil # Last processed update ID
|
|
34
42
|
|
|
43
|
+
# Start worker threads for processing updates
|
|
35
44
|
start_workers(options[:worker_count] || 5)
|
|
36
45
|
end
|
|
37
46
|
|
|
47
|
+
# ========================
|
|
48
|
+
# POLLING LIFECYCLE METHODS
|
|
49
|
+
# ========================
|
|
50
|
+
|
|
51
|
+
def start_polling(**options)
|
|
52
|
+
return if @running
|
|
53
|
+
|
|
54
|
+
@running = true
|
|
55
|
+
@polling_options = {
|
|
56
|
+
timeout: 30,
|
|
57
|
+
limit: 100,
|
|
58
|
+
allowed_updates: nil
|
|
59
|
+
}.merge(options)
|
|
60
|
+
|
|
61
|
+
@offset = nil # Reset offset when starting
|
|
62
|
+
|
|
63
|
+
@logger.info "🤖 Starting Telegem bot (polling mode)..."
|
|
64
|
+
|
|
65
|
+
@polling_thread = Thread.new do
|
|
66
|
+
Thread.current.abort_on_exception = false
|
|
67
|
+
poll_loop
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def shutdown
|
|
74
|
+
return unless @running
|
|
75
|
+
|
|
76
|
+
@logger.info "🛑 Shutting down bot..."
|
|
77
|
+
@running = false
|
|
78
|
+
|
|
79
|
+
# Gracefully stop polling thread
|
|
80
|
+
if @polling_thread&.alive?
|
|
81
|
+
@polling_thread.join(3)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Stop worker threads
|
|
85
|
+
stop_workers
|
|
86
|
+
|
|
87
|
+
# Close API connections
|
|
88
|
+
@api.close if @api.respond_to?(:close)
|
|
89
|
+
|
|
90
|
+
@logger.info "✅ Bot shutdown complete"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def running?
|
|
94
|
+
@running
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# ========================
|
|
98
|
+
# COMMAND & EVENT HANDLERS
|
|
99
|
+
# ========================
|
|
100
|
+
|
|
38
101
|
def command(name, **options, &block)
|
|
39
102
|
pattern = /^\/#{Regexp.escape(name)}(?:@\w+)?(?:\s+(.+))?$/i
|
|
40
103
|
|
|
@@ -69,25 +132,9 @@ module Telegem
|
|
|
69
132
|
@scenes[id] = Scene.new(id, &block)
|
|
70
133
|
end
|
|
71
134
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
@running = true
|
|
76
|
-
@polling_options = {
|
|
77
|
-
timeout: 30,
|
|
78
|
-
limit: 100,
|
|
79
|
-
allowed_updates: nil
|
|
80
|
-
}.merge(options)
|
|
81
|
-
|
|
82
|
-
@offset = nil
|
|
83
|
-
|
|
84
|
-
@polling_thread = Thread.new do
|
|
85
|
-
@logger.info "🤖 Starting Telegem bot (HTTPX async)..."
|
|
86
|
-
poll_loop
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
self
|
|
90
|
-
end
|
|
135
|
+
# ========================
|
|
136
|
+
# WEBHOOK METHODS
|
|
137
|
+
# ========================
|
|
91
138
|
|
|
92
139
|
def webhook(app = nil, port: nil, host: '0.0.0.0', logger: nil, &block)
|
|
93
140
|
require_relative '../webhook/server'
|
|
@@ -113,114 +160,136 @@ module Telegem
|
|
|
113
160
|
@api.call!('getWebhookInfo', {})
|
|
114
161
|
end
|
|
115
162
|
|
|
163
|
+
# ========================
|
|
164
|
+
# UPDATE PROCESSING
|
|
165
|
+
# ========================
|
|
166
|
+
|
|
116
167
|
def process(update_data)
|
|
117
168
|
update = Types::Update.new(update_data)
|
|
118
169
|
process_update(update)
|
|
119
170
|
end
|
|
120
171
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
172
|
+
# ========================
|
|
173
|
+
# PRIVATE METHODS
|
|
174
|
+
# ========================
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
# ----- POLLING LOGIC -----
|
|
179
|
+
|
|
180
|
+
def poll_loop
|
|
181
|
+
@logger.debug "Entering polling loop"
|
|
127
182
|
|
|
128
|
-
|
|
183
|
+
while @running
|
|
184
|
+
begin
|
|
185
|
+
# Use synchronous call to avoid HTTPX version conflicts
|
|
186
|
+
result = fetch_updates
|
|
187
|
+
|
|
188
|
+
if result && result.is_a?(Hash) && result['ok']
|
|
189
|
+
handle_updates_response(result)
|
|
190
|
+
elsif result
|
|
191
|
+
@logger.warn "Unexpected API response format"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Small delay between polls unless we just processed updates
|
|
195
|
+
sleep 0.1 unless @offset.nil?
|
|
196
|
+
|
|
197
|
+
rescue => e
|
|
198
|
+
handle_error(e)
|
|
199
|
+
sleep 5 # Wait before retrying after error
|
|
200
|
+
end
|
|
201
|
+
end
|
|
129
202
|
|
|
130
|
-
@
|
|
203
|
+
@logger.debug "Exiting polling loop"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def fetch_updates
|
|
207
|
+
params = {
|
|
208
|
+
timeout: @polling_options[:timeout],
|
|
209
|
+
limit: @polling_options[:limit]
|
|
210
|
+
}
|
|
211
|
+
params[:offset] = @offset if @offset
|
|
212
|
+
params[:allowed_updates] = @polling_options[:allowed_updates] if @polling_options[:allowed_updates]
|
|
131
213
|
|
|
132
|
-
|
|
214
|
+
# Use call! for synchronous operation (more reliable)
|
|
215
|
+
@api.call!('getUpdates', params)
|
|
133
216
|
end
|
|
134
217
|
|
|
135
|
-
def
|
|
136
|
-
|
|
218
|
+
def handle_updates_response(api_response)
|
|
219
|
+
updates = api_response['result'] || []
|
|
220
|
+
|
|
221
|
+
if updates.any?
|
|
222
|
+
@logger.debug "Processing #{updates.length} update(s)"
|
|
223
|
+
|
|
224
|
+
updates.each do |update_data|
|
|
225
|
+
# Queue for processing by worker threads
|
|
226
|
+
@update_queue << [update_data, nil]
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Update offset for next request
|
|
230
|
+
@offset = updates.last['update_id'] + 1
|
|
231
|
+
@logger.debug "Updated offset to #{@offset}"
|
|
232
|
+
end
|
|
137
233
|
end
|
|
138
234
|
|
|
139
|
-
|
|
235
|
+
# ----- WORKER THREAD MANAGEMENT -----
|
|
140
236
|
|
|
141
237
|
def start_workers(count)
|
|
142
238
|
count.times do |i|
|
|
143
239
|
@worker_threads << Thread.new do
|
|
240
|
+
Thread.current.abort_on_exception = false
|
|
144
241
|
worker_loop(i)
|
|
145
242
|
end
|
|
146
243
|
end
|
|
244
|
+
@logger.debug "Started #{count} worker threads"
|
|
147
245
|
end
|
|
148
246
|
|
|
149
247
|
def stop_workers
|
|
248
|
+
@logger.debug "Stopping worker threads"
|
|
249
|
+
|
|
250
|
+
# Send shutdown signals to all workers
|
|
150
251
|
@worker_threads.size.times { @update_queue << :shutdown }
|
|
252
|
+
|
|
253
|
+
# Wait for threads to finish
|
|
151
254
|
@worker_threads.each do |thread|
|
|
152
255
|
thread.join(2) if thread.alive?
|
|
153
256
|
end
|
|
257
|
+
|
|
154
258
|
@worker_threads.clear
|
|
155
259
|
end
|
|
156
260
|
|
|
157
261
|
def worker_loop(id)
|
|
158
|
-
@logger.debug "Worker #{id} started"
|
|
262
|
+
@logger.debug "Worker #{id} started"
|
|
263
|
+
|
|
159
264
|
while @running
|
|
160
265
|
begin
|
|
266
|
+
# Wait for a task from the queue
|
|
161
267
|
task = @update_queue.pop
|
|
268
|
+
|
|
269
|
+
# Check for shutdown signal
|
|
162
270
|
break if task == :shutdown
|
|
163
271
|
|
|
164
272
|
update_data, callback = task
|
|
165
|
-
|
|
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
|
-
|
|
175
|
-
def poll_loop
|
|
176
|
-
while @running
|
|
177
|
-
begin
|
|
178
|
-
updates_request = fetch_updates
|
|
273
|
+
process_update(Types::Update.new(update_data))
|
|
179
274
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if response && response.status == 200 && response.json
|
|
183
|
-
handle_updates_response(response.json)
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
sleep 0.1 unless @offset.nil?
|
|
275
|
+
# Execute callback if provided
|
|
276
|
+
callback&.call if callback.respond_to?(:call)
|
|
187
277
|
|
|
188
278
|
rescue => e
|
|
189
|
-
|
|
190
|
-
sleep 5
|
|
279
|
+
@logger.error "Worker #{id} error: #{e.message}"
|
|
191
280
|
end
|
|
192
281
|
end
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def fetch_updates
|
|
196
|
-
params = {
|
|
197
|
-
timeout: @polling_options[:timeout],
|
|
198
|
-
limit: @polling_options[:limit]
|
|
199
|
-
}
|
|
200
|
-
params[:offset] = @offset if @offset
|
|
201
|
-
params[:allowed_updates] = @polling_options[:allowed_updates] if @polling_options[:allowed_updates]
|
|
202
282
|
|
|
203
|
-
@
|
|
283
|
+
@logger.debug "Worker #{id} stopped"
|
|
204
284
|
end
|
|
205
285
|
|
|
206
|
-
|
|
207
|
-
return unless api_response['ok']
|
|
208
|
-
|
|
209
|
-
updates_data = api_response['result'] || []
|
|
210
|
-
|
|
211
|
-
updates_data.each do |update_data|
|
|
212
|
-
@update_queue << [update_data, nil]
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
if updates_data.any?
|
|
216
|
-
@offset = updates_data.last['update_id'] + 1
|
|
217
|
-
end
|
|
218
|
-
end
|
|
286
|
+
# ----- UPDATE PROCESSING PIPELINE -----
|
|
219
287
|
|
|
220
288
|
def process_update(update)
|
|
221
289
|
ctx = Context.new(update, self)
|
|
222
290
|
|
|
223
291
|
begin
|
|
292
|
+
# Run through middleware chain, then dispatch to handlers
|
|
224
293
|
run_middleware_chain(ctx) do |context|
|
|
225
294
|
dispatch_to_handlers(context)
|
|
226
295
|
end
|
|
@@ -237,6 +306,7 @@ module Telegem
|
|
|
237
306
|
def build_middleware_chain
|
|
238
307
|
chain = Composer.new
|
|
239
308
|
|
|
309
|
+
# Add user-defined middleware
|
|
240
310
|
@middleware.each do |middleware_class, args, block|
|
|
241
311
|
if middleware_class.respond_to?(:new)
|
|
242
312
|
middleware = middleware_class.new(*args, &block)
|
|
@@ -246,6 +316,7 @@ module Telegem
|
|
|
246
316
|
end
|
|
247
317
|
end
|
|
248
318
|
|
|
319
|
+
# Add session middleware if not already present
|
|
249
320
|
unless @middleware.any? { |m, _, _| m.is_a?(Session::Middleware) }
|
|
250
321
|
chain.use(Session::Middleware.new(@session_store))
|
|
251
322
|
end
|
|
@@ -257,14 +328,17 @@ module Telegem
|
|
|
257
328
|
update_type = detect_update_type(ctx.update)
|
|
258
329
|
handlers = @handlers[update_type] || []
|
|
259
330
|
|
|
331
|
+
# Find and execute the first matching handler
|
|
260
332
|
handlers.each do |handler|
|
|
261
333
|
if matches_filters?(ctx, handler[:filters])
|
|
262
334
|
handler[:handler].call(ctx)
|
|
263
|
-
break
|
|
335
|
+
break # First matching handler wins
|
|
264
336
|
end
|
|
265
337
|
end
|
|
266
338
|
end
|
|
267
339
|
|
|
340
|
+
# ----- FILTER MATCHING LOGIC -----
|
|
341
|
+
|
|
268
342
|
def detect_update_type(update)
|
|
269
343
|
return :message if update.message
|
|
270
344
|
return :callback_query if update.callback_query
|
|
@@ -313,13 +387,16 @@ module Telegem
|
|
|
313
387
|
ctx.message.command_name == command_name.to_s
|
|
314
388
|
end
|
|
315
389
|
|
|
390
|
+
# ----- ERROR HANDLING -----
|
|
391
|
+
|
|
316
392
|
def handle_error(error, ctx = nil)
|
|
317
393
|
if @error_handler
|
|
318
394
|
@error_handler.call(error, ctx)
|
|
319
395
|
else
|
|
320
396
|
@logger.error("❌ Unhandled error: #{error.class}: #{error.message}")
|
|
321
|
-
|
|
322
|
-
|
|
397
|
+
if ctx
|
|
398
|
+
@logger.error("Context - User: #{ctx.from&.id}, Chat: #{ctx.chat&.id}")
|
|
399
|
+
end
|
|
323
400
|
end
|
|
324
401
|
end
|
|
325
402
|
end
|
data/lib/telegem.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: telegem
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.0.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- sick_phantom
|
|
@@ -198,7 +198,7 @@ metadata:
|
|
|
198
198
|
bug_tracker_uri: https://gitlab.com/ruby-telegem/telegem/-/issues
|
|
199
199
|
documentation_uri: https://gitlab.com/ruby-telegem/telegem/-/blob/main/README.md
|
|
200
200
|
rubygems_mfa_required: 'false'
|
|
201
|
-
post_install_message: "Thanks for installing Telegem 2.0.
|
|
201
|
+
post_install_message: "Thanks for installing Telegem 2.0.6!\n\nQuick start:\n bot
|
|
202
202
|
= Telegem.new(\"YOUR_TOKEN\")\n bot.on(:message) { |ctx| ctx.reply(\"Hello!\")
|
|
203
203
|
}\n bot.start_polling\n\nDocumentation: https://gitlab.com/ruby-telegem/telegem\nHappy
|
|
204
204
|
bot building! \U0001F916\n"
|