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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/lib/core/bot.rb +160 -93
  3. data/lib/telegem.rb +1 -1
  4. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8a100bc051a797130ecebdf67164f6d2566fda1241a3240226396412b1cd6d7
4
- data.tar.gz: 6f41d93f14122381a14138be904987032abc577c8d0fde33e53d0632505d6363
3
+ metadata.gz: a800a5e96af8c42cd15dd4448e6b157beb99c6a1e5e29de786b9af0464903093
4
+ data.tar.gz: 25829f45ed0434bd0a8bd6f09a98296292d96037b21dea52323596f42cdec7a4
5
5
  SHA512:
6
- metadata.gz: 11ee3ef944c5ddafeb77c91e9e069bca3631061f073525abf3b7a60f009b8b2c30cc8f12e73a828001b2bec062039b73de55af23896d621b77f7c2dac6e544ca
7
- data.tar.gz: 9a378f859712a891550e5fd1647053a7a2dae80065d08f9c1b73f9fbdaf6ff7b006115165a403848866d6ba8da439a351166661200128ed7bfb23af83141f4a6
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
- attr_reader :token, :api, :handlers, :middleware, :logger, :scenes, :running, :polling_thread
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
- def start_polling(**options)
73
- return if @running
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
- def shutdown
122
- @logger.info "🛑 Shutting down..."
123
-
124
- @running = false
125
-
126
- @polling_thread&.join(5) if @polling_thread&.alive?
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
- stop_workers
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
- @api.close
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
- @logger.info "✅ Bot stopped"
214
+ # Use call! for synchronous operation (more reliable)
215
+ @api.call!('getUpdates', params)
133
216
  end
134
217
 
135
- def running?
136
- @running
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
- private
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" if @logger
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
- 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
-
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
- # Wait for the request to complete
182
- updates_request.wait(@polling_options[:timeout] + 5)
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
- handle_error(e)
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
- @api.call('getUpdates', params)
283
+ @logger.debug "Worker #{id} stopped"
212
284
  end
213
285
 
214
- def handle_updates_response(api_response)
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
- @logger.error("Context: #{ctx.raw_update}") if ctx
332
- @logger.error(error.backtrace&.join("\n")) if error.backtrace
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
@@ -3,7 +3,7 @@ require 'logger'
3
3
  require 'json'
4
4
 
5
5
  module Telegem
6
- VERSION = "2.0.5".freeze
6
+ VERSION = "2.0.6".freeze
7
7
  end
8
8
 
9
9
  # Load core components
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.5
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.5!\n\nQuick start:\n bot
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"