call_center 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use ree-1.8.7-2011.03@twilio_flow
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ source 'http://rubygems.org'
2
+
3
+ group :development do
4
+ gem 'shoulda', '>= 0'
5
+ gem 'bundler', '~> 1.0.0'
6
+ gem 'jeweler', '~> 1.6.4'
7
+ gem 'rcov', '>= 0'
8
+ gem 'test-unit', :require => 'test/unit'
9
+ gem 'guard'
10
+ gem 'guard-test'
11
+ gem 'actionpack', '~> 2.3.10'
12
+ gem 'mocha'
13
+ gem 'bourne'
14
+ gem 'pre-commit'
15
+ end
16
+
17
+ gem 'builder'
18
+ gem 'hsume2-state_machine', '~> 1.0.5', :require => 'state_machine'
data/Gemfile.lock ADDED
@@ -0,0 +1,52 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ actionpack (2.3.12)
5
+ activesupport (= 2.3.12)
6
+ rack (~> 1.1.0)
7
+ activesupport (2.3.12)
8
+ bourne (1.0)
9
+ mocha (= 0.9.8)
10
+ builder (3.0.0)
11
+ execjs (1.2.0)
12
+ multi_json (~> 1.0)
13
+ git (1.2.5)
14
+ guard (0.7.0)
15
+ thor (~> 0.14.6)
16
+ guard-test (0.3.0)
17
+ guard (>= 0.2.2)
18
+ test-unit (~> 2.2)
19
+ hsume2-state_machine (1.0.5)
20
+ jeweler (1.6.4)
21
+ bundler (~> 1.0)
22
+ git (>= 1.2.5)
23
+ rake
24
+ mocha (0.9.8)
25
+ rake
26
+ multi_json (1.0.3)
27
+ pre-commit (0.1.13)
28
+ execjs
29
+ rack (1.1.2)
30
+ rake (0.9.2)
31
+ rcov (0.9.9)
32
+ shoulda (2.11.3)
33
+ test-unit (2.3.0)
34
+ thor (0.14.6)
35
+
36
+ PLATFORMS
37
+ ruby
38
+
39
+ DEPENDENCIES
40
+ actionpack (~> 2.3.10)
41
+ bourne
42
+ builder
43
+ bundler (~> 1.0.0)
44
+ guard
45
+ guard-test
46
+ hsume2-state_machine (~> 1.0.5)
47
+ jeweler (~> 1.6.4)
48
+ mocha
49
+ pre-commit
50
+ rcov
51
+ shoulda
52
+ test-unit
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard 'test' do
2
+ watch(%r{^lib/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" }
3
+ watch(%r{^test/.+_test\.rb$})
4
+ watch('test/helper.rb') { "test" }
5
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Henry Hsu and Zendesk, Inc
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,159 @@
1
+ Call Center
2
+ ===========
3
+
4
+ Support for defining call center workflows.
5
+
6
+ Overview
7
+ --------
8
+ Call Center streamlines the process of defining multi-party call workflows in your application. Particularly, with [Twilio](http://www.twilio.com/docs) in mind.
9
+
10
+ [Twilio](http://www.twilio.com/docs) provides a two-part API for managing phone calls, and is mostly driven by callbacks. Call Center DRYs up the application logic dealing with a callback driven API so you can focus on the business logic of your call center.
11
+
12
+ ### Not DRY
13
+ Twilio requests your application to return [TwiML](http://www.twilio.com/docs/api/twiml/) that describes the call workflow. TwiML contains commands which Twilio then executes. It is essentially an application-to-application API, synonymous to making a REST call.
14
+
15
+ In the context of "[Skinny Controller, Fat Model](http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model)", outgoing REST calls for the function of business logic are not a view concern but a model concern. Therefore, so is TwiML.
16
+
17
+ Twilio supports callbacks URLs and redirects to URLs that also render TwiML as a method of modifying live calls. Incoming callbacks are handled by the controller first, but the response is still a model concern.
18
+
19
+ Terminology
20
+ -----------
21
+
22
+ * **Call** - An application resource of yours that encapsulates a phone call. Phone calls are then acted on: answered, transferred, declined, etc.
23
+ * **Event** - Is something that happens outside or inside your application in relation to a **Call**. Someone picks up, hangs up, presses a button, etc.
24
+ * **State** - Is the status a **Call** is in which is descriptive of what's happened so far and what are the next things that should happen. (e.g. a call on hold is waiting for the agent to return)
25
+ * **CallFlow** - Is a definition of the process a **Call** goes through. **Events** drive the flow between **States**. (e.g. a simple workflow is when noone answers the call, send the call to voicemail)
26
+ * **Render** - Is the ability of the **CallFlow** to return TwiML to bring the call into the **State** or modify the live call through a **Redirect**.
27
+ * **Redirect** - Is a way of modifying a live call outside of a TwiML response (e.g. background jobs)
28
+
29
+ Usage
30
+ -----
31
+
32
+ class Call
33
+ include CallCenter
34
+
35
+ call_flow :state, :intial => :answered do
36
+ state :answered do
37
+ event :incoming_call, :to => :voicemail, :if => :not_during_business_hours?
38
+ event :incoming_call, :to => :sales
39
+ end
40
+
41
+ state :voicemail do
42
+ event :customer_hangs_up, :to => :voicemail_completed
43
+ end
44
+
45
+ on_render(:sales) do |call, x|
46
+ x.Say "This is Sales!"
47
+ end
48
+
49
+ on_render(:voicemail) do |call, x|
50
+ x.Say "Leave a voicemail!"
51
+ end
52
+
53
+ on_flow_to(:voicemail) do |call, transition|
54
+ call.notify(:voicemail)
55
+ end
56
+ end
57
+ end
58
+
59
+ Benefits of **CallCenter** is that it's backed by [state_machine](https://github.com/pluginaweek/state_machine). Which means you can interact with events the same you do in state_machine.
60
+
61
+ @call.incoming_call!
62
+ @call.voicemail?
63
+ @call.sales?
64
+ @call.render # See Rendering
65
+
66
+ Flow
67
+ ----
68
+
69
+ The general application flow for a **CallFlow** is like this:
70
+
71
+ 1. An incoming call is posted to your application
72
+ * You create a **Call**
73
+ * You execute an initial event
74
+ * You respond by rendering TwiML. Your TwiML contains callbacks to events or redirects
75
+ 2. Something happens and Twilio posts an event to your application
76
+ * You find the **Call**
77
+ * You store any new information
78
+ * You execute the posted event
79
+ * You respond by rendering TwiML. Your TwiML contains callbacks to events or redirects
80
+ 3. Repeat 2.
81
+
82
+ Rendering
83
+ ---------
84
+
85
+ Rendering is your way of interacting with Twilio. Thus, it provides two facilities: access to an XML builder and access to your call.
86
+
87
+ on_render(:sales) do |the_call, xml_builder|
88
+ xml_builder.Say "This is Sales!"
89
+
90
+ the_call.flag! # You can access the call explicitly
91
+ flag! # Or access it implicitly
92
+ end
93
+
94
+ Renders:
95
+
96
+ <?xml version="1.0" encoding="UTF-8"?>
97
+ <Response>
98
+ <Say>This is Sales!</Say>
99
+ </Response>
100
+
101
+ Callbacks
102
+ ---------
103
+
104
+ If you ever want to do something special after entering a state, but only if it's a new transition (e.g. NOT from :voicemail => :voicemail), you can do this:
105
+
106
+ on_flow_to(:voicemail) do |the_call, the_transition|
107
+ the_call.notify(transition.event) # Explicitly
108
+ notify(transition.event) # Implicitly
109
+ end
110
+
111
+ Redirects
112
+ ---------
113
+
114
+ Redirects are a request made to the [Twilio REST API](http://www.twilio.com/docs/api/rest/) that points to a callback URL which returns TwiML to be executed on a call. It is up to you how you want to perform this (e.g. with your favority http libraries, or with [Twilio Libraries](http://www.twilio.com/docs/libraries/)).
115
+
116
+ Redirect to events look like this:
117
+
118
+ ...
119
+ call_flow :state, :intial => :answered do
120
+ state :answered do
121
+ ...
122
+ end
123
+
124
+ state :ending_call do
125
+ event :end_call, :to => :ended_call
126
+ end
127
+
128
+ on_render(:ending_call) do
129
+ redirect_and_end_call!(:status => 'completed')
130
+ end
131
+ end
132
+ ...
133
+
134
+ For your **Call** to support this syntax, it must adhere to the following API:
135
+
136
+ class Call
137
+ def redirect_to(event, *args)
138
+ # where:
139
+ # event #=> :end_call
140
+ # args #=> [:status => 'completed]
141
+ @account.calls.get(self.sid).update({:url => "http://myapp.com/call_flow?event=#{event}"})
142
+ end
143
+ end
144
+
145
+ Tools
146
+ -----
147
+
148
+ ### Drawing ###
149
+
150
+ Should you be interested in what your call center workflow looks like, you can draw.
151
+
152
+ Call.state_machines[:status].draw(:font => 'Helvetica Neue')
153
+ # OR
154
+ @call.draw_call_flow(:font => 'Helvetica Neue')
155
+
156
+ Future
157
+ ------
158
+
159
+ * Integrate making new calls into the **CallFlow** DSL
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "call_center"
18
+ gem.homepage = "http://github.com/zendesk/call_center"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Support for describing call center workflows}
21
+ gem.description = %Q{Support for describing call center workflows}
22
+ gem.email = "hhsu@zendesk.com"
23
+ gem.authors = ["Henry Hsu"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rake/testtask'
29
+ Rake::TestTask.new(:test) do |test|
30
+ test.libs << 'lib' << 'test'
31
+ test.pattern = 'test/**/*_test.rb'
32
+ test.verbose = true
33
+ end
34
+
35
+ require 'rcov/rcovtask'
36
+ Rcov::RcovTask.new do |test|
37
+ test.libs << 'test'
38
+ test.pattern = 'test/**/*_test.rb'
39
+ test.verbose = true
40
+ test.rcov_opts << '--exclude "gems/*"'
41
+ end
42
+
43
+ task :default => :test
44
+
45
+ require 'rake/rdoctask'
46
+ Rake::RDocTask.new do |rdoc|
47
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
48
+
49
+ rdoc.rdoc_dir = 'rdoc'
50
+ rdoc.title = "twilio_flow #{version}"
51
+ rdoc.rdoc_files.include('README*')
52
+ rdoc.rdoc_files.include('lib/**/*.rb')
53
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.2
@@ -0,0 +1,97 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{call_center}
8
+ s.version = "0.0.2"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Henry Hsu"]
12
+ s.date = %q{2011-09-21}
13
+ s.description = %q{Support for describing call center workflows}
14
+ s.email = %q{hhsu@zendesk.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".rvmrc",
22
+ "Gemfile",
23
+ "Gemfile.lock",
24
+ "Guardfile",
25
+ "LICENSE.txt",
26
+ "README.md",
27
+ "Rakefile",
28
+ "VERSION",
29
+ "call_center.gemspec",
30
+ "init.rb",
31
+ "lib/call_center.rb",
32
+ "lib/call_center/core_ext/object_instance_exec.rb",
33
+ "lib/call_center/state_machine_ext.rb",
34
+ "lib/call_center/test/dsl.rb",
35
+ "test/call_center_test.rb",
36
+ "test/core_ext_test.rb",
37
+ "test/examples/call.rb",
38
+ "test/examples/legacy_call.rb",
39
+ "test/examples/multiple_flow_call.rb",
40
+ "test/examples/non_standard_call.rb",
41
+ "test/helper.rb"
42
+ ]
43
+ s.homepage = %q{http://github.com/zendesk/call_center}
44
+ s.licenses = ["MIT"]
45
+ s.require_paths = ["lib"]
46
+ s.rubygems_version = %q{1.5.3}
47
+ s.summary = %q{Support for describing call center workflows}
48
+
49
+ if s.respond_to? :specification_version then
50
+ s.specification_version = 3
51
+
52
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
53
+ s.add_runtime_dependency(%q<builder>, [">= 0"])
54
+ s.add_runtime_dependency(%q<hsume2-state_machine>, ["~> 1.0.5"])
55
+ s.add_development_dependency(%q<shoulda>, [">= 0"])
56
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
57
+ s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
58
+ s.add_development_dependency(%q<rcov>, [">= 0"])
59
+ s.add_development_dependency(%q<test-unit>, [">= 0"])
60
+ s.add_development_dependency(%q<guard>, [">= 0"])
61
+ s.add_development_dependency(%q<guard-test>, [">= 0"])
62
+ s.add_development_dependency(%q<actionpack>, ["~> 2.3.10"])
63
+ s.add_development_dependency(%q<mocha>, [">= 0"])
64
+ s.add_development_dependency(%q<bourne>, [">= 0"])
65
+ s.add_development_dependency(%q<pre-commit>, [">= 0"])
66
+ else
67
+ s.add_dependency(%q<builder>, [">= 0"])
68
+ s.add_dependency(%q<hsume2-state_machine>, ["~> 1.0.5"])
69
+ s.add_dependency(%q<shoulda>, [">= 0"])
70
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
71
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
72
+ s.add_dependency(%q<rcov>, [">= 0"])
73
+ s.add_dependency(%q<test-unit>, [">= 0"])
74
+ s.add_dependency(%q<guard>, [">= 0"])
75
+ s.add_dependency(%q<guard-test>, [">= 0"])
76
+ s.add_dependency(%q<actionpack>, ["~> 2.3.10"])
77
+ s.add_dependency(%q<mocha>, [">= 0"])
78
+ s.add_dependency(%q<bourne>, [">= 0"])
79
+ s.add_dependency(%q<pre-commit>, [">= 0"])
80
+ end
81
+ else
82
+ s.add_dependency(%q<builder>, [">= 0"])
83
+ s.add_dependency(%q<hsume2-state_machine>, ["~> 1.0.5"])
84
+ s.add_dependency(%q<shoulda>, [">= 0"])
85
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
86
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
87
+ s.add_dependency(%q<rcov>, [">= 0"])
88
+ s.add_dependency(%q<test-unit>, [">= 0"])
89
+ s.add_dependency(%q<guard>, [">= 0"])
90
+ s.add_dependency(%q<guard-test>, [">= 0"])
91
+ s.add_dependency(%q<actionpack>, ["~> 2.3.10"])
92
+ s.add_dependency(%q<mocha>, [">= 0"])
93
+ s.add_dependency(%q<bourne>, [">= 0"])
94
+ s.add_dependency(%q<pre-commit>, [">= 0"])
95
+ end
96
+ end
97
+
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'call_center'
@@ -0,0 +1,100 @@
1
+ require 'call_center/core_ext/object_instance_exec'
2
+ require 'state_machine'
3
+ require 'call_center/state_machine_ext'
4
+
5
+ module CallCenter
6
+ def self.included(base)
7
+ base.send(:include, InstanceMethods)
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ class << self
12
+ attr_accessor :cached_state_machines
13
+ end
14
+ self.cached_state_machines ||= {}
15
+
16
+ def self.cache(klass, state_machine)
17
+ self.cached_state_machines["#{klass.name}_#{state_machine.name}"] ||= state_machine
18
+ end
19
+
20
+ def self.cached(klass, state_machine_name)
21
+ self.cached_state_machines["#{klass.name}_#{state_machine_name}"]
22
+ end
23
+
24
+ module ClassMethods
25
+ attr_accessor :call_flow_state_machine_name
26
+
27
+ # Calls state_machine ... with :syntax => :alternate
28
+ def call_flow(*args, &blk)
29
+ options = args.last.is_a?(Hash) ? args.pop : {}
30
+ args << options.merge(:syntax => :alternate)
31
+ state_machine_name = args.first || :state
32
+ if state_machine = CallCenter.cached(self, state_machine_name)
33
+ state_machine = state_machine.duplicate_to(self)
34
+ else
35
+ state_machine = state_machine(*args, &blk)
36
+ state_machine.instance_eval do
37
+ after_transition any => any do |call, transition|
38
+ call.flow_to(transition) if transition.from_name != transition.to_name
39
+ end
40
+ end
41
+ CallCenter.cache(self, state_machine)
42
+ end
43
+ self.call_flow_state_machine_name ||= state_machine.name
44
+ state_machine
45
+ end
46
+
47
+ def current_state_machine
48
+ self.state_machines[self.call_flow_state_machine_name]
49
+ end
50
+ end
51
+
52
+ module InstanceMethods
53
+ def render(state_machine_name = self.class.call_flow_state_machine_name)
54
+ xml = Builder::XmlMarkup.new
55
+ render_block = current_block_accessor(:render_blocks, state_machine_name)
56
+
57
+ xml.instruct!
58
+ xml.Response do
59
+ self.instance_exec(self, xml, &render_block) if render_block
60
+ end
61
+ xml.target!
62
+ end
63
+
64
+ def flow_to(transition, state_machine_name = self.class.call_flow_state_machine_name)
65
+ block = current_block_accessor(:flow_to_blocks, state_machine_name)
66
+ self.instance_exec(self, transition, &block) if block
67
+ end
68
+
69
+ def draw_call_flow(*args)
70
+ current_state_machine.draw(*args)
71
+ end
72
+
73
+ private
74
+
75
+ def current_block_accessor(accessor, state_machine_name)
76
+ csm = self.class.state_machines[state_machine_name]
77
+ return unless csm.respond_to?(accessor)
78
+ blocks, name = csm.send(accessor), csm.name
79
+ blocks[current_flow_state(state_machine_name)] if blocks
80
+ end
81
+
82
+ def current_state_machine
83
+ self.class.current_state_machine
84
+ end
85
+
86
+ def current_flow_state(state_machine_name)
87
+ send(state_machine_name).to_sym
88
+ end
89
+
90
+ def method_missing(*args, &blk)
91
+ method_name = args.first.to_s
92
+ if method_name =~ /^redirect_and_(.+)!$/
93
+ args.shift
94
+ redirect_to($1.to_sym, *args)
95
+ else
96
+ super
97
+ end
98
+ end
99
+ end
100
+ end