class Socrates::Core::Dispatcher
Constants
- DEFAULT_ERROR_MESSAGE
Public Class Methods
new(adapter:, state_factory:, storage: nil)
click to toggle source
# File lib/socrates/core/dispatcher.rb, line 15 def initialize(adapter:, state_factory:, storage: nil) @adapter = adapter @state_factory = state_factory @storage = storage || Socrates.config.storage @logger = Socrates.config.logger @error_handler = Socrates.config.error_handler @error_message = Socrates.config.error_message || DEFAULT_ERROR_MESSAGE end
Public Instance Methods
conversation_state(user)
click to toggle source
# File lib/socrates/core/dispatcher.rb, line 56 def conversation_state(user) client_id = @adapter.client_id_from(user: user) return nil unless @storage.has_key?(client_id) state_data = @storage.fetch(client_id) state_data = nil if state_data&.expired? || state_data&.finished? state_data end
dispatch(message, context: {})
click to toggle source
# File lib/socrates/core/dispatcher.rb, line 25 def dispatch(message, context: {}) client_id = @adapter.client_id_from(context: context) channel = @adapter.channel_from(context: context) user = @adapter.user_from(context: context) session = Session.new(client_id: client_id, channel: channel, user: user) do_dispatch(session, message) end
start_conversation(user, state_id, message: nil)
click to toggle source
# File lib/socrates/core/dispatcher.rb, line 35 def start_conversation(user, state_id, message: nil) client_id = @adapter.client_id_from(user: user) channel = @adapter.channel_from(user: user) session = Session.new(client_id: client_id, channel: channel, user: user) # Now, we assume the user of this code does this check on their own... # return false unless conversation_state(user).nil? # Create state data to match the request. state_data = StateData.new(state_id: state_id, state_action: :ask) persist_state_data(session.client_id, state_data) # Send our initial message if one was passed to us. @adapter.queue_direct_message(session, message, user) if message.present? do_dispatch(session, nil) true end
Private Instance Methods
do_dispatch(session, message)
click to toggle source
rubocop:disable Metrics/AbcSize
# File lib/socrates/core/dispatcher.rb, line 72 def do_dispatch(session, message) message = message&.strip @logger.info %Q(#{session.client_id} recv: "#{message}") # In many cases, a two actions will run in this loop: :listen => :ask, but it's possible that a chain of 2 or # more :ask actions could run, before stopping at a :listen (and waiting for the next input). loop do state_data = fetch_state_data(session.client_id) state = instantiate_state(session, state_data) args = [state.data.state_action] args << message if state.data.state_action == :listen msg = "#{session.client_id} processing :#{state.data.state_id} / :#{args.first}" msg += %Q( / message: "#{args.second}") if args.count > 1 @logger.debug msg begin state.send(*args) rescue StandardError => e handle_action_error(e, session, state) return end # Update the persisted state data so we know what to run next time. state.data.state_id = state.next_state_id state.data.state_action = state.next_state_action @logger.debug "#{session.client_id} transition to :#{state.data.state_id} / :#{state.data.state_action}" persist_state_data(session.client_id, state.data) # Break from the loop if there's nothing left to do, i.e. no more state transitions. break if done_transitioning?(state) end # rubocop:enable Metrics/AbcSize # Flush the session, which contains any not-yet-send messages. @adapter.flush_session(session) end
done_transitioning?(state)
click to toggle source
# File lib/socrates/core/dispatcher.rb, line 146 def done_transitioning?(state) # Stop transitioning if we're waiting for the user to respond (i.e. we're listening). return true if state.data.state_action == :listen # Stop transitioning if there's no state to transition to, or the conversation has ended. state.data.state_id.nil? || state.data.state_id == StateData::END_OF_CONVERSATION end
fetch_state_data(client_id)
click to toggle source
rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
# File lib/socrates/core/dispatcher.rb, line 115 def fetch_state_data(client_id) state_data = @storage.fetch(client_id) || StateData.new # If the current state is nil or END_OF_CONVERSATION, set it to the default state, which is typically a state # that waits for an initial command or input from the user (e.g. help, start, etc). if state_data.state_id.nil? || state_data.state_id == StateData::END_OF_CONVERSATION default_state, default_action = @state_factory.default state_data.state_id = default_state state_data.state_action = default_action || :listen # Check to see if the last interation was too long ago. elsif state_data.expired? && @state_factory.expired(state_data).present? expired_state, expired_action = @state_factory.expired(state_data) state_data.state_id = expired_state state_data.state_action = expired_action || :ask end state_data end
handle_action_error(error, session, state)
click to toggle source
# File lib/socrates/core/dispatcher.rb, line 154 def handle_action_error(error, session, state) msg = "Error while processing action #{state.data.state_id}/#{state.data.state_action}: #{error.message}" @logger.warn msg @logger.warn error @error_handler.call(msg, error) if @error_handler.present? @adapter.queue_message(session, @error_message, send_now: true) state.data.clear state.data.state_id = nil state.data.state_action = nil persist_state_data(session.client_id, state.data) end
instantiate_state(session, state_data)
click to toggle source
# File lib/socrates/core/dispatcher.rb, line 142 def instantiate_state(session, state_data) @state_factory.build(state_data: state_data, adapter: @adapter, session: session) end
persist_state_data(client_id, state_data)
click to toggle source
rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
# File lib/socrates/core/dispatcher.rb, line 138 def persist_state_data(client_id, state_data) @storage.persist(client_id, state_data) end