telegem 2.0.5 → 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 -93
- 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,124 +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
|
-
# Get updates - returns HTTPX request object
|
|
179
|
-
updates_request = fetch_updates
|
|
273
|
+
process_update(Types::Update.new(update_data))
|
|
180
274
|
|
|
181
|
-
#
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
# Get the response AFTER waiting
|
|
185
|
-
response = updates_request.response
|
|
186
|
-
|
|
187
|
-
if response && response.status == 200
|
|
188
|
-
if json = response.json
|
|
189
|
-
handle_updates_response(json)
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
# Small delay to prevent tight loop
|
|
194
|
-
sleep 0.1 unless @offset.nil?
|
|
275
|
+
# Execute callback if provided
|
|
276
|
+
callback&.call if callback.respond_to?(:call)
|
|
195
277
|
|
|
196
278
|
rescue => e
|
|
197
|
-
|
|
198
|
-
sleep 5
|
|
279
|
+
@logger.error "Worker #{id} error: #{e.message}"
|
|
199
280
|
end
|
|
200
281
|
end
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def fetch_updates
|
|
204
|
-
params = {
|
|
205
|
-
timeout: @polling_options[:timeout],
|
|
206
|
-
limit: @polling_options[:limit]
|
|
207
|
-
}
|
|
208
|
-
params[:offset] = @offset if @offset
|
|
209
|
-
params[:allowed_updates] = @polling_options[:allowed_updates] if @polling_options[:allowed_updates]
|
|
210
282
|
|
|
211
|
-
@
|
|
283
|
+
@logger.debug "Worker #{id} stopped"
|
|
212
284
|
end
|
|
213
285
|
|
|
214
|
-
|
|
215
|
-
return unless api_response['ok']
|
|
216
|
-
|
|
217
|
-
updates_data = api_response['result'] || []
|
|
218
|
-
|
|
219
|
-
# Queue updates for worker processing
|
|
220
|
-
updates_data.each do |update_data|
|
|
221
|
-
@update_queue << [update_data, nil]
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
# Update offset for next poll
|
|
225
|
-
if updates_data.any?
|
|
226
|
-
@offset = updates_data.last['update_id'] + 1
|
|
227
|
-
end
|
|
228
|
-
end
|
|
286
|
+
# ----- UPDATE PROCESSING PIPELINE -----
|
|
229
287
|
|
|
230
288
|
def process_update(update)
|
|
231
289
|
ctx = Context.new(update, self)
|
|
232
290
|
|
|
233
291
|
begin
|
|
292
|
+
# Run through middleware chain, then dispatch to handlers
|
|
234
293
|
run_middleware_chain(ctx) do |context|
|
|
235
294
|
dispatch_to_handlers(context)
|
|
236
295
|
end
|
|
@@ -247,6 +306,7 @@ module Telegem
|
|
|
247
306
|
def build_middleware_chain
|
|
248
307
|
chain = Composer.new
|
|
249
308
|
|
|
309
|
+
# Add user-defined middleware
|
|
250
310
|
@middleware.each do |middleware_class, args, block|
|
|
251
311
|
if middleware_class.respond_to?(:new)
|
|
252
312
|
middleware = middleware_class.new(*args, &block)
|
|
@@ -256,6 +316,7 @@ module Telegem
|
|
|
256
316
|
end
|
|
257
317
|
end
|
|
258
318
|
|
|
319
|
+
# Add session middleware if not already present
|
|
259
320
|
unless @middleware.any? { |m, _, _| m.is_a?(Session::Middleware) }
|
|
260
321
|
chain.use(Session::Middleware.new(@session_store))
|
|
261
322
|
end
|
|
@@ -267,14 +328,17 @@ module Telegem
|
|
|
267
328
|
update_type = detect_update_type(ctx.update)
|
|
268
329
|
handlers = @handlers[update_type] || []
|
|
269
330
|
|
|
331
|
+
# Find and execute the first matching handler
|
|
270
332
|
handlers.each do |handler|
|
|
271
333
|
if matches_filters?(ctx, handler[:filters])
|
|
272
334
|
handler[:handler].call(ctx)
|
|
273
|
-
break
|
|
335
|
+
break # First matching handler wins
|
|
274
336
|
end
|
|
275
337
|
end
|
|
276
338
|
end
|
|
277
339
|
|
|
340
|
+
# ----- FILTER MATCHING LOGIC -----
|
|
341
|
+
|
|
278
342
|
def detect_update_type(update)
|
|
279
343
|
return :message if update.message
|
|
280
344
|
return :callback_query if update.callback_query
|
|
@@ -323,13 +387,16 @@ module Telegem
|
|
|
323
387
|
ctx.message.command_name == command_name.to_s
|
|
324
388
|
end
|
|
325
389
|
|
|
390
|
+
# ----- ERROR HANDLING -----
|
|
391
|
+
|
|
326
392
|
def handle_error(error, ctx = nil)
|
|
327
393
|
if @error_handler
|
|
328
394
|
@error_handler.call(error, ctx)
|
|
329
395
|
else
|
|
330
396
|
@logger.error("❌ Unhandled error: #{error.class}: #{error.message}")
|
|
331
|
-
|
|
332
|
-
|
|
397
|
+
if ctx
|
|
398
|
+
@logger.error("Context - User: #{ctx.from&.id}, Chat: #{ctx.chat&.id}")
|
|
399
|
+
end
|
|
333
400
|
end
|
|
334
401
|
end
|
|
335
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"
|