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,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowChat
4
+ # Generic background job that uses the Factory pattern
5
+ # Automatically used when use_async is called without a job class
6
+ #
7
+ # Example:
8
+ # # In webhook controller
9
+ # processor = FlowChat::Processor.new(self) do |config|
10
+ # config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
11
+ # config.use_session_store FlowChat::Session::CacheSessionStore
12
+ # config.use_async(factory: :whatsapp) # No job class - uses GenericAsyncJob
13
+ # end
14
+ #
15
+ # # Background job executes:
16
+ # FlowChat::Factory.execute(:whatsapp, controller: controller)
17
+ class GenericAsyncJob < AsyncJob
18
+ def execute(controller, factory:, **job_params)
19
+ FlowChat.logger.debug { "GenericAsyncJob: Executing factory '#{factory}' with params: #{job_params.inspect}" }
20
+
21
+ unless FlowChat::Factory.registered?(factory)
22
+ raise FlowChat::Factory::FactoryNotFoundError, "Factory '#{factory}' not registered"
23
+ end
24
+
25
+ FlowChat::Factory.execute(factory, controller: controller)
26
+
27
+ FlowChat.logger.debug { "GenericAsyncJob: Factory '#{factory}' executed successfully" }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,125 @@
1
+ module FlowChat
2
+ module Http
3
+ class ConfigurationError < StandardError; end
4
+
5
+ module Gateway
6
+ class Simple
7
+ include FlowChat::Instrumentation
8
+ include FlowChat::GatewayAsyncSupport
9
+
10
+ attr_reader :context
11
+
12
+ def initialize(app, user_params)
13
+ @app = app
14
+ @user_params = user_params
15
+
16
+ validate_user_params!
17
+ end
18
+
19
+ def call(context)
20
+ @context = context
21
+ @controller = context.controller
22
+ params = @controller.request.params
23
+ request = @controller.request
24
+
25
+ # Validate request method
26
+ unless request.get? || request.post?
27
+ @controller.head :bad_request
28
+ return
29
+ end
30
+
31
+ # Set request information from user_params
32
+ context["request.id"] = @user_params[:session_id]
33
+ context["request.user_id"] = @user_params[:user_id]
34
+ context["request.user_name"] = @user_params[:name] if @user_params[:name]
35
+ context["request.msisdn"] = @user_params[:msisdn] if @user_params[:msisdn]
36
+ context["request.email"] = @user_params[:email] if @user_params[:email]
37
+ context["request.message_id"] = SecureRandom.uuid
38
+ context["request.timestamp"] = Time.current.iso8601
39
+ context["request.gateway"] = :http_simple
40
+ context["request.platform"] = :http
41
+ context["request.body"] = (params.respond_to?(:to_unsafe_h) ? params.to_unsafe_h : params.to_h).transform_keys(&:to_s)
42
+
43
+ # HTTP-specific request metadata
44
+ context["http.method"] = request.method
45
+ context["http.path"] = request.path
46
+ context["http.user_agent"] = request.user_agent
47
+ context.input = params["input"].presence || ""
48
+
49
+ # Instrument message received when user provides input
50
+ if context.input.present?
51
+ instrument(Events::MESSAGE_RECEIVED, {
52
+ from: context["request.user_id"],
53
+ message: context.input,
54
+ timestamp: context["request.timestamp"]
55
+ })
56
+ end
57
+
58
+ # Determine routing: async enqueue, background execute, or inline
59
+ if should_enqueue_async?
60
+ # HTTP request with async enabled → enqueue job and return immediately
61
+ enqueue_async_job
62
+ @controller.render json: {status: "processing"}
63
+ else
64
+ # Background OR inline → process message
65
+ # Process the request
66
+ response = @app.call(context)
67
+
68
+ # Handle nil response (e.g., from middleware that handles the response itself)
69
+ unless response
70
+ return @controller.render json: {
71
+ type: :skip,
72
+ session_id: context["request.id"],
73
+ user_id: context["request.user_id"],
74
+ timestamp: context["request.timestamp"]
75
+ }
76
+ end
77
+
78
+ type, prompt, choices, media = response
79
+
80
+ # Instrument message sent
81
+ instrument(Events::MESSAGE_SENT, {
82
+ to: context["request.user_id"],
83
+ session_id: context["request.id"],
84
+ message: context.input || "",
85
+ message_type: (type == :prompt) ? "prompt" : "terminal",
86
+ gateway: :http_simple,
87
+ platform: :http,
88
+ content_length: prompt.to_s.length,
89
+ timestamp: context["request.timestamp"]
90
+ })
91
+
92
+ # Render response as JSON
93
+ response_data = render_response(type, prompt, choices, media)
94
+ @controller.render json: response_data
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ def validate_user_params!
101
+ required_keys = [:session_id, :user_id]
102
+
103
+ required_keys.each do |key|
104
+ unless @user_params.key?(key)
105
+ raise FlowChat::Http::ConfigurationError,
106
+ "HTTP Simple gateway requires :#{key} in user_params"
107
+ end
108
+ end
109
+ end
110
+
111
+ def render_response(type, prompt, choices, media)
112
+ rendered = FlowChat::Http::Renderer.new(prompt, choices: choices, media: media).render
113
+
114
+ {
115
+ type: type,
116
+ session_id: context["request.id"],
117
+ user_id: context["request.user_id"],
118
+ timestamp: context["request.timestamp"],
119
+ **rendered
120
+ }
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,41 @@
1
+ module FlowChat
2
+ module Http
3
+ class Renderer
4
+ attr_reader :prompt, :choices, :media
5
+
6
+ def initialize(prompt, choices: nil, media: nil)
7
+ @prompt = prompt
8
+ @choices = choices
9
+ @media = media
10
+ end
11
+
12
+ def render = build_response
13
+
14
+ private
15
+
16
+ def build_response
17
+ {
18
+ message: prompt,
19
+ choices: format_choices,
20
+ media: format_media
21
+ }.compact
22
+ end
23
+
24
+ def format_choices
25
+ return unless choices.present?
26
+
27
+ choices.map { |key, value| {key: key, value: value} }
28
+ end
29
+
30
+ def format_media
31
+ return unless media.present?
32
+
33
+ {
34
+ url: media[:url],
35
+ type: media[:type] || :image,
36
+ caption: media[:caption]
37
+ }.compact
38
+ end
39
+ end
40
+ end
41
+ end
@@ -2,7 +2,7 @@ module FlowChat
2
2
  module Instrumentation
3
3
  module Setup
4
4
  class << self
5
- attr_accessor :log_subscriber, :metrics_collector
5
+ attr_accessor :log_subscriber
6
6
 
7
7
  # Initialize instrumentation with default subscribers
8
8
  def initialize!
@@ -8,9 +8,11 @@ module FlowChat
8
8
  def instrument(event_name, payload = {}, &block)
9
9
  enriched_payload = payload&.dup || {}
10
10
  if respond_to?(:context) && context
11
+ enriched_payload[:request_id] = context["request.id"] if context["request.id"]
11
12
  enriched_payload[:session_id] = context["session.id"] if context["session.id"]
12
13
  enriched_payload[:flow_name] = context["flow.name"] if context["flow.name"]
13
14
  enriched_payload[:gateway] = context["request.gateway"] if context["request.gateway"]
15
+ enriched_payload[:platform] = context["request.platform"] if context["request.platform"]
14
16
  end
15
17
 
16
18
  self.class.instrument(event_name, enriched_payload, &block)
@@ -33,6 +35,23 @@ module FlowChat
33
35
  ActiveSupport::Notifications.instrument(full_event_name, enriched_payload, &block)
34
36
  end
35
37
 
38
+ # Shared helper for reporting API errors with instrumentation and Rails.error
39
+ # @param message [String] Error message
40
+ # @param error [Exception, nil] Original exception if available
41
+ # @param context [Hash] Platform-specific error context (must include :platform)
42
+ def self.report_api_error(message, error: nil, **context)
43
+ error_context = context.compact
44
+
45
+ # Instrument for custom subscribers
46
+ instrument(Events::API_ERROR, error_context.merge(message: message))
47
+
48
+ # Report to Rails.error if available
49
+ if defined?(Rails) && Rails.respond_to?(:error) && Rails.error.respond_to?(:report)
50
+ exception = error || StandardError.new(message)
51
+ Rails.error.report(exception, handled: true, context: error_context)
52
+ end
53
+ end
54
+
36
55
  # Predefined event names for consistency
37
56
  module Events
38
57
  # Core framework events
@@ -59,12 +78,18 @@ module FlowChat
59
78
  WEBHOOK_FAILED = "webhook.failed"
60
79
  API_REQUEST = "api.request"
61
80
  MEDIA_UPLOAD = "media.upload"
81
+ API_ERROR = "api.error"
62
82
 
63
83
  PAGINATION_TRIGGERED = "pagination.triggered"
64
84
 
65
85
  # Middleware events
66
86
  MIDDLEWARE_BEFORE = "middleware.before"
67
87
  MIDDLEWARE_AFTER = "middleware.after"
88
+
89
+ # Conversation management events (for Intercom and similar platforms)
90
+ CONVERSATION_ASSIGNED = "conversation.assigned"
91
+ CONVERSATION_TAGGED = "conversation.tagged"
92
+ CONVERSATION_STATE_CHANGED = "conversation.state_changed"
68
93
  end
69
94
  end
70
95
  end
@@ -0,0 +1,155 @@
1
+ require "intercom"
2
+ require "json"
3
+ require "reverse_markdown"
4
+
5
+ module FlowChat
6
+ module Intercom
7
+ # Configuration-related errors
8
+ class ConfigurationError < StandardError; end
9
+
10
+ # Rate limiting error
11
+ class RateLimitError < StandardError
12
+ attr_reader :retry_after
13
+
14
+ def initialize(message, retry_after = nil)
15
+ super(message)
16
+ @retry_after = retry_after
17
+ end
18
+ end
19
+
20
+ class Client
21
+ include FlowChat::Instrumentation
22
+
23
+ attr_reader :intercom
24
+ attr_accessor :app_id
25
+
26
+ # Convert HTML from Intercom messages to Markdown
27
+ def self.parse_html(html)
28
+ ReverseMarkdown.convert(html.to_s).strip.presence || ""
29
+ end
30
+
31
+ def parse_message(html)
32
+ self.class.parse_html(html)
33
+ end
34
+
35
+ def initialize(config)
36
+ @config = config
37
+ @intercom = ::Intercom::Client.new(token: @config.access_token)
38
+ FlowChat.logger.info { "Intercom::Client: Initialized Intercom client" }
39
+ FlowChat.logger.debug { "Intercom::Client: API base URL: #{@config.api_base_url}" }
40
+ end
41
+
42
+ # Send a reply to a conversation
43
+ # @param conversation_id [String] Conversation ID
44
+ # @param response [Array] FlowChat response array [type, content, options]
45
+ # @return [Hash] API response or nil on error
46
+ def send_message(conversation_id, prompt, choices: nil, media: nil)
47
+ FlowChat.logger.info { "Intercom::Client: Sending message to conversation #{conversation_id}" }
48
+ FlowChat.logger.debug { "Intercom::Client: Message content: '#{prompt.to_s.truncate(100)}'" }
49
+
50
+ # Use renderer to convert to structured response
51
+ response = FlowChat::Intercom::Renderer.new(prompt, choices: choices, media: media).render
52
+ type, content, _ = response
53
+
54
+ result = instrument(Events::MESSAGE_SENT, {
55
+ to: conversation_id,
56
+ message_type: type.to_s,
57
+ content_length: content.to_s.length,
58
+ platform: :intercom
59
+ }) do
60
+ # Determine message type based on response type
61
+ message_type = case type
62
+ when :note
63
+ "note"
64
+ else
65
+ "comment"
66
+ end
67
+
68
+ # Send using official gem
69
+ reply = intercom.conversations.reply(
70
+ id: conversation_id,
71
+ type: "admin",
72
+ admin_id: @config.admin_id.to_s,
73
+ message_type: message_type,
74
+ body: content.to_s
75
+ )
76
+
77
+ reply.to_hash
78
+ end
79
+
80
+ if result
81
+ message_id = result["id"]
82
+ FlowChat.logger.debug { "Intercom::Client: Message sent successfully to conversation #{conversation_id}, message_id: #{message_id}" }
83
+ else
84
+ FlowChat.logger.error { "Intercom::Client: Failed to send message to conversation #{conversation_id}" }
85
+ end
86
+
87
+ result
88
+ rescue ::Intercom::ResourceNotFound => e
89
+ FlowChat.logger.error { "Intercom::Client: Conversation not found: #{e.message}" }
90
+ report_api_error("Intercom conversation not found", error: e, conversation_id: conversation_id)
91
+ nil
92
+ rescue ::Intercom::AuthenticationError => e
93
+ FlowChat.logger.error { "Intercom::Client: Authentication failed - check access token" }
94
+ report_api_error("Intercom authentication failed", error: e, conversation_id: conversation_id)
95
+ raise ConfigurationError, "Invalid Intercom access token"
96
+ rescue ::Intercom::RateLimitExceeded
97
+ retry_after = 60
98
+ FlowChat.logger.warn { "Intercom::Client: Rate limit exceeded - retry after #{retry_after}s" }
99
+ raise RateLimitError.new("Intercom API rate limit exceeded", retry_after)
100
+ rescue ::Intercom::ServerError => e
101
+ FlowChat.logger.error { "Intercom::Client: Server error: #{e.message}" }
102
+ report_api_error("Intercom server error", error: e, conversation_id: conversation_id)
103
+ nil
104
+ rescue => e
105
+ FlowChat.logger.error { "Intercom::Client: API request exception: #{e.class.name}: #{e.message}" }
106
+ report_api_error("Intercom API request exception: #{e.class.name}", error: e, conversation_id: conversation_id)
107
+ nil
108
+ end
109
+
110
+ # Build reply payload for Intercom API
111
+ # This method is exposed so the gateway can use it for simulator mode
112
+ def build_reply_payload(response, conversation_id)
113
+ type, content, _ = response
114
+
115
+ case type
116
+ when :text
117
+ {
118
+ message_type: "comment",
119
+ type: "admin",
120
+ admin_id: @config.admin_id.to_s,
121
+ body: content.to_s
122
+ }
123
+ when :note
124
+ {
125
+ message_type: "note",
126
+ type: "admin",
127
+ admin_id: @config.admin_id.to_s,
128
+ body: content.to_s
129
+ }
130
+ else
131
+ # Default to comment
132
+ {
133
+ message_type: "comment",
134
+ type: "admin",
135
+ admin_id: @config.admin_id.to_s,
136
+ body: content.to_s
137
+ }
138
+ end
139
+ end
140
+
141
+ private
142
+
143
+ def report_api_error(message, error: nil, conversation_id: nil)
144
+ FlowChat::Instrumentation.report_api_error(
145
+ message,
146
+ error: error,
147
+ platform: :intercom,
148
+ app_id: @app_id,
149
+ conversation_id: conversation_id,
150
+ admin_id: @config.admin_id
151
+ )
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,149 @@
1
+ module FlowChat
2
+ module Intercom
3
+ class Configuration
4
+ attr_accessor :access_token, :client_secret, :admin_id, :name, :skip_signature_validation
5
+
6
+ # Class-level storage for named configurations
7
+ @@configurations = {}
8
+
9
+ def initialize(name)
10
+ @name = name
11
+ @access_token = nil
12
+ @client_secret = nil
13
+ @admin_id = nil
14
+ @skip_signature_validation = false
15
+
16
+ FlowChat.logger.debug { "Intercom::Configuration: Initialized configuration with name: #{name || "anonymous"}" }
17
+
18
+ register_as(name) if name.present?
19
+ end
20
+
21
+ # Load configuration from Rails credentials or environment variables
22
+ def self.from_credentials
23
+ FlowChat.logger.info { "Intercom::Configuration: Loading configuration from credentials/environment" }
24
+
25
+ config = new(nil)
26
+
27
+ if defined?(Rails) && Rails.application.credentials.intercom
28
+ FlowChat.logger.debug { "Intercom::Configuration: Loading from Rails credentials" }
29
+ credentials = Rails.application.credentials.intercom
30
+ config.access_token = credentials[:access_token]
31
+ config.client_secret = credentials[:client_secret]
32
+ config.admin_id = credentials[:admin_id]
33
+ config.skip_signature_validation = credentials[:skip_signature_validation] || false
34
+ else
35
+ FlowChat.logger.debug { "Intercom::Configuration: Loading from environment variables" }
36
+ # Fallback to environment variables
37
+ config.access_token = ENV["INTERCOM_ACCESS_TOKEN"]
38
+ config.client_secret = ENV["INTERCOM_CLIENT_SECRET"]
39
+ config.admin_id = ENV["INTERCOM_ADMIN_ID"]
40
+ config.skip_signature_validation = ENV["INTERCOM_SKIP_SIGNATURE_VALIDATION"] == "true"
41
+ end
42
+
43
+ if config.valid?
44
+ FlowChat.logger.info { "Intercom::Configuration: Configuration loaded successfully" }
45
+ else
46
+ FlowChat.logger.warn { "Intercom::Configuration: Incomplete configuration loaded - missing required fields" }
47
+ end
48
+
49
+ config
50
+ end
51
+
52
+ # Register a named configuration
53
+ def self.register(name, config)
54
+ FlowChat.logger.debug { "Intercom::Configuration: Registering configuration '#{name}'" }
55
+ @@configurations[name.to_sym] = config
56
+ end
57
+
58
+ # Get a named configuration
59
+ def self.get(name)
60
+ config = @@configurations[name.to_sym]
61
+ if config
62
+ FlowChat.logger.debug { "Intercom::Configuration: Retrieved configuration '#{name}'" }
63
+ config
64
+ else
65
+ FlowChat.logger.error { "Intercom::Configuration: Configuration '#{name}' not found" }
66
+ raise ArgumentError, "Intercom configuration '#{name}' not found"
67
+ end
68
+ end
69
+
70
+ # Check if a named configuration exists
71
+ def self.exists?(name)
72
+ exists = @@configurations.key?(name.to_sym)
73
+ FlowChat.logger.debug { "Intercom::Configuration: Configuration '#{name}' exists: #{exists}" }
74
+ exists
75
+ end
76
+
77
+ # Get all configuration names
78
+ def self.configuration_names
79
+ names = @@configurations.keys
80
+ FlowChat.logger.debug { "Intercom::Configuration: Available configurations: #{names}" }
81
+ names
82
+ end
83
+
84
+ # Clear all registered configurations (useful for testing)
85
+ def self.clear_all!
86
+ FlowChat.logger.debug { "Intercom::Configuration: Clearing all registered configurations" }
87
+ @@configurations.clear
88
+ end
89
+
90
+ # Register this configuration with a name
91
+ def register_as(name)
92
+ FlowChat.logger.debug { "Intercom::Configuration: Registering configuration as '#{name}'" }
93
+ @name = name.to_sym
94
+ self.class.register(@name, self)
95
+ self
96
+ end
97
+
98
+ def valid?
99
+ is_valid = !!(access_token && !access_token.to_s.empty? && admin_id && !admin_id.to_s.empty?)
100
+
101
+ FlowChat.logger.debug { "Intercom::Configuration: Configuration valid: #{is_valid}" }
102
+ is_valid
103
+ end
104
+
105
+ # API endpoints
106
+ def api_base_url
107
+ "https://api.intercom.io"
108
+ end
109
+
110
+ def conversations_url(conversation_id = nil)
111
+ if conversation_id
112
+ "#{api_base_url}/conversations/#{conversation_id}"
113
+ else
114
+ "#{api_base_url}/conversations"
115
+ end
116
+ end
117
+
118
+ def conversation_reply_url(conversation_id)
119
+ "#{conversations_url(conversation_id)}/reply"
120
+ end
121
+
122
+ def conversation_parts_url(conversation_id)
123
+ "#{conversations_url(conversation_id)}/parts"
124
+ end
125
+
126
+ def conversation_tags_url(conversation_id, tag_id = nil)
127
+ if tag_id
128
+ "#{conversations_url(conversation_id)}/tags/#{tag_id}"
129
+ else
130
+ "#{conversations_url(conversation_id)}/tags"
131
+ end
132
+ end
133
+
134
+ def admins_url
135
+ "#{api_base_url}/admins"
136
+ end
137
+
138
+ # Headers for API requests
139
+ def api_headers
140
+ {
141
+ "Authorization" => "Bearer #{access_token}",
142
+ "Content-Type" => "application/json",
143
+ "Accept" => "application/json",
144
+ "Intercom-Version" => "2.11"
145
+ }
146
+ end
147
+ end
148
+ end
149
+ end