socrates 0.1.10 → 0.1.11

Sign up to get free protection for your applications and to get access to all the features.
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