telegem 3.3.0 → 3.4.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.
@@ -0,0 +1,536 @@
1
+ # Middleware System
2
+
3
+ Middleware functions process updates before they reach handlers. They enable cross-cutting concerns like authentication, logging, and rate limiting.
4
+
5
+ ## How Middleware Works
6
+
7
+ Middleware forms a pipeline that each update passes through:
8
+
9
+ ```
10
+ Update → Middleware 1 → Middleware 2 → ... → Handlers → Response
11
+ ```
12
+
13
+ Each middleware can:
14
+ - Modify the context
15
+ - Skip to the next middleware
16
+ - Stop processing
17
+ - Handle errors
18
+
19
+ ## Basic Middleware
20
+
21
+ ### Creating Middleware
22
+
23
+ ```ruby
24
+ # Class-based middleware
25
+ class LoggingMiddleware
26
+ def initialize(logger = nil)
27
+ @logger = logger || Logger.new(STDOUT)
28
+ end
29
+
30
+ def call(ctx, next_middleware)
31
+ @logger.info("Update from #{ctx.from&.id}")
32
+ next_middleware.call(ctx)
33
+ end
34
+ end
35
+
36
+ # Use it
37
+ bot.use LoggingMiddleware.new
38
+ ```
39
+
40
+ ### Inline Middleware
41
+
42
+ ```ruby
43
+ bot.use do |ctx, next_middleware|
44
+ puts "Processing update #{ctx.update_id}"
45
+ next_middleware.call(ctx)
46
+ end
47
+ ```
48
+
49
+ ### Middleware with Arguments
50
+
51
+ ```ruby
52
+ class RateLimitMiddleware
53
+ def initialize(limit_per_minute: 10)
54
+ @limit = limit_per_minute
55
+ @requests = {}
56
+ end
57
+
58
+ def call(ctx, next_middleware)
59
+ user_id = ctx.from&.id
60
+ return next_middleware.call(ctx) unless user_id
61
+
62
+ now = Time.now.to_i
63
+ window_start = now - 60
64
+
65
+ @requests[user_id] ||= []
66
+ @requests[user_id].select! { |time| time > window_start }
67
+
68
+ if @requests[user_id].size >= @limit
69
+ ctx.reply("Rate limit exceeded")
70
+ return
71
+ end
72
+
73
+ @requests[user_id] << now
74
+ next_middleware.call(ctx)
75
+ end
76
+ end
77
+
78
+ bot.use RateLimitMiddleware.new(limit_per_minute: 5)
79
+ ```
80
+
81
+ ## Built-in Middleware
82
+
83
+ Telegem automatically includes some middleware:
84
+
85
+ ### Session Middleware
86
+
87
+ Loads and saves user sessions automatically.
88
+
89
+ ```ruby
90
+ # Automatically included
91
+ # Uses bot's session_store
92
+ ```
93
+
94
+ ### Scene Middleware
95
+
96
+ Handles scene-based conversations.
97
+
98
+ ```ruby
99
+ # Automatically included
100
+ # Processes scene steps
101
+ ```
102
+
103
+ ## Common Middleware Patterns
104
+
105
+ ### Authentication
106
+
107
+ ```ruby
108
+ class AuthMiddleware
109
+ def initialize(allowed_users: [])
110
+ @allowed_users = allowed_users
111
+ end
112
+
113
+ def call(ctx, next_middleware)
114
+ user_id = ctx.from&.id
115
+
116
+ unless @allowed_users.include?(user_id)
117
+ ctx.reply("Access denied")
118
+ return
119
+ end
120
+
121
+ next_middleware.call(ctx)
122
+ end
123
+ end
124
+
125
+ bot.use AuthMiddleware.new(allowed_users: [123456, 789012])
126
+ ```
127
+
128
+ ### Logging
129
+
130
+ ```ruby
131
+ class DetailedLoggingMiddleware
132
+ def call(ctx, next_middleware)
133
+ start_time = Time.now
134
+
135
+ log_request(ctx)
136
+
137
+ begin
138
+ next_middleware.call(ctx)
139
+ log_success(ctx, Time.now - start_time)
140
+ rescue => e
141
+ log_error(ctx, e, Time.now - start_time)
142
+ raise
143
+ end
144
+ end
145
+
146
+ private
147
+
148
+ def log_request(ctx)
149
+ puts "[#{Time.now}] #{ctx.update_type} from #{ctx.from&.id}"
150
+ end
151
+
152
+ def log_success(ctx, duration)
153
+ puts "[#{Time.now}] SUCCESS #{duration.round(3)}s"
154
+ end
155
+
156
+ def log_error(ctx, error, duration)
157
+ puts "[#{Time.now}] ERROR #{error.class}: #{error.message} (#{duration.round(3)}s)"
158
+ end
159
+ end
160
+ ```
161
+
162
+ ### Input Validation
163
+
164
+ ```ruby
165
+ class ValidationMiddleware
166
+ def call(ctx, next_middleware)
167
+ if ctx.message&.text
168
+ # Sanitize input
169
+ ctx.message.instance_variable_set(:@text, sanitize_text(ctx.message.text))
170
+ end
171
+
172
+ next_middleware.call(ctx)
173
+ end
174
+
175
+ private
176
+
177
+ def sanitize_text(text)
178
+ # Remove potentially harmful content
179
+ text.gsub(/[<>'"&]/, '').strip
180
+ end
181
+ end
182
+ ```
183
+
184
+ ### Language Detection
185
+
186
+ ```ruby
187
+ class LanguageMiddleware
188
+ def call(ctx, next_middleware)
189
+ if ctx.from&.language_code
190
+ ctx.state[:language] = ctx.from.language_code
191
+ ctx.session[:language] ||= ctx.from.language_code
192
+ end
193
+
194
+ next_middleware.call(ctx)
195
+ end
196
+ end
197
+ ```
198
+
199
+ ### Bot Command Filtering
200
+
201
+ ```ruby
202
+ class BotCommandMiddleware
203
+ def call(ctx, next_middleware)
204
+ if ctx.message&.text&.include?('@')
205
+ # Check if command is for this bot
206
+ bot_username = ctx.bot.api.call('getMe')['username']
207
+ unless ctx.message.text.include?("@#{bot_username}")
208
+ return # Skip this update
209
+ end
210
+ end
211
+
212
+ next_middleware.call(ctx)
213
+ end
214
+ end
215
+ ```
216
+
217
+ ## Advanced Middleware
218
+
219
+ ### Conditional Middleware
220
+
221
+ ```ruby
222
+ class ConditionalMiddleware
223
+ def initialize(condition_proc, middleware)
224
+ @condition = condition_proc
225
+ @middleware = middleware
226
+ end
227
+
228
+ def call(ctx, next_middleware)
229
+ if @condition.call(ctx)
230
+ @middleware.call(ctx, next_middleware)
231
+ else
232
+ next_middleware.call(ctx)
233
+ end
234
+ end
235
+ end
236
+
237
+ # Use only in groups
238
+ group_only = ConditionalMiddleware.new(
239
+ ->(ctx) { ctx.chat&.type == 'group' },
240
+ GroupMiddleware.new
241
+ )
242
+ bot.use group_only
243
+ ```
244
+
245
+ ### Async Middleware
246
+
247
+ ```ruby
248
+ class AsyncProcessingMiddleware
249
+ def call(ctx, next_middleware)
250
+ Async do
251
+ # Do async work
252
+ result = await some_async_operation(ctx)
253
+
254
+ # Modify context
255
+ ctx.state[:async_result] = result
256
+
257
+ # Continue
258
+ next_middleware.call(ctx)
259
+ end
260
+ end
261
+ end
262
+ ```
263
+
264
+ ### Middleware Chains
265
+
266
+ ```ruby
267
+ class MiddlewareChain
268
+ def initialize(*middlewares)
269
+ @middlewares = middlewares
270
+ end
271
+
272
+ def call(ctx, final_handler)
273
+ chain = build_chain(final_handler)
274
+ chain.call(ctx)
275
+ end
276
+
277
+ private
278
+
279
+ def build_chain(final_handler)
280
+ @middlewares.reverse.inject(final_handler) do |next_middleware, middleware|
281
+ ->(ctx) { middleware.call(ctx, next_middleware) }
282
+ end
283
+ end
284
+ end
285
+
286
+ # Usage
287
+ chain = MiddlewareChain.new(
288
+ LoggingMiddleware.new,
289
+ AuthMiddleware.new,
290
+ RateLimitMiddleware.new
291
+ )
292
+
293
+ bot.use chain
294
+ ```
295
+
296
+ ## Middleware Order Matters
297
+
298
+ Order middleware from most general to most specific:
299
+
300
+ ```ruby
301
+ bot.use LoggingMiddleware.new # Log everything
302
+ bot.use RateLimitMiddleware.new # Rate limit all requests
303
+ bot.use AuthMiddleware.new # Authenticate users
304
+ bot.use LanguageMiddleware.new # Set language preferences
305
+ # Handlers...
306
+ ```
307
+
308
+ ## Error Handling in Middleware
309
+
310
+ ```ruby
311
+ class SafeMiddleware
312
+ def call(ctx, next_middleware)
313
+ begin
314
+ next_middleware.call(ctx)
315
+ rescue => e
316
+ ctx.logger.error("Middleware error: #{e.message}")
317
+ ctx.reply("Something went wrong") if ctx.chat
318
+ end
319
+ end
320
+ end
321
+
322
+ # Wrap all middleware in safety
323
+ bot.use SafeMiddleware.new
324
+ ```
325
+
326
+ ## Testing Middleware
327
+
328
+ ```ruby
329
+ # Test middleware in isolation
330
+ def test_middleware(middleware_class, ctx)
331
+ called = false
332
+
333
+ middleware_class.new.call(ctx, ->(ctx) { called = true })
334
+
335
+ assert called, "Next middleware should be called"
336
+ end
337
+
338
+ # Integration test
339
+ def test_middleware_chain(bot, update_data)
340
+ update = Telegem::Types::Update.new(update_data)
341
+ ctx = Telegem::Core::Context.new(update, bot)
342
+
343
+ # Process through middleware
344
+ bot.process_update(update)
345
+
346
+ # Assert expected behavior
347
+ end
348
+ ```
349
+
350
+ ## Built-in Middleware Reference
351
+
352
+ ### Session::Middleware
353
+
354
+ - Loads session data before handlers
355
+ - Saves session data after handlers
356
+ - Handles session store errors gracefully
357
+
358
+ ### Scene::Middleware
359
+
360
+ - Intercepts updates for active scenes
361
+ - Routes to scene steps
362
+ - Handles scene timeouts
363
+
364
+ ## Best Practices
365
+
366
+ ### 1. Keep Middleware Focused
367
+
368
+ Each middleware should do one thing well.
369
+
370
+ ```ruby
371
+ # Bad: one middleware doing everything
372
+ class MonolithicMiddleware
373
+ def call(ctx, next_middleware)
374
+ log_request(ctx)
375
+ check_auth(ctx)
376
+ validate_input(ctx)
377
+ next_middleware.call(ctx)
378
+ end
379
+ end
380
+
381
+ # Good: separate concerns
382
+ bot.use LoggingMiddleware.new
383
+ bot.use AuthMiddleware.new
384
+ bot.use ValidationMiddleware.new
385
+ ```
386
+
387
+ ### 2. Handle Errors Gracefully
388
+
389
+ ```ruby
390
+ class RobustMiddleware
391
+ def call(ctx, next_middleware)
392
+ begin
393
+ next_middleware.call(ctx)
394
+ rescue => e
395
+ handle_error(ctx, e)
396
+ # Decide whether to continue or stop
397
+ end
398
+ end
399
+ end
400
+ ```
401
+
402
+ ### 3. Make Middleware Configurable
403
+
404
+ ```ruby
405
+ class ConfigurableMiddleware
406
+ def initialize(options = {})
407
+ @options = default_options.merge(options)
408
+ end
409
+
410
+ def call(ctx, next_middleware)
411
+ if @options[:enabled]
412
+ # Do work
413
+ end
414
+ next_middleware.call(ctx)
415
+ end
416
+ end
417
+ ```
418
+
419
+ ### 4. Use Appropriate Scoping
420
+
421
+ ```ruby
422
+ # Global middleware (affects all updates)
423
+ bot.use GlobalLoggingMiddleware.new
424
+
425
+ # Conditional middleware
426
+ bot.use ConditionalMiddleware.new(
427
+ ->(ctx) { ctx.chat&.group? },
428
+ GroupOnlyMiddleware.new
429
+ )
430
+ ```
431
+
432
+ ### 5. Document Middleware Behavior
433
+
434
+ ```ruby
435
+ class DocumentedMiddleware
436
+ # This middleware:
437
+ # - Validates user input
438
+ # - Sanitizes text content
439
+ # - Rejects messages over 1000 characters
440
+ # - Logs validation failures
441
+
442
+ def call(ctx, next_middleware)
443
+ # Implementation
444
+ end
445
+ end
446
+ ```
447
+
448
+ ### 6. Test Middleware Thoroughly
449
+
450
+ ```ruby
451
+ describe ValidationMiddleware do
452
+ it "rejects empty messages" do
453
+ ctx = create_context(text: "")
454
+ middleware.call(ctx, ->(_) { @called = true })
455
+ expect(@called).to be false
456
+ end
457
+
458
+ it "allows valid messages" do
459
+ ctx = create_context(text: "valid")
460
+ middleware.call(ctx, ->(_) { @called = true })
461
+ expect(@called).to be true
462
+ end
463
+ end
464
+ ```
465
+
466
+ ## Common Middleware Examples
467
+
468
+ ### User Tracking
469
+
470
+ ```ruby
471
+ class UserTrackingMiddleware
472
+ def call(ctx, next_middleware)
473
+ if ctx.from
474
+ ctx.session[:last_seen] = Time.now.to_i
475
+ ctx.session[:message_count] ||= 0
476
+ ctx.session[:message_count] += 1
477
+ end
478
+
479
+ next_middleware.call(ctx)
480
+ end
481
+ end
482
+ ```
483
+
484
+ ### Feature Flags
485
+
486
+ ```ruby
487
+ class FeatureFlagMiddleware
488
+ def initialize(feature_name)
489
+ @feature_name = feature_name
490
+ end
491
+
492
+ def call(ctx, next_middleware)
493
+ if feature_enabled?(@feature_name, ctx.from&.id)
494
+ next_middleware.call(ctx)
495
+ else
496
+ ctx.reply("Feature not available")
497
+ end
498
+ end
499
+ end
500
+ ```
501
+
502
+ ### Request Timing
503
+
504
+ ```ruby
505
+ class TimingMiddleware
506
+ def call(ctx, next_middleware)
507
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
508
+ next_middleware.call(ctx)
509
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
510
+
511
+ ctx.logger.info("Request took #{duration.round(3)}s")
512
+ end
513
+ end
514
+ ```
515
+
516
+ ### Content Filtering
517
+
518
+ ```ruby
519
+ class ContentFilterMiddleware
520
+ BANNED_WORDS = ['spam', 'inappropriate']
521
+
522
+ def call(ctx, next_middleware)
523
+ if ctx.message&.text
524
+ if BANNED_WORDS.any? { |word| ctx.message.text.include?(word) }
525
+ ctx.delete_message
526
+ return
527
+ end
528
+ end
529
+
530
+ next_middleware.call(ctx)
531
+ end
532
+ end
533
+ ```
534
+
535
+ Middleware is powerful for adding cross-cutting functionality to your bot. Use it to separate concerns and keep handlers focused on business logic.</content>
536
+ <parameter name="filePath">/home/slick/telegem/docs/middleware.md