siriproxy 0.4.1
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.
- data/.gitignore +11 -0
- data/.rvmrc +1 -0
- data/COPYING +674 -0
- data/Gemfile +25 -0
- data/README.md +262 -0
- data/Rakefile +9 -0
- data/bin/siriproxy +6 -0
- data/config.example.yml +30 -0
- data/lib/siri_objects.rb +331 -0
- data/lib/siriproxy/command_line.rb +201 -0
- data/lib/siriproxy/connection/guzzoni.rb +24 -0
- data/lib/siriproxy/connection/iphone.rb +50 -0
- data/lib/siriproxy/connection.rb +212 -0
- data/lib/siriproxy/interpret_siri.rb +50 -0
- data/lib/siriproxy/plugin.rb +58 -0
- data/lib/siriproxy/plugin_manager.rb +78 -0
- data/lib/siriproxy/version.rb +3 -0
- data/lib/siriproxy.rb +37 -0
- data/plugins/siriproxy-example/.gitignore +4 -0
- data/plugins/siriproxy-example/Gemfile +4 -0
- data/plugins/siriproxy-example/Rakefile +1 -0
- data/plugins/siriproxy-example/lib/siriproxy-example.rb +95 -0
- data/plugins/siriproxy-example/siriproxy-example.gemspec +23 -0
- data/scripts/gen_certs.sh +80 -0
- data/scripts/openssl.cnf +353 -0
- data/siriproxy.gemspec +27 -0
- metadata +147 -0
@@ -0,0 +1,201 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'yaml'
|
3
|
+
require 'ostruct'
|
4
|
+
|
5
|
+
# @todo want to make SiriProxy::Commandline without having to
|
6
|
+
# require 'siriproxy'. Im sure theres a better way.
|
7
|
+
class SiriProxy
|
8
|
+
|
9
|
+
end
|
10
|
+
|
11
|
+
class SiriProxy::CommandLine
|
12
|
+
BANNER = <<-EOS
|
13
|
+
Siri Proxy is a proxy server for Apple's Siri "assistant." The idea is to allow for the creation of custom handlers for different actions. This can allow developers to easily add functionality to Siri.
|
14
|
+
|
15
|
+
See: http://github.com/plamoni/SiriProxy/
|
16
|
+
|
17
|
+
Usage: siriproxy COMMAND OPTIONS
|
18
|
+
|
19
|
+
Commands:
|
20
|
+
server Start up the Siri proxy server
|
21
|
+
gencerts Generate a the certificates needed for SiriProxy
|
22
|
+
bundle Install any dependancies needed by plugins
|
23
|
+
console Launch the plugin test console
|
24
|
+
update [dir] Updates to the latest code from GitHub or from a provided directory
|
25
|
+
help Show this usage information
|
26
|
+
|
27
|
+
Options:
|
28
|
+
Option Command Description
|
29
|
+
EOS
|
30
|
+
|
31
|
+
def initialize
|
32
|
+
@branch = nil
|
33
|
+
parse_options
|
34
|
+
command = ARGV.shift
|
35
|
+
subcommand = ARGV.shift
|
36
|
+
case command
|
37
|
+
when 'server' then run_server(subcommand)
|
38
|
+
when 'gencerts' then gen_certs
|
39
|
+
when 'bundle' then run_bundle(subcommand)
|
40
|
+
when 'console' then run_console
|
41
|
+
when 'update' then update(subcommand)
|
42
|
+
when 'help' then usage
|
43
|
+
else usage
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def run_console
|
48
|
+
load_code
|
49
|
+
$LOG_LEVEL = 0
|
50
|
+
# this is ugly, but works for now
|
51
|
+
SiriProxy::PluginManager.class_eval do
|
52
|
+
def respond(text, options={})
|
53
|
+
puts "=> #{text}"
|
54
|
+
end
|
55
|
+
def process(text)
|
56
|
+
super(text)
|
57
|
+
end
|
58
|
+
def send_request_complete_to_iphone
|
59
|
+
end
|
60
|
+
def no_matches
|
61
|
+
puts "No plugin responded"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
SiriProxy::Plugin.class_eval do
|
65
|
+
def last_ref_id
|
66
|
+
0
|
67
|
+
end
|
68
|
+
def send_object(object, options={:target => :iphone})
|
69
|
+
puts "=> #{object}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
cora = SiriProxy::PluginManager.new
|
74
|
+
repl = -> prompt { print prompt; cora.process(gets.chomp!) }
|
75
|
+
loop { repl[">> "] }
|
76
|
+
end
|
77
|
+
|
78
|
+
def run_bundle(subcommand='')
|
79
|
+
setup_bundler_path
|
80
|
+
puts `bundle #{subcommand} #{ARGV.join(' ')}`
|
81
|
+
end
|
82
|
+
|
83
|
+
def run_server(subcommand='start')
|
84
|
+
load_code
|
85
|
+
start_server
|
86
|
+
# @todo: support for forking server into bg and start/stop/restart
|
87
|
+
# subcommand ||= 'start'
|
88
|
+
# case subcommand
|
89
|
+
# when 'start' then start_server
|
90
|
+
# when 'stop' then stop_server
|
91
|
+
# when 'restart' then restart_server
|
92
|
+
# end
|
93
|
+
end
|
94
|
+
|
95
|
+
def start_server
|
96
|
+
proxy = SiriProxy.new
|
97
|
+
proxy.start()
|
98
|
+
end
|
99
|
+
|
100
|
+
def gen_certs
|
101
|
+
ca_name = @ca_name ||= ""
|
102
|
+
command = File.join(File.dirname(__FILE__), '..', "..", "scripts", 'gen_certs.sh')
|
103
|
+
sp_root = File.join(File.dirname(__FILE__), '..', "..")
|
104
|
+
puts `#{command} "#{sp_root}" "#{ca_name}"`
|
105
|
+
end
|
106
|
+
|
107
|
+
def update(directory=nil)
|
108
|
+
if(directory)
|
109
|
+
puts "=== Installing from '#{directory}' ==="
|
110
|
+
puts `cd #{directory} && rake install`
|
111
|
+
puts "=== Bundling ===" if $?.exitstatus == 0
|
112
|
+
puts `siriproxy bundle` if $?.exitstatus == 0
|
113
|
+
puts "=== SUCCESS ===" if $?.exitstatus == 0
|
114
|
+
|
115
|
+
exit $?.exitstatus
|
116
|
+
else
|
117
|
+
branch_opt = @branch ? "-b #{@branch}" : ""
|
118
|
+
@branch = "master" if @branch == nil
|
119
|
+
puts "=== Installing latest code from git://github.com/plamoni/SiriProxy.git [#{@branch}] ==="
|
120
|
+
|
121
|
+
tmp_dir = "/tmp/SiriProxy.install." + (rand 9999).to_s.rjust(4, "0")
|
122
|
+
|
123
|
+
`mkdir -p #{tmp_dir}`
|
124
|
+
puts `git clone #{branch_opt} git://github.com/plamoni/SiriProxy.git #{tmp_dir}` if $?.exitstatus == 0
|
125
|
+
puts "=== Performing Rake Install ===" if $?.exitstatus == 0
|
126
|
+
puts `cd #{tmp_dir} && rake install` if $?.exitstatus == 0
|
127
|
+
puts "=== Bundling ===" if $?.exitstatus == 0
|
128
|
+
puts `siriproxy bundle` if $?.exitstatus == 0
|
129
|
+
puts "=== Cleaning Up ===" and puts `rm -rf #{tmp_dir}` if $?.exitstatus == 0
|
130
|
+
puts "=== SUCCESS ===" if $?.exitstatus == 0
|
131
|
+
|
132
|
+
exit $?.exitstatus
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def usage
|
137
|
+
puts "\n#{@option_parser}\n"
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def parse_options
|
143
|
+
$APP_CONFIG = OpenStruct.new(YAML.load_file(File.expand_path('~/.siriproxy/config.yml')))
|
144
|
+
|
145
|
+
# Google Public DNS servers
|
146
|
+
$APP_CONFIG.upstream_dns ||= %w[8.8.8.8 8.8.4.4]
|
147
|
+
|
148
|
+
@branch = nil
|
149
|
+
@option_parser = OptionParser.new do |opts|
|
150
|
+
opts.on('-L', '--listen ADDRESS', '[server] address to listen on (central or node)') do |listen|
|
151
|
+
$APP_CONFIG.listen = listen
|
152
|
+
end
|
153
|
+
opts.on('-p', '--port PORT', '[server] port number for server (central or node)') do |port_num|
|
154
|
+
$APP_CONFIG.port = port_num
|
155
|
+
end
|
156
|
+
opts.on('-l', '--log LOG_LEVEL', '[server] The level of debug information displayed (higher is more)') do |log_level|
|
157
|
+
$APP_CONFIG.log_level = log_level
|
158
|
+
end
|
159
|
+
opts.on( '--upstream-dns SERVERS', Array, '[server] List of upstream DNS servers to query for the real guzzoni.apple.com. Defaults to Google DNS servers') do |servers|
|
160
|
+
$APP_CONFIG.upstream_dns = servers
|
161
|
+
end
|
162
|
+
opts.on('-u', '--user USER', '[server] The user to run as after launch') do |user|
|
163
|
+
$APP_CONFIG.user = user
|
164
|
+
end
|
165
|
+
opts.on('-b', '--branch BRANCH', '[update] Choose the branch to update from (default: master)') do |branch|
|
166
|
+
@branch = branch
|
167
|
+
end
|
168
|
+
opts.on('-n', '--name CA_NAME', '[gencerts] Define a common name for the CA (default: "SiriProxyCA")') do |ca_name|
|
169
|
+
@ca_name = ca_name
|
170
|
+
end
|
171
|
+
opts.on_tail('-v', '--version', ' show version') do
|
172
|
+
require "siriproxy/version"
|
173
|
+
puts "SiriProxy version #{SiriProxy::VERSION}"
|
174
|
+
exit
|
175
|
+
end
|
176
|
+
end
|
177
|
+
@option_parser.banner = BANNER
|
178
|
+
@option_parser.parse!(ARGV)
|
179
|
+
end
|
180
|
+
|
181
|
+
def setup_bundler_path
|
182
|
+
require 'pathname'
|
183
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../../Gemfile",
|
184
|
+
Pathname.new(__FILE__).realpath)
|
185
|
+
end
|
186
|
+
|
187
|
+
def load_code
|
188
|
+
setup_bundler_path
|
189
|
+
|
190
|
+
require 'bundler'
|
191
|
+
require 'bundler/setup'
|
192
|
+
|
193
|
+
require 'siriproxy'
|
194
|
+
require 'siriproxy/connection'
|
195
|
+
require 'siriproxy/connection/iphone'
|
196
|
+
require 'siriproxy/connection/guzzoni'
|
197
|
+
|
198
|
+
require 'siriproxy/plugin'
|
199
|
+
require 'siriproxy/plugin_manager'
|
200
|
+
end
|
201
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
#####
|
2
|
+
# This is the connection to the Guzzoni (the Siri server backend)
|
3
|
+
#####
|
4
|
+
class SiriProxy::Connection::Guzzoni < SiriProxy::Connection
|
5
|
+
def initialize
|
6
|
+
super
|
7
|
+
self.name = "Guzzoni"
|
8
|
+
end
|
9
|
+
|
10
|
+
def connection_completed
|
11
|
+
super
|
12
|
+
start_tls(:verify_peer => false)
|
13
|
+
end
|
14
|
+
|
15
|
+
def received_object(object)
|
16
|
+
return plugin_manager.process_filters(object, :from_guzzoni)
|
17
|
+
|
18
|
+
#plugin_manager.object_from_guzzoni(object, self)
|
19
|
+
end
|
20
|
+
|
21
|
+
def block_rest_of_session
|
22
|
+
@block_rest_of_session = true
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'resolv'
|
2
|
+
|
3
|
+
#####
|
4
|
+
# This is the connection to the iPhone
|
5
|
+
#####
|
6
|
+
class SiriProxy::Connection::Iphone < SiriProxy::Connection
|
7
|
+
def initialize upstream_dns
|
8
|
+
puts "Create server for iPhone connection"
|
9
|
+
super()
|
10
|
+
self.name = "iPhone"
|
11
|
+
@upstream_dns = upstream_dns
|
12
|
+
end
|
13
|
+
|
14
|
+
def post_init
|
15
|
+
super
|
16
|
+
start_tls(:cert_chain_file => File.expand_path("~/.siriproxy/server.passless.crt"),
|
17
|
+
:private_key_file => File.expand_path("~/.siriproxy/server.passless.key"),
|
18
|
+
:verify_peer => false)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Resolves guzzoni.apple.com using the Google DNS servers. This allows the
|
22
|
+
# machine running siriproxy to use the DNS server returning fake records for
|
23
|
+
# guzzoni.apple.com.
|
24
|
+
|
25
|
+
def resolve_guzzoni
|
26
|
+
addresses = Resolv::DNS.open(nameserver: @upstream_dns) do |dns|
|
27
|
+
res = dns.getresources('guzzoni.apple.com', Resolv::DNS::Resource::IN::A)
|
28
|
+
|
29
|
+
res.map { |r| r.address }
|
30
|
+
end
|
31
|
+
|
32
|
+
addresses.map do |address|
|
33
|
+
address.address.unpack('C*').join('.')
|
34
|
+
end.sample
|
35
|
+
end
|
36
|
+
|
37
|
+
def ssl_handshake_completed
|
38
|
+
super
|
39
|
+
self.other_connection = EventMachine.connect(resolve_guzzoni, 443, SiriProxy::Connection::Guzzoni)
|
40
|
+
self.plugin_manager.guzzoni_conn = self.other_connection
|
41
|
+
other_connection.other_connection = self #hehe
|
42
|
+
other_connection.plugin_manager = plugin_manager
|
43
|
+
end
|
44
|
+
|
45
|
+
def received_object(object)
|
46
|
+
return plugin_manager.process_filters(object, :from_iphone)
|
47
|
+
|
48
|
+
#plugin_manager.object_from_client(object, self)
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
require 'cfpropertylist'
|
2
|
+
require 'siriproxy/interpret_siri'
|
3
|
+
|
4
|
+
class SiriProxy::Connection < EventMachine::Connection
|
5
|
+
include EventMachine::Protocols::LineText2
|
6
|
+
|
7
|
+
attr_accessor :other_connection, :name, :ssled, :output_buffer, :input_buffer, :processed_headers, :unzip_stream, :zip_stream, :consumed_ace, :unzipped_input, :unzipped_output, :last_ref_id, :plugin_manager
|
8
|
+
|
9
|
+
def last_ref_id=(ref_id)
|
10
|
+
@last_ref_id = ref_id
|
11
|
+
self.other_connection.last_ref_id = ref_id if other_connection.last_ref_id != ref_id
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
super
|
16
|
+
self.processed_headers = false
|
17
|
+
self.output_buffer = ""
|
18
|
+
self.input_buffer = ""
|
19
|
+
self.unzipped_input = ""
|
20
|
+
self.unzipped_output = ""
|
21
|
+
self.unzip_stream = Zlib::Inflate.new
|
22
|
+
self.zip_stream = Zlib::Deflate.new
|
23
|
+
self.consumed_ace = false
|
24
|
+
end
|
25
|
+
|
26
|
+
def post_init
|
27
|
+
self.ssled = false
|
28
|
+
end
|
29
|
+
|
30
|
+
def ssl_handshake_completed
|
31
|
+
self.ssled = true
|
32
|
+
|
33
|
+
puts "[Info - #{self.name}] SSL completed for #{self.name}" if $LOG_LEVEL > 1
|
34
|
+
end
|
35
|
+
|
36
|
+
def receive_line(line) #Process header
|
37
|
+
puts "[Header - #{self.name}] #{line}" if $LOG_LEVEL > 2
|
38
|
+
if(line == "") #empty line indicates end of headers
|
39
|
+
puts "[Debug - #{self.name}] Found end of headers" if $LOG_LEVEL > 3
|
40
|
+
set_binary_mode
|
41
|
+
self.processed_headers = true
|
42
|
+
end
|
43
|
+
self.output_buffer << (line + "\x0d\x0a") #Restore the CR-LF to the end of the line
|
44
|
+
|
45
|
+
flush_output_buffer()
|
46
|
+
end
|
47
|
+
|
48
|
+
def receive_binary_data(data)
|
49
|
+
self.input_buffer << data
|
50
|
+
|
51
|
+
##Consume the "0xAACCEE02" data at the start of the stream if necessary (by forwarding it to the output buffer)
|
52
|
+
if(self.consumed_ace == false)
|
53
|
+
self.output_buffer << input_buffer[0..3]
|
54
|
+
self.input_buffer = input_buffer[4..-1]
|
55
|
+
self.consumed_ace = true;
|
56
|
+
end
|
57
|
+
|
58
|
+
process_compressed_data()
|
59
|
+
|
60
|
+
flush_output_buffer()
|
61
|
+
end
|
62
|
+
|
63
|
+
def flush_output_buffer
|
64
|
+
return if output_buffer.empty?
|
65
|
+
|
66
|
+
if other_connection.ssled
|
67
|
+
puts "[Debug - #{self.name}] Forwarding #{self.output_buffer.length} bytes of data to #{other_connection.name}" if $LOG_LEVEL > 5
|
68
|
+
#puts self.output_buffer.to_hex if $LOG_LEVEL > 5
|
69
|
+
other_connection.send_data(output_buffer)
|
70
|
+
self.output_buffer = ""
|
71
|
+
else
|
72
|
+
puts "[Debug - #{self.name}] Buffering some data for later (#{self.output_buffer.length} bytes buffered)" if $LOG_LEVEL > 5
|
73
|
+
#puts self.output_buffer.to_hex if $LOG_LEVEL > 5
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def process_compressed_data
|
78
|
+
self.unzipped_input << unzip_stream.inflate(self.input_buffer)
|
79
|
+
self.input_buffer = ""
|
80
|
+
puts "========UNZIPPED DATA (from #{self.name} =========" if $LOG_LEVEL > 5
|
81
|
+
puts unzipped_input.to_hex if $LOG_LEVEL > 5
|
82
|
+
puts "==================================================" if $LOG_LEVEL > 5
|
83
|
+
|
84
|
+
while(self.has_next_object?)
|
85
|
+
object = read_next_object_from_unzipped()
|
86
|
+
|
87
|
+
if(object != nil) #will be nil if the next object is a ping/pong
|
88
|
+
new_object = prep_received_object(object) #give the world a chance to mess with folks
|
89
|
+
|
90
|
+
inject_object_to_output_stream(new_object) if new_object != nil #might be nil if "the world" decides to rid us of the object
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def has_next_object?
|
96
|
+
return false if unzipped_input.empty? #empty
|
97
|
+
unpacked = unzipped_input[0...5].unpack('H*').first
|
98
|
+
return true if(unpacked.match(/^0[34]/)) #Ping or pong
|
99
|
+
return true if(unpacked.match(/^ff/)) #clear context
|
100
|
+
|
101
|
+
if unpacked.match(/^[0-9][15-9]/)
|
102
|
+
puts "ROGUE PACKET!!! WHAT IS IT?! TELL US!!! IN IRC!! COPY THE STUFF FROM BELOW"
|
103
|
+
puts unpacked.to_hex
|
104
|
+
end
|
105
|
+
|
106
|
+
objectLength = unpacked.match(/^0200(.{6})/)[1].to_i(16)
|
107
|
+
return ((objectLength + 5) < unzipped_input.length) #determine if the length of the next object (plus its prefix) is less than the input buffer
|
108
|
+
end
|
109
|
+
|
110
|
+
def read_next_object_from_unzipped
|
111
|
+
unpacked = unzipped_input[0...5].unpack('H*').first
|
112
|
+
info = unpacked.match(/^(..)(.{8})$/)
|
113
|
+
|
114
|
+
if(info[1] == "03" || info[1] == "04" || info[1] == "ff") #Ping or pong -- just get these out of the way (and log them for good measure)
|
115
|
+
object = unzipped_input[0...5]
|
116
|
+
self.unzipped_output << object
|
117
|
+
|
118
|
+
type = (info[1] == "03") ? "Ping" : ((info[1] == "04") ? "Pong" : "Clear Context")
|
119
|
+
puts "[#{type} - #{self.name}] (#{info[2].to_i(16)})" if $LOG_LEVEL > 3
|
120
|
+
self.unzipped_input = unzipped_input[5..-1]
|
121
|
+
|
122
|
+
flush_unzipped_output()
|
123
|
+
return nil
|
124
|
+
end
|
125
|
+
|
126
|
+
object_size = info[2].to_i(16)
|
127
|
+
prefix = unzipped_input[0...5]
|
128
|
+
object_data = unzipped_input[5...object_size+5]
|
129
|
+
self.unzipped_input = unzipped_input[object_size+5..-1]
|
130
|
+
|
131
|
+
parse_object(object_data)
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
def parse_object(object_data)
|
136
|
+
plist = CFPropertyList::List.new(:data => object_data)
|
137
|
+
object = CFPropertyList.native_types(plist.value)
|
138
|
+
|
139
|
+
object
|
140
|
+
end
|
141
|
+
|
142
|
+
def inject_object_to_output_stream(object)
|
143
|
+
if object["refId"] != nil && !object["refId"].empty?
|
144
|
+
@block_rest_of_session = false if @block_rest_of_session && self.last_ref_id != object["refId"] #new session
|
145
|
+
self.last_ref_id = object["refId"]
|
146
|
+
end
|
147
|
+
|
148
|
+
puts "[Info - Forwarding object to #{self.other_connection.name}] #{object["class"]}" if $LOG_LEVEL > 1
|
149
|
+
|
150
|
+
object_data = object.to_plist(:plist_format => CFPropertyList::List::FORMAT_BINARY)
|
151
|
+
|
152
|
+
#Recalculate the size in case the object gets modified. If new size is 0, then remove the object from the stream entirely
|
153
|
+
obj_len = object_data.length
|
154
|
+
|
155
|
+
if(obj_len > 0)
|
156
|
+
prefix = [(0x0200000000 + obj_len).to_s(16).rjust(10, '0')].pack('H*')
|
157
|
+
self.unzipped_output << prefix + object_data
|
158
|
+
end
|
159
|
+
|
160
|
+
flush_unzipped_output()
|
161
|
+
end
|
162
|
+
|
163
|
+
def flush_unzipped_output
|
164
|
+
self.zip_stream << self.unzipped_output
|
165
|
+
self.unzipped_output = ""
|
166
|
+
self.output_buffer << zip_stream.flush
|
167
|
+
|
168
|
+
flush_output_buffer()
|
169
|
+
end
|
170
|
+
|
171
|
+
def prep_received_object(object)
|
172
|
+
if object["refId"] == self.last_ref_id && @block_rest_of_session
|
173
|
+
puts "[Info - Dropping Object from Guzzoni] #{object["class"]}" if $LOG_LEVEL > 1
|
174
|
+
pp object if $LOG_LEVEL > 3
|
175
|
+
return nil
|
176
|
+
end
|
177
|
+
|
178
|
+
puts "[Info - #{self.name}] Received Object: #{object["class"]}" if $LOG_LEVEL == 1
|
179
|
+
puts "[Info - #{self.name}] Received Object: #{object["class"]} (group: #{object["group"]})" if $LOG_LEVEL == 2
|
180
|
+
puts "[Info - #{self.name}] Received Object: #{object["class"]} (group: #{object["group"]}, ref_id: #{object["refId"]}, ace_id: #{object["aceId"]})" if $LOG_LEVEL > 2
|
181
|
+
pp object if $LOG_LEVEL > 3
|
182
|
+
|
183
|
+
#keeping this for filters
|
184
|
+
new_obj = received_object(object)
|
185
|
+
if new_obj == nil
|
186
|
+
puts "[Info - Dropping Object from #{self.name}] #{object["class"]}" if $LOG_LEVEL > 1
|
187
|
+
pp object if $LOG_LEVEL > 3
|
188
|
+
return nil
|
189
|
+
end
|
190
|
+
|
191
|
+
#block the rest of the session if a plugin claims ownership
|
192
|
+
speech = SiriProxy::Interpret.speech_recognized(object)
|
193
|
+
if speech != nil
|
194
|
+
inject_object_to_output_stream(object)
|
195
|
+
block_rest_of_session if plugin_manager.process(speech)
|
196
|
+
return nil
|
197
|
+
end
|
198
|
+
|
199
|
+
|
200
|
+
#object = new_obj if ((new_obj = SiriProxy::Interpret.unknown_intent(object, self, plugin_manager.method(:unknown_command))) != false)
|
201
|
+
#object = new_obj if ((new_obj = SiriProxy::Interpret.speech_recognized(object, self, plugin_manager.method(:speech_recognized))) != false)
|
202
|
+
|
203
|
+
object
|
204
|
+
end
|
205
|
+
|
206
|
+
#Stub -- override in subclass
|
207
|
+
def received_object(object)
|
208
|
+
|
209
|
+
object
|
210
|
+
end
|
211
|
+
|
212
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
######
|
2
|
+
# The idea behind this class is that you can call the different
|
3
|
+
# methods to get different interpretations of a Siri object.
|
4
|
+
# For instance, you can "unknown_intent" and it will check
|
5
|
+
# to see if an object is a "Common#unknownIntent" response and
|
6
|
+
# call the provided processor method with the appropriate info.
|
7
|
+
# processor method signatures are provided in comments above each
|
8
|
+
# method.
|
9
|
+
#
|
10
|
+
# each method will return "nil" if the object is not the valid
|
11
|
+
# type. If it is, it will return the result of the processor.
|
12
|
+
#####
|
13
|
+
class SiriProxy::Interpret
|
14
|
+
class << self
|
15
|
+
#Checks if the object is Guzzoni responding that it can't
|
16
|
+
#determine the intent of the query
|
17
|
+
#processor(object, connection, unknown_text)
|
18
|
+
def unknown_intent(object, connection, processor)
|
19
|
+
return false if object == nil
|
20
|
+
return false if (!(object["properties"]["views"][0]["properties"]["dialogIdentifier"] == "Common#unknownIntent") rescue true)
|
21
|
+
|
22
|
+
searchUtterance = object["properties"]["views"][1]["properties"]["commands"][0]["properties"]["commands"][0]["properties"]["utterance"]
|
23
|
+
searchText = searchUtterance.split("^")[3]
|
24
|
+
return processor.call(object, connection, searchText)
|
25
|
+
|
26
|
+
return false
|
27
|
+
end
|
28
|
+
|
29
|
+
#Checks if the object is Guzzoni responding that it recognized
|
30
|
+
#speech. Sends "best interpretation" phrase to processor
|
31
|
+
#processor(object, connection, phrase)
|
32
|
+
def speech_recognized(object)
|
33
|
+
return nil if object == nil
|
34
|
+
return nil if (!(object["class"] == "SpeechRecognized") rescue true)
|
35
|
+
phrase = ""
|
36
|
+
|
37
|
+
object["properties"]["recognition"]["properties"]["phrases"].map { |phraseObj|
|
38
|
+
phraseObj["properties"]["interpretations"].first["properties"]["tokens"].map { |token|
|
39
|
+
tokenProps = token["properties"]
|
40
|
+
|
41
|
+
phrase = phrase[0..-2] if tokenProps["removeSpaceBefore"] and phrase[-1] == " "
|
42
|
+
phrase << tokenProps["text"]
|
43
|
+
phrase << " " if !tokenProps["removeSpaceAfter"]
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
phrase
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'cora'
|
2
|
+
|
3
|
+
class SiriProxy::Plugin < Cora::Plugin
|
4
|
+
def initialize(config)
|
5
|
+
|
6
|
+
end
|
7
|
+
|
8
|
+
def request_completed
|
9
|
+
self.manager.send_request_complete_to_iphone
|
10
|
+
end
|
11
|
+
|
12
|
+
#use send_object(object, target: :guzzoni) to send to guzzoni
|
13
|
+
def send_object(object, options={})
|
14
|
+
(object = object.to_hash) rescue nil #convert SiriObjects to a hash
|
15
|
+
options[:target] = options[:target] ||= :iphone
|
16
|
+
|
17
|
+
if(options[:target] == :iphone)
|
18
|
+
self.manager.guzzoni_conn.inject_object_to_output_stream(object)
|
19
|
+
elsif(options[:target] == :guzzoni)
|
20
|
+
self.manager.iphone_conn.inject_object_to_output_stream(object)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def last_ref_id
|
25
|
+
self.manager.iphone_conn.last_ref_id
|
26
|
+
end
|
27
|
+
|
28
|
+
#direction should be :from_iphone, or :from_guzzoni
|
29
|
+
def process_filters(object, direction)
|
30
|
+
return nil if object == nil
|
31
|
+
f = filters[object["class"]]
|
32
|
+
if(f != nil && (f[:direction] == :both || f[:direction] == direction))
|
33
|
+
object = instance_exec(object, &f[:block])
|
34
|
+
end
|
35
|
+
|
36
|
+
object
|
37
|
+
end
|
38
|
+
|
39
|
+
class << self
|
40
|
+
def filter(class_names, options={}, &block)
|
41
|
+
[class_names].flatten.each do |class_name|
|
42
|
+
filters[class_name] = {
|
43
|
+
direction: (options[:direction] ||= :both),
|
44
|
+
block: block
|
45
|
+
}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def filters
|
50
|
+
@filters ||= {}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def filters
|
55
|
+
self.class.filters
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'cora'
|
2
|
+
require 'pp'
|
3
|
+
|
4
|
+
class SiriProxy::PluginManager < Cora
|
5
|
+
attr_accessor :plugins, :iphone_conn, :guzzoni_conn
|
6
|
+
|
7
|
+
def initialize()
|
8
|
+
load_plugins()
|
9
|
+
end
|
10
|
+
|
11
|
+
def load_plugins()
|
12
|
+
@plugins = []
|
13
|
+
if $APP_CONFIG.plugins
|
14
|
+
$APP_CONFIG.plugins.each do |pluginConfig|
|
15
|
+
if pluginConfig.is_a? String
|
16
|
+
className = pluginConfig
|
17
|
+
requireName = "siriproxy-#{className.downcase}"
|
18
|
+
else
|
19
|
+
className = pluginConfig['name']
|
20
|
+
requireName = pluginConfig['require'] || "siriproxy-#{className.downcase}"
|
21
|
+
end
|
22
|
+
require requireName
|
23
|
+
plugin = SiriProxy::Plugin.const_get(className).new(pluginConfig)
|
24
|
+
plugin.manager = self
|
25
|
+
@plugins << plugin
|
26
|
+
end
|
27
|
+
end
|
28
|
+
log "Plugins loaded: #{@plugins}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def process_filters(object, direction)
|
32
|
+
object_class = object.class #This way, if we change the object class we won't need to modify this code.
|
33
|
+
|
34
|
+
if object['class'] == 'SetRequestOrigin'
|
35
|
+
properties = object['properties']
|
36
|
+
set_location(properties['latitude'], properties['longitude'], properties)
|
37
|
+
end
|
38
|
+
|
39
|
+
plugins.each do |plugin|
|
40
|
+
#log "Processing filters on #{plugin} for '#{object["class"]}'"
|
41
|
+
new_obj = plugin.process_filters(object, direction)
|
42
|
+
object = new_obj if(new_obj == false || new_obj.class == object_class) #prevent accidental poorly formed returns
|
43
|
+
return nil if object == false #if any filter returns "false," then the object should be dropped
|
44
|
+
end
|
45
|
+
|
46
|
+
return object
|
47
|
+
end
|
48
|
+
|
49
|
+
def process(text)
|
50
|
+
begin
|
51
|
+
result = super(text)
|
52
|
+
self.guzzoni_conn.block_rest_of_session if result
|
53
|
+
return result
|
54
|
+
rescue Exception=>e
|
55
|
+
log "Plugin Crashed: #{e}"
|
56
|
+
respond e.to_s, spoken: "a plugin crashed"
|
57
|
+
return true
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def send_request_complete_to_iphone
|
62
|
+
log "Sending Request Completed"
|
63
|
+
object = generate_request_completed(self.guzzoni_conn.last_ref_id)
|
64
|
+
self.guzzoni_conn.inject_object_to_output_stream(object)
|
65
|
+
end
|
66
|
+
|
67
|
+
def respond(text, options={})
|
68
|
+
self.guzzoni_conn.inject_object_to_output_stream(generate_siri_utterance(self.guzzoni_conn.last_ref_id, text, (options[:spoken] or text), options[:prompt_for_response] == true))
|
69
|
+
end
|
70
|
+
|
71
|
+
def no_matches
|
72
|
+
return false
|
73
|
+
end
|
74
|
+
|
75
|
+
def log(text)
|
76
|
+
puts "[Info - Plugin Manager] #{text}" if $LOG_LEVEL >= 1
|
77
|
+
end
|
78
|
+
end
|