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,723 @@
1
+ # Gateway Development Guide
2
+
3
+ This guide shows how to build custom gateways to extend FlowChat to new platforms and services. FlowChat's pluggable architecture makes it easy to add support for SMS, Telegram, Slack, voice calls, or any other conversational platform.
4
+
5
+ ## Gateway Interface
6
+
7
+ Every gateway must implement the basic interface:
8
+
9
+ > **Note:** The examples in this guide are illustrative implementations to demonstrate the concepts. They are not part of the FlowChat codebase and would need additional work to be production-ready.
10
+
11
+ ```ruby
12
+ class YourCustomGateway
13
+ # Required: Initialize with app and optional configuration
14
+ def initialize(app, *config_args)
15
+ @app = app
16
+ @config = config_args.first
17
+ end
18
+
19
+ # Required: Process requests through the middleware stack
20
+ def call(context)
21
+ # 1. Parse platform-specific request
22
+ parse_request(context)
23
+
24
+ # 2. Process through FlowChat middleware stack
25
+ type, prompt, choices, media = @app.call(context)
26
+
27
+ # 3. Render platform-specific response
28
+ render_response(type, prompt, choices, media, context)
29
+ end
30
+
31
+ # Optional: Configure platform-specific middleware
32
+ def self.configure_middleware_stack(builder, custom_middleware)
33
+ builder.use YourPlatform::SpecialMiddleware
34
+ builder.use custom_middleware
35
+ builder.use YourPlatform::ResponseMiddleware
36
+ end
37
+ end
38
+ ```
39
+
40
+ ## Example: Telegram Gateway (Hypothetical)
41
+
42
+ Here's how you would build a complete Telegram Bot API gateway:
43
+
44
+ ```ruby
45
+ module MyCompany
46
+ module Telegram
47
+ module Gateway
48
+ class BotAPI
49
+ include FlowChat::Instrumentation
50
+
51
+ def initialize(app, bot_token)
52
+ @app = app
53
+ @bot_token = bot_token
54
+ @api_base = "https://api.telegram.org/bot#{@bot_token}"
55
+ end
56
+
57
+ def call(context)
58
+ # 1. Parse Telegram webhook payload
59
+ payload = parse_telegram_webhook(context)
60
+
61
+ # 2. Set FlowChat context
62
+ set_flowchat_context(context, payload)
63
+
64
+ # 3. Process through middleware stack
65
+ type, prompt, choices, media = @app.call(context)
66
+
67
+ # 4. Send response via Telegram API
68
+ send_telegram_response(type, prompt, choices, media, context)
69
+ end
70
+
71
+ # Optional: Configure Telegram-specific middleware
72
+ def self.configure_middleware_stack(builder, custom_middleware)
73
+ builder.use MyCompany::Telegram::Middleware::MessageProcessor
74
+ builder.use custom_middleware
75
+ builder.use MyCompany::Telegram::Middleware::ResponseFormatter
76
+ end
77
+
78
+ private
79
+
80
+ def parse_telegram_webhook(context)
81
+ body = context.controller.request.body.read
82
+ JSON.parse(body)
83
+ rescue JSON::ParserError => e
84
+ Rails.logger.error "Telegram: Invalid JSON: #{e.message}"
85
+ raise "Invalid webhook payload"
86
+ end
87
+
88
+ def set_flowchat_context(context, payload)
89
+ message = payload.dig("message")
90
+ callback_query = payload.dig("callback_query")
91
+
92
+ if message
93
+ # Regular message
94
+ context["request.user_id"] = message.dig("from", "id").to_s
95
+ context["request.message_id"] = message["message_id"].to_s
96
+ context["request.platform"] = :telegram
97
+ context["request.gateway"] = :telegram_bot_api
98
+ context["request.timestamp"] = Time.at(message["date"]).iso8601
99
+ context.input = message["text"]
100
+
101
+ # Telegram-specific data
102
+ context["telegram.chat_id"] = message.dig("chat", "id")
103
+ context["telegram.username"] = message.dig("from", "username")
104
+
105
+ elsif callback_query
106
+ # Button press
107
+ context["request.user_id"] = callback_query.dig("from", "id").to_s
108
+ context["request.platform"] = :telegram
109
+ context.input = callback_query["data"] # Button callback data
110
+
111
+ context["telegram.chat_id"] = callback_query.dig("message", "chat", "id")
112
+ context["telegram.callback_query_id"] = callback_query["id"]
113
+ end
114
+ end
115
+
116
+ def send_telegram_response(type, prompt, choices, media, context)
117
+ chat_id = context["telegram.chat_id"]
118
+
119
+ response_data = {
120
+ chat_id: chat_id,
121
+ text: prompt
122
+ }
123
+
124
+ # Add inline keyboard for choices
125
+ if choices.present?
126
+ keyboard = build_inline_keyboard(choices)
127
+ response_data[:reply_markup] = { inline_keyboard: keyboard }
128
+ end
129
+
130
+ # Send message via Telegram API
131
+ send_telegram_api_request("sendMessage", response_data)
132
+ end
133
+
134
+ def build_inline_keyboard(choices)
135
+ # Convert FlowChat choices to Telegram inline keyboard
136
+ buttons = choices.map do |value, text|
137
+ [{ text: text, callback_data: value }]
138
+ end
139
+ buttons
140
+ end
141
+
142
+ def send_telegram_api_request(method, data)
143
+ uri = URI("#{@api_base}/#{method}")
144
+
145
+ http = Net::HTTP.new(uri.host, uri.port)
146
+ http.use_ssl = true
147
+
148
+ request = Net::HTTP::Post.new(uri)
149
+ request['Content-Type'] = 'application/json'
150
+ request.body = data.to_json
151
+
152
+ response = http.request(request)
153
+
154
+ unless response.code.to_i.between?(200, 299)
155
+ Rails.logger.error "Telegram API error: #{response.code} #{response.body}"
156
+ end
157
+
158
+ response
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
164
+ ```
165
+
166
+ ## Example: SMS Gateway (Hypothetical)
167
+
168
+ Here's how you would build an SMS gateway using Twilio:
169
+
170
+ ```ruby
171
+ module MyCompany
172
+ module Sms
173
+ module Gateway
174
+ class Twilio
175
+ include FlowChat::Instrumentation
176
+
177
+ def initialize(app, config)
178
+ @app = app
179
+ @account_sid = config.account_sid
180
+ @auth_token = config.auth_token
181
+ @from_number = config.from_number
182
+ end
183
+
184
+ def call(context)
185
+ # Parse Twilio webhook
186
+ params = context.controller.params
187
+
188
+ # Set FlowChat context
189
+ context["request.user_id"] = params["From"]
190
+ context["request.msisdn"] = params["From"]
191
+ context["request.message_id"] = params["MessageSid"]
192
+ context["request.platform"] = :sms
193
+ context["request.gateway"] = :twilio
194
+ context["request.timestamp"] = Time.current.iso8601
195
+ context.input = params["Body"]
196
+
197
+ # Process through middleware
198
+ type, prompt, choices, media = @app.call(context)
199
+
200
+ # Send SMS response
201
+ send_sms(prompt, to: params["From"], context: context)
202
+ end
203
+
204
+ def self.configure_middleware_stack(builder, custom_middleware)
205
+ # SMS-specific processing
206
+ builder.use MyCompany::Sms::Middleware::CharacterLimitMiddleware
207
+ builder.use custom_middleware
208
+ builder.use MyCompany::Sms::Middleware::ChoiceFormatter
209
+ end
210
+
211
+ private
212
+
213
+ def send_sms(message, to:, context:)
214
+ # Format choices for SMS
215
+ if context["choices"].present?
216
+ choice_text = format_choices_for_sms(context["choices"])
217
+ message = "#{message}\n\n#{choice_text}"
218
+ end
219
+
220
+ # Send via Twilio API
221
+ twilio_client.messages.create(
222
+ from: @from_number,
223
+ to: to,
224
+ body: message
225
+ )
226
+
227
+ # Instrument message sent
228
+ instrument(FlowChat::Events::MESSAGE_SENT, {
229
+ to: to,
230
+ message: message,
231
+ platform: :sms,
232
+ gateway: :twilio,
233
+ content_length: message.length,
234
+ timestamp: context["request.timestamp"]
235
+ })
236
+ end
237
+
238
+ def format_choices_for_sms(choices)
239
+ choices.map.with_index(1) { |(value, text), i| "#{i}. #{text}" }.join("\n")
240
+ end
241
+
242
+ def twilio_client
243
+ @twilio_client ||= ::Twilio::REST::Client.new(@account_sid, @auth_token)
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
249
+ ```
250
+
251
+ ## Custom Middleware
252
+
253
+ Gateways can define their own middleware for platform-specific processing:
254
+
255
+ ```ruby
256
+ module MyCompany
257
+ module Telegram
258
+ module Middleware
259
+ class MessageProcessor
260
+ def initialize(app)
261
+ @app = app
262
+ end
263
+
264
+ def call(context)
265
+ # Pre-process Telegram-specific features
266
+ handle_telegram_commands(context)
267
+ handle_telegram_media(context)
268
+
269
+ result = @app.call(context)
270
+
271
+ # Post-process response
272
+ format_telegram_response(context, result)
273
+
274
+ result
275
+ end
276
+
277
+ private
278
+
279
+ def handle_telegram_commands(context)
280
+ input = context.input
281
+ return unless input&.start_with?('/')
282
+
283
+ # Handle Telegram bot commands
284
+ command = input.split.first
285
+ context["telegram.command"] = command
286
+
287
+ case command
288
+ when "/start"
289
+ context.input = nil # Start fresh conversation
290
+ when "/help"
291
+ context.input = "help"
292
+ when "/cancel"
293
+ context.input = "cancel"
294
+ end
295
+ end
296
+
297
+ def handle_telegram_media(context)
298
+ # Handle photos, documents, etc.
299
+ # Implementation depends on your needs
300
+ end
301
+
302
+ def format_telegram_response(context, result)
303
+ # Format response for Telegram's markdown
304
+ type, prompt, choices, media = result
305
+
306
+ if prompt.is_a?(String)
307
+ # Escape special Telegram markdown characters
308
+ prompt = prompt.gsub(/[_*\[\]()~`>#+=|{}.!-]/, '\\\\\&')
309
+ result[1] = prompt
310
+ end
311
+
312
+ result
313
+ end
314
+ end
315
+ end
316
+ end
317
+ end
318
+ ```
319
+
320
+ ## Using Your Custom Gateway
321
+
322
+ Once built, use your gateway like any other:
323
+
324
+ ```ruby
325
+ class TelegramController < ApplicationController
326
+ def webhook
327
+ processor = FlowChat::Processor.new(self) do |config|
328
+ config.use_gateway MyCompany::Telegram::Gateway::BotAPI, telegram_bot_token
329
+ config.use_session_store FlowChat::Session::CacheSessionStore
330
+ config.use_durable_sessions # Use user_id for persistent sessions
331
+ end
332
+
333
+ processor.run WelcomeFlow, :start
334
+ end
335
+
336
+ private
337
+
338
+ def telegram_bot_token
339
+ ENV["TELEGRAM_BOT_TOKEN"]
340
+ end
341
+ end
342
+ ```
343
+
344
+ ## Gateway Configuration
345
+
346
+ ### Configuration Objects
347
+
348
+ Create configuration classes for complex gateways:
349
+
350
+ ```ruby
351
+ module MyCompany
352
+ module Slack
353
+ class Configuration
354
+ attr_accessor :bot_token, :signing_secret, :app_token
355
+ attr_accessor :default_channel, :enable_threads
356
+
357
+ def initialize
358
+ @enable_threads = true
359
+ @default_channel = "#general"
360
+ end
361
+
362
+ def validate!
363
+ raise "bot_token required" if bot_token.blank?
364
+ raise "signing_secret required" if signing_secret.blank?
365
+ end
366
+ end
367
+ end
368
+ end
369
+
370
+ # Usage
371
+ slack_config = MyCompany::Slack::Configuration.new
372
+ slack_config.bot_token = ENV["SLACK_BOT_TOKEN"]
373
+ slack_config.signing_secret = ENV["SLACK_SIGNING_SECRET"]
374
+
375
+ processor = FlowChat::Processor.new(self) do |config|
376
+ config.use_gateway MyCompany::Slack::Gateway::BoltJS, slack_config
377
+ end
378
+ ```
379
+
380
+ ### Environment-Based Configuration
381
+
382
+ ```ruby
383
+ class MyCompany::Sms::Gateway::Twilio
384
+ def initialize(app, config = nil)
385
+ @app = app
386
+
387
+ # Use config object or fall back to environment
388
+ if config
389
+ @account_sid = config.account_sid
390
+ @auth_token = config.auth_token
391
+ @from_number = config.from_number
392
+ else
393
+ @account_sid = ENV["TWILIO_ACCOUNT_SID"]
394
+ @auth_token = ENV["TWILIO_AUTH_TOKEN"]
395
+ @from_number = ENV["TWILIO_FROM_NUMBER"]
396
+ end
397
+
398
+ validate_configuration!
399
+ end
400
+
401
+ private
402
+
403
+ def validate_configuration!
404
+ required = [@account_sid, @auth_token, @from_number]
405
+ raise "Twilio configuration incomplete" if required.any?(&:blank?)
406
+ end
407
+ end
408
+ ```
409
+
410
+ ## Testing Custom Gateways
411
+
412
+ ### Unit Testing
413
+
414
+ ```ruby
415
+ # test/unit/telegram_gateway_test.rb
416
+ class TelegramGatewayTest < Minitest::Test
417
+ def setup
418
+ @mock_app = proc { |context| [:text, "Test response", {}, nil] }
419
+ @gateway = MyCompany::Telegram::Gateway::BotAPI.new(@mock_app, "test_token")
420
+ end
421
+
422
+ def test_parses_telegram_message
423
+ context = create_mock_context(telegram_payload: {
424
+ "message" => {
425
+ "from" => { "id" => 12345, "username" => "testuser" },
426
+ "text" => "Hello",
427
+ "message_id" => 123
428
+ }
429
+ })
430
+
431
+ @gateway.call(context)
432
+
433
+ assert_equal "12345", context["request.user_id"]
434
+ assert_equal :telegram, context["request.platform"]
435
+ assert_equal "Hello", context.input
436
+ end
437
+
438
+ def test_handles_callback_queries
439
+ context = create_mock_context(telegram_payload: {
440
+ "callback_query" => {
441
+ "from" => { "id" => 12345 },
442
+ "data" => "button_value",
443
+ "id" => "callback123"
444
+ }
445
+ })
446
+
447
+ @gateway.call(context)
448
+
449
+ assert_equal "button_value", context.input
450
+ assert_equal "callback123", context["telegram.callback_query_id"]
451
+ end
452
+
453
+ private
454
+
455
+ def create_mock_context(telegram_payload:)
456
+ controller = Minitest::Mock.new
457
+ request = Minitest::Mock.new
458
+
459
+ request.expect :body, StringIO.new(telegram_payload.to_json)
460
+ controller.expect :request, request
461
+
462
+ context = FlowChat::Context.new
463
+ context["controller"] = controller
464
+ context
465
+ end
466
+ end
467
+ ```
468
+
469
+ ### Integration Testing
470
+
471
+ ```ruby
472
+ # test/integration/telegram_flow_test.rb
473
+ class TelegramFlowTest < ActionDispatch::IntegrationTest
474
+ def test_complete_telegram_conversation
475
+ # Simulate Telegram webhook for /start command
476
+ post telegram_webhook_path,
477
+ params: telegram_message_payload(text: "/start"),
478
+ headers: telegram_headers
479
+
480
+ assert_response :success
481
+
482
+ # Simulate button press
483
+ post telegram_webhook_path,
484
+ params: telegram_callback_payload(data: "option_1"),
485
+ headers: telegram_headers
486
+
487
+ assert_response :success
488
+ end
489
+
490
+ private
491
+
492
+ def telegram_message_payload(text:)
493
+ {
494
+ message: {
495
+ from: { id: 12345, username: "testuser" },
496
+ text: text,
497
+ message_id: rand(1000),
498
+ date: Time.current.to_i
499
+ }
500
+ }.to_json
501
+ end
502
+
503
+ def telegram_headers
504
+ {
505
+ "Content-Type" => "application/json"
506
+ }
507
+ end
508
+ end
509
+ ```
510
+
511
+ ## Advanced Gateway Features
512
+
513
+ ### File Upload Support
514
+
515
+ ```ruby
516
+ def handle_file_upload(context, telegram_payload)
517
+ message = telegram_payload["message"]
518
+
519
+ if message["photo"]
520
+ # Handle photo upload
521
+ photo = message["photo"].last # Get highest resolution
522
+ file_info = get_telegram_file(photo["file_id"])
523
+
524
+ context["request.media"] = {
525
+ type: "image",
526
+ file_id: photo["file_id"],
527
+ url: download_telegram_file(file_info["file_path"])
528
+ }
529
+
530
+ context.input = "$media$" # Special input to indicate media
531
+
532
+ elsif message["document"]
533
+ # Handle document upload
534
+ doc = message["document"]
535
+ context["request.media"] = {
536
+ type: "document",
537
+ file_id: doc["file_id"],
538
+ filename: doc["file_name"],
539
+ mime_type: doc["mime_type"]
540
+ }
541
+
542
+ context.input = "$document$"
543
+ end
544
+ end
545
+ ```
546
+
547
+ ### Real-time Updates
548
+
549
+ ```ruby
550
+ class MyCompany::Slack::Gateway::BoltJS
551
+ def call(context)
552
+ # ... standard processing ...
553
+
554
+ # Send typing indicator for long operations
555
+ if processing_time_estimate > 2.seconds
556
+ send_typing_indicator(context["slack.channel"])
557
+ end
558
+
559
+ # ... continue processing ...
560
+ end
561
+
562
+ private
563
+
564
+ def send_typing_indicator(channel)
565
+ slack_client.web_client.conversations_typing(channel: channel)
566
+ end
567
+ end
568
+ ```
569
+
570
+ ### Multi-Message Responses
571
+
572
+ ```ruby
573
+ def send_slack_response(type, prompt, choices, media, context)
574
+ channel = context["slack.channel"]
575
+
576
+ # Send main message
577
+ response = slack_client.web_client.chat_postMessage(
578
+ channel: channel,
579
+ text: prompt
580
+ )
581
+
582
+ # Send media as separate attachment if present
583
+ if media.present?
584
+ slack_client.web_client.files_upload(
585
+ channels: channel,
586
+ file: media[:url],
587
+ title: media[:filename]
588
+ )
589
+ end
590
+
591
+ # Add interactive buttons if choices present
592
+ if choices.present?
593
+ slack_client.web_client.chat_update(
594
+ channel: channel,
595
+ ts: response.ts,
596
+ text: prompt,
597
+ blocks: build_slack_blocks(choices)
598
+ )
599
+ end
600
+ end
601
+ ```
602
+
603
+ ## Best Practices
604
+
605
+ ### Error Handling
606
+
607
+ ```ruby
608
+ def call(context)
609
+ parse_request(context)
610
+ type, prompt, choices, media = @app.call(context)
611
+ render_response(type, prompt, choices, media, context)
612
+ rescue JSON::ParserError => e
613
+ Rails.logger.error "Gateway: Invalid JSON payload: #{e.message}"
614
+ send_error_response("Invalid request format", context)
615
+ rescue NetworkError => e
616
+ Rails.logger.error "Gateway: Network error: #{e.message}"
617
+ send_error_response("Service temporarily unavailable", context)
618
+ rescue => e
619
+ Rails.logger.error "Gateway: Unexpected error: #{e.class.name}: #{e.message}"
620
+ Rails.logger.debug e.backtrace.join("\n")
621
+ send_error_response("An error occurred", context)
622
+ end
623
+ ```
624
+
625
+ ### Rate Limiting
626
+
627
+ ```ruby
628
+ def call(context)
629
+ check_rate_limit(context["request.user_id"])
630
+ # ... continue processing ...
631
+ end
632
+
633
+ private
634
+
635
+ def check_rate_limit(user_id)
636
+ key = "rate_limit:#{user_id}"
637
+ count = Rails.cache.increment(key, 1, expires_in: 1.minute) || 1
638
+
639
+ if count > 30 # 30 requests per minute
640
+ raise RateLimitExceeded, "Too many requests"
641
+ end
642
+ end
643
+ ```
644
+
645
+ ### Security
646
+
647
+ ```ruby
648
+ def call(context)
649
+ verify_webhook_signature(context)
650
+ # ... continue processing ...
651
+ end
652
+
653
+ private
654
+
655
+ def verify_webhook_signature(context)
656
+ signature = context.controller.request.headers["X-Platform-Signature"]
657
+ payload = context.controller.request.body.read
658
+
659
+ expected = generate_signature(payload, @webhook_secret)
660
+
661
+ unless secure_compare(signature, expected)
662
+ raise SecurityError, "Invalid webhook signature"
663
+ end
664
+ end
665
+
666
+ def secure_compare(a, b)
667
+ return false unless a.bytesize == b.bytesize
668
+
669
+ l = a.unpack("C*")
670
+ r = b.unpack("C*")
671
+
672
+ l.zip(r).reduce(0) { |sum, (x, y)| sum | (x ^ y) } == 0
673
+ end
674
+ ```
675
+
676
+ ## Publishing Your Gateway
677
+
678
+ ### Gem Structure
679
+
680
+ If you're building a reusable gateway, structure it as a gem:
681
+
682
+ ```
683
+ my_platform_gateway/
684
+ ├── lib/
685
+ │ └── flow_chat/
686
+ │ └── my_platform/
687
+ │ ├── gateway.rb
688
+ │ ├── configuration.rb
689
+ │ ├── middleware/
690
+ │ └── renderer.rb
691
+ ├── spec/
692
+ ├── README.md
693
+ └── my_platform_gateway.gemspec
694
+ ```
695
+
696
+ ### Documentation
697
+
698
+ Document your gateway's:
699
+ - Installation instructions
700
+ - Configuration options
701
+ - Platform-specific features
702
+ - Middleware stack
703
+ - Testing helpers
704
+
705
+ ### Examples
706
+
707
+ Provide working examples:
708
+
709
+ ```ruby
710
+ # examples/basic_controller.rb
711
+ class MyPlatformController < ApplicationController
712
+ def webhook
713
+ processor = FlowChat::Processor.new(self) do |config|
714
+ config.use_gateway FlowChat::MyPlatform::Gateway, platform_config
715
+ config.use_session_store FlowChat::Session::CacheSessionStore
716
+ end
717
+
718
+ processor.run WelcomeFlow, :start
719
+ end
720
+ end
721
+ ```
722
+
723
+ Custom gateways make FlowChat incredibly powerful and flexible. With this foundation, you can integrate virtually any conversational platform into FlowChat's unified framework.