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.
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. 🚀