socrates 0.1.1 → 0.1.2

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: 7aa5675086ee96c3c8a47ae71f43f3eeb834432c
4
- data.tar.gz: 4ac0a6948f1b1c46a0adbba2fe82525016e452b2
3
+ metadata.gz: 18f251a1edba1096bdb4e8e1e93a2f9662100da0
4
+ data.tar.gz: e24c7497b5ac98e6d9b0c53830ede79a9dbcbee6
5
5
  SHA512:
6
- metadata.gz: ef7c44bf3192f787974b132c2b6291c82bc8c864a219727d6bb5e54c89393946379dd6b0dd8b674608d90e35a8b61541afc75a5d8b026708c16427d53d2e0995
7
- data.tar.gz: fd2a4d81d1ffeb91abf02f0dd48d72a619da2b2cdf64d8c932bb1ff315897039509abc1c858c8407f607215c0439155e1a28f760f4136146366ee4a392d36b78
6
+ metadata.gz: ddf938c670c619986a693c7f52e9a0d35304a6757f25a7031f013d55ecdb1cbcc13c75144ba4ea286664e478a127605e6b208fd210d2416e06a6062f27e84442
7
+ data.tar.gz: 24d70a3a10cc50f47f829f15a7c51ee96f2699ad4c1c4551800a67f66e1513f86b7d1b1749acc7a019671180be2607e6da21914a710ba1e104ab0bb6378f3ef7
@@ -1,34 +1,53 @@
1
- class ConsoleAdapter
2
- CLIENT_ID = "CONSOLE"
1
+ module Socrates
2
+ module Adapters
3
+ class ConsoleAdapter
4
+ CLIENT_ID = "CONSOLE"
3
5
 
4
- def initialize(name: "@socrates")
5
- @name = name
6
- end
6
+ attr_accessor :email, :users
7
7
 
8
- def client_id_from_context(_context)
9
- CLIENT_ID
10
- end
8
+ def initialize(name: "@socrates")
9
+ @name = name
10
+ @users = []
11
+ end
11
12
 
12
- def send_message(message, *)
13
- puts "\n#{colorize(@name, "32;1")}: #{message}"
14
- end
13
+ def client_id_from_context(_context)
14
+ CLIENT_ID
15
+ end
15
16
 
16
- def send_direct_message(message, user, *)
17
- name =
18
- if user.respond_to?(:name)
19
- user.name
20
- elsif user.respond_to?(:id)
21
- user.id
22
- else
23
- user
17
+ def send_message(message, *)
18
+ puts "\n#{colorize(@name, "32;1")}: #{message}"
24
19
  end
25
20
 
26
- puts "\n[DM] #{colorize(name, "34;1")}: #{message}"
27
- end
21
+ def send_direct_message(message, user, *)
22
+ name =
23
+ if user.respond_to?(:name)
24
+ user.name
25
+ elsif user.respond_to?(:id)
26
+ user.id
27
+ else
28
+ user
29
+ end
28
30
 
29
- private
31
+ puts "\n[DM] #{colorize(name, "34;1")}: #{message}"
32
+ end
30
33
 
31
- def colorize(str, color_code)
32
- "\e[#{color_code}m#{str}\e[0m"
34
+ def add_user(id: nil, name: nil, first: nil, last: nil, email: nil)
35
+ users << User.new(id, name, Profile.new(first, last, email))
36
+ end
37
+
38
+ def users_list
39
+ Response.new(users)
40
+ end
41
+
42
+ def lookup_email(*)
43
+ email
44
+ end
45
+
46
+ private
47
+
48
+ def colorize(str, color_code)
49
+ "\e[#{color_code}m#{str}\e[0m"
50
+ end
51
+ end
33
52
  end
34
53
  end
@@ -1,29 +1,46 @@
1
- class MemoryAdapter
2
- CLIENT_ID = "MEMORY"
1
+ module Socrates
2
+ module Adapters
3
+ class MemoryAdapter
4
+ CLIENT_ID = "MEMORY"
3
5
 
4
- attr_reader :history, :dms
6
+ attr_reader :history, :dms
7
+ attr_accessor :email, :users
5
8
 
6
- def initialize
7
- @history = []
8
- @dms = {}
9
- end
9
+ def initialize
10
+ @history = []
11
+ @dms = Hash.new { |hash, key| hash[key] = [] }
12
+ @users = []
13
+ end
10
14
 
11
- def client_id_from_context(_context)
12
- CLIENT_ID
13
- end
15
+ def client_id_from_context(_context)
16
+ CLIENT_ID
17
+ end
14
18
 
15
- def send_message(message, *)
16
- @history << message
17
- end
19
+ def send_message(message, *)
20
+ @history << message
21
+ end
18
22
 
19
- def send_direct_message(message, user, *)
20
- user = user.id if user.respond_to?(:id)
23
+ def send_direct_message(message, user, *)
24
+ user = user.id if user.respond_to?(:id)
21
25
 
22
- @dms[user] = [] unless @dms.key?(user)
23
- @dms[user] << message
24
- end
26
+ @dms[user] << message
27
+ end
28
+
29
+ def last_message
30
+ @history.last
31
+ end
32
+
33
+ def add_user(id: nil, name: nil, first: nil, last: nil, email: nil)
34
+ users << User.new(id, name, Profile.new(first, last, email))
35
+ end
36
+
37
+ def users_list
38
+ Response.new(users)
39
+ end
25
40
 
26
- def last_message
27
- @history.last
41
+ def lookup_email(*)
42
+ email
43
+ end
44
+ end
28
45
  end
29
46
  end
@@ -1,33 +1,47 @@
1
- class SlackAdapter
2
- def initialize(slack_real_time_client)
3
- @slack_real_time_client = slack_real_time_client
4
- end
1
+ module Socrates
2
+ module Adapters
3
+ class SlackAdapter
4
+ def initialize(slack_real_time_client)
5
+ @slack_real_time_client = slack_real_time_client
6
+ end
5
7
 
6
- def client_id_from_context(context)
7
- context&.user
8
- end
8
+ def client_id_from_context(context)
9
+ context&.user
10
+ end
9
11
 
10
- def send_message(message, context:)
11
- @slack_real_time_client.message(text: message, channel: context.channel)
12
- end
12
+ def send_message(message, context:)
13
+ @slack_real_time_client.message(text: message, channel: context.channel)
14
+ end
13
15
 
14
- def send_direct_message(message, user, *)
15
- user = user.id if user.respond_to?(:id)
16
+ def send_direct_message(message, user, *)
17
+ user = user.id if user.respond_to?(:id)
16
18
 
17
- im_channel = lookup_im_channel(user)
19
+ im_channel = lookup_im_channel(user)
18
20
 
19
- @slack_real_time_client.message(text: message, channel: im_channel)
20
- end
21
+ @slack_real_time_client.message(text: message, channel: im_channel)
22
+ end
23
+
24
+ def users_list
25
+ client = @slack_real_time_client.web_client
26
+ client.users_list
27
+ end
28
+
29
+ def lookup_email(context:)
30
+ client = @slack_real_time_client.web_client
31
+ client.users_info(user: context.user)
32
+ end
21
33
 
22
- private
34
+ private
23
35
 
24
- def lookup_im_channel(user)
25
- im = @slack_real_time_client.ims.values.find { |i| i.user == user }
36
+ def lookup_im_channel(user)
37
+ im = @slack_real_time_client.ims.values.find { |i| i.user == user }
26
38
 
27
- return im if im.present?
39
+ return im if im.present?
28
40
 
29
- # Start a new conversation with this user.
30
- response = @slack_real_time_client.web_client.im_open(user: user.id)
31
- response.channel.id
41
+ # Start a new conversation with this user.
42
+ response = @slack_real_time_client.web_client.im_open(user: user.id)
43
+ response.channel.id
44
+ end
45
+ end
32
46
  end
33
47
  end
@@ -0,0 +1,7 @@
1
+ module Socrates
2
+ module Adapters
3
+ Response = Struct.new(:members)
4
+ User = Struct.new(:id, :name, :profile)
5
+ Profile = Struct.new(:first_name, :last_name, :email)
6
+ end
7
+ end
@@ -1,15 +1,12 @@
1
1
  module Socrates
2
2
  module Bots
3
3
  class CLIBot
4
- def initialize(state_factory:)
5
- @adapter = ConsoleAdapter.new
4
+ def initialize(adapter:, state_factory:)
5
+ @adapter = adapter || Adapters::ConsoleAdapter.new
6
6
  @dispatcher = Core::Dispatcher.new(adapter: @adapter, state_factory: state_factory)
7
7
  end
8
8
 
9
9
  def start
10
- # Clear out any remnants from previous runs.
11
- @storage.clear(ConsoleAdapter::CLIENT_ID)
12
-
13
10
  while (input = gets.chomp)
14
11
  @dispatcher.dispatch(message: input)
15
12
  end
@@ -13,7 +13,7 @@ module Socrates
13
13
  end
14
14
 
15
15
  @slack_client = Slack::RealTime::Client.new
16
- @adapter = SlackAdapter.new(@slack_client)
16
+ @adapter = Adapters::SlackAdapter.new(@slack_client)
17
17
  @dispatcher = Core::Dispatcher.new(adapter: @adapter, state_factory: state_factory)
18
18
  end
19
19
 
@@ -5,6 +5,7 @@ module Socrates
5
5
  attr_accessor :view_path
6
6
  attr_accessor :storage
7
7
  attr_accessor :error_message
8
+ attr_accessor :expired_timeout # seconds
8
9
  attr_accessor :logger
9
10
  end
10
11
 
@@ -63,34 +63,51 @@ module Socrates
63
63
 
64
64
  DEFAULT_ERROR_MESSAGE = "Sorry, an error occurred. We'll have to start over..."
65
65
 
66
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
66
67
  def fetch_snapshot(client_id)
67
- state_data =
68
- if @storage.has_key?(client_id)
69
- begin
70
- snapshot = @storage.get(client_id)
71
- StateData.deserialize(snapshot)
72
- rescue => e
73
- @logger.warn "Error while fetching snapshot for client id '#{client_id}', resetting state: #{e.message}"
74
- @logger.warn e
75
- end
68
+ if @storage.has_key?(client_id)
69
+ begin
70
+ snapshot = @storage.get(client_id)
71
+ state_data = StateData.deserialize(snapshot)
72
+ rescue => e
73
+ @logger.warn "Error while fetching snapshot for client id '#{client_id}', resetting state: #{e.message}"
74
+ @logger.warn e
76
75
  end
76
+ end
77
77
 
78
78
  state_data ||= StateData.new
79
79
 
80
80
  # If the current state is nil or END_OF_CONVERSATION, set it to the default state, which is typically a state
81
81
  # that waits for an initial command or input from the user (e.g. help, start, etc).
82
82
  if state_data.state_id.nil? || state_data.state_id == State::END_OF_CONVERSATION
83
- state_data.state_id = @state_factory.default_state
84
- state_data.state_action = :listen
83
+ default_state, default_action = @state_factory.default
84
+
85
+ state_data.state_id = default_state
86
+ state_data.state_action = default_action || :listen
87
+
88
+ # Check to see if the last interation was too long ago.
89
+ elsif state_data_expired?(state_data) && @state_factory.expired(state_data).present?
90
+ expired_state, expired_action = @state_factory.expired(state_data)
91
+
92
+ state_data.state_id = expired_state
93
+ state_data.state_action = expired_action || :ask
85
94
  end
86
95
 
87
96
  state_data
88
97
  end
98
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
89
99
 
90
100
  def persist_snapshot(client_id, state_data)
101
+ state_data.reset_elapsed_time
91
102
  @storage.put(client_id, state_data.serialize)
92
103
  end
93
104
 
105
+ def state_data_expired?(state_data)
106
+ return unless state_data.timestamp.present?
107
+
108
+ state_data.elapsed_time > (Config.expired_timeout || 30.minutes)
109
+ end
110
+
94
111
  def instantiate_state(state_data, context)
95
112
  @state_factory.build(state_data: state_data, adapter: @adapter, context: context)
96
113
  end
@@ -100,9 +117,7 @@ module Socrates
100
117
  return true if state.data.state_action == :listen
101
118
 
102
119
  # Stop transitioning if there's no state to transition to, or the conversation has ended.
103
- return true if state.data.state_id.nil? || state.data.state_id == State::END_OF_CONVERSATION
104
-
105
- false
120
+ state.data.state_id.nil? || state.data.state_id == State::END_OF_CONVERSATION
106
121
  end
107
122
 
108
123
  def handle_action_error(e, client_id, state, context)
@@ -6,15 +6,24 @@ require "yaml"
6
6
  module Socrates
7
7
  module Core
8
8
  class StateData
9
- attr_accessor :state_id, :state_action
9
+ attr_accessor :state_id, :state_action, :timestamp
10
10
 
11
11
  def initialize(state_id: nil, state_action: nil, data: {}, temporary_keys: [])
12
12
  @state_id = state_id
13
13
  @state_action = state_action
14
+ @timestamp = Time.current
14
15
  @data = data
15
16
  @temporary_keys = Set.new(temporary_keys)
16
17
  end
17
18
 
19
+ def elapsed_time
20
+ Time.current - @timestamp
21
+ end
22
+
23
+ def reset_elapsed_time
24
+ @timestamp = Time.current
25
+ end
26
+
18
27
  def keys
19
28
  @data.keys
20
29
  end
@@ -5,10 +5,14 @@ require "socrates/core/state"
5
5
  module Socrates
6
6
  module SampleStates
7
7
  class StateFactory
8
- def default_state
8
+ def default
9
9
  :get_started
10
10
  end
11
11
 
12
+ def expired(*)
13
+ :expired
14
+ end
15
+
12
16
  def build(state_data:, adapter:, context: nil)
13
17
  classname = StringHelpers.underscore_to_classname(state_data.state_id)
14
18
 
@@ -21,7 +25,7 @@ module Socrates
21
25
  include Socrates::Core::State
22
26
 
23
27
  def listen(message)
24
- case message.strip
28
+ case message.downcase
25
29
  when "help"
26
30
  transition_to :help
27
31
  when "age"
@@ -62,6 +66,16 @@ module Socrates
62
66
  end
63
67
  end
64
68
 
69
+ class Expired
70
+ include Socrates::Core::State
71
+
72
+ def ask
73
+ respond message: "I've forgotten what we're talking about, let's start over."
74
+
75
+ transition_to :help
76
+ end
77
+ end
78
+
65
79
  class AskForName
66
80
  include Socrates::Core::State
67
81
 
@@ -1,3 +1,3 @@
1
1
  module Socrates
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2"
3
3
  end
data/lib/socrates.rb CHANGED
@@ -6,6 +6,7 @@ require "socrates/string_helpers"
6
6
  require "socrates/adapters/console_adapter"
7
7
  require "socrates/adapters/memory_adapter"
8
8
  require "socrates/adapters/slack_adapter"
9
+ require "socrates/adapters/stubs"
9
10
  require "socrates/storage/storage"
10
11
  require "socrates/core/state_data"
11
12
  require "socrates/core/state"
data/socrates.gemspec CHANGED
@@ -29,6 +29,7 @@ Gem::Specification.new do |spec|
29
29
  spec.add_development_dependency "rake", "~> 10.0"
30
30
  spec.add_development_dependency "rspec", "~> 3.5"
31
31
  spec.add_development_dependency "rubocop", "~> 0.48.1"
32
+ spec.add_development_dependency "timecop", "~> 0.8.1"
32
33
 
33
34
  spec.add_dependency "activesupport", ">= 5.0.2"
34
35
  spec.add_dependency "celluloid-io", ">= 0.17.3"
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.1
4
+ version: 0.1.2
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-04-21 00:00:00.000000000 Z
11
+ date: 2017-04-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: 0.48.1
69
+ - !ruby/object:Gem::Dependency
70
+ name: timecop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.8.1
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.8.1
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: activesupport
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -159,6 +173,7 @@ files:
159
173
  - lib/socrates/adapters/console_adapter.rb
160
174
  - lib/socrates/adapters/memory_adapter.rb
161
175
  - lib/socrates/adapters/slack_adapter.rb
176
+ - lib/socrates/adapters/stubs.rb
162
177
  - lib/socrates/bots/cli_bot.rb
163
178
  - lib/socrates/bots/slack_bot.rb
164
179
  - lib/socrates/config.rb