conversational 0.3.2 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -7
- data/Gemfile +4 -0
- data/Gemfile.lock +32 -0
- data/README.markdown +23 -177
- data/Rakefile +6 -35
- data/conversational.gemspec +19 -69
- data/lib/conversational/conversation.rb +177 -98
- data/lib/conversational/version.rb +4 -0
- data/lib/conversational.rb +2 -3
- data/spec/conversation_spec.rb +188 -20
- data/spec/spec_helper.rb +3 -23
- metadata +76 -78
- data/VERSION +0 -1
- data/features/configure_blank_topic.feature +0 -9
- data/features/configure_exclusion_conversations.feature +0 -20
- data/features/configure_unknown_topic.feature +0 -9
- data/features/find_existing_conversation.feature +0 -21
- data/features/find_or_create_with.feature +0 -33
- data/features/retrieve_conversation_details.feature +0 -13
- data/features/step_definitions/conversation_steps.rb +0 -60
- data/features/step_definitions/pickle_steps.rb +0 -87
- data/features/support/conversation.rb +0 -2
- data/features/support/email_spec.rb +0 -1
- data/features/support/env.rb +0 -58
- data/features/support/mail.rb +0 -6
- data/features/support/paths.rb +0 -33
- data/features/support/pickle.rb +0 -24
- data/features/support/sample_conversation.rb +0 -3
- data/lib/conversational/active_record_additions.rb +0 -121
- data/lib/conversational/conversation_definition.rb +0 -98
- data/lib/generators/conversational/migration/USAGE +0 -5
- data/lib/generators/conversational/migration/migration_generator.rb +0 -23
- data/lib/generators/conversational/migration/templates/migration.rb +0 -14
- data/lib/generators/conversational/skeleton/USAGE +0 -6
- data/lib/generators/conversational/skeleton/skeleton_generator.rb +0 -17
- data/lib/generators/conversational/skeleton/templates/conversation.rb +0 -3
- data/spec/active_record_additions_spec.rb +0 -120
- data/spec/conversation_definition_spec.rb +0 -248
- data/spec/support/conversation.rb +0 -3
data/.gitignore
CHANGED
data/Gemfile
ADDED
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.
|
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
|
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
|
-
##
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
`gem install conversational`
|
8
10
|
|
9
|
-
|
11
|
+
## Usage
|
10
12
|
|
11
|
-
|
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
|
-
|
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
|
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
|
-
|
29
|
-
|
30
|
+
def say
|
31
|
+
# code to send an SMS
|
30
32
|
end
|
31
33
|
end
|
32
34
|
|
33
35
|
class UsConversation < Conversation
|
34
|
-
def
|
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
|
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
|
-
|
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
|
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 <friend>"
|
92
|
-
* "rfr <friend>"
|
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
|
-
|
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
|
-
|
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
|
178
|
-
blank_topic_subclass
|
67
|
+
unknown_topic_subclass(UnknownTopicConversation)
|
68
|
+
blank_topic_subclass(BlankTopicConversation)
|
179
69
|
end
|
180
70
|
|
181
71
|
class UnknownTopicNotification < Conversation
|
182
|
-
def
|
72
|
+
def process
|
183
73
|
say "Sorry. Unknown Command"
|
184
74
|
end
|
185
75
|
end
|
186
76
|
|
187
77
|
class BlankTopicNotification < Conversation
|
188
|
-
def
|
78
|
+
def process
|
189
79
|
say "Hey. What do you want?"
|
190
80
|
end
|
191
81
|
end
|
192
82
|
|
193
|
-
Now
|
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
|
-
|
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 '
|
2
|
-
|
3
|
-
require 'rake/rdoctask'
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
4
3
|
|
5
|
-
|
6
|
-
|
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
|
|
data/conversational.gemspec
CHANGED
@@ -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
|
8
|
-
s.version
|
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.
|
11
|
-
|
12
|
-
s.
|
13
|
-
s.
|
14
|
-
s.
|
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
|
-
|
69
|
-
|
70
|
-
s.specification_version = 3
|
22
|
+
s.add_runtime_dependency("activesupport")
|
23
|
+
s.add_runtime_dependency("i18n")
|
71
24
|
|
72
|
-
|
73
|
-
|
74
|
-
end
|
75
|
-
else
|
76
|
-
end
|
25
|
+
s.add_development_dependency("rspec")
|
26
|
+
s.add_development_dependency("simplecov")
|
77
27
|
end
|
78
28
|
|