colloquy 1.0.0
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 +15 -0
- data/README.md +160 -0
- data/TODO.md +256 -0
- data/bin/colloquy +14 -0
- data/examples/config/flows.yaml +22 -0
- data/examples/config/logger.yaml +2 -0
- data/examples/config/messages.yaml +3 -0
- data/examples/config/mysql.yaml +18 -0
- data/examples/config/redis.yaml +9 -0
- data/examples/config/scribe.yaml +2 -0
- data/examples/config/settings.yaml +2 -0
- data/examples/config/test.yaml +8 -0
- data/examples/config/urls.yaml +18 -0
- data/examples/flows/active_record_flow.rb +42 -0
- data/examples/flows/art_of_war_flow.rb +27 -0
- data/examples/flows/calculator_flow.rb +71 -0
- data/examples/flows/crossover_flow.rb +17 -0
- data/examples/flows/database_flow.rb +33 -0
- data/examples/flows/hangman_flow.rb +82 -0
- data/examples/flows/metadata_flow.rb +11 -0
- data/examples/flows/pagination_flow.rb +23 -0
- data/examples/flows/pass_flow.rb +29 -0
- data/examples/flows/prefix_menu_flow.rb +24 -0
- data/examples/flows/scribe_flow.rb +26 -0
- data/examples/flows/settings_flow.rb +23 -0
- data/examples/flows/special/special_redis_flow.rb +28 -0
- data/examples/flows/url_flow.rb +27 -0
- data/examples/log/renderer.log +198381 -0
- data/examples/log/urls.log +3269 -0
- data/examples/messages/active_record.yaml +2 -0
- data/examples/messages/art_of_war.yaml +1 -0
- data/examples/messages/calculator.yaml +2 -0
- data/examples/messages/database.yaml +1 -0
- data/examples/messages/hangman.yaml +0 -0
- data/examples/messages/prefix_menu.yaml +3 -0
- data/examples/models/activations.rb +5 -0
- data/lib/colloquy.rb +39 -0
- data/lib/colloquy/exceptions.rb +64 -0
- data/lib/colloquy/flow_parser.rb +315 -0
- data/lib/colloquy/flow_pool.rb +21 -0
- data/lib/colloquy/helpers.rb +15 -0
- data/lib/colloquy/helpers/mysql.rb +110 -0
- data/lib/colloquy/helpers/redis.rb +103 -0
- data/lib/colloquy/helpers/scribe.rb +103 -0
- data/lib/colloquy/helpers/settings.rb +111 -0
- data/lib/colloquy/helpers/url.rb +10 -0
- data/lib/colloquy/input.rb +14 -0
- data/lib/colloquy/logger.rb +11 -0
- data/lib/colloquy/menu.rb +128 -0
- data/lib/colloquy/message_builder.rb +54 -0
- data/lib/colloquy/node.rb +67 -0
- data/lib/colloquy/paginator.rb +59 -0
- data/lib/colloquy/paginator/menu.rb +79 -0
- data/lib/colloquy/paginator/prompt.rb +32 -0
- data/lib/colloquy/prompt.rb +35 -0
- data/lib/colloquy/renderer.rb +423 -0
- data/lib/colloquy/response.rb +4 -0
- data/lib/colloquy/runner.rb +93 -0
- data/lib/colloquy/server.rb +157 -0
- data/lib/colloquy/session_store.rb +46 -0
- data/lib/colloquy/session_store/memory.rb +14 -0
- data/lib/colloquy/session_store/redis.rb +24 -0
- data/lib/colloquy/simulator.rb +114 -0
- data/lib/colloquy/spec_helpers.rb +21 -0
- metadata +459 -0
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'goliath/api'
|
2
|
+
require 'goliath/runner'
|
3
|
+
|
4
|
+
require 'goliath/rack/default_response_format'
|
5
|
+
require 'goliath/rack/heartbeat'
|
6
|
+
require 'goliath/rack/params'
|
7
|
+
require 'goliath/rack/render'
|
8
|
+
require 'goliath/rack/default_mime_type'
|
9
|
+
require 'goliath/rack/tracer'
|
10
|
+
require 'goliath/rack/formatters/json'
|
11
|
+
require 'goliath/rack/formatters/html'
|
12
|
+
require 'goliath/rack/formatters/xml'
|
13
|
+
require 'goliath/rack/jsonp'
|
14
|
+
|
15
|
+
require 'goliath/rack/validation/request_method'
|
16
|
+
require 'goliath/rack/validation/required_param'
|
17
|
+
require 'goliath/rack/validation/required_value'
|
18
|
+
require 'goliath/rack/validation/numeric_range'
|
19
|
+
require 'goliath/rack/validation/default_params'
|
20
|
+
require 'goliath/rack/validation/boolean_value'
|
21
|
+
|
22
|
+
class Colloquy::Runner
|
23
|
+
class << self
|
24
|
+
# Much of this is borrowed verbatim from Goliath internals so we
|
25
|
+
# can use our own class structure
|
26
|
+
def run!(argv)
|
27
|
+
options = {}
|
28
|
+
goliath_argv = []
|
29
|
+
|
30
|
+
option_parser = OptionParser.new do |opt|
|
31
|
+
opt.banner = 'Usage: colloquy [options] /path/to/flow/root'
|
32
|
+
|
33
|
+
opt.on('-a', '--address HOST', 'Hostname or IP address to bind to') do |host|
|
34
|
+
goliath_argv << '-a' << host
|
35
|
+
end
|
36
|
+
|
37
|
+
opt.on('-p', '---port PORT', 'Port to run the server on') do |port|
|
38
|
+
goliath_argv << '-p' << port
|
39
|
+
end
|
40
|
+
|
41
|
+
opt.on('-e', '---environment ENV', 'Rack environment to run the renderer') do |env|
|
42
|
+
goliath_argv << '-e' << env
|
43
|
+
options[:environment] = env
|
44
|
+
end
|
45
|
+
|
46
|
+
opt.on('-P', '--pidfile PATH_TO_FILE', 'Location to write the PID file to') do |pid_file|
|
47
|
+
goliath_argv << '-P' << pid_file
|
48
|
+
end
|
49
|
+
|
50
|
+
opt.on('-d', '--daemonize', 'Daemonize the server') do
|
51
|
+
goliath_argv << '-d'
|
52
|
+
end
|
53
|
+
|
54
|
+
opt.on('-v', '--verbose', 'Turn on debug logging') do
|
55
|
+
goliath_argv << '-v'
|
56
|
+
options[:verbose] = true
|
57
|
+
end
|
58
|
+
|
59
|
+
opt.on('-s', '--simulator', 'Run the flow simulator instead') do
|
60
|
+
options[:interactive] = true
|
61
|
+
end
|
62
|
+
|
63
|
+
opt.on( '-h', '--help', 'Display this screen' ) do
|
64
|
+
puts opt
|
65
|
+
exit!
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
option_parser.parse!(argv)
|
70
|
+
|
71
|
+
path_root = argv.pop
|
72
|
+
unless path_root
|
73
|
+
puts 'You have to provide a flow root directory. See colloquy --help'
|
74
|
+
exit!
|
75
|
+
end
|
76
|
+
|
77
|
+
goliath_argv << '-l' << Pathname.new(path_root).realpath.join('log', 'server.log').to_s
|
78
|
+
|
79
|
+
if options[:interactive]
|
80
|
+
simulator = Colloquy::Simulator.new(path_root: path_root, verbose: options[:verbose])
|
81
|
+
simulator.run
|
82
|
+
else
|
83
|
+
klass = Colloquy::Server
|
84
|
+
api = klass.new(path_root: path_root, verbose: options[:verbose])
|
85
|
+
runner = Goliath::Runner.new(goliath_argv, api)
|
86
|
+
|
87
|
+
runner.app = Goliath::Rack::Builder.build(klass, api)
|
88
|
+
runner.load_plugins(klass.plugins)
|
89
|
+
runner.run
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'goliath/api'
|
2
|
+
require 'goliath/runner'
|
3
|
+
require 'goliath/rack/params'
|
4
|
+
|
5
|
+
require 'em-synchrony'
|
6
|
+
require 'em-synchrony/em-http'
|
7
|
+
require 'yajl'
|
8
|
+
|
9
|
+
class Colloquy::Server < Goliath::API
|
10
|
+
use Goliath::Rack::Params # parse and merge query and body parameters
|
11
|
+
|
12
|
+
# Create an instance of Colloquy::Renderer with given options
|
13
|
+
# and set's it up
|
14
|
+
# @param options [Hash] The options hash.
|
15
|
+
def initialize(options = {})
|
16
|
+
@renderer = Colloquy::Renderer.new(options)
|
17
|
+
@renderer.prepare!
|
18
|
+
end
|
19
|
+
|
20
|
+
# This methods overrides #response of Goliath::API.
|
21
|
+
# So this is where parameters are validated and fed to an instance of Renderer.
|
22
|
+
# This methods returns response array with a http 200 status and body on successful execution.
|
23
|
+
#
|
24
|
+
# It validates the incoming request by checking presence of flow, msisdn and session_id. It sanitizes
|
25
|
+
# the parameters and obtains a hash containing flow, msisdn, session_id and input. Any exception raised
|
26
|
+
# at this point is rescued and a default error message is stored in response.
|
27
|
+
#
|
28
|
+
# After validation params are passed on the #apply method of instance of Renderer which was created and
|
29
|
+
# configured in Server constructor.
|
30
|
+
#
|
31
|
+
# @param [Goliath::Env] env The request environment.
|
32
|
+
# @return [Array] Array contains [Status code, Headers Hash, Body]
|
33
|
+
def response(env)
|
34
|
+
response = Colloquy::Response.new
|
35
|
+
|
36
|
+
begin
|
37
|
+
parameters = {}
|
38
|
+
parameters = validate_request(env)
|
39
|
+
parameters = sanitize_parameters(parameters)
|
40
|
+
logger.debug "REQUEST flow: #{parameters[:flow_name]}, msisdn: #{parameters[:msisdn]}, \
|
41
|
+
session_id: #{parameters[:session_id]}, input: #{parameters[:input]}, other: #{parameters[:params].inspect}"
|
42
|
+
rescue Exception => e
|
43
|
+
logger.error "Exception #{e.inspect} when trying to validate request flow: #{parameters[:flow_name]}, \
|
44
|
+
msisdn: #{parameters[:msisdn]}, session_id: #{parameters[:session_id]}, input: #{parameters[:input]}"
|
45
|
+
logger.debug "#{e.backtrace.inspect}"
|
46
|
+
logger.info 'Responding with default error message'
|
47
|
+
|
48
|
+
response = Colloquy::Response.new(Colloquy::Renderer::DEFAULT_ERROR_MESSAGE)
|
49
|
+
response.flow_state = :notify
|
50
|
+
end
|
51
|
+
|
52
|
+
response = @renderer.apply(parameters[:flow_name], parameters[:msisdn], parameters[:session_id], parameters[:input], parameters[:params]) if response.empty?
|
53
|
+
|
54
|
+
body = case parameters[:params][:accept]
|
55
|
+
when 'text/plain'
|
56
|
+
response.to_s
|
57
|
+
else
|
58
|
+
Yajl.dump({ response: response, flow_state: response.flow_state })
|
59
|
+
end
|
60
|
+
|
61
|
+
[200, {}, body]
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
def logger
|
66
|
+
@renderer.logger
|
67
|
+
end
|
68
|
+
|
69
|
+
# Validate request and return parameters
|
70
|
+
# @param [Goliath::Env] env The request environment
|
71
|
+
# @return [Hash] parameters The extracted parameters
|
72
|
+
def validate_request(env)
|
73
|
+
parameters = extract_request_parameters
|
74
|
+
|
75
|
+
validate_flow_presence!(parameters)
|
76
|
+
validate_msisdn!(parameters)
|
77
|
+
validate_session_id!(parameters)
|
78
|
+
|
79
|
+
parameters
|
80
|
+
end
|
81
|
+
|
82
|
+
# Create a hash with all request parameters combined
|
83
|
+
# @return [Hash] The request parameters
|
84
|
+
def extract_request_parameters
|
85
|
+
flow_name = env['REQUEST_PATH'][1..-1].to_s
|
86
|
+
|
87
|
+
# Use dup to preserve original env.
|
88
|
+
params = env['params'].dup.to_options
|
89
|
+
msisdn = params.delete(:msisdn).to_s
|
90
|
+
session_id = params.delete(:session_id).to_s
|
91
|
+
input = params.delete(:input).to_s
|
92
|
+
|
93
|
+
{
|
94
|
+
flow_name: flow_name,
|
95
|
+
params: params,
|
96
|
+
msisdn: msisdn,
|
97
|
+
session_id: session_id,
|
98
|
+
input: input
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
# Check if required flow is present. Raise FlowNotFound exception if not
|
103
|
+
# @param [Hash] parameters Extracted request parameters
|
104
|
+
# @return [nil]
|
105
|
+
# @raise [Colloquy::FlowNotFound]
|
106
|
+
# if flow is not present
|
107
|
+
def validate_flow_presence!(parameters)
|
108
|
+
unless @renderer.flow_exists?(parameters[:flow_name])
|
109
|
+
raise Colloquy::FlowNotFound, "Flow not found: #{parameters[:flow_name]}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Checks whether msisdn is present, raises MSISDNParameterEmpty if not
|
114
|
+
# @param [Hash] parameters The paramters hash
|
115
|
+
# @return [nil]
|
116
|
+
# @raise [Colloquy::MSISDNParameterEmpty]
|
117
|
+
# if msisdn parameter not present
|
118
|
+
def validate_msisdn!(parameters)
|
119
|
+
if parameters[:msisdn] == ''
|
120
|
+
raise Colloquy::MSISDNParameterEmpty, 'The msisdn parameter should not be empty.'
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Checks whether session_id is present
|
125
|
+
# @param [Hash] parameters The parameters hash
|
126
|
+
# @return [nil]
|
127
|
+
# @raise [Colloquy::SessionIDParameterEmpty]
|
128
|
+
# if session_id not present
|
129
|
+
def validate_session_id!(parameters)
|
130
|
+
if parameters[:session_id] == ''
|
131
|
+
raise Colloquy::SessionIDParameterEmpty, 'The session_id parameter should not be empty.'
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Clean parameter values(exact type, size etc)
|
136
|
+
# @param [Hash] parameters The Parameters hash
|
137
|
+
# @return [Hash] Parameters Sanitized parameters hash
|
138
|
+
def sanitize_parameters(parameters)
|
139
|
+
flow_name = parameters[:flow_name].to_sym
|
140
|
+
msisdn = parameters[:msisdn].to_i.to_s
|
141
|
+
session_id = parameters[:session_id].to_s[0..20]
|
142
|
+
input = parameters[:input].to_s[0..160]
|
143
|
+
|
144
|
+
#remove default proc so that the hash can be serialized using Marshal
|
145
|
+
params = parameters[:params].tap do |p|
|
146
|
+
p.default = nil
|
147
|
+
end
|
148
|
+
|
149
|
+
{
|
150
|
+
flow_name: flow_name,
|
151
|
+
params: params,
|
152
|
+
msisdn: msisdn,
|
153
|
+
session_id: session_id,
|
154
|
+
input: input
|
155
|
+
}
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class Colloquy::SessionStore
|
2
|
+
KEY_PREFIX = 'ussd_renderer:'
|
3
|
+
|
4
|
+
class << self
|
5
|
+
# Returns a Memory store object according to the type of memory.
|
6
|
+
# @param [Symbol, Hash], Type and Options
|
7
|
+
# @return [Colloquy::SessionStore::Memory Colloquy::SessionStore::Redis]
|
8
|
+
def haystack(type = :memory, options = {})
|
9
|
+
case type.to_sym
|
10
|
+
when :memory
|
11
|
+
require_relative 'session_store/memory'
|
12
|
+
|
13
|
+
Colloquy::SessionStore::Memory.new(options)
|
14
|
+
else :redis
|
15
|
+
require_relative 'session_store/redis'
|
16
|
+
|
17
|
+
Colloquy::SessionStore::Redis.new(options)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(options = {})
|
23
|
+
@identifier = options[:identifier] || :sessions
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def normalized_key_name(key)
|
28
|
+
KEY_PREFIX + @identifier.to_s + ":" + key.to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
def encode_value(value)
|
32
|
+
Marshal.dump(value)
|
33
|
+
end
|
34
|
+
|
35
|
+
def decode_value(string)
|
36
|
+
if string
|
37
|
+
Marshal.load(string)
|
38
|
+
else
|
39
|
+
{}
|
40
|
+
end
|
41
|
+
rescue TypeError
|
42
|
+
{}
|
43
|
+
rescue ArgumentError
|
44
|
+
{}
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class Colloquy::SessionStore::Memory < Colloquy::SessionStore
|
2
|
+
|
3
|
+
def []=(key, value)
|
4
|
+
@store ||= {}
|
5
|
+
@store[normalized_key_name(key)] = encode_value(value)
|
6
|
+
end
|
7
|
+
|
8
|
+
def [](key)
|
9
|
+
@store ||= {}
|
10
|
+
string = @store[normalized_key_name(key)]
|
11
|
+
|
12
|
+
decode_value(string)
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Colloquy::SessionStore::Redis < Colloquy::SessionStore
|
2
|
+
KEY_EXPIRY = 300 # 5 minutes
|
3
|
+
|
4
|
+
def []=(key, value)
|
5
|
+
@store ||= redis_connection
|
6
|
+
@store.set(normalized_key_name(key), encode_value(value), KEY_EXPIRY)
|
7
|
+
end
|
8
|
+
|
9
|
+
def [](key)
|
10
|
+
@store ||= redis_connection
|
11
|
+
string = @store.get(normalized_key_name(key))
|
12
|
+
|
13
|
+
decode_value(string)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def redis_connection
|
18
|
+
@redis = Colloquy::Helpers::Redis::RedisProxy.instance
|
19
|
+
@redis.configure
|
20
|
+
|
21
|
+
@redis[@identifier]
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'em-synchrony'
|
2
|
+
|
3
|
+
class Colloquy::Simulator
|
4
|
+
def initialize(options = {})
|
5
|
+
@renderer = Colloquy::Renderer.new(options)
|
6
|
+
@renderer.prepare!
|
7
|
+
end
|
8
|
+
|
9
|
+
def construct_response
|
10
|
+
@renderer.apply(@flow_name, @msisdn, @session_id, @input)
|
11
|
+
end
|
12
|
+
|
13
|
+
def run_simulator(input)
|
14
|
+
response = construct_response
|
15
|
+
puts response
|
16
|
+
|
17
|
+
if response.flow_state == :notify
|
18
|
+
puts "\n---Flow complete---"
|
19
|
+
reset
|
20
|
+
else
|
21
|
+
read_input
|
22
|
+
end
|
23
|
+
|
24
|
+
run_simulator(@input)
|
25
|
+
end
|
26
|
+
|
27
|
+
def reset
|
28
|
+
puts "\n--Going back to beginning of flow--\n"
|
29
|
+
|
30
|
+
# Get a new session_id when the simulator is reset
|
31
|
+
@session_id = @session_id.to_i + 1
|
32
|
+
|
33
|
+
puts 'Initial input (for direct flow):'
|
34
|
+
read_input
|
35
|
+
|
36
|
+
run!
|
37
|
+
end
|
38
|
+
|
39
|
+
def ask_for_flow_parameters
|
40
|
+
puts 'Please enter flow name:'
|
41
|
+
@flow_name = EM::Synchrony.gets.strip
|
42
|
+
puts 'Please enter msisdn: '
|
43
|
+
@msisdn = EM::Synchrony.gets.strip
|
44
|
+
puts 'Please enter session_id: '
|
45
|
+
@session_id = EM::Synchrony.gets.strip
|
46
|
+
puts 'Initial input (for direct flow): '
|
47
|
+
read_input
|
48
|
+
end
|
49
|
+
|
50
|
+
# Run simulator inside EM.synchrony loop
|
51
|
+
def run
|
52
|
+
EM.synchrony do
|
53
|
+
ask_for_flow_parameters
|
54
|
+
run!
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def run!
|
59
|
+
validate_request
|
60
|
+
sanitize_parameters!
|
61
|
+
|
62
|
+
run_simulator(@input)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
def read_input
|
67
|
+
print '> '
|
68
|
+
@input = EM::Synchrony.gets.strip
|
69
|
+
sanitize_parameters!
|
70
|
+
|
71
|
+
case @input
|
72
|
+
when 'reset'
|
73
|
+
reset
|
74
|
+
when 'quit'
|
75
|
+
puts 'Bye!'
|
76
|
+
exit!
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def logger
|
81
|
+
@renderer.logger
|
82
|
+
end
|
83
|
+
|
84
|
+
def validate_request
|
85
|
+
validate_flow_presence!
|
86
|
+
validate_msisdn!
|
87
|
+
validate_session_id!
|
88
|
+
end
|
89
|
+
|
90
|
+
def validate_flow_presence!
|
91
|
+
unless @renderer.flow_exists?(@flow_name)
|
92
|
+
raise Colloquy::FlowNotFound, "Flow not found: #{@flow_name}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def validate_msisdn!
|
97
|
+
if @msisdn == ''
|
98
|
+
raise Colloquy::MSISDNParameterEmpty, 'The msisdn parameter should not be empty.'
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def validate_session_id!
|
103
|
+
if @session_id == ''
|
104
|
+
raise Colloquy::SessionIDParameterEmpty, 'The session_id parameter should not be empty.'
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def sanitize_parameters!
|
109
|
+
@flow_name = @flow_name.to_sym
|
110
|
+
@msisdn = @msisdn.to_i.to_s
|
111
|
+
@session_id = @session_id.to_s[0..20].strip
|
112
|
+
@input = @input.to_s[0..160].strip
|
113
|
+
end
|
114
|
+
end
|