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.
- checksums.yaml +4 -4
- data/.rubocop.yml +57 -0
- data/CHANGELOG.md +121 -0
- data/Gemfile +1 -1
- data/README.md +147 -0
- data/bin/telegem-ssl +71 -25
- data/contributing.md +375 -0
- data/docs/api.md +663 -0
- data/docs/bot.md +332 -0
- data/docs/changelog.md +182 -0
- data/docs/context.md +554 -0
- data/docs/core_concepts.md +218 -0
- data/docs/deployment.md +702 -0
- data/docs/error_handling.md +435 -0
- data/docs/examples.md +752 -0
- data/docs/getting_started.md +151 -0
- data/docs/handlers.md +580 -0
- data/docs/keyboards.md +446 -0
- data/docs/middleware.md +536 -0
- data/docs/plugins.md +384 -0
- data/docs/scenes.md +517 -0
- data/docs/sessions.md +544 -0
- data/docs/testing.md +612 -0
- data/docs/troubleshooting.md +574 -0
- data/docs/types.md +538 -0
- data/docs/webhooks.md +456 -0
- data/lib/api/client.rb +38 -10
- data/lib/api/types.rb +433 -172
- data/lib/core/composer.rb +3 -3
- data/lib/core/context.rb +17 -11
- data/lib/plugins/cc +3 -0
- data/lib/plugins/file_extract.rb +2 -2
- data/lib/plugins/translate.rb +43 -0
- data/lib/session/memory_store.rb +90 -103
- data/lib/session/redis.rb +91 -0
- data/lib/telegem.rb +4 -4
- data/lib/webhook/server.rb +5 -3
- metadata +51 -35
- data/CHANGELOG +0 -95
- data/Contributing.md +0 -161
- data/Readme.md +0 -302
- data/examples/.gitkeep +0 -0
- data/public/.gitkeep +0 -0
data/docs/middleware.md
ADDED
|
@@ -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
|