colloquy 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|