flow_chat 0.8.1 → 0.9.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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.cliff.toml +74 -0
  3. data/.github/workflows/ci.yml +2 -3
  4. data/.github/workflows/release.yml +56 -0
  5. data/.standard.yml +4 -0
  6. data/CHANGELOG.md +22 -0
  7. data/CLAUDE.md +327 -0
  8. data/CONTRIBUTING.md +134 -0
  9. data/Gemfile +1 -0
  10. data/README.md +290 -105
  11. data/Rakefile +5 -1
  12. data/SECURITY.md +42 -349
  13. data/docs/architecture.md +510 -0
  14. data/docs/async-background-processing.md +298 -0
  15. data/docs/configuration.md +556 -226
  16. data/docs/factory-pattern.md +355 -0
  17. data/docs/gateway-context-variables.md +171 -0
  18. data/docs/gateway-development.md +723 -0
  19. data/docs/getting-started.md +429 -0
  20. data/docs/instrumentation.md +264 -153
  21. data/docs/platforms/telegram.md +1013 -0
  22. data/docs/platforms/ussd.md +693 -0
  23. data/docs/platforms/whatsapp.md +1395 -0
  24. data/docs/testing.md +243 -365
  25. data/examples/custom_session_id_example.rb +119 -0
  26. data/examples/http_controller.rb +154 -0
  27. data/examples/intercom_configuration_example.rb +118 -0
  28. data/examples/intercom_controller.rb +194 -0
  29. data/examples/multi_tenant_whatsapp_controller.rb +4 -4
  30. data/examples/simulator_controller.rb +21 -1
  31. data/examples/ussd_controller.rb +10 -10
  32. data/examples/whatsapp_controller.rb +2 -2
  33. data/flow_chat.gemspec +4 -0
  34. data/lib/flow_chat/{base_app.rb → app.rb} +28 -9
  35. data/lib/flow_chat/async_job.rb +176 -0
  36. data/lib/flow_chat/config.rb +23 -28
  37. data/lib/flow_chat/{base_executor.rb → executor.rb} +7 -12
  38. data/lib/flow_chat/factory.rb +94 -0
  39. data/lib/flow_chat/gateway_async_support.rb +106 -0
  40. data/lib/flow_chat/generic_async_job.rb +30 -0
  41. data/lib/flow_chat/http/gateway/simple.rb +125 -0
  42. data/lib/flow_chat/http/renderer.rb +41 -0
  43. data/lib/flow_chat/instrumentation/setup.rb +1 -1
  44. data/lib/flow_chat/instrumentation.rb +25 -0
  45. data/lib/flow_chat/intercom/client.rb +155 -0
  46. data/lib/flow_chat/intercom/configuration.rb +149 -0
  47. data/lib/flow_chat/intercom/gateway/intercom_api.rb +396 -0
  48. data/lib/flow_chat/intercom/renderer.rb +71 -0
  49. data/lib/flow_chat/phone_number_util.rb +49 -0
  50. data/lib/flow_chat/processor.rb +188 -0
  51. data/lib/flow_chat/renderers/markdown_support.rb +58 -0
  52. data/lib/flow_chat/session/cache_session_store.rb +1 -17
  53. data/lib/flow_chat/session/middleware.rb +43 -26
  54. data/lib/flow_chat/simulator/controller.rb +17 -5
  55. data/lib/flow_chat/simulator/views/simulator.html.erb +220 -8
  56. data/lib/flow_chat/telegram/client.rb +240 -0
  57. data/lib/flow_chat/telegram/configuration.rb +118 -0
  58. data/lib/flow_chat/telegram/gateway/bot_api.rb +300 -0
  59. data/lib/flow_chat/telegram/middleware/choice_mapper.rb +35 -0
  60. data/lib/flow_chat/telegram/renderer.rb +125 -0
  61. data/lib/flow_chat/telegram.rb +7 -0
  62. data/lib/flow_chat/ussd/gateway/nalo.rb +27 -11
  63. data/lib/flow_chat/ussd/middleware/pagination.rb +9 -5
  64. data/lib/flow_chat/ussd/renderer.rb +1 -1
  65. data/lib/flow_chat/version.rb +1 -1
  66. data/lib/flow_chat/whatsapp/client.rb +144 -13
  67. data/lib/flow_chat/whatsapp/configuration.rb +1 -1
  68. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +144 -106
  69. data/lib/flow_chat/whatsapp/id_generator.rb +124 -0
  70. data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +147 -0
  71. data/lib/flow_chat/whatsapp/renderer.rb +145 -11
  72. data/lib/flow_chat.rb +11 -1
  73. data/lib/tasks/release.rake +165 -0
  74. metadata +88 -21
  75. data/docs/flows.md +0 -320
  76. data/docs/images/simulator.png +0 -0
  77. data/docs/media.md +0 -153
  78. data/docs/sessions.md +0 -433
  79. data/docs/ussd-setup.md +0 -322
  80. data/docs/whatsapp-setup.md +0 -162
  81. data/examples/whatsapp_message_job.rb +0 -113
  82. data/lib/flow_chat/base_processor.rb +0 -145
  83. data/lib/flow_chat/session/rails_session_store.rb +0 -68
  84. data/lib/flow_chat/ussd/app.rb +0 -6
  85. data/lib/flow_chat/ussd/gateway/nsano.rb +0 -98
  86. data/lib/flow_chat/ussd/middleware/executor.rb +0 -24
  87. data/lib/flow_chat/ussd/processor.rb +0 -39
  88. data/lib/flow_chat/whatsapp/app.rb +0 -29
  89. data/lib/flow_chat/whatsapp/middleware/executor.rb +0 -24
  90. data/lib/flow_chat/whatsapp/processor.rb +0 -32
  91. data/lib/flow_chat/whatsapp/send_job_support.rb +0 -79
@@ -0,0 +1,1013 @@
1
+ # Telegram Development Guide
2
+
3
+ FlowChat provides comprehensive Telegram Bot API integration with support for inline keyboards, rich media, secure webhook validation, and flexible deployment modes.
4
+
5
+ ## Key Features
6
+
7
+ - **Inline Keyboards**: Interactive buttons with callback data
8
+ - **Rich Media Support**: Photos, documents, videos, audio, and voice messages
9
+ - **Secure Webhooks**: Secret token validation via X-Telegram-Bot-Api-Secret-Token
10
+ - **Multiple Processing Modes**: Inline, background, and async
11
+ - **Callback Query Handling**: Automatic acknowledgment and processing
12
+ - **Multi-Tenant Support**: Named configurations for different bots
13
+ - **Location and Contact Sharing**: Handle user-shared locations and contacts
14
+
15
+ ## Quick Start
16
+
17
+ ### 1. Create Controller
18
+
19
+ ```ruby
20
+ # app/controllers/telegram_controller.rb
21
+ class TelegramController < ApplicationController
22
+ skip_forgery_protection
23
+
24
+ def webhook
25
+ processor = FlowChat::Processor.new(self) do |config|
26
+ config.use_gateway FlowChat::Telegram::Gateway::BotApi
27
+ config.use_session_store FlowChat::Session::CacheSessionStore
28
+ end
29
+
30
+ processor.run WelcomeFlow, :main_page
31
+ rescue => e
32
+ Rails.logger.error { "Telegram webhook error: #{e.message}" }
33
+ head :internal_server_error
34
+ end
35
+ end
36
+ ```
37
+
38
+ ### 2. Add Routes
39
+
40
+ ```ruby
41
+ # config/routes.rb
42
+ post '/telegram/webhook', to: 'telegram#webhook'
43
+ ```
44
+
45
+ ### 3. Create a Flow
46
+
47
+ ```ruby
48
+ # app/flow_chat/welcome_flow.rb
49
+ class WelcomeFlow < FlowChat::Flow
50
+ def main_page
51
+ name = app.screen(:name) do |prompt|
52
+ prompt.ask "Hello! What's your name?",
53
+ transform: ->(input) { input.strip.titleize }
54
+ end
55
+
56
+ choice = app.screen(:main_menu) do |prompt|
57
+ prompt.select "Hi #{name}! How can I help?", {
58
+ "info" => "Get Information",
59
+ "support" => "Contact Support",
60
+ "feedback" => "Give Feedback"
61
+ }
62
+ end
63
+
64
+ case choice
65
+ when "info"
66
+ show_info
67
+ when "support"
68
+ contact_support
69
+ when "feedback"
70
+ collect_feedback
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def show_info
77
+ app.say "Located at 123 Main Street\nHours: Mon-Fri 9AM-6PM"
78
+ end
79
+
80
+ def contact_support
81
+ app.say "Call us at (555) 123-4567\nEmail: support@example.com"
82
+ end
83
+
84
+ def collect_feedback
85
+ rating = app.screen(:rating) do |prompt|
86
+ prompt.select "Rate our service:", {
87
+ "1" => "1 Star",
88
+ "2" => "2 Stars",
89
+ "3" => "3 Stars",
90
+ "4" => "4 Stars",
91
+ "5" => "5 Stars"
92
+ }
93
+ end
94
+
95
+ app.say "Thank you for your #{rating}-star rating!"
96
+ end
97
+ end
98
+ ```
99
+
100
+ ## Configuration
101
+
102
+ ### Option 1: Rails Credentials (Recommended)
103
+
104
+ ```bash
105
+ rails credentials:edit
106
+ ```
107
+
108
+ ```yaml
109
+ telegram:
110
+ bot_token: "123456789:ABCdefGHIjklMNOpqrsTUVwxyz"
111
+ secret_token: "your_webhook_secret_token"
112
+ skip_signature_validation: false # Set to true only for development
113
+ ```
114
+
115
+ ### Option 2: Environment Variables
116
+
117
+ ```bash
118
+ export TELEGRAM_BOT_TOKEN="123456789:ABCdefGHIjklMNOpqrsTUVwxyz"
119
+ export TELEGRAM_SECRET_TOKEN="your_webhook_secret_token"
120
+ export TELEGRAM_SKIP_SIGNATURE_VALIDATION="false"
121
+ ```
122
+
123
+ ### Option 3: Programmatic Configuration
124
+
125
+ ```ruby
126
+ telegram_config = FlowChat::Telegram::Configuration.new(:my_bot)
127
+ telegram_config.bot_token = "123456789:ABCdefGHIjklMNOpqrsTUVwxyz"
128
+ telegram_config.secret_token = "your_webhook_secret_token"
129
+ telegram_config.skip_signature_validation = false
130
+
131
+ processor = FlowChat::Processor.new(self) do |config|
132
+ config.use_gateway FlowChat::Telegram::Gateway::BotApi, telegram_config
133
+ config.use_session_store FlowChat::Session::CacheSessionStore
134
+ end
135
+ ```
136
+
137
+ ### Configuration Options
138
+
139
+ | Option | Description | Required |
140
+ |--------|-------------|----------|
141
+ | `bot_token` | Your Telegram bot token from @BotFather | Yes |
142
+ | `secret_token` | Secret token for webhook validation | No (recommended) |
143
+ | `skip_signature_validation` | Disable webhook signature checks | No (default: false) |
144
+
145
+ ### Configuration Helper Methods
146
+
147
+ The configuration object provides several helper methods:
148
+
149
+ ```ruby
150
+ config = FlowChat::Telegram::Configuration.from_credentials
151
+
152
+ # Check if configuration is valid
153
+ config.valid? # => true if bot_token is present
154
+
155
+ # Get API base URL
156
+ config.api_base_url # => "https://api.telegram.org/bot123456789:ABC..."
157
+
158
+ # Get bot ID from token
159
+ config.bot_id # => "123456789"
160
+
161
+ # API endpoint URLs
162
+ config.send_message_url
163
+ config.set_webhook_url
164
+ config.get_webhook_info_url
165
+ config.delete_webhook_url
166
+ ```
167
+
168
+ ## Webhook Setup
169
+
170
+ Before your bot can receive messages, you must register your webhook URL with Telegram.
171
+
172
+ ### Using the Client
173
+
174
+ ```ruby
175
+ # In a Rails console or initializer
176
+ config = FlowChat::Telegram::Configuration.from_credentials
177
+ client = FlowChat::Telegram::Client.new(config)
178
+
179
+ # Set webhook with secret token for validation
180
+ result = client.set_webhook(
181
+ "https://yourdomain.com/telegram/webhook",
182
+ secret_token: config.secret_token,
183
+ allowed_updates: ["message", "callback_query"]
184
+ )
185
+
186
+ if result["ok"]
187
+ puts "Webhook set successfully!"
188
+ else
189
+ puts "Error: #{result["description"]}"
190
+ end
191
+ ```
192
+
193
+ ### Using a Rake Task
194
+
195
+ ```ruby
196
+ # lib/tasks/telegram.rake
197
+ namespace :telegram do
198
+ desc "Set up Telegram webhook"
199
+ task setup_webhook: :environment do
200
+ config = FlowChat::Telegram::Configuration.from_credentials
201
+ client = FlowChat::Telegram::Client.new(config)
202
+
203
+ webhook_url = ENV["TELEGRAM_WEBHOOK_URL"] || "https://yourdomain.com/telegram/webhook"
204
+
205
+ result = client.set_webhook(
206
+ webhook_url,
207
+ secret_token: config.secret_token
208
+ )
209
+
210
+ if result["ok"]
211
+ puts "Webhook configured: #{webhook_url}"
212
+ else
213
+ puts "Failed to set webhook: #{result["description"]}"
214
+ end
215
+ end
216
+
217
+ desc "Get webhook info"
218
+ task webhook_info: :environment do
219
+ config = FlowChat::Telegram::Configuration.from_credentials
220
+ client = FlowChat::Telegram::Client.new(config)
221
+
222
+ info = client.get_webhook_info
223
+ puts JSON.pretty_generate(info)
224
+ end
225
+
226
+ desc "Delete webhook"
227
+ task delete_webhook: :environment do
228
+ config = FlowChat::Telegram::Configuration.from_credentials
229
+ client = FlowChat::Telegram::Client.new(config)
230
+
231
+ result = client.delete_webhook
232
+ puts result["ok"] ? "Webhook deleted" : "Error: #{result["description"]}"
233
+ end
234
+ end
235
+ ```
236
+
237
+ ### Webhook Requirements
238
+
239
+ - **HTTPS**: Telegram requires a valid SSL certificate
240
+ - **Port**: Must be on port 443, 80, 88, or 8443
241
+ - **Response Time**: Respond within 60 seconds (use background processing for slow flows)
242
+
243
+ ## Inline Keyboards
244
+
245
+ FlowChat automatically renders choices as inline keyboards:
246
+
247
+ ### Basic Keyboard
248
+
249
+ ```ruby
250
+ def main_menu
251
+ choice = app.screen(:menu) do |prompt|
252
+ prompt.select "What would you like to do?", {
253
+ "balance" => "Check Balance",
254
+ "transfer" => "Transfer Money",
255
+ "history" => "View History"
256
+ }
257
+ end
258
+
259
+ handle_choice(choice)
260
+ end
261
+ ```
262
+
263
+ ### Keyboard Layout
264
+
265
+ The renderer automatically arranges buttons based on the number of choices:
266
+
267
+ - **1-4 choices**: 2 buttons per row
268
+ - **5+ choices**: 1 button per row
269
+
270
+ ```ruby
271
+ # 4 choices = 2x2 grid
272
+ prompt.select "Pick one:", {
273
+ "a" => "Option A",
274
+ "b" => "Option B",
275
+ "c" => "Option C",
276
+ "d" => "Option D"
277
+ }
278
+
279
+ # 5+ choices = vertical list
280
+ prompt.select "Select category:", {
281
+ "electronics" => "Electronics",
282
+ "clothing" => "Clothing",
283
+ "books" => "Books",
284
+ "home" => "Home & Garden",
285
+ "sports" => "Sports & Outdoors"
286
+ }
287
+ ```
288
+
289
+ ### Button Limits
290
+
291
+ - **Button text**: Maximum 64 characters (auto-truncated with "...")
292
+ - **Callback data**: Maximum 64 characters per button
293
+
294
+ ## Rich Media Support
295
+
296
+ ### Sending Media in Flows
297
+
298
+ ```ruby
299
+ def product_showcase
300
+ # Photo with prompt
301
+ feedback = app.screen(:product_feedback) do |prompt|
302
+ prompt.ask "What do you think of this product?",
303
+ media: {
304
+ type: :photo,
305
+ url: "https://example.com/product.jpg"
306
+ }
307
+ end
308
+
309
+ # Document response
310
+ app.say "Thanks! Here's the product catalog:",
311
+ media: {
312
+ type: :document,
313
+ url: "https://example.com/catalog.pdf",
314
+ filename: "product_catalog.pdf"
315
+ }
316
+ end
317
+
318
+ def video_tutorial
319
+ app.say "Watch this tutorial:",
320
+ media: {
321
+ type: :video,
322
+ url: "https://example.com/tutorial.mp4"
323
+ }
324
+ end
325
+ ```
326
+
327
+ ### Media Types Supported
328
+
329
+ | Type | Usage | Options |
330
+ |------|-------|---------|
331
+ | `:photo` | Photos, images | `url` or `file_id` |
332
+ | `:document` | PDFs, files | `url` or `file_id`, `filename` |
333
+ | `:video` | Video files | `url` or `file_id` |
334
+ | `:audio` | Audio files | `url` or `file_id` |
335
+ | `:voice` | Voice messages | `url` or `file_id` |
336
+
337
+ ### Media with Inline Keyboard
338
+
339
+ ```ruby
340
+ def media_menu
341
+ choice = app.screen(:options) do |prompt|
342
+ prompt.select "Choose an option:",
343
+ {
344
+ "details" => "More Details",
345
+ "buy" => "Buy Now",
346
+ "share" => "Share"
347
+ },
348
+ media: {
349
+ type: :photo,
350
+ url: "https://example.com/product.jpg"
351
+ }
352
+ end
353
+ end
354
+ ```
355
+
356
+ ### Handling Incoming Media
357
+
358
+ ```ruby
359
+ def handle_media_upload
360
+ media = context["request.media"]
361
+
362
+ if media
363
+ case media[:type]
364
+ when :photo
365
+ app.say "Thanks for the photo! Processing..."
366
+ # Access file_id: media[:file_id]
367
+ when :document
368
+ app.say "Document received: #{media[:file_name]}"
369
+ # Access mime_type: media[:mime_type]
370
+ when :voice
371
+ app.say "Got your voice message (#{media[:duration]}s)"
372
+ end
373
+ else
374
+ app.say "Please send a photo."
375
+ end
376
+ end
377
+ ```
378
+
379
+ ### Handling Location
380
+
381
+ ```ruby
382
+ def handle_location
383
+ location = context["request.location"]
384
+
385
+ if location
386
+ lat = location["latitude"]
387
+ lon = location["longitude"]
388
+ app.say "You're at: #{lat}, #{lon}"
389
+ else
390
+ app.say "Please share your location."
391
+ end
392
+ end
393
+ ```
394
+
395
+ ### Handling Contact
396
+
397
+ ```ruby
398
+ def handle_contact
399
+ contact = context["request.contact"]
400
+
401
+ if contact
402
+ phone = contact[:phone_number]
403
+ name = contact[:first_name]
404
+ app.say "Thanks #{name}! We'll contact you at #{phone}."
405
+ else
406
+ app.say "Please share your contact."
407
+ end
408
+ end
409
+ ```
410
+
411
+ ## Telegram Client API
412
+
413
+ The `FlowChat::Telegram::Client` provides methods for sending messages outside of flows.
414
+
415
+ ### Client Initialization
416
+
417
+ ```ruby
418
+ # From credentials/environment variables
419
+ config = FlowChat::Telegram::Configuration.from_credentials
420
+ client = FlowChat::Telegram::Client.new(config)
421
+
422
+ # Using named configuration
423
+ config = FlowChat::Telegram::Configuration.get(:my_bot)
424
+ client = FlowChat::Telegram::Client.new(config)
425
+ ```
426
+
427
+ ### Text Messages
428
+
429
+ ```ruby
430
+ # Simple text message
431
+ client.send_text(chat_id, "Hello! How can I help you today?")
432
+
433
+ # With parse mode (default is HTML)
434
+ client.send_text(chat_id, "<b>Bold</b> and <i>italic</i>", parse_mode: "HTML")
435
+ client.send_text(chat_id, "*Bold* and _italic_", parse_mode: "Markdown")
436
+ ```
437
+
438
+ ### Messages with Inline Keyboard
439
+
440
+ ```ruby
441
+ keyboard = [
442
+ [
443
+ { text: "Option 1", callback_data: "opt1" },
444
+ { text: "Option 2", callback_data: "opt2" }
445
+ ],
446
+ [
447
+ { text: "Option 3", callback_data: "opt3" }
448
+ ]
449
+ ]
450
+
451
+ client.send_text_with_keyboard(chat_id, "Choose an option:", keyboard)
452
+ ```
453
+
454
+ ### Media Messages
455
+
456
+ ```ruby
457
+ # Send photo
458
+ client.send_photo(chat_id, "https://example.com/image.jpg", caption: "Check this out!")
459
+
460
+ # Send document
461
+ client.send_document(chat_id, "https://example.com/file.pdf", caption: "Your document")
462
+
463
+ # Send video
464
+ client.send_video(chat_id, "https://example.com/video.mp4", caption: "Watch this")
465
+
466
+ # Send audio
467
+ client.send_audio(chat_id, "https://example.com/audio.mp3", caption: "Listen to this")
468
+
469
+ # Send voice message
470
+ client.send_voice(chat_id, "https://example.com/voice.ogg")
471
+ ```
472
+
473
+ ### Photo with Inline Keyboard
474
+
475
+ ```ruby
476
+ keyboard = [
477
+ [{ text: "Like", callback_data: "like" }, { text: "Share", callback_data: "share" }]
478
+ ]
479
+
480
+ client.send_photo_with_keyboard(
481
+ chat_id,
482
+ "https://example.com/product.jpg",
483
+ caption: "New product!",
484
+ keyboard: keyboard
485
+ )
486
+ ```
487
+
488
+ ### Message Management
489
+
490
+ ```ruby
491
+ # Edit message text
492
+ client.edit_message_text(chat_id, message_id, "Updated text", keyboard: new_keyboard)
493
+
494
+ # Delete message
495
+ client.delete_message(chat_id, message_id)
496
+
497
+ # Answer callback query (acknowledge button press)
498
+ client.answer_callback_query(callback_query_id, text: "Processing...", show_alert: false)
499
+ ```
500
+
501
+ ### Chat Actions / Typing Indicator
502
+
503
+ Broadcast a chat action (e.g. "typing…") to a Telegram chat. The action lasts ~5 seconds or until the next outbound message; there is **no stop-typing call**.
504
+
505
+ ```ruby
506
+ client = FlowChat::Telegram::Client.new(config)
507
+
508
+ # Show "typing…"
509
+ client.send_chat_action(chat_id)
510
+
511
+ # Convenience equivalent of the line above
512
+ client.indicate_typing(chat_id)
513
+
514
+ # Other actions (e.g. while uploading a generated image)
515
+ client.send_chat_action(chat_id, action: "upload_photo")
516
+ ```
517
+
518
+ Valid actions per the Telegram Bot API: `typing`, `upload_photo`, `record_video`, `upload_video`, `record_voice`, `upload_voice`, `upload_document`, `choose_sticker`, `find_location`, `record_video_note`, `upload_video_note`. FlowChat does not validate the action client-side; if Telegram rejects it the response will contain `"ok" => false` with the error description.
519
+
520
+ ### Bot Information
521
+
522
+ ```ruby
523
+ # Get bot info
524
+ bot_info = client.get_me
525
+ puts "Bot username: @#{bot_info["result"]["username"]}"
526
+ ```
527
+
528
+ ### Complete Service Example
529
+
530
+ ```ruby
531
+ # Service for sending notifications
532
+ class TelegramNotificationService
533
+ def initialize(config_name = :default)
534
+ @config = FlowChat::Telegram::Configuration.get(config_name)
535
+ @client = FlowChat::Telegram::Client.new(@config)
536
+ end
537
+
538
+ def send_order_confirmation(chat_id, order_id)
539
+ # Send confirmation message with buttons
540
+ keyboard = [
541
+ [
542
+ { text: "Track Order", callback_data: "track_#{order_id}" },
543
+ { text: "Contact Support", callback_data: "support" }
544
+ ]
545
+ ]
546
+
547
+ @client.send_text_with_keyboard(
548
+ chat_id,
549
+ "Order ##{order_id} confirmed!\n\nThank you for your purchase.",
550
+ keyboard
551
+ )
552
+ end
553
+
554
+ def send_document_notification(chat_id, document_url, filename)
555
+ @client.send_document(
556
+ chat_id,
557
+ document_url,
558
+ caption: "Your document is ready: #{filename}"
559
+ )
560
+ end
561
+
562
+ def broadcast_message(chat_ids, message)
563
+ chat_ids.each do |chat_id|
564
+ @client.send_text(chat_id, message)
565
+ rescue => e
566
+ Rails.logger.error { "Failed to send to #{chat_id}: #{e.message}" }
567
+ end
568
+ end
569
+ end
570
+
571
+ # Usage
572
+ service = TelegramNotificationService.new(:main_bot)
573
+ service.send_order_confirmation(123456789, "ORD-12345")
574
+ ```
575
+
576
+ ## Context Variables
577
+
578
+ The gateway sets these context variables during request processing:
579
+
580
+ ### Request Context
581
+
582
+ | Variable | Description | Example |
583
+ |----------|-------------|---------|
584
+ | `request.id` | Chat ID | `"123456789"` |
585
+ | `request.user_id` | User ID | `"987654321"` |
586
+ | `request.user_name` | User's full name | `"John Doe"` |
587
+ | `request.username` | Telegram username | `"johndoe"` |
588
+ | `request.gateway` | Gateway identifier | `:telegram_bot_api` |
589
+ | `request.platform` | Platform identifier | `:telegram` |
590
+ | `request.message_id` | Message ID | `"456"` |
591
+ | `request.timestamp` | ISO8601 timestamp | `"2024-01-15T10:30:00Z"` |
592
+ | `request.body` | Raw webhook body | `{...}` |
593
+
594
+ ### Telegram-Specific Context
595
+
596
+ | Variable | Description |
597
+ |----------|-------------|
598
+ | `telegram.client` | Telegram client instance |
599
+ | `telegram.chat_type` | Chat type (`private`, `group`, `supergroup`, `channel`) |
600
+ | `telegram.callback_query_id` | Callback query ID (for button presses) |
601
+ | `telegram.original_message_id` | Original message ID (for callbacks) |
602
+
603
+ ### Media Context
604
+
605
+ | Variable | Description |
606
+ |----------|-------------|
607
+ | `request.media` | Media info hash with `:type`, `:file_id`, etc. |
608
+ | `request.location` | Location hash with `latitude`, `longitude` |
609
+ | `request.contact` | Contact hash with `:phone_number`, `:first_name`, etc. |
610
+
611
+ ### Accessing Context in Flows
612
+
613
+ ```ruby
614
+ class MyFlow < FlowChat::Flow
615
+ def main_page
616
+ chat_id = context["request.id"]
617
+ user_name = context["request.user_name"]
618
+ chat_type = context["telegram.chat_type"]
619
+
620
+ # Access Telegram client for custom operations
621
+ client = context["telegram.client"]
622
+
623
+ if chat_type == "group"
624
+ app.say "Hello, group members!"
625
+ else
626
+ app.say "Hello, #{user_name}!"
627
+ end
628
+ end
629
+ end
630
+ ```
631
+
632
+ ## Security and Validation
633
+
634
+ ### Webhook Signature Validation
635
+
636
+ Telegram sends a secret token in the `X-Telegram-Bot-Api-Secret-Token` header that you define when setting up the webhook. FlowChat validates this automatically.
637
+
638
+ #### How It Works
639
+
640
+ 1. You set a `secret_token` when registering the webhook
641
+ 2. Telegram includes this token in every webhook request
642
+ 3. FlowChat compares the received token with your configured `secret_token`
643
+ 4. Requests with invalid tokens are rejected with 401 Unauthorized
644
+
645
+ #### Production Configuration (Recommended)
646
+
647
+ ```ruby
648
+ # config/credentials/production.yml
649
+ telegram:
650
+ bot_token: "123456789:ABCdefGHIjklMNOpqrsTUVwxyz"
651
+ secret_token: "a_very_secure_random_string_at_least_32_chars"
652
+ skip_signature_validation: false
653
+ ```
654
+
655
+ ```ruby
656
+ # Set up webhook with secret token
657
+ client.set_webhook(
658
+ "https://yourdomain.com/telegram/webhook",
659
+ secret_token: "a_very_secure_random_string_at_least_32_chars"
660
+ )
661
+ ```
662
+
663
+ #### Development Configuration
664
+
665
+ ```ruby
666
+ # Option 1: Use real secret token (recommended for staging)
667
+ config = FlowChat::Telegram::Configuration.new(:dev_bot)
668
+ config.bot_token = "your_token"
669
+ config.secret_token = "your_secret"
670
+ config.skip_signature_validation = false
671
+
672
+ # Option 2: Disable validation (development only)
673
+ config = FlowChat::Telegram::Configuration.new(:dev_bot)
674
+ config.bot_token = "your_token"
675
+ config.skip_signature_validation = true
676
+ ```
677
+
678
+ #### Security Warning
679
+
680
+ Never disable signature validation in production. An attacker could send fake webhook requests to your endpoint.
681
+
682
+ ### Error Handling
683
+
684
+ ```ruby
685
+ def webhook
686
+ processor = FlowChat::Processor.new(self) do |config|
687
+ config.use_gateway FlowChat::Telegram::Gateway::BotApi
688
+ config.use_session_store FlowChat::Session::CacheSessionStore
689
+ end
690
+
691
+ processor.run WelcomeFlow, :main_page
692
+ rescue FlowChat::Telegram::ConfigurationError => e
693
+ Rails.logger.error { "Telegram configuration error: #{e.message}" }
694
+ head :internal_server_error
695
+ rescue => e
696
+ Rails.logger.error { "Telegram webhook error: #{e.message}" }
697
+ head :internal_server_error
698
+ end
699
+ ```
700
+
701
+ ## Multi-Tenant Support
702
+
703
+ ### Named Configurations
704
+
705
+ ```ruby
706
+ # config/initializers/telegram_configs.rb
707
+ bot_a_config = FlowChat::Telegram::Configuration.new(:bot_a)
708
+ bot_a_config.bot_token = "bot_a_token"
709
+ bot_a_config.secret_token = "bot_a_secret"
710
+
711
+ bot_b_config = FlowChat::Telegram::Configuration.new(:bot_b)
712
+ bot_b_config.bot_token = "bot_b_token"
713
+ bot_b_config.secret_token = "bot_b_secret"
714
+ ```
715
+
716
+ ### Configuration API
717
+
718
+ ```ruby
719
+ # Check if configuration exists
720
+ FlowChat::Telegram::Configuration.exists?(:bot_a) # => true/false
721
+
722
+ # Get configuration by name
723
+ config = FlowChat::Telegram::Configuration.get(:bot_a)
724
+
725
+ # List all registered configurations
726
+ FlowChat::Telegram::Configuration.configuration_names # => [:bot_a, :bot_b]
727
+
728
+ # Clear all configurations (useful for testing)
729
+ FlowChat::Telegram::Configuration.clear_all!
730
+ ```
731
+
732
+ ### Multi-Bot Controller
733
+
734
+ ```ruby
735
+ class MultiTelegramController < ApplicationController
736
+ skip_forgery_protection
737
+
738
+ def webhook
739
+ bot_name = params[:bot_name]&.to_sym
740
+
741
+ unless FlowChat::Telegram::Configuration.exists?(bot_name)
742
+ Rails.logger.error { "No configuration found for bot: #{bot_name}" }
743
+ head :not_found
744
+ return
745
+ end
746
+
747
+ config = FlowChat::Telegram::Configuration.get(bot_name)
748
+
749
+ processor = FlowChat::Processor.new(self) do |c|
750
+ c.use_gateway FlowChat::Telegram::Gateway::BotApi, config
751
+ c.use_session_store FlowChat::Session::CacheSessionStore
752
+ c.use_session_config(
753
+ boundaries: [:flow, :platform, :bot],
754
+ identifier: :user_id,
755
+ bot: bot_name
756
+ )
757
+ end
758
+
759
+ processor.run WelcomeFlow, :main_page
760
+ end
761
+ end
762
+ ```
763
+
764
+ ```ruby
765
+ # config/routes.rb
766
+ post '/telegram/:bot_name/webhook', to: 'multi_telegram#webhook'
767
+ ```
768
+
769
+ ## Testing and Development
770
+
771
+ ### Local Development with ngrok
772
+
773
+ ```bash
774
+ # Start ngrok tunnel
775
+ ngrok http 3000
776
+
777
+ # Use the HTTPS URL for webhook
778
+ # https://abc123.ngrok.io/telegram/webhook
779
+ ```
780
+
781
+ ### Test Setup
782
+
783
+ ```ruby
784
+ # test/integration/telegram_test.rb
785
+ class TelegramIntegrationTest < ActionDispatch::IntegrationTest
786
+ def setup
787
+ @config = FlowChat::Telegram::Configuration.new(:test)
788
+ @config.bot_token = "test_token"
789
+ @config.skip_signature_validation = true
790
+ end
791
+
792
+ def test_text_message_webhook
793
+ webhook_data = {
794
+ update_id: 123,
795
+ message: {
796
+ message_id: 1,
797
+ from: { id: 123456, first_name: "Test", username: "testuser" },
798
+ chat: { id: 123456, type: "private" },
799
+ date: Time.now.to_i,
800
+ text: "Hello"
801
+ }
802
+ }
803
+
804
+ post "/telegram/webhook",
805
+ params: webhook_data.to_json,
806
+ headers: { "Content-Type" => "application/json" }
807
+
808
+ assert_response :ok
809
+ end
810
+
811
+ def test_callback_query_webhook
812
+ webhook_data = {
813
+ update_id: 124,
814
+ callback_query: {
815
+ id: "callback_123",
816
+ from: { id: 123456, first_name: "Test" },
817
+ message: {
818
+ message_id: 1,
819
+ chat: { id: 123456, type: "private" }
820
+ },
821
+ data: "option_1"
822
+ }
823
+ }
824
+
825
+ post "/telegram/webhook",
826
+ params: webhook_data.to_json,
827
+ headers: { "Content-Type" => "application/json" }
828
+
829
+ assert_response :ok
830
+ end
831
+ end
832
+ ```
833
+
834
+ ### Mock Client for Tests
835
+
836
+ ```ruby
837
+ class MockTelegramClient
838
+ attr_reader :sent_messages
839
+
840
+ def initialize
841
+ @sent_messages = []
842
+ end
843
+
844
+ def send_message(chat_id, prompt, choices: nil, media: nil)
845
+ @sent_messages << {
846
+ chat_id: chat_id,
847
+ prompt: prompt,
848
+ choices: choices,
849
+ media: media
850
+ }
851
+ { "ok" => true, "result" => { "message_id" => 123 } }
852
+ end
853
+
854
+ def answer_callback_query(*)
855
+ { "ok" => true }
856
+ end
857
+ end
858
+ ```
859
+
860
+ ## Troubleshooting
861
+
862
+ ### Common Issues
863
+
864
+ **1. Webhook not receiving updates**
865
+ ```
866
+ No updates being received at webhook endpoint
867
+ ```
868
+ **Solution**:
869
+ - Verify webhook is set: `client.get_webhook_info`
870
+ - Check URL is HTTPS with valid certificate
871
+ - Ensure secret_token matches (if using validation)
872
+ - Check server logs for 401 errors
873
+
874
+ **2. Invalid webhook signature**
875
+ ```
876
+ 401 Unauthorized responses
877
+ ```
878
+ **Solution**:
879
+ - Verify `secret_token` matches what was set in `set_webhook`
880
+ - Check `X-Telegram-Bot-Api-Secret-Token` header is being sent
881
+ - For development, set `skip_signature_validation: true`
882
+
883
+ **3. Callback query not working**
884
+ ```
885
+ Button presses don't trigger flow
886
+ ```
887
+ **Solution**:
888
+ - Ensure `callback_query` is in `allowed_updates` when setting webhook
889
+ - Check callback_data is within 64 character limit
890
+ - Verify the callback is being answered (done automatically by gateway)
891
+
892
+ **4. Media not sending**
893
+ ```
894
+ Photos/documents fail to send
895
+ ```
896
+ **Solution**:
897
+ - Use HTTPS URLs for media
898
+ - Check file size limits (photos: 10MB, documents: 50MB)
899
+ - Verify MIME types are correct
900
+ - For large files, upload first and use file_id
901
+
902
+ ### Debug Mode
903
+
904
+ Enable debug logging:
905
+
906
+ ```ruby
907
+ # config/environments/development.rb
908
+ config.log_level = :debug
909
+
910
+ # This will log:
911
+ # - Webhook request parsing
912
+ # - Signature validation
913
+ # - Message extraction
914
+ # - API requests/responses
915
+ ```
916
+
917
+ ### Webhook Diagnostics
918
+
919
+ ```ruby
920
+ # Check webhook status
921
+ config = FlowChat::Telegram::Configuration.from_credentials
922
+ client = FlowChat::Telegram::Client.new(config)
923
+
924
+ info = client.get_webhook_info
925
+ puts "URL: #{info.dig("result", "url")}"
926
+ puts "Pending updates: #{info.dig("result", "pending_update_count")}"
927
+ puts "Last error: #{info.dig("result", "last_error_message")}"
928
+ puts "Last error date: #{info.dig("result", "last_error_date")}"
929
+ ```
930
+
931
+ ## Best Practices
932
+
933
+ ### 1. Message Design
934
+ - Keep button text concise (under 64 characters)
935
+ - Use clear, actionable labels
936
+ - Limit inline keyboard to reasonable number of options
937
+ - Consider user experience on mobile
938
+
939
+ ### 2. Media Usage
940
+ - Compress images appropriately
941
+ - Provide meaningful captions
942
+ - Use appropriate media types
943
+ - Consider bandwidth limitations
944
+
945
+ ### 3. Flow Design
946
+ - Handle unexpected input gracefully
947
+ - Provide clear error messages
948
+ - Use progressive disclosure for complex flows
949
+ - Always acknowledge user actions
950
+
951
+ ### 4. Security
952
+ - Always validate webhooks in production
953
+ - Use environment variables for tokens
954
+ - Rotate secret tokens periodically
955
+ - Monitor for unusual patterns
956
+
957
+ ### 5. Performance
958
+ - Use background processing for slow operations
959
+ - Respond to webhooks quickly (under 60s)
960
+ - Cache frequently used data
961
+ - Handle rate limits gracefully
962
+
963
+ ## API Reference
964
+
965
+ ### Core Classes
966
+
967
+ | Class | Description |
968
+ |-------|-------------|
969
+ | `FlowChat::Telegram::Gateway::BotApi` | Telegram Bot API gateway |
970
+ | `FlowChat::Telegram::Client` | API client for messaging |
971
+ | `FlowChat::Telegram::Configuration` | Configuration management |
972
+ | `FlowChat::Telegram::Renderer` | Message rendering logic |
973
+ | `FlowChat::Telegram::Middleware::ChoiceMapper` | Choice validation middleware |
974
+
975
+ ### Client Methods
976
+
977
+ | Method | Description | Parameters |
978
+ |--------|-------------|------------|
979
+ | `send_message(chat_id, prompt, choices:, media:)` | Send FlowChat response | chat_id, prompt, options |
980
+ | `send_text(chat_id, text, parse_mode:)` | Send text message | chat_id, text, parse_mode |
981
+ | `send_text_with_keyboard(chat_id, text, keyboard)` | Send text with inline keyboard | chat_id, text, keyboard array |
982
+ | `send_photo(chat_id, photo, caption:)` | Send photo | chat_id, URL/file_id, caption |
983
+ | `send_photo_with_keyboard(chat_id, photo, caption:, keyboard:)` | Send photo with buttons | chat_id, photo, options |
984
+ | `send_document(chat_id, document, caption:)` | Send document | chat_id, URL/file_id, caption |
985
+ | `send_video(chat_id, video, caption:)` | Send video | chat_id, URL/file_id, caption |
986
+ | `send_audio(chat_id, audio, caption:)` | Send audio | chat_id, URL/file_id, caption |
987
+ | `send_voice(chat_id, voice)` | Send voice message | chat_id, URL/file_id |
988
+ | `edit_message_text(chat_id, message_id, text, keyboard:)` | Edit message | chat_id, msg_id, text, keyboard |
989
+ | `delete_message(chat_id, message_id)` | Delete message | chat_id, message_id |
990
+ | `answer_callback_query(id, text:, show_alert:)` | Acknowledge callback | query_id, options |
991
+ | `set_webhook(url, secret_token:, allowed_updates:)` | Set webhook URL | url, options |
992
+ | `delete_webhook` | Remove webhook | none |
993
+ | `get_webhook_info` | Get webhook status | none |
994
+ | `get_me` | Get bot information | none |
995
+
996
+ ### Configuration Methods
997
+
998
+ | Method | Description |
999
+ |--------|-------------|
1000
+ | `Configuration.from_credentials` | Load from Rails credentials/ENV |
1001
+ | `Configuration.new(name)` | Create named configuration |
1002
+ | `Configuration.get(name)` | Get configuration by name |
1003
+ | `Configuration.exists?(name)` | Check if config exists |
1004
+ | `Configuration.configuration_names` | List all config names |
1005
+ | `Configuration.clear_all!` | Remove all configurations |
1006
+ | `config.valid?` | Check if config is valid |
1007
+ | `config.api_base_url` | Get API base URL |
1008
+ | `config.bot_id` | Get bot ID from token |
1009
+ | `config.register_as(name)` | Register config with name |
1010
+
1011
+ ---
1012
+
1013
+ This guide covers everything you need to build Telegram bot integrations with FlowChat. For more examples and advanced patterns, check the [examples directory](../../examples/) in the FlowChat repository.