socrates 0.1.10 → 0.1.11

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 83c1f9e21bcee220a261ff122a3e705e3e0c6887
4
- data.tar.gz: 6a4ccc95e6b65cb45d92c9ff17376061d12d2e7b
3
+ metadata.gz: 5888e238834056167ec5db70cabf1a25c59aa1f7
4
+ data.tar.gz: 360b99fbb350f111fadd4c9ad8596e3c3f7351fc
5
5
  SHA512:
6
- metadata.gz: 6a5b772caa5b5232f329972cfc4750a44c9bd1a741864811d58037e9814881d38f9b661494d067df006af32f359c6dc1af820c54d871ad5c1dfc815b2dd616f5
7
- data.tar.gz: b9083e36dba73f3e215a495e42cfd7cc1d921891debef8ed10d79c69d0fa3cdad29c7968e5718e8e42bb7d915ff470f42b882d07857f043fc50c74a55d64a91c
6
+ metadata.gz: 3404c0d12161fa105efaa44085f47af4780490102f4ca836ac1c2d5db0995c21a565c6e9185ee9cec9f65fcc027bfc92ff449924f385529550b3bcc1b64d787c
7
+ data.tar.gz: 51192c1becf7d53506895b13e9a4f0fc933996f786bfa34c74075c1ee62a0c08a1a1bf07223ade0a9182b1a015f75e764c35f86b25ce23ef89eb0f8b7d86b143
@@ -3,24 +3,37 @@ require "socrates/adapters/stubs"
3
3
  module Socrates
4
4
  module Adapters
5
5
  class Console
6
- CLIENT_ID = "CONSOLE"
6
+ include StubUserDirectory
7
7
 
8
- attr_accessor :email, :users
8
+ CLIENT_ID = "CONSOLE"
9
+ CHANNEL = "C1"
9
10
 
10
11
  def initialize(name: "@socrates")
11
- @name = name
12
- @users = []
12
+ super()
13
+ @name = name
13
14
  end
14
15
 
15
- def client_id_from_context(_context)
16
+ def client_id_from(context: nil, user: nil)
17
+ raise ArgumentError, "Must provide one of :context or :user" if context.nil? && user.nil?
18
+
16
19
  CLIENT_ID
17
20
  end
18
21
 
19
- def send_message(message, *)
22
+ def channel_from(context: nil, user: nil)
23
+ raise ArgumentError, "Must provide one of :context or :user" if context.nil? && user.nil?
24
+
25
+ CHANNEL
26
+ end
27
+
28
+ def send_message(message, channel)
29
+ raise ArgumentError, "Channel is required" unless channel.present?
30
+
20
31
  puts "\n#{colorize(@name, "32;1")}: #{message}"
21
32
  end
22
33
 
23
- def send_direct_message(message, user, *)
34
+ def send_direct_message(message, user)
35
+ raise ArgumentError, "User is required" unless user.present?
36
+
24
37
  name =
25
38
  if user.respond_to?(:name)
26
39
  user.name
@@ -33,18 +46,6 @@ module Socrates
33
46
  puts "\n[DM] #{colorize(name, "34;1")}: #{message}"
34
47
  end
35
48
 
36
- def add_user(id: nil, name: nil, first: nil, last: nil, email: nil)
37
- users << User.new(id, name, Profile.new(first, last, email))
38
- end
39
-
40
- def users_list(*)
41
- Response.new(users)
42
- end
43
-
44
- def lookup_email(*)
45
- email
46
- end
47
-
48
49
  private
49
50
 
50
51
  def colorize(str, color_code)
@@ -3,45 +3,67 @@ require "socrates/adapters/stubs"
3
3
  module Socrates
4
4
  module Adapters
5
5
  class Memory
6
+ include StubUserDirectory
7
+
6
8
  CLIENT_ID = "MEMORY"
9
+ CHANNEL = "C1"
7
10
 
8
- attr_reader :history, :dms
9
- attr_accessor :email, :users
11
+ attr_reader :history
12
+ attr_accessor :client_id
10
13
 
11
14
  def initialize
12
- @history = []
13
- @dms = Hash.new { |hash, key| hash[key] = [] }
14
- @users = []
15
+ super()
16
+ @history = Hash.new { |hash, key| hash[key] = [] }
17
+ end
18
+
19
+ def client_id_from(context: nil, user: nil)
20
+ raise ArgumentError, "Must provide one of :context or :user" if context.nil? && user.nil?
21
+
22
+ @client_id || CLIENT_ID
23
+ end
24
+
25
+ def channel_from(context: nil, user: nil)
26
+ raise ArgumentError, "Must provide one of :context or :user" if context.nil? && user.nil?
27
+
28
+ user.nil? ? CHANNEL : users_channel(user)
15
29
  end
16
30
 
17
- def client_id_from_context(_context)
18
- CLIENT_ID
31
+ def send_message(message, channel)
32
+ raise ArgumentError, "Channel is required" unless channel.present?
33
+
34
+ @history[channel] << message
19
35
  end
20
36
 
21
- def send_message(message, *)
22
- @history << message
37
+ def send_direct_message(message, user)
38
+ raise ArgumentError, "User is required" unless user.present?
39
+
40
+ @history[users_channel(user)] << message
23
41
  end
24
42
 
25
- def send_direct_message(message, user, *)
26
- user = user.id if user.respond_to?(:id)
43
+ #
44
+ # Methods for fetching messages and dms in specs...
45
+ #
27
46
 
28
- @dms[user] << message
47
+ def msgs
48
+ @history[CHANNEL]
29
49
  end
30
50
 
31
- def last_message
32
- @history.last
51
+ def last_msg
52
+ msgs[-1]
33
53
  end
34
54
 
35
- def add_user(id: nil, name: nil, first: nil, last: nil, email: nil)
36
- users << User.new(id, name, Profile.new(first, last, email))
55
+ def dms(user)
56
+ @history[users_channel(user)]
37
57
  end
38
58
 
39
- def users_list(*)
40
- Response.new(users)
59
+ def last_dm(user)
60
+ dms(user)[-1]
41
61
  end
42
62
 
43
- def lookup_email(*)
44
- email
63
+ private
64
+
65
+ def users_channel(user)
66
+ user.respond_to?(:id) ? user.id : user
45
67
  end
46
68
  end
47
69
  end
@@ -7,20 +7,35 @@ module Socrates
7
7
  @real_time_client = real_time_client
8
8
  end
9
9
 
10
- def client_id_from_context(context)
11
- raise ArgumentError, "Context cannot be nil" if context.nil?
12
- raise ArgumentError, "Expected context to respond to :user" unless context.respond_to?(:user)
13
-
14
- context.user
10
+ def client_id_from(context: nil, user: nil)
11
+ unless context.nil?
12
+ raise ArgumentError, "Expected :context to respond to :user" unless context.respond_to?(:user)
13
+ return context.user
14
+ end
15
+ unless user.nil?
16
+ raise ArgumentError, "Expected :user to respond to :id" unless user.respond_to?(:id)
17
+ return user.id
18
+ end
19
+ raise ArgumentError, "Must provide one of :context or :user"
15
20
  end
16
21
 
17
- def send_message(message, context:)
18
- raise ArgumentError, "Expected context to respond to :channel" unless context.respond_to?(:channel)
22
+ def channel_from(context: nil, user: nil)
23
+ unless context.nil?
24
+ raise ArgumentError, "Expected :context to respond to :channel" unless context.respond_to?(:channel)
25
+ return context.channel
26
+ end
27
+ unless user.nil?
28
+ # raise ArgumentError, "Expected :user to respond to :id" unless user.respond_to?(:id)
29
+ return lookup_im_channel(user)
30
+ end
31
+ raise ArgumentError, "Must provide one of :context or :user"
32
+ end
19
33
 
20
- @real_time_client.message(text: message, channel: context.channel)
34
+ def send_message(message, channel)
35
+ @real_time_client.message(text: message, channel: channel)
21
36
  end
22
37
 
23
- def send_direct_message(message, user, *)
38
+ def send_direct_message(message, user)
24
39
  raise ArgumentError, "Expected user to respond to :id" unless user.respond_to?(:id)
25
40
 
26
41
  im_channel = lookup_im_channel(user)
@@ -38,7 +53,7 @@ module Socrates
38
53
  end
39
54
 
40
55
  def lookup_email(context:)
41
- raise ArgumentError, "Expected context to respond to :user" unless context.respond_to?(:user)
56
+ raise ArgumentError, "Expected :context to respond to :user" unless context.respond_to?(:user)
42
57
 
43
58
  client = @real_time_client.web_client
44
59
  info = client.users_info(user: context.user)
@@ -1,7 +1,48 @@
1
1
  module Socrates
2
2
  module Adapters
3
+ #
4
+ # Response, User, Profile are POROs that represent keys concepts that exist in Slack (or other chat systems).
5
+ #
3
6
  Response = Struct.new(:members)
4
- User = Struct.new(:id, :name, :profile)
5
- Profile = Struct.new(:first_name, :last_name, :email)
7
+
8
+ User = Struct.new(:id, :name, :tz_offset, :profile) do
9
+ def real_name
10
+ return "" if profile.nil?
11
+
12
+ "#{profile.first_name} #{profile.last_name}"
13
+ end
14
+ end
15
+
16
+ Profile = Struct.new(:first_name, :last_name, :email)
17
+
18
+ #
19
+ # StubUserDirectory provides some simple stub behavior for adding stubbed users and querying against them. This are
20
+ # to be used by stubbed versions of adapters (like Console, Memory, etc).
21
+ #
22
+ module StubUserDirectory
23
+ attr_accessor :email, :users
24
+
25
+ def initialize
26
+ @users = []
27
+ end
28
+
29
+ # rubocop:disable Metrics/ParameterLists
30
+ def add_user(id: nil, name: nil, first: nil, last: nil, email: nil, tz_offset: 0)
31
+ users << User.new(id, name, tz_offset, Profile.new(first, last, email))
32
+ end
33
+ # rubocop:enable Metrics/ParameterLists
34
+
35
+ def users_list(*)
36
+ Response.new(users)
37
+ end
38
+
39
+ def lookup_user(email:)
40
+ users.find { |user| email == user.profile&.email }
41
+ end
42
+
43
+ def lookup_email(*)
44
+ email
45
+ end
46
+ end
6
47
  end
7
48
  end
@@ -20,31 +20,72 @@ module Socrates
20
20
  @error_message = Socrates.config.error_message || DEFAULT_ERROR_MESSAGE
21
21
  end
22
22
 
23
- # rubocop:disable Metrics/AbcSize
24
23
  def dispatch(message, context: {})
25
- message = message.strip
24
+ client_id = @adapter.client_id_from(context: context)
25
+ channel = @adapter.channel_from(context: context)
26
26
 
27
- client_id = @adapter.client_id_from_context(context)
27
+ do_dispatch(message, client_id, channel)
28
+ end
29
+
30
+ def start_conversation(user, state_id)
31
+ client_id = @adapter.client_id_from(user: user)
32
+ channel = @adapter.channel_from(user: user)
33
+
34
+ return false unless conversation_state(user).nil?
35
+
36
+ # Create state data to match the request.
37
+ state_data = Socrates::Core::StateData.new(state_id: state_id, state_action: :ask)
38
+
39
+ persist_state_data(client_id, state_data)
40
+
41
+ do_dispatch(nil, client_id, channel)
42
+ true
43
+ end
44
+
45
+ def conversation_state(user)
46
+ client_id = @adapter.client_id_from(user: user)
47
+
48
+ return nil unless @storage.has_key?(client_id)
28
49
 
29
- @logger.info %(#{client_id} recv: "#{message}")
50
+ begin
51
+ snapshot = @storage.get(client_id)
52
+ state_data = StateData.deserialize(snapshot)
53
+ state_data = nil if state_data_expired?(state_data)
54
+ rescue => e
55
+ @logger.warn "Error while fetching state_data for client id '#{client_id}'."
56
+ @logger.warn e
57
+ state_data = nil
58
+ end
59
+
60
+ state_data
61
+ end
62
+
63
+ private
64
+
65
+ DEFAULT_ERROR_MESSAGE = "Sorry, an error occurred. We'll have to start over..."
66
+
67
+ def do_dispatch(message, client_id, channel)
68
+ message = message&.strip
69
+
70
+ @logger.info %Q(#{client_id} recv: "#{message}")
30
71
 
31
72
  # In many cases, a two actions will run in this loop: :listen => :ask, but it's possible that a chain of 2 or
32
73
  # more :ask actions could run, before stopping at a :listen (and waiting for the next input).
33
74
  loop do
34
- state_data = fetch_snapshot(client_id)
35
- state = instantiate_state(state_data, context)
75
+ state_data = fetch_state_data(client_id)
76
+ state = instantiate_state(state_data, channel)
36
77
 
37
78
  args = [state.data.state_action]
38
79
  args << message if state.data.state_action == :listen
39
80
 
40
- msg = %(#{client_id} processing :#{state.data.state_id} / :#{args.first})
41
- msg += %( / message: "#{args.second}") if args.count > 1
81
+ msg = "#{client_id} processing :#{state.data.state_id} / :#{args.first}"
82
+ msg += %Q( / message: "#{args.second}") if args.count > 1
42
83
  @logger.debug msg
43
84
 
44
85
  begin
45
86
  state.send(*args)
46
87
  rescue => e
47
- handle_action_error(e, client_id, state, context)
88
+ handle_action_error(e, client_id, state, channel)
48
89
  return
49
90
  end
50
91
 
@@ -52,28 +93,23 @@ module Socrates
52
93
  state.data.state_id = state.next_state_id
53
94
  state.data.state_action = state.next_state_action
54
95
 
55
- @logger.debug %(#{client_id} transition to :#{state.data.state_id} / :#{state.data.state_action})
96
+ @logger.debug "#{client_id} transition to :#{state.data.state_id} / :#{state.data.state_action}"
56
97
 
57
- persist_snapshot(client_id, state.data)
98
+ persist_state_data(client_id, state.data)
58
99
 
59
100
  # Break from the loop if there's nothing left to do, i.e. no more state transitions.
60
101
  break if done_transitioning?(state)
61
102
  end
62
103
  end
63
- # rubocop:enable Metrics/AbcSize
64
-
65
- private
66
-
67
- DEFAULT_ERROR_MESSAGE = "Sorry, an error occurred. We'll have to start over..."
68
104
 
69
105
  # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
70
- def fetch_snapshot(client_id)
106
+ def fetch_state_data(client_id)
71
107
  if @storage.has_key?(client_id)
72
108
  begin
73
109
  snapshot = @storage.get(client_id)
74
110
  state_data = StateData.deserialize(snapshot)
75
111
  rescue => e
76
- @logger.warn "Error while fetching snapshot for client id '#{client_id}', resetting state: #{e.message}"
112
+ @logger.warn "Error while fetching state_data for client id '#{client_id}', resetting state: #{e.message}"
77
113
  @logger.warn e
78
114
  end
79
115
  end
@@ -100,19 +136,19 @@ module Socrates
100
136
  end
101
137
  # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
102
138
 
103
- def persist_snapshot(client_id, state_data)
139
+ def persist_state_data(client_id, state_data)
104
140
  state_data.reset_elapsed_time
105
141
  @storage.put(client_id, state_data.serialize)
106
142
  end
107
143
 
108
144
  def state_data_expired?(state_data)
109
- return unless state_data.timestamp.present?
145
+ return false unless state_data.last_interaction_timestamp.present?
110
146
 
111
147
  state_data.elapsed_time > (Socrates.config.expired_timeout || 30.minutes)
112
148
  end
113
149
 
114
- def instantiate_state(state_data, context)
115
- @state_factory.build(state_data: state_data, adapter: @adapter, context: context)
150
+ def instantiate_state(state_data, channel)
151
+ @state_factory.build(state_data: state_data, adapter: @adapter, channel: channel)
116
152
  end
117
153
 
118
154
  def done_transitioning?(state)
@@ -123,16 +159,16 @@ module Socrates
123
159
  state.data.state_id.nil? || state.data.state_id == State::END_OF_CONVERSATION
124
160
  end
125
161
 
126
- def handle_action_error(e, client_id, state, context)
162
+ def handle_action_error(e, client_id, state, channel)
127
163
  @logger.warn "Error while processing action #{state.data.state_id}/#{state.data.state_action}: #{e.message}"
128
164
  @logger.warn e
129
165
 
130
- @adapter.send_message(@error_message, context: context)
166
+ @adapter.send_message(@error_message, channel)
131
167
  state.data.clear
132
168
  state.data.state_id = nil
133
169
  state.data.state_action = nil
134
170
 
135
- persist_snapshot(client_id, state.data)
171
+ persist_state_data(client_id, state.data)
136
172
  end
137
173
  end
138
174
  end
@@ -11,10 +11,10 @@ module Socrates
11
11
  module State
12
12
  attr_reader :data, :context
13
13
 
14
- def initialize(data: StateData.new, adapter:, context: nil)
14
+ def initialize(data: StateData.new, adapter:, channel:)
15
15
  @data = data
16
16
  @adapter = adapter
17
- @context = context
17
+ @channel = channel
18
18
  @next_state_id = nil
19
19
  @next_state_action = nil
20
20
  @logger = Socrates.config.logger || Socrates::Logger.default
@@ -46,15 +46,15 @@ module Socrates
46
46
 
47
47
  return if message.empty?
48
48
 
49
- @logger.info %(#{client_id} send: "#{format_for_logging(message)}")
50
- @adapter.send_message(message, context: @context)
49
+ @logger.info %Q(#{@channel} send: "#{format_for_logging(message)}")
50
+ @adapter.send_message(message, @channel)
51
51
  end
52
52
 
53
53
  def send_message(to:, message:)
54
54
  displayable_to = to.respond_to?(:id) ? to.id : to
55
55
 
56
- @logger.info %(#{client_id} send direct to #{displayable_to}: "#{format_for_logging(message)}")
57
- @adapter.send_direct_message(message, to, context: @context)
56
+ @logger.info %Q(#{@channel} send direct to #{displayable_to}: "#{format_for_logging(message)}")
57
+ @adapter.send_direct_message(message, to)
58
58
  end
59
59
 
60
60
  def transition_to(state_id, action: nil, data: {})
@@ -102,10 +102,6 @@ module Socrates
102
102
  (%i[ask listen] - [current_action]).first
103
103
  end
104
104
 
105
- def client_id
106
- @adapter.client_id_from_context(@context)
107
- end
108
-
109
105
  def format_for_logging(message)
110
106
  message.gsub("\n", "\\n")
111
107
  end
@@ -6,22 +6,21 @@ require "active_support/core_ext/time"
6
6
  module Socrates
7
7
  module Core
8
8
  class StateData
9
- attr_accessor :state_id, :state_action, :timestamp
9
+ attr_accessor :state_id, :state_action, :last_interaction_timestamp
10
10
 
11
- def initialize(state_id: nil, state_action: nil, data: {}, temporary_keys: [])
11
+ def initialize(state_id: nil, state_action: nil, data: {})
12
12
  @state_id = state_id
13
13
  @state_action = state_action
14
- @timestamp = Time.current
15
14
  @data = data
16
- @temporary_keys = temporary_keys
15
+ @temporary_keys = []
17
16
  end
18
17
 
19
18
  def elapsed_time
20
- Time.current - @timestamp
19
+ Time.current - @last_interaction_timestamp
21
20
  end
22
21
 
23
22
  def reset_elapsed_time
24
- @timestamp = Time.current
23
+ @last_interaction_timestamp = Time.current
25
24
  end
26
25
 
27
26
  def keys
@@ -14,11 +14,11 @@ module Socrates
14
14
  :expired
15
15
  end
16
16
 
17
- def build(state_data:, adapter:, context: nil)
17
+ def build(state_data:, adapter:, channel:)
18
18
  classname = StringHelpers.underscore_to_classname(state_data.state_id)
19
19
 
20
20
  Object.const_get("Socrates::SampleStates::#{classname}")
21
- .new(data: state_data, adapter: adapter, context: context)
21
+ .new(data: state_data, adapter: adapter, channel: channel)
22
22
  end
23
23
  end
24
24
 
@@ -20,6 +20,10 @@ module Socrates
20
20
  def put(key, value)
21
21
  @memory[key] = value
22
22
  end
23
+
24
+ def clear_all
25
+ @memory.clear
26
+ end
23
27
  end
24
28
  end
25
29
  end
@@ -1,3 +1,3 @@
1
1
  module Socrates
2
- VERSION = "0.1.10"
2
+ VERSION = "0.1.11"
3
3
  end
data/socrates.gemspec CHANGED
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
28
28
  spec.add_development_dependency "bundler", "~> 1.14"
29
29
  spec.add_development_dependency "rake", "~> 10.5"
30
30
  spec.add_development_dependency "rspec", "~> 3.5"
31
- spec.add_development_dependency "rubocop", "~> 0.48.1"
31
+ spec.add_development_dependency "rubocop", "~> 0.49.1"
32
32
  spec.add_development_dependency "timecop", "~> 0.8.1"
33
33
 
34
34
  spec.add_dependency "activesupport", ">= 5.0.2"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: socrates
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.10
4
+ version: 0.1.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christian Nelson
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-05-18 00:00:00.000000000 Z
11
+ date: 2017-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 0.48.1
61
+ version: 0.49.1
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: 0.48.1
68
+ version: 0.49.1
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: timecop
71
71
  requirement: !ruby/object:Gem::Requirement