conversational 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/.gitignore +4 -7
  2. data/Gemfile +4 -0
  3. data/Gemfile.lock +32 -0
  4. data/README.markdown +23 -177
  5. data/Rakefile +6 -35
  6. data/conversational.gemspec +19 -69
  7. data/lib/conversational/conversation.rb +177 -98
  8. data/lib/conversational/version.rb +4 -0
  9. data/lib/conversational.rb +2 -3
  10. data/spec/conversation_spec.rb +188 -20
  11. data/spec/spec_helper.rb +3 -23
  12. metadata +76 -78
  13. data/VERSION +0 -1
  14. data/features/configure_blank_topic.feature +0 -9
  15. data/features/configure_exclusion_conversations.feature +0 -20
  16. data/features/configure_unknown_topic.feature +0 -9
  17. data/features/find_existing_conversation.feature +0 -21
  18. data/features/find_or_create_with.feature +0 -33
  19. data/features/retrieve_conversation_details.feature +0 -13
  20. data/features/step_definitions/conversation_steps.rb +0 -60
  21. data/features/step_definitions/pickle_steps.rb +0 -87
  22. data/features/support/conversation.rb +0 -2
  23. data/features/support/email_spec.rb +0 -1
  24. data/features/support/env.rb +0 -58
  25. data/features/support/mail.rb +0 -6
  26. data/features/support/paths.rb +0 -33
  27. data/features/support/pickle.rb +0 -24
  28. data/features/support/sample_conversation.rb +0 -3
  29. data/lib/conversational/active_record_additions.rb +0 -121
  30. data/lib/conversational/conversation_definition.rb +0 -98
  31. data/lib/generators/conversational/migration/USAGE +0 -5
  32. data/lib/generators/conversational/migration/migration_generator.rb +0 -23
  33. data/lib/generators/conversational/migration/templates/migration.rb +0 -14
  34. data/lib/generators/conversational/skeleton/USAGE +0 -6
  35. data/lib/generators/conversational/skeleton/skeleton_generator.rb +0 -17
  36. data/lib/generators/conversational/skeleton/templates/conversation.rb +0 -3
  37. data/spec/active_record_additions_spec.rb +0 -120
  38. data/spec/conversation_definition_spec.rb +0 -248
  39. data/spec/support/conversation.rb +0 -3
data/.gitignore CHANGED
@@ -1,8 +1,5 @@
1
+ pkg/*
2
+ *.gem
1
3
  .bundle
2
- db/*.sqlite3
3
- log/*.log
4
- tmp/**/*
5
- config/database.yml
6
- *~
7
- config/initializers/ebay_config*
8
- rerun.txt
4
+ .rvmrc
5
+ coverage
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in conversational.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,32 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ conversational (0.3.2)
5
+ activesupport
6
+ i18n
7
+
8
+ GEM
9
+ remote: http://rubygems.org/
10
+ specs:
11
+ activesupport (3.0.5)
12
+ diff-lcs (1.1.2)
13
+ i18n (0.5.0)
14
+ rspec (2.5.0)
15
+ rspec-core (~> 2.5.0)
16
+ rspec-expectations (~> 2.5.0)
17
+ rspec-mocks (~> 2.5.0)
18
+ rspec-core (2.5.1)
19
+ rspec-expectations (2.5.0)
20
+ diff-lcs (~> 1.1.2)
21
+ rspec-mocks (2.5.0)
22
+ simplecov (0.4.1)
23
+ simplecov-html (~> 0.4.3)
24
+ simplecov-html (0.4.3)
25
+
26
+ PLATFORMS
27
+ ruby
28
+
29
+ DEPENDENCIES
30
+ conversational!
31
+ rspec
32
+ simplecov
data/README.markdown CHANGED
@@ -1,44 +1,46 @@
1
1
  # Conversational
2
2
 
3
- Conversational makes it easier to accept incoming text messages (SMS) and respond to them in your application. You could also use Conversational to respond to incoming email or IM.
3
+ Conversational makes it easier to accept incoming text messages (SMS), emails and IM and respond to them in your application.
4
4
 
5
- Conversational allows you to have have *stateful* or *stateless* interactions with your users.
5
+ NOTE: Conversational no longer supports ActiveRecord additions or "stateful" conversations. If you still need this feature use version 0.3.2
6
6
 
7
- ## Stateless Conversations
7
+ ## Installation
8
+
9
+ `gem install conversational`
8
10
 
9
- This is the most common way of dealing with incoming messages in SMS apps. Stateless conversations don't know anything about a previous request and therefore require you to text in an explicit command. Let's look at an example:
11
+ ## Usage
10
12
 
11
- Say you have an application which allows you to interact with Facebook over SMS. You want be able to write on on your friends wall and change your status by sending an SMS to your app. You also want to receive SMS alerts when a friend writes on your wall or you have a new friend request.
13
+ Let's say you have an application which allows you to interact with Facebook over SMS. You want be able to write on on your friends wall and change your status by sending an SMS to your app. You also want to receive SMS alerts when a friend writes on your wall or you have a new friend request.
12
14
 
13
15
  Let's tackle the incoming messages first.
14
16
 
15
17
  ### Incoming Text Messages
16
18
 
17
- Say your commands are:
19
+ Let's say your commands are:
18
20
 
19
21
  * us is ready for a beer
20
22
  * wow johnny wanna go 4 a beer?
21
23
 
22
24
  Where "us" is update status and "wow" is write on wall
23
25
 
24
- Assuming you have set up a controller in your application to accept the incoming text messages when they are posted from your SMS gateway you could use Conversational as follows:
26
+ Assuming you have set up a controller in your application to accept incoming text messages you could use Conversational as follows:
25
27
 
26
28
  class Conversation
27
29
  include Conversational::Conversation
28
- converse do |with, message|
29
- OutgoingTextMessage.send(with, message)
30
+ def say
31
+ # code to send an SMS
30
32
  end
31
33
  end
32
34
 
33
35
  class UsConversation < Conversation
34
- def move_along(message)
36
+ def process
35
37
  # code to update status
36
38
  say "Successfully updated your status"
37
39
  end
38
40
  end
39
41
 
40
42
  class WowConversation < Conversation
41
- def move_along(message)
43
+ def process
42
44
  # Code to write on wall
43
45
  say "Successfully wrote on wall"
44
46
  end
@@ -46,195 +48,39 @@ Assuming you have set up a controller in your application to accept the incoming
46
48
 
47
49
  class IncomingTextMessageController
48
50
  def create
49
- message = params[:message]
50
51
  topic = params[:message].split(" ").first
51
- number = params[:number]
52
- Conversation.new(:with => number, :topic => topic).details.move_along(message)
52
+ Conversation.new(:topic => topic).details.process
53
53
  end
54
54
  end
55
55
 
56
56
  There's quite a bit going on here so let's have a bit more of a look.
57
57
 
58
- In the controller a new Conversation is created with the number of the incoming message and the topic as the first word of the text message. In our case the topic will be either "us" or "wow". Calling `details` on an instance of Conversation will try and return an instance of a class in your project that subclasses your main Conversation class and has the same name as the topic. In our case our main Conversation class *is* called Conversation so a topic of "us" will map to `UsConversation`. Similarly "wow" maps to `WowConversation`. So say we text in "us is ready for a beer" an instance of `UsConversation` is returned and move_along is then called on the instance.
59
-
60
- Inside our subclassed conversations there is a method available to us called `say`. `say` executes the converse block you set up inside your main Conversation class.In our case this will call `OutgoingTextMessage.send` passing in the number and the message. Obviously this example doesn't contain any error checking. If we text in something starting with other than "us" or "wow" we'll get an error because `details` will return `nil` and `move_along` will be called on `nil`
61
-
62
- ### Outgoing Text Messages
63
-
64
- To handle your Facebook alerts you can simply make use of Conversation's `say` method: You might do something like this:
65
-
66
- class FacebookAlert < Conversation
67
- def wrote_on_wall(facebook_notification)
68
- # Code to get the wall details
69
- say "Someone wrote on your wall..."
70
- end
71
-
72
- def friend_request(facebook_notification)
73
- # Code to get the name of the person who befriended you
74
- say "You have a new friend request from ..."
75
- end
76
- end
77
-
78
- Then when you get a facebook alert simply do either:
79
-
80
- FacebookAlert.new(:with => "your number").wrote_on_wall(facebook_notification)
81
- FacebookAlert.new(:with => "your number").friend_request(facebook_notification)
82
-
83
- ## Stateful Conversations
84
-
85
- Conversational also allows you to have *stateful* conversations. A stateful conversation knows about prevous interactions and therefore allows you to respond differently. Note this is currently only supported in Rails.
86
-
87
- Let's build on the prevous example using stateful conversations.
88
-
89
- Our application so far is *stateless*. Currently if we get a friend request we have no way of accepting or rejecting it. If we were to continue building a stateless application we would simply add a couple of new commands such as:
90
-
91
- * "afr &lt;friend&gt;"
92
- * "rfr &lt;friend&gt;"
93
- where "afr" is accept friend request and "rfr" is reject friend request.
94
-
95
- But instead of doing that let's allow us to respond to a friend request notification with yes or no.
96
-
97
- So we'll change our existing friend request alert to: "You have a new friend request from Johnnie Cash. Do you want to accept? Text yes or no."
98
-
99
- With the current stateless implementation if they text "yes" then `details` will look to see if `YesConversation` is defined in our project, won't be able to find and return `nil` So here's how to make our application stateful.
100
-
101
- class Conversation < ActiveRecord::Base
102
- include Conversational::Conversation
103
- converse do |with, message|
104
- OutgoingTextMessage.send(with, message)
105
- end
106
- protected
107
- def finish
108
- state = "finished"
109
- self.save!
110
- end
111
- end
112
-
113
- class IncomingTextMessageController
114
- def create
115
- message = params[:message]
116
- topic = params[:message].split(" ").first
117
- number = params[:number]
118
- Conversation.find_or_create_with(number, topic).move_along(message)
119
- end
120
- end
121
-
122
- class FacebookAlertConversation < Conversation
123
- def move_along(message)
124
- if message == "yes"
125
- # code to accept friend request
126
- say "You successfully accepted the friend request"
127
- elsif message == "no"
128
- # say "You successfully rejected the friend request"
129
- else
130
- say "Invalid response. Reply with yes or no"
131
- end
132
- finish
133
- end
134
-
135
- def friend_request(facebook_notification)
136
- # Code to get the name of the person who befriended you
137
- say "You have a new friend request from ...Do you want to accept? Text yes or no."
138
- end
139
- end
140
-
141
- The first change you'll notice is that our Conversation base class now extends from ActiveRecord::Base. This does a couple of things. But first we will need a migration file (which can be generated with `rails g conversational:migration`). Once we have that we can simply migrate our database `rake db:migrate`. Extending from ActiveRecord::Base adds a couple of methods for us which i'll describe in some more detail later. For our example we only care about one of them.
58
+ In the controller a new Conversation is created with the topic as the first word of the text message. In this case the topic might be "us" or "wow". Calling `details` on an instance of Conversation will try and return an instance of a class in your project that subclasses the class that includes `Conversational::Conversation` and has the same name as the topic. In this case a topic of "us" will map to `UsConversation` and a topic of "wow" maps to `WowConversation`. Let's say the user texts in "us is ready for a beer". The code will create an instance of `UsConversation` then call `process` on the instance.
142
59
 
143
- Jumping over to our controller you can see that now we are calling `Conversation.find_or_create_with`. This method was added when we extended Conversation from ActiveRecord::Base. The method tries to return the last *recent*, *open* conversation with this number. By *open* we mean that it's state is *not* finished and by *recent* we mean that it was updated within the last 24 hours. More on how you can override this later. If it finds one it will return an instance of this conversation subclass. If it doesn't it will create a new conversation with the topic specified and return it as an instance of its subclass (just like `details`).
144
-
145
- Now take a look at our `FacebookAlert` class. The first thing is that we renamed it to `FacebookAlertConversation`. This is important so that it is now recognised as a type of conversation.
146
-
147
- There is also a new method `move_along` which looks at the message to see if the user replied with "yes" or "no" and responds appropriately. Notice it also calls `finish`.
148
-
149
- If we jump back and take a look at our main Conversation class we see that `finish` marks the conversation state as finished so it won't be found by `find_or_create_with`. It is important that you remember to call `finish` on all conversations where you don't expect a response.
150
-
151
- So how does this all tie together?
152
-
153
- The application receives an alert from Facebook with a new friend request and calls:
154
-
155
- FacebookAlert.create!(
156
- :with => "your number", :topic => "facebook_alert"
157
- ).friend_request(facebook_notification)
158
-
159
- This sends the following message to you: "You have a new friend request from...Do you want to accept? Text yes or no."
160
-
161
- Notice that it calls `create!` and *not* `new` as we want to save this conversation. Also notice that topic is set to "facebook_alert" which is the name of the class minus "Conversation". This is important so `find_or_create_with` can find the conversation. Also notice that `friend_request` does *not* call finish. This conversation isn't over yet!
162
-
163
- Now sometime later you reply with "yes". The controller calls
164
- `Conversation.find_or_create_with` which finds your open conversation and returns an instance as a `FacebookAlertConversation`.
165
-
166
- The controller then calls `move_along` on this conversation which looks at your message, sees that you replied with "yes" and replies with "You successfully accepted the friend request". It also calls `finish` which marks the conversation as finished, so that next time you text something in it won't find any open conversations.
167
-
168
- There are still a few more things we need to do in order to make this application work properly. Right now if we text in something other than "us", "wow" or "yes" we will still get an exception. Let's fix it in the following section.
60
+ There's a problem though, if the user texts in something starting with other than "us" or "wow" we'll get an error because `details` will return `nil` and `process` will be called on `nil`. We'll handle that in the next section.
169
61
 
170
62
  ## Configuration
171
63
 
172
- In our example application we are responding to requests based on the first word of the incoming text message. But what if we text in something unknown or something blank?
173
-
174
- You can configure Conversation to deal with this situation as follows:
64
+ You can configure Conversation to deal with unknown or blank topics as follows:
175
65
 
176
66
  class Conversation
177
- unknown_topic_subclass = UnknownTopicNotification
178
- blank_topic_subclass = BlankTopicNotification
67
+ unknown_topic_subclass(UnknownTopicConversation)
68
+ blank_topic_subclass(BlankTopicConversation)
179
69
  end
180
70
 
181
71
  class UnknownTopicNotification < Conversation
182
- def move_along
72
+ def process
183
73
  say "Sorry. Unknown Command"
184
74
  end
185
75
  end
186
76
 
187
77
  class BlankTopicNotification < Conversation
188
- def move_along
78
+ def process
189
79
  say "Hey. What do you want?"
190
80
  end
191
81
  end
192
82
 
193
- Now when we text in "hey jonnie", `details` will try and find a conversation defined as `HeyConversation`, won't be able to find it and will return an instance of `UnknownTopicConversation` instead.
194
-
195
- `move_along` then causes a message to be sent saying "Sorry. Unknown Command."
196
-
197
- The same thing happens for a blank conversation.
198
-
199
- There is one more subtle issue with our application. What if we text in "facebook_alert"? The reply will be: "Invalid response. Reply with yes or no" when it should actually be "Sorry. Unknown Command". This is because if `find_or_create_with` cannot find an existing conversation it will try and create one with the topic "facebook_alert" if `FacebookAlertConversation` is defined in our application (which it is). To solve this problem we can use `exclude`.
200
-
201
- class Conversation
202
- exclude FacebookAlertConversation
203
- end
204
-
205
- Now we'll get "Sorry. Unknown Command."
206
-
207
- ## Overriding Defaults
208
-
209
- When you extend your base conversation class from ActiveRecord::Base in addition to `find_or_create_with` you will also get the following class methods:
210
-
211
- `converser("someone")` returns all conversations with "someone"
212
-
213
- `in_progress` returns all conversations that are not "finished".
214
-
215
- `recent(time)` returns all conversations in the last *time* or within the last 24 if *time* is not suppied
216
-
217
- `with("someone")` a convienience method for `converse("someone").in_progress.recent`
218
-
219
-
220
- ## Installation
221
-
222
- Add the following to your Gemfile: `gem "conversational"`
223
-
224
- ## Setup
225
-
226
- `rails g conversational:skeleton`
227
- Generates a base conversation class under app/conversations
228
-
229
- `rails g conversational:migration`
230
- Generates a migration file if you want to use Conversational with Rails
231
-
232
- ## More Examples
233
-
234
- Here's an [example](http://github.com/dwilkie/drinking) *stateful* conversation app about drinking
235
-
236
- ## Notes
83
+ Now if the user texts something like "hey jonnie", they'll receive: "Sorry. Unknown Command." Similarly, if they text nothing, they'll receive "Hey. What do you want?"
237
84
 
238
- Conversational is only compatible with Rails 3 however you do not need Rails if you are not using stateful conversations.
85
+ Copyright (c) 2011 David Wilkie, released under the MIT license
239
86
 
240
- Copyright (c) 2010 David Wilkie, released under the MIT license
data/Rakefile CHANGED
@@ -1,38 +1,9 @@
1
- require 'rake'
2
- require 'rake/testtask'
3
- require 'rake/rdoctask'
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
4
3
 
5
- desc 'Default: run unit tests.'
6
- task :default => :test
7
-
8
- desc 'Test the conversational plugin.'
9
- Rake::TestTask.new(:test) do |t|
10
- t.libs << 'lib'
11
- t.libs << 'spec'
12
- t.libs << 'features'
13
- t.pattern = 'spec/**/*_spec.rb'
14
- t.verbose = true
15
- end
16
-
17
- begin
18
- require 'jeweler'
19
- Jeweler::Tasks.new do |gemspec|
20
- gemspec.name = "conversational"
21
- gemspec.summary = "Have conversations with your users over SMS"
22
- gemspec.email = "dwilkie@gmail.com"
23
- gemspec.homepage = "http://github.com/dwilkie/conversational"
24
- gemspec.authors = ["David Wilkie"]
25
- end
26
- rescue LoadError
27
- puts "Jeweler not available. Install it with: gem install jeweler"
28
- end
29
-
30
- desc 'Generate documentation for the conversational plugin.'
31
- Rake::RDocTask.new(:rdoc) do |rdoc|
32
- rdoc.rdoc_dir = 'rdoc'
33
- rdoc.title = 'Conversational'
34
- rdoc.options << '--line-numbers' << '--inline-source'
35
- rdoc.rdoc_files.include('README')
36
- rdoc.rdoc_files.include('lib/**/*.rb')
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec) do |t|
6
+ t.rspec_opts = "--color"
37
7
  end
8
+ task :default => :spec
38
9
 
@@ -1,78 +1,28 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
1
  # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "conversational/version"
5
4
 
6
5
  Gem::Specification.new do |s|
7
- s.name = %q{conversational}
8
- s.version = "0.3.2"
6
+ s.name = "conversational"
7
+ s.version = Conversational::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["David Wilkie"]
10
+ s.email = ["dwilkie@gmail.com"]
11
+ s.homepage = ""
12
+ s.summary = %q{Have topic based conversations}
13
+ s.description = %q{Allows you to instansiate conversations based on keywords}
9
14
 
10
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
- s.authors = ["David Wilkie"]
12
- s.date = %q{2010-08-31}
13
- s.email = %q{dwilkie@gmail.com}
14
- s.extra_rdoc_files = [
15
- "README.markdown"
16
- ]
17
- s.files = [
18
- ".gitignore",
19
- "MIT-LICENSE",
20
- "README.markdown",
21
- "Rakefile",
22
- "VERSION",
23
- "conversational.gemspec",
24
- "features/configure_blank_topic.feature",
25
- "features/configure_exclusion_conversations.feature",
26
- "features/configure_unknown_topic.feature",
27
- "features/find_existing_conversation.feature",
28
- "features/find_or_create_with.feature",
29
- "features/retrieve_conversation_details.feature",
30
- "features/step_definitions/conversation_steps.rb",
31
- "features/step_definitions/pickle_steps.rb",
32
- "features/support/conversation.rb",
33
- "features/support/email_spec.rb",
34
- "features/support/env.rb",
35
- "features/support/mail.rb",
36
- "features/support/paths.rb",
37
- "features/support/pickle.rb",
38
- "features/support/sample_conversation.rb",
39
- "lib/conversational.rb",
40
- "lib/conversational/active_record_additions.rb",
41
- "lib/conversational/conversation.rb",
42
- "lib/conversational/conversation_definition.rb",
43
- "lib/generators/conversational/migration/USAGE",
44
- "lib/generators/conversational/migration/migration_generator.rb",
45
- "lib/generators/conversational/migration/templates/migration.rb",
46
- "lib/generators/conversational/skeleton/USAGE",
47
- "lib/generators/conversational/skeleton/skeleton_generator.rb",
48
- "lib/generators/conversational/skeleton/templates/conversation.rb",
49
- "spec/active_record_additions_spec.rb",
50
- "spec/conversation_definition_spec.rb",
51
- "spec/conversation_spec.rb",
52
- "spec/spec_helper.rb",
53
- "spec/support/conversation.rb"
54
- ]
55
- s.homepage = %q{http://github.com/dwilkie/conversational}
56
- s.rdoc_options = ["--charset=UTF-8"]
15
+ s.rubyforge_project = "conversational"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
57
20
  s.require_paths = ["lib"]
58
- s.rubygems_version = %q{1.3.7}
59
- s.summary = %q{Have conversations with your users over SMS}
60
- s.test_files = [
61
- "spec/conversation_spec.rb",
62
- "spec/active_record_additions_spec.rb",
63
- "spec/conversation_definition_spec.rb",
64
- "spec/spec_helper.rb",
65
- "spec/support/conversation.rb"
66
- ]
67
21
 
68
- if s.respond_to? :specification_version then
69
- current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
70
- s.specification_version = 3
22
+ s.add_runtime_dependency("activesupport")
23
+ s.add_runtime_dependency("i18n")
71
24
 
72
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
73
- else
74
- end
75
- else
76
- end
25
+ s.add_development_dependency("rspec")
26
+ s.add_development_dependency("simplecov")
77
27
  end
78
28