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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/lib/core/bot.rb +160 -83
  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: 1fe252e1a7c876b5972ebcd9bd9d9628380fa685f1d946bb7de1b31d2fc2b36c
4
- data.tar.gz: ad9193ee1aaf94d9dcc1a7e1de0b33f08b096cab84ccbd946bd452b993da5aa4
3
+ metadata.gz: a800a5e96af8c42cd15dd4448e6b157beb99c6a1e5e29de786b9af0464903093
4
+ data.tar.gz: 25829f45ed0434bd0a8bd6f09a98296292d96037b21dea52323596f42cdec7a4
5
5
  SHA512:
6
- metadata.gz: 07c915cfc64d885e8e873f3d45f8c40ccd2130519c3389b44e88992b94f2fc32a55ae88a163d75a63a3df9f72a9da1b7117bf0cf96dffc3e302197a4f16e535e
7
- data.tar.gz: 3563db219318115c0f6043cb57d6fadd4bef858474a5694b2ecdba874e3361dc858f52eb4a38d2eb240112a439983973cba7ac7ee15fc989f458a258254a8538
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
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,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
- 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
- updates_request = fetch_updates
273
+ process_update(Types::Update.new(update_data))
179
274
 
180
- response = updates_request.wait(@polling_options[:timeout] + 5)
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
- handle_error(e)
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
- @api.call('getUpdates', params)
283
+ @logger.debug "Worker #{id} stopped"
204
284
  end
205
285
 
206
- def handle_updates_response(api_response)
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
- @logger.error("Context: #{ctx.raw_update}") if ctx
322
- @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
323
400
  end
324
401
  end
325
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.4".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.4
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.4!\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"