telegem 0.2.5 → 1.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.
- checksums.yaml +4 -4
- data/.replit +13 -0
- data/Contributing.md +553 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +11 -0
- data/LICENSE +21 -0
- data/Readme.md +353 -0
- data/Test-Projects/.gitkeep +0 -0
- data/Test-Projects/bot_test1.rb +75 -0
- data/Test-Projects/pizza_test_bot_guide.md +163 -0
- data/docs/.gitkeep +0 -0
- data/docs/Api.md +419 -0
- data/docs/Cookbook.md +407 -0
- data/docs/How_to_use.md +571 -0
- data/docs/QuickStart.md +258 -0
- data/docs/Usage.md +717 -0
- data/lib/api/client.rb +89 -116
- data/lib/core/bot.rb +103 -92
- data/lib/core/composer.rb +36 -18
- data/lib/core/context.rb +180 -177
- data/lib/core/scene.rb +81 -71
- data/lib/session/memory_store.rb +1 -1
- data/lib/session/middleware.rb +20 -36
- data/lib/telegem.rb +57 -54
- data/lib/webhook/.gitkeep +0 -0
- data/lib/webhook/server.rb +193 -0
- metadata +38 -35
- data/telegem.gemspec +0 -43
- data/webhook/server.rb +0 -86
data/docs/Usage.md
ADDED
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
📘 Telegem Usage Guide
|
|
2
|
+
|
|
3
|
+
For developers who know Ruby and want to build real, production-ready bots. This guide focuses on practical patterns, best practices, and the powerful features that make Telegem special.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
🎯 Quick Navigation
|
|
8
|
+
|
|
9
|
+
• Scenes & Wizard Flows
|
|
10
|
+
• Middleware Patterns
|
|
11
|
+
• Session Management
|
|
12
|
+
• Error Handling Strategies
|
|
13
|
+
• Webhook Deployment
|
|
14
|
+
• Testing Your Bot
|
|
15
|
+
• Performance Tips
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
🧙 Scenes & Wizard Flows
|
|
20
|
+
|
|
21
|
+
Scenes handle multi-step conversations - perfect for forms, surveys, onboarding, or any back-and-forth interaction.
|
|
22
|
+
|
|
23
|
+
Why Scenes?
|
|
24
|
+
|
|
25
|
+
Imagine building a job application bot without scenes:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
# ❌ The messy way (without scenes)
|
|
29
|
+
bot.command('apply') do |ctx|
|
|
30
|
+
ctx.reply "What's your name?"
|
|
31
|
+
# Now what? How do we wait for response?
|
|
32
|
+
# Where do we store their answer?
|
|
33
|
+
# How do we know what step we're on?
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
With scenes, it becomes clean:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
# ✅ The clean way (with scenes)
|
|
41
|
+
bot.scene :job_application do
|
|
42
|
+
step :ask_name do |ctx|
|
|
43
|
+
ctx.reply "What's your full name?"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
step :save_name do |ctx|
|
|
47
|
+
ctx.session[:name] = ctx.message.text
|
|
48
|
+
ctx.reply "Thanks #{ctx.session[:name]}! What's your email?"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
step :save_email do |ctx|
|
|
52
|
+
ctx.session[:email] = ctx.message.text
|
|
53
|
+
ctx.reply "Great! One more question..."
|
|
54
|
+
# Automatically goes to next step
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Scene Lifecycle
|
|
60
|
+
|
|
61
|
+
Every scene has a clear lifecycle:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
┌─────────────────────┐
|
|
65
|
+
│ Scene Created │
|
|
66
|
+
│ (bot.scene :id) │
|
|
67
|
+
└──────────┬──────────┘
|
|
68
|
+
│
|
|
69
|
+
┌──────────▼──────────┐
|
|
70
|
+
│ on_enter │ ← Runs once when scene starts
|
|
71
|
+
└──────────┬──────────┘
|
|
72
|
+
│
|
|
73
|
+
┌──────────▼──────────┐
|
|
74
|
+
│ step 1 │ ← First interaction
|
|
75
|
+
└──────────┬──────────┘
|
|
76
|
+
│
|
|
77
|
+
┌──────────▼──────────┐
|
|
78
|
+
│ step 2 │ ← Waits for user input
|
|
79
|
+
└──────────┬──────────┘
|
|
80
|
+
│
|
|
81
|
+
(more steps...)
|
|
82
|
+
│
|
|
83
|
+
┌──────────▼──────────┐
|
|
84
|
+
│ on_leave │ ← Runs when scene ends
|
|
85
|
+
└──────────┬──────────┘
|
|
86
|
+
│
|
|
87
|
+
┌──────────▼──────────┐
|
|
88
|
+
│ Scene Ended │
|
|
89
|
+
│ (ctx.leave_scene) │
|
|
90
|
+
└─────────────────────┘
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Practical Scene Example: Restaurant Booking
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
bot.scene :restaurant_booking do
|
|
97
|
+
on_enter do |ctx|
|
|
98
|
+
ctx.session[:booking_id] = SecureRandom.hex(6)
|
|
99
|
+
ctx.reply "Welcome to our booking system! 🍽️"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
step :ask_date do |ctx|
|
|
103
|
+
keyboard = Telegem::Markup.inline do
|
|
104
|
+
row callback("Today", "date_today"),
|
|
105
|
+
callback("Tomorrow", "date_tomorrow")
|
|
106
|
+
row callback("Pick date...", "date_custom")
|
|
107
|
+
end
|
|
108
|
+
ctx.reply "When would you like to book?", reply_markup: keyboard
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
step :handle_date do |ctx|
|
|
112
|
+
case ctx.data
|
|
113
|
+
when "date_today"
|
|
114
|
+
ctx.session[:date] = Date.today
|
|
115
|
+
when "date_tomorrow"
|
|
116
|
+
ctx.session[:date] = Date.today + 1
|
|
117
|
+
when "date_custom"
|
|
118
|
+
ctx.reply "Please send the date (YYYY-MM-DD):"
|
|
119
|
+
return # Stay in this step for custom input
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
ctx.reply "Great! How many people?"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
step :save_people do |ctx|
|
|
126
|
+
ctx.session[:people] = ctx.message.text.to_i
|
|
127
|
+
|
|
128
|
+
# Show summary
|
|
129
|
+
summary = <<~SUMMARY
|
|
130
|
+
📋 Booking Summary:
|
|
131
|
+
|
|
132
|
+
ID: #{ctx.session[:booking_id]}
|
|
133
|
+
Date: #{ctx.session[:date]}
|
|
134
|
+
People: #{ctx.session[:people]}
|
|
135
|
+
|
|
136
|
+
Confirm? (yes/no)
|
|
137
|
+
SUMMARY
|
|
138
|
+
|
|
139
|
+
ctx.reply summary
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
step :confirm do |ctx|
|
|
143
|
+
if ctx.message.text.downcase == 'yes'
|
|
144
|
+
# Save to database here
|
|
145
|
+
ctx.reply "✅ Booking confirmed! Your ID: #{ctx.session[:booking_id]}"
|
|
146
|
+
ctx.leave_scene
|
|
147
|
+
else
|
|
148
|
+
ctx.reply "❌ Booking cancelled."
|
|
149
|
+
ctx.leave_scene
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
on_leave do |ctx|
|
|
154
|
+
ctx.reply "Thank you for using our booking system!"
|
|
155
|
+
# Clean up session if needed
|
|
156
|
+
ctx.session.delete(:booking_id)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Scene Best Practices
|
|
162
|
+
|
|
163
|
+
• Keep scenes focused - One scene per flow (booking, survey, game)
|
|
164
|
+
• Use session for state - Don't use global variables
|
|
165
|
+
• Handle cancellation - Always allow ctx.leave_scene
|
|
166
|
+
• Validate input - Check user responses make sense
|
|
167
|
+
• Clear session on exit - Avoid stale data
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
🔌 Middleware Patterns
|
|
172
|
+
|
|
173
|
+
Middleware runs on every update, making it perfect for cross-cutting concerns.
|
|
174
|
+
|
|
175
|
+
The Middleware Stack
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
# Order matters! Top runs first
|
|
179
|
+
bot.use AuthenticationMiddleware.new
|
|
180
|
+
bot.use LoggingMiddleware.new
|
|
181
|
+
bot.use RateLimiter.new
|
|
182
|
+
bot.use SessionMiddleware.new
|
|
183
|
+
# Your command handlers run here
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Common Middleware Examples
|
|
187
|
+
|
|
188
|
+
1. Authentication
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
class AdminOnlyMiddleware
|
|
192
|
+
ADMIN_IDS = [12345, 67890]
|
|
193
|
+
|
|
194
|
+
def call(ctx, next_middleware)
|
|
195
|
+
if ADMIN_IDS.include?(ctx.from.id)
|
|
196
|
+
next_middleware.call(ctx)
|
|
197
|
+
else
|
|
198
|
+
ctx.reply "⛔ Admin access required"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
2. Logging
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
class RequestLogger
|
|
208
|
+
def call(ctx, next_middleware)
|
|
209
|
+
start_time = Time.now
|
|
210
|
+
|
|
211
|
+
# Before handler
|
|
212
|
+
log_request(ctx)
|
|
213
|
+
|
|
214
|
+
# Run the handler
|
|
215
|
+
next_middleware.call(ctx)
|
|
216
|
+
|
|
217
|
+
# After handler
|
|
218
|
+
duration = Time.now - start_time
|
|
219
|
+
log_completion(ctx, duration)
|
|
220
|
+
rescue => e
|
|
221
|
+
log_error(ctx, e)
|
|
222
|
+
raise
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
private
|
|
226
|
+
|
|
227
|
+
def log_request(ctx)
|
|
228
|
+
puts "[#{Time.now}] #{ctx.from.username}: #{ctx.message&.text}"
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
3. Rate Limiting
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
class RateLimiter
|
|
237
|
+
def initialize(limit: 10, period: 60) # 10 requests per minute
|
|
238
|
+
@limit = limit
|
|
239
|
+
@period = period
|
|
240
|
+
@requests = Hash.new { |h, k| h[k] = [] }
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def call(ctx, next_middleware)
|
|
244
|
+
user_id = ctx.from.id
|
|
245
|
+
now = Time.now
|
|
246
|
+
|
|
247
|
+
# Clean old requests
|
|
248
|
+
@requests[user_id].reject! { |time| time < now - @period }
|
|
249
|
+
|
|
250
|
+
if @requests[user_id].size >= @limit
|
|
251
|
+
ctx.reply "⚠️ Too many requests. Please wait."
|
|
252
|
+
return
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
@requests[user_id] << now
|
|
256
|
+
next_middleware.call(ctx)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Async-Aware Middleware
|
|
262
|
+
|
|
263
|
+
Since Telegem is async, your middleware should be too:
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
class AsyncLogger
|
|
267
|
+
def call(ctx, next_middleware)
|
|
268
|
+
Async do
|
|
269
|
+
# Do async work
|
|
270
|
+
await log_to_database(ctx)
|
|
271
|
+
|
|
272
|
+
# Continue chain
|
|
273
|
+
await next_middleware.call(ctx)
|
|
274
|
+
|
|
275
|
+
# More async work
|
|
276
|
+
await update_analytics(ctx)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
💾 Session Management
|
|
285
|
+
|
|
286
|
+
Session Storage Options
|
|
287
|
+
|
|
288
|
+
```ruby
|
|
289
|
+
# 1. Memory (default, development only)
|
|
290
|
+
store = Telegem::Session::MemoryStore.new
|
|
291
|
+
|
|
292
|
+
# 2. Redis (production)
|
|
293
|
+
require 'redis'
|
|
294
|
+
redis = Redis.new(url: ENV['REDIS_URL'])
|
|
295
|
+
store = Telegem::Session::RedisStore.new(redis)
|
|
296
|
+
|
|
297
|
+
# 3. Custom (database, file, etc.)
|
|
298
|
+
class DatabaseStore
|
|
299
|
+
def get(user_id)
|
|
300
|
+
User.find_by(telegram_id: user_id).session_data
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def set(user_id, data)
|
|
304
|
+
user = User.find_or_create_by(telegram_id: user_id)
|
|
305
|
+
user.update!(session_data: data)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Session Data Patterns
|
|
311
|
+
|
|
312
|
+
Store minimal data:
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
# ❌ Don't store large objects
|
|
316
|
+
ctx.session[:user] = large_user_object
|
|
317
|
+
|
|
318
|
+
# ✅ Store IDs and essential data
|
|
319
|
+
ctx.session[:user_id] = user.id
|
|
320
|
+
ctx.session[:step] = :collecting_email
|
|
321
|
+
ctx.session[:cart] = { item_ids: [1, 2, 3], total: 45.99 }
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Clear session properly:
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
bot.command('logout') do |ctx|
|
|
328
|
+
# Clear specific keys
|
|
329
|
+
ctx.session.delete(:auth_token)
|
|
330
|
+
ctx.session.delete(:user_id)
|
|
331
|
+
|
|
332
|
+
# Or clear everything
|
|
333
|
+
ctx.session.clear
|
|
334
|
+
|
|
335
|
+
ctx.reply "Logged out successfully!"
|
|
336
|
+
end
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Session timeouts:
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
class TimedSession
|
|
343
|
+
def call(ctx, next_middleware)
|
|
344
|
+
# Check if session expired
|
|
345
|
+
if ctx.session[:created_at] &&
|
|
346
|
+
Time.now - ctx.session[:created_at] > 3600 # 1 hour
|
|
347
|
+
ctx.session.clear
|
|
348
|
+
ctx.reply "Session expired. Starting fresh!"
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Update timestamp
|
|
352
|
+
ctx.session[:created_at] ||= Time.now
|
|
353
|
+
|
|
354
|
+
next_middleware.call(ctx)
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
🚨 Error Handling Strategies
|
|
362
|
+
|
|
363
|
+
Global Error Handler
|
|
364
|
+
|
|
365
|
+
```ruby
|
|
366
|
+
bot.error do |error, ctx|
|
|
367
|
+
case error
|
|
368
|
+
when Telegem::APIError
|
|
369
|
+
# Telegram API errors (invalid token, rate limit)
|
|
370
|
+
ctx.reply "⚠️ API Error: #{error.message}"
|
|
371
|
+
log_to_sentry(error, ctx)
|
|
372
|
+
|
|
373
|
+
when Net::OpenTimeout, SocketError
|
|
374
|
+
# Network issues
|
|
375
|
+
ctx.reply "🌐 Connection issue. Try again?"
|
|
376
|
+
|
|
377
|
+
when => e
|
|
378
|
+
# Unexpected errors
|
|
379
|
+
ctx.reply "❌ Something went wrong. We've been notified."
|
|
380
|
+
|
|
381
|
+
# Notify developers
|
|
382
|
+
notify_developers(e, ctx)
|
|
383
|
+
|
|
384
|
+
# Log everything
|
|
385
|
+
logger.error("Unhandled error: #{e.class}: #{e.message}")
|
|
386
|
+
logger.error("Context: #{ctx.raw_update}")
|
|
387
|
+
logger.error(e.backtrace.join("\n"))
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Per-Command Error Handling
|
|
393
|
+
|
|
394
|
+
```ruby
|
|
395
|
+
bot.command('admin') do |ctx|
|
|
396
|
+
begin
|
|
397
|
+
# Risky operation
|
|
398
|
+
result = do_admin_thing(ctx)
|
|
399
|
+
ctx.reply "Success: #{result}"
|
|
400
|
+
rescue PermissionError => e
|
|
401
|
+
ctx.reply "⛔ Permission denied: #{e.message}"
|
|
402
|
+
rescue => e
|
|
403
|
+
ctx.reply "Admin command failed. Check logs."
|
|
404
|
+
raise # Still goes to global handler
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Recovery Patterns
|
|
410
|
+
|
|
411
|
+
Retry logic:
|
|
412
|
+
|
|
413
|
+
```ruby
|
|
414
|
+
def with_retries(max_attempts: 3)
|
|
415
|
+
attempts = 0
|
|
416
|
+
|
|
417
|
+
while attempts < max_attempts
|
|
418
|
+
begin
|
|
419
|
+
return yield
|
|
420
|
+
rescue Net::ReadTimeout => e
|
|
421
|
+
attempts += 1
|
|
422
|
+
sleep(2 ** attempts) # Exponential backoff
|
|
423
|
+
retry if attempts < max_attempts
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
raise "Failed after #{max_attempts} attempts"
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
bot.command('fetch') do |ctx|
|
|
431
|
+
with_retries do
|
|
432
|
+
data = fetch_external_api(ctx.message.text)
|
|
433
|
+
ctx.reply "Found: #{data}"
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
🌐 Webhook Deployment
|
|
441
|
+
|
|
442
|
+
Production Webhook Setup
|
|
443
|
+
|
|
444
|
+
```ruby
|
|
445
|
+
# config/bot.rb
|
|
446
|
+
require 'telegem'
|
|
447
|
+
|
|
448
|
+
bot = Telegem.new(ENV['TELEGRAM_BOT_TOKEN'])
|
|
449
|
+
|
|
450
|
+
# Your bot logic here
|
|
451
|
+
bot.command('start') { |ctx| ctx.reply "Running on webhook!" }
|
|
452
|
+
|
|
453
|
+
# For Rack apps (Rails, Sinatra)
|
|
454
|
+
use Telegem::Webhook::Middleware, bot
|
|
455
|
+
|
|
456
|
+
# OR standalone server
|
|
457
|
+
if $0 == __FILE__ # Direct execution
|
|
458
|
+
server = bot.webhook_server(
|
|
459
|
+
port: ENV['PORT'] || 3000,
|
|
460
|
+
endpoint: Async::HTTP::Endpoint.parse("https://#{ENV['DOMAIN']}")
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
# Set webhook automatically
|
|
464
|
+
Async do
|
|
465
|
+
await bot.set_webhook(
|
|
466
|
+
url: "https://#{ENV['DOMAIN']}/webhook/#{bot.token}",
|
|
467
|
+
certificate: ENV['SSL_CERT_PATH'] # Optional for self-signed
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
server.run
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Nginx Configuration
|
|
476
|
+
|
|
477
|
+
```nginx
|
|
478
|
+
# For webhook endpoint
|
|
479
|
+
location /webhook/ {
|
|
480
|
+
proxy_pass http://localhost:3000;
|
|
481
|
+
proxy_set_header Host $host;
|
|
482
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
483
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
484
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
485
|
+
|
|
486
|
+
# Telegram requires these timeouts
|
|
487
|
+
proxy_connect_timeout 90;
|
|
488
|
+
proxy_send_timeout 90;
|
|
489
|
+
proxy_read_timeout 90;
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
SSL Configuration
|
|
494
|
+
|
|
495
|
+
```bash
|
|
496
|
+
# Let's Encrypt for production
|
|
497
|
+
certbot certonly --nginx -d yourdomain.com
|
|
498
|
+
|
|
499
|
+
# Self-signed for testing (Telegram requires SSL for webhooks)
|
|
500
|
+
openssl req -newkey rsa:2048 -sha256 -nodes \
|
|
501
|
+
-keyout private.key -x509 -days 365 \
|
|
502
|
+
-out cert.pem -subj "/C=US/ST=State/L=City/O=Org/CN=yourdomain.com"
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
Health Checks
|
|
506
|
+
|
|
507
|
+
```ruby
|
|
508
|
+
# Add to your webhook server
|
|
509
|
+
bot.on(:health) do
|
|
510
|
+
[200, { 'Content-Type' => 'application/json' },
|
|
511
|
+
{ status: 'ok', time: Time.now.to_i }.to_json]
|
|
512
|
+
end
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
🧪 Testing Your Bot
|
|
518
|
+
|
|
519
|
+
Unit Testing Scenes
|
|
520
|
+
|
|
521
|
+
```ruby
|
|
522
|
+
# test/scenes/registration_scene_test.rb
|
|
523
|
+
require 'test_helper'
|
|
524
|
+
|
|
525
|
+
class RegistrationSceneTest < Minitest::Test
|
|
526
|
+
def setup
|
|
527
|
+
@bot = Telegem.new('test_token')
|
|
528
|
+
@bot.scene :registration do
|
|
529
|
+
step :ask_name { |ctx| ctx.reply "What's your name?" }
|
|
530
|
+
step :save_name { |ctx| ctx.session[:name] = ctx.message.text }
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def test_scene_flow
|
|
535
|
+
# Mock context
|
|
536
|
+
ctx = Minitest::Mock.new
|
|
537
|
+
ctx.expect(:reply, nil, ["What's your name?"])
|
|
538
|
+
ctx.expect(:session, {})
|
|
539
|
+
|
|
540
|
+
# Trigger scene
|
|
541
|
+
scene = @bot.scenes[:registration]
|
|
542
|
+
scene.enter(ctx)
|
|
543
|
+
|
|
544
|
+
assert ctx.verify
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Integration Testing
|
|
550
|
+
|
|
551
|
+
```ruby
|
|
552
|
+
# test/integration/bot_test.rb
|
|
553
|
+
require 'test_helper'
|
|
554
|
+
|
|
555
|
+
class BotIntegrationTest < Minitest::Test
|
|
556
|
+
def test_command_flow
|
|
557
|
+
# Start bot in test mode
|
|
558
|
+
bot = Telegem.new('test_token')
|
|
559
|
+
|
|
560
|
+
# Capture replies
|
|
561
|
+
replies = []
|
|
562
|
+
bot.command('test') { |ctx| replies << ctx.reply("Working!") }
|
|
563
|
+
|
|
564
|
+
# Simulate update
|
|
565
|
+
update = {
|
|
566
|
+
'update_id' => 1,
|
|
567
|
+
'message' => {
|
|
568
|
+
'message_id' => 1,
|
|
569
|
+
'from' => { 'id' => 123, 'first_name' => 'Test' },
|
|
570
|
+
'chat' => { 'id' => 456 },
|
|
571
|
+
'text' => '/test'
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
# Process update
|
|
576
|
+
bot.process(update)
|
|
577
|
+
|
|
578
|
+
assert_equal 1, replies.size
|
|
579
|
+
assert_includes replies.first, "Working!"
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
Mocking Telegram API
|
|
585
|
+
|
|
586
|
+
```ruby
|
|
587
|
+
# spec/spec_helper.rb
|
|
588
|
+
RSpec.configure do |config|
|
|
589
|
+
config.before(:each) do
|
|
590
|
+
# Stub Telegram API calls
|
|
591
|
+
allow_any_instance_of(Telegem::API::Client).to receive(:call) do |_, method, params|
|
|
592
|
+
case method
|
|
593
|
+
when 'getMe'
|
|
594
|
+
{ 'ok' => true, 'result' => { 'username' => 'test_bot' } }
|
|
595
|
+
when 'sendMessage'
|
|
596
|
+
{ 'ok' => true, 'result' => { 'message_id' => 1 } }
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
---
|
|
604
|
+
|
|
605
|
+
⚡ Performance Tips
|
|
606
|
+
|
|
607
|
+
1. Database Connections
|
|
608
|
+
|
|
609
|
+
```ruby
|
|
610
|
+
class DatabaseMiddleware
|
|
611
|
+
def initialize(pool_size: 5)
|
|
612
|
+
@pool = ConnectionPool.new(size: pool_size) do
|
|
613
|
+
ActiveRecord::Base.connection_pool.checkout
|
|
614
|
+
end
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
def call(ctx, next_middleware)
|
|
618
|
+
@pool.with do |conn|
|
|
619
|
+
# Use connection within this block
|
|
620
|
+
ctx.db = conn
|
|
621
|
+
next_middleware.call(ctx)
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
end
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
2. Caching Frequently Used Data
|
|
628
|
+
|
|
629
|
+
```ruby
|
|
630
|
+
class CacheMiddleware
|
|
631
|
+
def initialize(ttl: 300) # 5 minutes
|
|
632
|
+
@cache = {}
|
|
633
|
+
@ttl = ttl
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def call(ctx, next_middleware)
|
|
637
|
+
# Cache user data
|
|
638
|
+
user_key = "user:#{ctx.from.id}"
|
|
639
|
+
|
|
640
|
+
if cached = @cache[user_key]&.[](:data)
|
|
641
|
+
ctx.cached_user = cached
|
|
642
|
+
else
|
|
643
|
+
# Fetch and cache
|
|
644
|
+
ctx.cached_user = fetch_user(ctx.from.id)
|
|
645
|
+
@cache[user_key] = { data: ctx.cached_user, expires: Time.now + @ttl }
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
# Clean expired cache
|
|
649
|
+
cleanup_cache
|
|
650
|
+
|
|
651
|
+
next_middleware.call(ctx)
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
3. Batch Operations
|
|
657
|
+
|
|
658
|
+
```ruby
|
|
659
|
+
# Instead of sending one-by-one
|
|
660
|
+
messages.each { |msg| ctx.reply(msg) } # ❌ Slow
|
|
661
|
+
|
|
662
|
+
# Batch when possible
|
|
663
|
+
Async do
|
|
664
|
+
tasks = messages.map do |msg|
|
|
665
|
+
Async { ctx.reply(msg) }
|
|
666
|
+
end
|
|
667
|
+
await_all(tasks) # ✅ Faster
|
|
668
|
+
end
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
4. Monitor Performance
|
|
672
|
+
|
|
673
|
+
```ruby
|
|
674
|
+
class PerformanceMonitor
|
|
675
|
+
def call(ctx, next_middleware)
|
|
676
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
677
|
+
|
|
678
|
+
next_middleware.call(ctx)
|
|
679
|
+
|
|
680
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
681
|
+
|
|
682
|
+
if duration > 1.0 # More than 1 second
|
|
683
|
+
logger.warn("Slow handler: #{duration.round(2)}s for #{ctx.message&.text}")
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
# Record metrics
|
|
687
|
+
record_metric('handler_duration', duration)
|
|
688
|
+
end
|
|
689
|
+
end
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
---
|
|
693
|
+
|
|
694
|
+
🎯 Next Steps
|
|
695
|
+
|
|
696
|
+
You're ready to build production bots! Here's what to explore next:
|
|
697
|
+
|
|
698
|
+
1. Database Integration - Connect to PostgreSQL, Redis
|
|
699
|
+
2. Background Jobs - Use Sidekiq for heavy processing
|
|
700
|
+
3. Monitoring - Add New Relic, Sentry, or Datadog
|
|
701
|
+
4. CI/CD - Automate testing and deployment
|
|
702
|
+
5. Scaling - Run multiple bot instances behind a load balancer
|
|
703
|
+
|
|
704
|
+
Remember: The best way to learn is to build something real. Pick a project and start coding!
|
|
705
|
+
|
|
706
|
+
---
|
|
707
|
+
|
|
708
|
+
📚 Additional Resources
|
|
709
|
+
|
|
710
|
+
• Telegram Bot API Reference
|
|
711
|
+
• Async Ruby Documentation
|
|
712
|
+
• Example Bots Repository
|
|
713
|
+
• Community Forum
|
|
714
|
+
|
|
715
|
+
---
|
|
716
|
+
|
|
717
|
+
Happy Building! Your journey from library user to expert contributor starts with the next bot you create. 🚀
|