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.
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
- require 'webhook/server'
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
- Thread.new do
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 # First matching handler wins
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)