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.
- checksums.yaml +4 -4
- data/.cliff.toml +74 -0
- data/.github/workflows/ci.yml +2 -3
- data/.github/workflows/release.yml +56 -0
- data/.standard.yml +4 -0
- data/CHANGELOG.md +22 -0
- data/CLAUDE.md +327 -0
- data/CONTRIBUTING.md +134 -0
- data/Gemfile +1 -0
- data/README.md +290 -105
- data/Rakefile +5 -1
- data/SECURITY.md +42 -349
- data/docs/architecture.md +510 -0
- data/docs/async-background-processing.md +298 -0
- data/docs/configuration.md +556 -226
- data/docs/factory-pattern.md +355 -0
- data/docs/gateway-context-variables.md +171 -0
- data/docs/gateway-development.md +723 -0
- data/docs/getting-started.md +429 -0
- data/docs/instrumentation.md +264 -153
- data/docs/platforms/telegram.md +1013 -0
- data/docs/platforms/ussd.md +693 -0
- data/docs/platforms/whatsapp.md +1395 -0
- data/docs/testing.md +243 -365
- data/examples/custom_session_id_example.rb +119 -0
- data/examples/http_controller.rb +154 -0
- data/examples/intercom_configuration_example.rb +118 -0
- data/examples/intercom_controller.rb +194 -0
- data/examples/multi_tenant_whatsapp_controller.rb +4 -4
- data/examples/simulator_controller.rb +21 -1
- data/examples/ussd_controller.rb +10 -10
- data/examples/whatsapp_controller.rb +2 -2
- data/flow_chat.gemspec +4 -0
- data/lib/flow_chat/{base_app.rb → app.rb} +28 -9
- data/lib/flow_chat/async_job.rb +176 -0
- data/lib/flow_chat/config.rb +23 -28
- data/lib/flow_chat/{base_executor.rb → executor.rb} +7 -12
- data/lib/flow_chat/factory.rb +94 -0
- data/lib/flow_chat/gateway_async_support.rb +106 -0
- data/lib/flow_chat/generic_async_job.rb +30 -0
- data/lib/flow_chat/http/gateway/simple.rb +125 -0
- data/lib/flow_chat/http/renderer.rb +41 -0
- data/lib/flow_chat/instrumentation/setup.rb +1 -1
- data/lib/flow_chat/instrumentation.rb +25 -0
- data/lib/flow_chat/intercom/client.rb +155 -0
- data/lib/flow_chat/intercom/configuration.rb +149 -0
- data/lib/flow_chat/intercom/gateway/intercom_api.rb +396 -0
- data/lib/flow_chat/intercom/renderer.rb +71 -0
- data/lib/flow_chat/phone_number_util.rb +49 -0
- data/lib/flow_chat/processor.rb +188 -0
- data/lib/flow_chat/renderers/markdown_support.rb +58 -0
- data/lib/flow_chat/session/cache_session_store.rb +1 -17
- data/lib/flow_chat/session/middleware.rb +43 -26
- data/lib/flow_chat/simulator/controller.rb +17 -5
- data/lib/flow_chat/simulator/views/simulator.html.erb +220 -8
- data/lib/flow_chat/telegram/client.rb +240 -0
- data/lib/flow_chat/telegram/configuration.rb +118 -0
- data/lib/flow_chat/telegram/gateway/bot_api.rb +300 -0
- data/lib/flow_chat/telegram/middleware/choice_mapper.rb +35 -0
- data/lib/flow_chat/telegram/renderer.rb +125 -0
- data/lib/flow_chat/telegram.rb +7 -0
- data/lib/flow_chat/ussd/gateway/nalo.rb +27 -11
- data/lib/flow_chat/ussd/middleware/pagination.rb +9 -5
- data/lib/flow_chat/ussd/renderer.rb +1 -1
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/client.rb +144 -13
- data/lib/flow_chat/whatsapp/configuration.rb +1 -1
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +144 -106
- data/lib/flow_chat/whatsapp/id_generator.rb +124 -0
- data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +147 -0
- data/lib/flow_chat/whatsapp/renderer.rb +145 -11
- data/lib/flow_chat.rb +11 -1
- data/lib/tasks/release.rake +165 -0
- metadata +88 -21
- data/docs/flows.md +0 -320
- data/docs/images/simulator.png +0 -0
- data/docs/media.md +0 -153
- data/docs/sessions.md +0 -433
- data/docs/ussd-setup.md +0 -322
- data/docs/whatsapp-setup.md +0 -162
- data/examples/whatsapp_message_job.rb +0 -113
- data/lib/flow_chat/base_processor.rb +0 -145
- data/lib/flow_chat/session/rails_session_store.rb +0 -68
- data/lib/flow_chat/ussd/app.rb +0 -6
- data/lib/flow_chat/ussd/gateway/nsano.rb +0 -98
- data/lib/flow_chat/ussd/middleware/executor.rb +0 -24
- data/lib/flow_chat/ussd/processor.rb +0 -39
- data/lib/flow_chat/whatsapp/app.rb +0 -29
- data/lib/flow_chat/whatsapp/middleware/executor.rb +0 -24
- data/lib/flow_chat/whatsapp/processor.rb +0 -32
- data/lib/flow_chat/whatsapp/send_job_support.rb +0 -79
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example: Custom Session ID Configuration with Proc
|
|
4
|
+
#
|
|
5
|
+
# This example demonstrates how to use a custom proc for session ID generation
|
|
6
|
+
# in FlowChat applications. The proc allows complete customization of how
|
|
7
|
+
# session IDs are generated based on context data.
|
|
8
|
+
|
|
9
|
+
require "flow_chat"
|
|
10
|
+
|
|
11
|
+
class CustomSessionIdController < ApplicationController
|
|
12
|
+
def ussd_endpoint
|
|
13
|
+
processor = FlowChat::Processor.new(self) do |config|
|
|
14
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
|
15
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
16
|
+
|
|
17
|
+
# Example 1: Custom session ID using block/proc
|
|
18
|
+
config.use_session_config do |context|
|
|
19
|
+
# Create a custom session ID based on your business logic
|
|
20
|
+
user_phone = context["request.msisdn"]
|
|
21
|
+
flow_name = context["flow.name"]
|
|
22
|
+
gateway = context["request.gateway"]
|
|
23
|
+
timestamp = Time.current.strftime("%Y%m%d")
|
|
24
|
+
|
|
25
|
+
# Custom format: flow_gateway_date_hashedphone
|
|
26
|
+
"#{flow_name}_#{gateway}_#{timestamp}_#{hash_phone(user_phone)}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
processor.run(SurveyFlow, :main_menu)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def whatsapp_endpoint
|
|
34
|
+
processor = FlowChat::Processor.new(self) do |config|
|
|
35
|
+
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
|
36
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
37
|
+
|
|
38
|
+
# Example 2: Multi-tenant session IDs
|
|
39
|
+
config.use_session_config do |context|
|
|
40
|
+
tenant_id = extract_tenant_from_request(context)
|
|
41
|
+
user_id = context["request.user_id"] || context["request.msisdn"]
|
|
42
|
+
flow_name = context["flow.name"]
|
|
43
|
+
|
|
44
|
+
"tenant_#{tenant_id}_flow_#{flow_name}_user_#{hash_identifier(user_id)}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
processor.run(CustomerSupportFlow, :handle_inquiry)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def http_endpoint
|
|
52
|
+
processor = FlowChat::Processor.new(self) do |config|
|
|
53
|
+
config.use_gateway FlowChat::Http::Gateway::Simple
|
|
54
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
55
|
+
|
|
56
|
+
# Example 3: API session with custom expiration tracking
|
|
57
|
+
config.use_session_config do |context|
|
|
58
|
+
api_key = context.controller.request.headers["X-API-Key"]
|
|
59
|
+
request_id = context["request.id"]
|
|
60
|
+
|
|
61
|
+
# Include API key hash for session isolation per API client
|
|
62
|
+
"api_#{hash_identifier(api_key)}_req_#{request_id}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
processor.run(ApiFlow, :handle_request)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def hash_phone(phone)
|
|
72
|
+
require "digest"
|
|
73
|
+
Digest::SHA256.hexdigest(phone)[0, 8]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def hash_identifier(identifier)
|
|
77
|
+
require "digest"
|
|
78
|
+
Digest::SHA256.hexdigest(identifier.to_s)[0, 8]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def extract_tenant_from_request(context)
|
|
82
|
+
# Extract tenant from subdomain or header
|
|
83
|
+
request = context.controller&.request
|
|
84
|
+
return "default" unless request
|
|
85
|
+
|
|
86
|
+
host = request.host
|
|
87
|
+
subdomain = host.split(".").first
|
|
88
|
+
subdomain if subdomain != "www"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Example flows for demonstration
|
|
93
|
+
class SurveyFlow < FlowChat::Flow
|
|
94
|
+
def main_menu
|
|
95
|
+
app.screen(:menu) { |p| p.ask "Welcome! Choose an option:", choices: ["Survey", "Exit"] }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class CustomerSupportFlow < FlowChat::Flow
|
|
100
|
+
def handle_inquiry
|
|
101
|
+
app.screen(:inquiry) { |p| p.ask "How can we help you today?" }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
class ApiFlow < FlowChat::Flow
|
|
106
|
+
def handle_request
|
|
107
|
+
app.screen(:request) { |p| p.ask "API request received. Provide data:" }
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Note: The custom session ID proc approach provides:
|
|
112
|
+
# - Complete control over session ID format
|
|
113
|
+
# - Access to full context (request data, flow info, etc.)
|
|
114
|
+
# - Ability to implement complex business logic
|
|
115
|
+
# - Support for multi-tenancy, API authentication, etc.
|
|
116
|
+
#
|
|
117
|
+
# The proc should return a string that will be used as the session ID.
|
|
118
|
+
# Make sure the returned ID is unique for your use case to avoid
|
|
119
|
+
# session collisions.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Example HTTP Controller for FlowChat HTTP Gateway
|
|
2
|
+
#
|
|
3
|
+
# This controller demonstrates how to use FlowChat with HTTP requests
|
|
4
|
+
# for simple JSON-based conversational interfaces.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# POST /http/webhook
|
|
8
|
+
# Content-Type: application/json
|
|
9
|
+
#
|
|
10
|
+
# {
|
|
11
|
+
# "session_id": "unique_session_123",
|
|
12
|
+
# "user_id": "user_456",
|
|
13
|
+
# "input": "Hello"
|
|
14
|
+
# }
|
|
15
|
+
#
|
|
16
|
+
# Response:
|
|
17
|
+
# {
|
|
18
|
+
# "type": "prompt",
|
|
19
|
+
# "session_id": "unique_session_123",
|
|
20
|
+
# "user_id": "user_456",
|
|
21
|
+
# "timestamp": "2024-01-01T12:00:00Z",
|
|
22
|
+
# "message": "Hello! What's your name?",
|
|
23
|
+
# "choices": [
|
|
24
|
+
# {"key": "1", "text": "Continue"}
|
|
25
|
+
# ]
|
|
26
|
+
# }
|
|
27
|
+
|
|
28
|
+
class HttpController < ApplicationController
|
|
29
|
+
skip_forgery_protection
|
|
30
|
+
|
|
31
|
+
def webhook
|
|
32
|
+
processor = FlowChat::Processor.new(self) do |config|
|
|
33
|
+
config.use_gateway FlowChat::Http::Gateway::Simple
|
|
34
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
35
|
+
|
|
36
|
+
# Configure session management
|
|
37
|
+
config.use_session_config(
|
|
38
|
+
boundaries: [:flow, :platform],
|
|
39
|
+
hash_identifiers: true,
|
|
40
|
+
identifier: :msisdn # Use phone number for durable sessions
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
processor.run WelcomeFlow, :main_page
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Example flow for HTTP gateway
|
|
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
|
+
validate: ->(input) { "Name is required" if input.blank? },
|
|
54
|
+
transform: ->(input) { input.strip.titleize }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
age = app.screen(:age) do |prompt|
|
|
58
|
+
prompt.ask "Nice to meet you, #{name}! How old are you?",
|
|
59
|
+
validate: ->(input) {
|
|
60
|
+
return "Please enter a number" unless input.match?(/^\d+$/)
|
|
61
|
+
return "Age must be between 1 and 120" unless (1..120).cover?(input.to_i)
|
|
62
|
+
nil
|
|
63
|
+
},
|
|
64
|
+
transform: ->(input) { input.to_i }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
preferences = app.screen(:preferences) do |prompt|
|
|
68
|
+
prompt.select "What are you interested in?", {
|
|
69
|
+
"tech" => "Technology",
|
|
70
|
+
"sports" => "Sports",
|
|
71
|
+
"music" => "Music",
|
|
72
|
+
"travel" => "Travel"
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Summary
|
|
77
|
+
app.say "Great! Here's what I learned about you:"
|
|
78
|
+
app.say "Name: #{name}"
|
|
79
|
+
app.say "Age: #{age}"
|
|
80
|
+
app.say "Interest: #{preferences.capitalize}"
|
|
81
|
+
|
|
82
|
+
# Ask if they want to continue
|
|
83
|
+
continue = app.screen(:continue) do |prompt|
|
|
84
|
+
prompt.yes? "Would you like to explore more features?"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if continue
|
|
88
|
+
features_demo
|
|
89
|
+
else
|
|
90
|
+
app.say "Thanks for trying FlowChat HTTP Gateway! 👋"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def features_demo
|
|
97
|
+
choice = app.screen(:feature_choice) do |prompt|
|
|
98
|
+
prompt.select "What would you like to try?", {
|
|
99
|
+
"media" => "Media Support",
|
|
100
|
+
"validation" => "Input Validation",
|
|
101
|
+
"session" => "Session Management"
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
case choice
|
|
106
|
+
when "media"
|
|
107
|
+
media_demo
|
|
108
|
+
when "validation"
|
|
109
|
+
validation_demo
|
|
110
|
+
when "session"
|
|
111
|
+
session_demo
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def media_demo
|
|
116
|
+
app.say "FlowChat supports rich media in HTTP responses!",
|
|
117
|
+
media: {
|
|
118
|
+
url: "https://via.placeholder.com/300x200.png?text=FlowChat+HTTP",
|
|
119
|
+
type: :image,
|
|
120
|
+
caption: "FlowChat HTTP Gateway Demo"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
app.say "Media is returned in the JSON response for your frontend to display."
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def validation_demo
|
|
127
|
+
email = app.screen(:email) do |prompt|
|
|
128
|
+
prompt.ask "Enter your email address:",
|
|
129
|
+
validate: ->(input) {
|
|
130
|
+
return "Email is required" if input.blank?
|
|
131
|
+
return "Invalid email format" unless input.match?(/\A[\w+\-.]+@[a-z\d-]+(\.[a-z\d-]+)*\.[a-z]+\z/i)
|
|
132
|
+
nil
|
|
133
|
+
},
|
|
134
|
+
transform: ->(input) { input.downcase.strip }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
app.say "Perfect! Your email #{email} has been validated and normalized."
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def session_demo
|
|
141
|
+
# Store some data in session
|
|
142
|
+
app.session.set("demo_timestamp", Time.current.to_s)
|
|
143
|
+
app.session.set("demo_counter", (app.session.get("demo_counter") || 0) + 1)
|
|
144
|
+
|
|
145
|
+
counter = app.session.get("demo_counter")
|
|
146
|
+
timestamp = app.session.get("demo_timestamp")
|
|
147
|
+
|
|
148
|
+
app.say "Session Demo:"
|
|
149
|
+
app.say "• This is visit ##{counter} in this session"
|
|
150
|
+
app.say "• Session started at: #{timestamp}"
|
|
151
|
+
app.say "• Session data persists across HTTP requests"
|
|
152
|
+
app.say "• Session ID: #{app.session.context["session.id"]}"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Example configuration for using FlowChat with Intercom
|
|
2
|
+
|
|
3
|
+
# 1. Rails Credentials Configuration (recommended)
|
|
4
|
+
# Run: rails credentials:edit
|
|
5
|
+
# Add to config/credentials.yml.enc:
|
|
6
|
+
|
|
7
|
+
# intercom:
|
|
8
|
+
# access_token: "your_intercom_access_token_here"
|
|
9
|
+
# client_secret: "your_intercom_client_secret_here" # For webhook signature validation
|
|
10
|
+
# admin_id: "your_admin_id_here" # Required - admin ID for sending messages
|
|
11
|
+
# skip_signature_validation: false # Set to true for development/testing
|
|
12
|
+
|
|
13
|
+
# 2. Environment Variables Configuration (alternative)
|
|
14
|
+
# Set these environment variables:
|
|
15
|
+
|
|
16
|
+
# INTERCOM_ACCESS_TOKEN=your_intercom_access_token_here
|
|
17
|
+
# INTERCOM_CLIENT_SECRET=your_intercom_client_secret_here
|
|
18
|
+
# INTERCOM_ADMIN_ID=your_admin_id_here
|
|
19
|
+
# INTERCOM_SKIP_SIGNATURE_VALIDATION=false
|
|
20
|
+
|
|
21
|
+
# 3. Named Configuration Example (for multi-tenant apps)
|
|
22
|
+
class MultiTenantIntercomSetup
|
|
23
|
+
def self.setup_configurations
|
|
24
|
+
# Main company configuration
|
|
25
|
+
FlowChat::Intercom::Configuration.new("main") do |config|
|
|
26
|
+
config.access_token = Rails.application.credentials.dig(:intercom, :main, :access_token)
|
|
27
|
+
config.client_secret = Rails.application.credentials.dig(:intercom, :main, :client_secret)
|
|
28
|
+
config.admin_id = Rails.application.credentials.dig(:intercom, :main, :admin_id)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Enterprise customer configuration
|
|
32
|
+
FlowChat::Intercom::Configuration.new("enterprise") do |config|
|
|
33
|
+
config.access_token = Rails.application.credentials.dig(:intercom, :enterprise, :access_token)
|
|
34
|
+
config.client_secret = Rails.application.credentials.dig(:intercom, :enterprise, :client_secret)
|
|
35
|
+
config.admin_id = Rails.application.credentials.dig(:intercom, :enterprise, :admin_id)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# 4. Routes Configuration
|
|
41
|
+
# Add to config/routes.rb:
|
|
42
|
+
|
|
43
|
+
# Rails.application.routes.draw do
|
|
44
|
+
# # Intercom webhook endpoint - supports both HEAD (for URL validation) and POST (for events)
|
|
45
|
+
# match '/intercom/webhook', to: 'intercom#webhook', via: [:head, :post]
|
|
46
|
+
#
|
|
47
|
+
# # Multiple tenant support (if needed)
|
|
48
|
+
# match '/intercom/:tenant/webhook', to: 'intercom#webhook', via: [:head, :post]
|
|
49
|
+
# end
|
|
50
|
+
|
|
51
|
+
# 5. Controller with Named Configuration
|
|
52
|
+
class TenantAwareIntercomController < ApplicationController
|
|
53
|
+
skip_forgery_protection
|
|
54
|
+
|
|
55
|
+
def webhook
|
|
56
|
+
# Get tenant-specific configuration
|
|
57
|
+
tenant = params[:tenant] || "main"
|
|
58
|
+
FlowChat::Intercom::Configuration.get(tenant)
|
|
59
|
+
|
|
60
|
+
processor = FlowChat::Processor.new(self) do |config|
|
|
61
|
+
config.use_gateway FlowChat::Intercom::Gateway::IntercomApi, config
|
|
62
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
63
|
+
config.use_session_config(
|
|
64
|
+
boundaries: [:flow, :url], # Separate sessions per tenant
|
|
65
|
+
identifier: :conversation_id
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
processor.run CustomerSupportFlow, :handle_conversation
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# 6. Intercom Webhook Setup Instructions
|
|
74
|
+
# To set up webhooks in Intercom:
|
|
75
|
+
#
|
|
76
|
+
# 1. Go to your Intercom Developer Hub
|
|
77
|
+
# 2. Navigate to Configure → Webhooks
|
|
78
|
+
# 3. Add your webhook endpoint:
|
|
79
|
+
# - Endpoint URL: https://yourdomain.com/intercom/webhook (must use HTTPS)
|
|
80
|
+
# - Intercom will send a HEAD request to validate your URL
|
|
81
|
+
# - Your app automatically responds with 200 OK
|
|
82
|
+
#
|
|
83
|
+
# 4. Subscribe to webhook topics:
|
|
84
|
+
# - conversation.user.created (new conversations)
|
|
85
|
+
# - conversation.user.replied (user replies)
|
|
86
|
+
#
|
|
87
|
+
# 5. Configure signature validation:
|
|
88
|
+
# - Find your app's Client Secret in Basic Information
|
|
89
|
+
# - Add this as client_secret in your Rails credentials
|
|
90
|
+
# - Webhooks are validated using X-Hub-Signature header with SHA1 HMAC
|
|
91
|
+
#
|
|
92
|
+
# 6. Test the webhook:
|
|
93
|
+
# - Send a test message in Intercom
|
|
94
|
+
# - Verify your endpoint receives the webhook notification
|
|
95
|
+
# - Check logs for any signature validation issues
|
|
96
|
+
|
|
97
|
+
# 7. Finding Your Admin ID (Required)
|
|
98
|
+
# To find your admin ID for message sending, use this Rails console command:
|
|
99
|
+
#
|
|
100
|
+
# rails console
|
|
101
|
+
# ```
|
|
102
|
+
# config = FlowChat::Intercom::Configuration.from_credentials
|
|
103
|
+
# client = FlowChat::Intercom::Client.new(config)
|
|
104
|
+
# result = client.list_admins
|
|
105
|
+
# result["admins"].each { |admin| puts "#{admin["name"]} (#{admin["email"]}) - ID: #{admin["id"]}" }
|
|
106
|
+
# ```
|
|
107
|
+
#
|
|
108
|
+
# This will display all admins with their IDs. Copy the ID of the admin you want to use for sending messages.
|
|
109
|
+
#
|
|
110
|
+
# Alternative methods:
|
|
111
|
+
# 1. Go to your Intercom Developer Hub (https://developers.intercom.com/)
|
|
112
|
+
# 2. Navigate to your app → Configure → Basic Information
|
|
113
|
+
# 3. Find the Admin ID in the app details
|
|
114
|
+
#
|
|
115
|
+
# The admin_id is required for sending messages from FlowChat to Intercom conversations.
|
|
116
|
+
# The API uses this to identify which admin is sending the message.
|
|
117
|
+
#
|
|
118
|
+
# Note: The old bot_user_id configuration is no longer needed.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Example controller showing how to use the Intercom gateway
|
|
2
|
+
# This controller handles webhooks from Intercom for conversation events
|
|
3
|
+
|
|
4
|
+
class IntercomController < ApplicationController
|
|
5
|
+
skip_forgery_protection
|
|
6
|
+
|
|
7
|
+
# POST /intercom/webhook
|
|
8
|
+
# Handle Intercom webhook notifications for conversation events
|
|
9
|
+
def webhook
|
|
10
|
+
processor = FlowChat::Processor.new(self) do |config|
|
|
11
|
+
# Use Intercom gateway with automatic configuration loading
|
|
12
|
+
config.use_gateway FlowChat::Intercom::Gateway::IntercomApi
|
|
13
|
+
|
|
14
|
+
# Use cache-based session storage for longer-lived sessions
|
|
15
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
16
|
+
|
|
17
|
+
# Configure session boundaries - use conversation ID for session isolation
|
|
18
|
+
config.use_session_config(
|
|
19
|
+
boundaries: [:conversation], # Each conversation gets its own session
|
|
20
|
+
identifier: :conversation_id # Use conversation ID as session key
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Run the customer support flow
|
|
25
|
+
processor.run CustomerSupportFlow, :handle_conversation
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Example flow for handling customer conversations via Intercom
|
|
30
|
+
class CustomerSupportFlow < FlowChat::Flow
|
|
31
|
+
def handle_conversation
|
|
32
|
+
# Greet the user
|
|
33
|
+
greeting = if first_message?
|
|
34
|
+
"Hello! I'm here to help you. How can I assist you today?"
|
|
35
|
+
else
|
|
36
|
+
"Thanks for your message! Let me help you with that."
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Get the user's inquiry
|
|
40
|
+
inquiry = app.screen(:inquiry) do |prompt|
|
|
41
|
+
prompt.ask greeting
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Categorize the inquiry and provide appropriate response
|
|
45
|
+
category = categorize_inquiry(inquiry)
|
|
46
|
+
|
|
47
|
+
case category
|
|
48
|
+
when :technical_support
|
|
49
|
+
handle_technical_support
|
|
50
|
+
when :billing
|
|
51
|
+
handle_billing_inquiry
|
|
52
|
+
when :general
|
|
53
|
+
handle_general_inquiry
|
|
54
|
+
when :escalate
|
|
55
|
+
escalate_to_human("Complex issue requiring human attention")
|
|
56
|
+
else
|
|
57
|
+
app.say "I understand you need help. Let me connect you with one of our team members who can assist you better."
|
|
58
|
+
escalate_to_human("Unrecognized inquiry type")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def first_message?
|
|
65
|
+
# Check if this is the first message in the conversation
|
|
66
|
+
app.session.get("message_count").nil?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def categorize_inquiry(inquiry)
|
|
70
|
+
# Simple keyword-based categorization
|
|
71
|
+
# In a real app, you might use AI/ML for better categorization
|
|
72
|
+
text = inquiry.downcase
|
|
73
|
+
|
|
74
|
+
if text.include?("bug") || text.include?("error") || text.include?("not working")
|
|
75
|
+
:technical_support
|
|
76
|
+
elsif text.include?("bill") || text.include?("payment") || text.include?("refund")
|
|
77
|
+
:billing
|
|
78
|
+
elsif text.include?("urgent") || text.include?("important") || text.include?("asap")
|
|
79
|
+
:escalate
|
|
80
|
+
else
|
|
81
|
+
:general
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def handle_technical_support
|
|
86
|
+
solution = app.screen(:tech_solution) do |prompt|
|
|
87
|
+
prompt.select "I can help you with technical issues! What type of problem are you experiencing?", {
|
|
88
|
+
"login" => "Can't log in",
|
|
89
|
+
"performance" => "App is slow",
|
|
90
|
+
"features" => "Feature not working",
|
|
91
|
+
"other" => "Something else"
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
case solution
|
|
96
|
+
when "login"
|
|
97
|
+
provide_login_help
|
|
98
|
+
when "performance"
|
|
99
|
+
provide_performance_tips
|
|
100
|
+
when "features"
|
|
101
|
+
provide_feature_help
|
|
102
|
+
when "other"
|
|
103
|
+
escalate_to_human("Technical issue requiring human review")
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def handle_billing_inquiry
|
|
108
|
+
app.say "For billing questions, I'm connecting you with our billing team who can access your account securely."
|
|
109
|
+
escalate_to_human("Billing inquiry", team_id: "billing_team_id")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def handle_general_inquiry
|
|
113
|
+
satisfaction = app.screen(:satisfaction) do |prompt|
|
|
114
|
+
prompt.select "I hope I was able to help! How would you rate your experience?", {
|
|
115
|
+
"1" => "😞 Poor",
|
|
116
|
+
"2" => "😐 Okay",
|
|
117
|
+
"3" => "😊 Good",
|
|
118
|
+
"4" => "😍 Excellent"
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
if satisfaction.to_i >= 3
|
|
123
|
+
app.say "Thank you for the positive feedback! Feel free to reach out if you need anything else."
|
|
124
|
+
else
|
|
125
|
+
app.say "I'm sorry the experience wasn't better. Let me connect you with a team member who can help."
|
|
126
|
+
escalate_to_human("User reported poor experience")
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def provide_login_help
|
|
131
|
+
app.say <<~MESSAGE
|
|
132
|
+
Here are some steps to try for login issues:
|
|
133
|
+
|
|
134
|
+
1. Make sure you're using the correct email address
|
|
135
|
+
2. Try resetting your password using the "Forgot Password" link
|
|
136
|
+
3. Clear your browser cache and cookies
|
|
137
|
+
4. Try logging in from an incognito/private window
|
|
138
|
+
|
|
139
|
+
If none of these work, I'll connect you with our technical support team.
|
|
140
|
+
MESSAGE
|
|
141
|
+
|
|
142
|
+
still_stuck = app.screen(:login_resolved) do |prompt|
|
|
143
|
+
prompt.yes? "Did these steps help resolve your login issue?"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
unless still_stuck
|
|
147
|
+
escalate_to_human("Login issue not resolved by self-service steps")
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def provide_performance_tips
|
|
152
|
+
app.say <<~MESSAGE
|
|
153
|
+
Here are some tips to improve app performance:
|
|
154
|
+
|
|
155
|
+
1. Close other browser tabs or apps
|
|
156
|
+
2. Check your internet connection
|
|
157
|
+
3. Try refreshing the page
|
|
158
|
+
4. Update your browser to the latest version
|
|
159
|
+
|
|
160
|
+
If the problem persists, our technical team can investigate further.
|
|
161
|
+
MESSAGE
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def provide_feature_help
|
|
165
|
+
feature = app.screen(:which_feature) do |prompt|
|
|
166
|
+
prompt.ask "Which feature are you having trouble with? Please describe what you're trying to do."
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
app.say "Thanks for the details about #{feature}. Let me connect you with our product team who can provide specific guidance."
|
|
170
|
+
escalate_to_human("Feature support request: #{feature}")
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def escalate_to_human(reason, team_id: nil)
|
|
174
|
+
conversation_id = app.context["request.conversation_id"]
|
|
175
|
+
|
|
176
|
+
# Business logic: Use Intercom manager for conversation control
|
|
177
|
+
if app.gateway == :intercom_api
|
|
178
|
+
# Access the gateway's client and manager for escalation
|
|
179
|
+
# This would be implemented by exposing these through the gateway
|
|
180
|
+
app.say "I'm connecting you with one of our team members who can provide more personalized assistance. They'll be with you shortly!"
|
|
181
|
+
|
|
182
|
+
# Business logic would handle:
|
|
183
|
+
# - Remove AI_HANDLING tag
|
|
184
|
+
# - Add ESCALATED tag
|
|
185
|
+
# - Assign to appropriate team
|
|
186
|
+
# - Set priority based on reason
|
|
187
|
+
# - Add internal note for context
|
|
188
|
+
|
|
189
|
+
FlowChat.logger.info { "CustomerSupportFlow: Escalating conversation #{conversation_id} - Reason: #{reason}" }
|
|
190
|
+
else
|
|
191
|
+
app.say "Let me transfer you to a human agent who can help you further."
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -9,7 +9,7 @@ class MultiTenantWhatsappController < ApplicationController
|
|
|
9
9
|
tenant = determine_tenant(request)
|
|
10
10
|
whatsapp_config = get_whatsapp_config_for_tenant(tenant)
|
|
11
11
|
|
|
12
|
-
processor = FlowChat::
|
|
12
|
+
processor = FlowChat::Processor.new(self, enable_simulator: !Rails.env.production?) do |config|
|
|
13
13
|
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, whatsapp_config
|
|
14
14
|
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
15
15
|
end
|
|
@@ -96,7 +96,7 @@ class DatabaseWhatsappController < ApplicationController
|
|
|
96
96
|
config.app_secret = business_account.whatsapp_app_secret
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
-
processor = FlowChat::
|
|
99
|
+
processor = FlowChat::Processor.new(self) do |config|
|
|
100
100
|
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, whatsapp_config
|
|
101
101
|
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
102
102
|
end
|
|
@@ -135,7 +135,7 @@ class EnvironmentWhatsappController < ApplicationController
|
|
|
135
135
|
FlowChat::Whatsapp::Configuration.from_credentials
|
|
136
136
|
end
|
|
137
137
|
|
|
138
|
-
processor = FlowChat::
|
|
138
|
+
processor = FlowChat::Processor.new(self) do |config|
|
|
139
139
|
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, whatsapp_config
|
|
140
140
|
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
141
141
|
end
|
|
@@ -193,7 +193,7 @@ class CustomWhatsappController < ApplicationController
|
|
|
193
193
|
my_config.app_secret = "your_app_secret"
|
|
194
194
|
my_config.business_account_id = "your_business_account_id"
|
|
195
195
|
|
|
196
|
-
processor = FlowChat::
|
|
196
|
+
processor = FlowChat::Processor.new(self) do |config|
|
|
197
197
|
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, my_config
|
|
198
198
|
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
199
199
|
end
|
|
@@ -31,6 +31,15 @@ class SimulatorController < ApplicationController
|
|
|
31
31
|
icon: "💬",
|
|
32
32
|
color: "#25D366"
|
|
33
33
|
},
|
|
34
|
+
http_main: {
|
|
35
|
+
name: "Main HTTP API",
|
|
36
|
+
description: "JSON HTTP API endpoint",
|
|
37
|
+
processor_type: "http",
|
|
38
|
+
gateway: "http_simple",
|
|
39
|
+
endpoint: "/http/webhook",
|
|
40
|
+
icon: "🌐",
|
|
41
|
+
color: "#0066cc"
|
|
42
|
+
},
|
|
34
43
|
whatsapp_tenant_a: {
|
|
35
44
|
name: "Tenant A WhatsApp",
|
|
36
45
|
description: "Multi-tenant endpoint for Tenant A",
|
|
@@ -40,6 +49,15 @@ class SimulatorController < ApplicationController
|
|
|
40
49
|
icon: "🏢",
|
|
41
50
|
color: "#fd7e14"
|
|
42
51
|
},
|
|
52
|
+
http_external: {
|
|
53
|
+
name: "External HTTP Test",
|
|
54
|
+
description: "Test with external HTTP server",
|
|
55
|
+
processor_type: "http",
|
|
56
|
+
gateway: "http_simple",
|
|
57
|
+
endpoint: "http://localhost:4567/http/webhook",
|
|
58
|
+
icon: "🔗",
|
|
59
|
+
color: "#17a2b8"
|
|
60
|
+
},
|
|
43
61
|
whatsapp_legacy: {
|
|
44
62
|
name: "Legacy WhatsApp",
|
|
45
63
|
description: "Legacy endpoint for compatibility",
|
|
@@ -54,7 +72,7 @@ class SimulatorController < ApplicationController
|
|
|
54
72
|
|
|
55
73
|
# Default configuration to start with
|
|
56
74
|
def default_config_key
|
|
57
|
-
:
|
|
75
|
+
:http_main
|
|
58
76
|
end
|
|
59
77
|
|
|
60
78
|
# Default test phone number
|
|
@@ -79,8 +97,10 @@ end
|
|
|
79
97
|
# 5. View request/response logs in real-time
|
|
80
98
|
|
|
81
99
|
# This allows you to test:
|
|
100
|
+
# - Different protocol types (USSD, WhatsApp, HTTP)
|
|
82
101
|
# - Different controller implementations on the same server
|
|
83
102
|
# - Different API versions (v1, v2, etc.)
|
|
84
103
|
# - Multi-tenant endpoints with different configurations
|
|
85
104
|
# - Legacy endpoints alongside new ones
|
|
105
|
+
# - External HTTP servers (run examples/http_simulator_test.rb)
|
|
86
106
|
# - Different flow implementations for different endpoints
|