sgx-catapult.rb

  1#!/usr/bin/env ruby
  2#
  3# Copyright (C) 2017  Denver Gingerich <denver@ossguy.com>
  4# Copyright (C) 2017  Stephen Paul Weber <singpolyma@singpolyma.net>
  5#
  6# This file is part of sgx-catapult.
  7#
  8# sgx-catapult is free software: you can redistribute it and/or modify it under
  9# the terms of the GNU Affero General Public License as published by the Free
 10# Software Foundation, either version 3 of the License, or (at your option) any
 11# later version.
 12#
 13# sgx-catapult is distributed in the hope that it will be useful, but WITHOUT
 14# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 15# FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
 16# details.
 17#
 18# You should have received a copy of the GNU Affero General Public License along
 19# with sgx-catapult.  If not, see <http://www.gnu.org/licenses/>.
 20
 21require 'blather/client/dsl'
 22require 'json'
 23require 'net/http'
 24require 'redis/connection/hiredis'
 25require 'time'
 26require 'uri'
 27require 'uuid'
 28
 29require 'goliath/api'
 30require 'goliath/server'
 31require 'log4r'
 32
 33puts "Soprani.ca/SMS Gateway for XMPP - Catapult        v0.018\n\n"
 34
 35if ARGV.size != 9 then
 36	puts "Usage: sgx-catapult.rb <component_jid> <component_password> " +
 37		"<server_hostname> <server_port> " +
 38		"<redis_hostname> <redis_port> <delivery_receipt_url> " +
 39		"<http_listen_port> <mms_proxy_prefix_url>"
 40	exit 0
 41end
 42
 43t = Time.now
 44puts "LOG %d.%09d: starting...\n\n" % [t.to_i, t.nsec]
 45
 46module SGXcatapult
 47	extend Blather::DSL
 48
 49	@jingle_sids = Hash.new
 50	@jingle_fnames = Hash.new
 51	@partial_data = Hash.new
 52	@uuid_gen = UUID.new
 53
 54	def self.run
 55		client.run
 56	end
 57
 58	# so classes outside this module can write messages, too
 59	def self.write(stanza)
 60		client.write(stanza)
 61	end
 62
 63	def self.error_msg(orig, query_node, type, name, text = nil)
 64		if not query_node.nil?
 65			orig.add_child(query_node)
 66			orig.type = :error
 67		end
 68
 69		error = Nokogiri::XML::Node.new 'error', orig.document
 70		error['type'] = type
 71		orig.add_child(error)
 72
 73		suberr = Nokogiri::XML::Node.new name, orig.document
 74		suberr['xmlns'] = 'urn:ietf:params:xml:ns:xmpp-stanzas'
 75		error.add_child(suberr)
 76
 77		# TODO: add some explanatory xml:lang='en' text (see text param)
 78		puts "RESPONSE3: #{orig.inspect}"
 79		return orig
 80	end
 81
 82	setup ARGV[0], ARGV[1], ARGV[2], ARGV[3]
 83
 84	message :chat?, :body do |m|
 85		num_dest = m.to.to_s.split('@', 2)[0]
 86
 87		if num_dest[0] != '+'
 88			# check to see if a valid shortcode context is specified
 89			num_and_context = num_dest.split(';', 2)
 90			if num_and_context[1] and num_and_context[1] ==
 91				'phone-context=ca-us.phone-context.soprani.ca'
 92
 93				# TODO: check if num_dest is fully numeric
 94				num_dest = num_and_context[0]
 95			else
 96				# TODO: text re num not (yet) supportd/implmentd
 97				write_to_stream error_msg(m.reply, m.body,
 98					:cancel, 'item-not-found')
 99				next
100			end
101		end
102
103		bare_jid = m.from.to_s.split('/', 2)[0]
104		cred_key = "catapult_cred-" + bare_jid
105
106		conn = Hiredis::Connection.new
107		conn.connect(ARGV[4], ARGV[5].to_i)
108
109		conn.write ["EXISTS", cred_key]
110		if conn.read == 0
111			conn.disconnect
112
113			# TODO: add text re credentials not being registered
114			write_to_stream error_msg(m.reply, m.body, :auth,
115				'registration-required')
116			next
117		end
118
119		conn.write ["LRANGE", cred_key, 0, 3]
120		user_id, api_token, api_secret, users_num = conn.read
121		conn.disconnect
122
123		uri = URI.parse('https://api.catapult.inetwork.com')
124		http = Net::HTTP.new(uri.host, uri.port)
125		http.use_ssl = true
126		request = Net::HTTP::Post.new('/v1/users/' + user_id +
127			'/messages')
128		request.basic_auth api_token, api_secret
129		request.add_field('Content-Type', 'application/json')
130		request.body = JSON.dump({
131			'from'			=> users_num,
132			'to'			=> num_dest,
133			'text'			=> m.body,
134			'tag'			=> m.id, # TODO: message has it?
135			'receiptRequested'	=> 'all',
136			'callbackUrl'		=> ARGV[6]
137		})
138		response = http.request(request)
139
140		puts 'API response to send: ' + response.to_s + ' with code ' +
141			response.code + ', body "' + response.body + '"'
142
143		if response.code != '201'
144			# TODO: add text re unexpected code; mention code number
145			write_to_stream error_msg(m.reply, m.body, :cancel,
146				'internal-server-error')
147			next
148		end
149	end
150
151	def self.user_cap_identities()
152		[{:category => 'client', :type => 'sms'}]
153	end
154
155	def self.user_cap_features()
156	[
157		"urn:xmpp:receipts",
158		"urn:xmpp:jingle:1", "urn:xmpp:jingle:transports:ibb:1",
159
160		# TODO: eventually add more efficient file transfer mechanisms
161		#"urn:xmpp:jingle:transports:s5b:1",
162
163		# TODO: MUST add all relevant/reasonable vers of file-transfer
164		#"urn:xmpp:jingle:apps:file-transfer:4"
165		"urn:xmpp:jingle:apps:file-transfer:3"
166	]
167	end
168
169	presence :subscribe? do |p|
170		puts "PRESENCE1: #{p.inspect}"
171
172		msg = Blather::Stanza::Presence.new
173		msg.to = p.from
174		msg.from = p.to
175		msg.type = :subscribed
176
177		puts "RESPONSE5: #{msg.inspect}"
178		write_to_stream msg
179	end
180
181	presence :probe? do |p|
182		puts 'PRESENCE2: ' + p.inspect
183
184		caps = Blather::Stanza::Capabilities.new
185		# TODO: user a better node URI (?)
186		caps.node = 'http://catapult.sgx.soprani.ca/'
187		caps.identities = user_cap_identities()
188		caps.features = user_cap_features()
189
190		msg = caps.c
191		msg.to = p.from
192		msg.from = p.to.to_s + '/sgx'
193
194		puts 'RESPONSE6: ' + msg.inspect
195		write_to_stream msg
196	end
197
198	iq '/iq/ns:jingle', :ns => 'urn:xmpp:jingle:1' do |i, jn|
199		puts "IQj: #{i.inspect}"
200
201		if jn[0]['action'] == 'transport-accept'
202			puts "REPLY0: #{i.reply.inspect}"
203			write_to_stream i.reply
204			next
205		elsif jn[0]['action'] == 'session-terminate'
206			# TODO: unexpected (usually we do this; handle?)
207			puts "TERMINATED"
208			next
209		elsif jn[0]['action'] == 'transport-info'
210			# TODO: unexpected, but should handle in a nice way
211			puts "FAIL!!!"
212			next
213		elsif i.type == :error
214			# TODO: do something, maybe terminating the connection
215			puts 'ERROR!!!'
216			next
217		end
218
219		# TODO: should probably confirm we got session-initiate here
220
221		write_to_stream i.reply
222		puts "RESPONSE8: #{i.reply.inspect}"
223
224		msg = Blather::Stanza::Iq.new :set
225		msg.to = i.from
226		msg.from = i.to
227
228		cn = jn.children.find { |v| v.element_name == "content" }
229		puts 'CN-name: ' + cn['name']
230		puts 'JN-sid: ' + jn[0]['sid']
231
232		ibb_found = false
233		last_sid = ''
234		for child in cn.children
235			if child.element_name == 'transport'
236				puts 'TPORT: ' + child.namespace.href
237				last_sid = child['sid']
238				if 'urn:xmpp:jingle:transports:ibb:1' ==
239					child.namespace.href
240
241					ibb_found = true
242					break
243				end
244			end
245		end
246
247		j = Nokogiri::XML::Node.new 'jingle',msg.document
248		j['xmlns'] = 'urn:xmpp:jingle:1'
249		j['sid'] = jn[0]['sid']
250		msg.add_child(j)
251
252		content = Nokogiri::XML::Node.new 'content',msg.document
253		content['name'] = cn['name']
254		content['creator'] = 'initiator'
255		j.add_child(content)
256
257		transport = Nokogiri::XML::Node.new 'transport',msg.document
258		# TODO: make block-size more variable and/or dependent on sender
259		transport['block-size'] = '4096'
260		transport['xmlns'] = 'urn:xmpp:jingle:transports:ibb:1'
261		if ibb_found
262			transport['sid'] = last_sid
263			j['action'] = 'session-accept'
264			j['responder'] = i.from
265
266			dsc = Nokogiri::XML::Node.new 'description',msg.document
267			dsc['xmlns'] = 'urn:xmpp:jingle:apps:file-transfer:3'
268			content.add_child(dsc)
269		else
270			# for Conversations - it tries s5b even if caps ibb-only
271			transport['sid'] = @uuid_gen.generate
272			j['action'] = 'transport-replace'
273			j['initiator'] = i.from
274		end
275		content.add_child(transport)
276
277		@jingle_sids[transport['sid']] = jn[0]['sid']
278
279		# TODO: save <date> as well? Gajim sends, Conversations does not
280		# TODO: save/validate <size> with eventual full received length
281		fname = cn.children.find { |v| v.element_name == "description"
282			}.children.find { |w| w.element_name == "offer"
283			}.children.find { |x| x.element_name == "file"
284			}.children.find { |y| y.element_name == "name" }
285		@jingle_fnames[transport['sid']] = fname.text
286
287		puts "RESPONSE9: #{msg.inspect}"
288		write_to_stream msg
289	end
290
291	iq '/iq/ns:open', :ns =>
292		'http://jabber.org/protocol/ibb' do |i, on|
293
294		puts "IQo: #{i.inspect}"
295
296		@partial_data[on[0]['sid']] = ''
297		write_to_stream i.reply
298	end
299
300	iq '/iq/ns:data', :ns =>
301		'http://jabber.org/protocol/ibb' do |i, dn|
302
303		@partial_data[dn[0]['sid']] += Base64.decode64(dn[0].text)
304		write_to_stream i.reply
305	end
306
307	iq '/iq/ns:close', :ns =>
308		'http://jabber.org/protocol/ibb' do |i, cn|
309
310		puts "IQc: #{i.inspect}"
311		write_to_stream i.reply
312
313		# TODO: refactor below so that "message :chat?" uses same code
314		num_dest = i.to.to_s.split('@', 2)[0]
315
316		if num_dest[0] != '+'
317			# check to see if a valid shortcode context is specified
318			num_and_context = num_dest.split(';', 2)
319			if num_and_context[1] and num_and_context[1] ==
320				'phone-context=ca-us.phone-context.soprani.ca'
321
322				# TODO: check if num_dest is fully numeric
323				num_dest = num_and_context[0]
324			else
325				# TODO: text re num not (yet) supportd/implmentd
326				write_to_stream error_msg(i.reply, nil,
327					:cancel, 'item-not-found')
328				next
329			end
330		end
331
332		bare_jid = i.from.to_s.split('/', 2)[0]
333		cred_key = "catapult_cred-" + bare_jid
334
335		# TODO: connect at start of program instead
336		conn = Hiredis::Connection.new
337		conn.connect(ARGV[4], ARGV[5].to_i)
338
339		conn.write ["EXISTS", cred_key]
340		if conn.read == 0
341			conn.disconnect
342
343			# TODO: add text re credentials not being registered
344			write_to_stream error_msg(i.reply, nil, :auth,
345				'registration-required')
346			next
347		end
348
349		conn.write ["LRANGE", cred_key, 0, 3]
350		user_id, api_token, api_secret, users_num = conn.read
351		conn.disconnect
352
353		# upload cached data to server (before success reply)
354		media_name = Time.now.utc.iso8601 + '_' + @uuid_gen.generate +
355			'_' + @jingle_fnames[cn[0]['sid']]
356		puts 'name to save: ' + media_name
357
358		uri = URI.parse('https://api.catapult.inetwork.com')
359		http = Net::HTTP.new(uri.host, uri.port)
360		http.use_ssl = true
361		request = Net::HTTP::Put.new('/v1/users/' + user_id +
362			'/media/' + media_name)
363		request.basic_auth api_token, api_secret
364		request.body = @partial_data[cn[0]['sid']]
365		response = http.request(request)
366
367		puts 'eAPI response to send: ' + response.to_s + ' with code ' +
368			response.code + ', body "' + response.body + '"'
369
370		if response.code != '200'
371			# TODO: add text re unexpected code; mention code number
372			write_to_stream error_msg(i.reply, nil, :cancel,
373				'internal-server-error')
374			next
375		end
376
377		uri = URI.parse('https://api.catapult.inetwork.com')
378		http = Net::HTTP.new(uri.host, uri.port)
379		http.use_ssl = true
380		request = Net::HTTP::Post.new('/v1/users/' + user_id +
381			'/messages')
382		request.basic_auth api_token, api_secret
383		request.add_field('Content-Type', 'application/json')
384		request.body = JSON.dump({
385			'from'			=> users_num,
386			'to'			=> num_dest,
387			'text'			=> '',
388			'media'			=> [
389				'https://api.catapult.inetwork.com/v1/users/' +
390				user_id + '/media/' + media_name],
391			'tag'			=> i.id  # TODO: message has it?
392			# TODO: add back when Bandwidth AP supports it (?); now:
393			#  "The ''messages'' resource property
394			#  ''receiptRequested'' is not supported for MMS"
395			#'receiptRequested'	=> 'all',
396			#'callbackUrl'		=> ARGV[6]
397		})
398		response = http.request(request)
399
400		puts 'mAPI response to send: ' + response.to_s + ' with code ' +
401			response.code + ', body "' + response.body + '"'
402
403		if response.code != '201'
404			# TODO: add text re unexpected code; mention code number
405			write_to_stream error_msg(i.reply, nil, :cancel,
406				'internal-server-error')
407			next
408		end
409
410		@partial_data[cn[0]['sid']] = ''
411
412		# received the complete file so now close the stream
413		msg = Blather::Stanza::Iq.new :set
414		msg.to = i.from
415		msg.from = i.to
416
417		j = Nokogiri::XML::Node.new 'jingle',msg.document
418		j['xmlns'] = 'urn:xmpp:jingle:1'
419		j['action'] = 'session-terminate'
420		j['sid'] = @jingle_sids[cn[0]['sid']]
421		msg.add_child(j)
422
423		r = Nokogiri::XML::Node.new 'reason',msg.document
424		s = Nokogiri::XML::Node.new 'success',msg.document
425		r.add_child(s)
426		j.add_child(r)
427
428		puts 'RESPONSE1: ' + msg.inspect
429		write_to_stream msg
430	end
431
432	iq '/iq/ns:query', :ns =>
433		'http://jabber.org/protocol/disco#items' do |i, xpath_result|
434
435		write_to_stream i.reply
436	end
437
438	iq '/iq/ns:query', :ns =>
439		'http://jabber.org/protocol/disco#info' do |i, xpath_result|
440
441		if i.to.to_s.include? '@'
442			# TODO: confirm the node URL is expected using below
443			#puts "XR[node]: #{xpath_result[0]['node']}"
444
445			msg = i.reply
446			msg.identities = user_cap_identities()
447			msg.features = user_cap_features()
448
449			puts 'RESPONSE7: ' + msg.inspect
450			write_to_stream msg
451			next
452		end
453
454		msg = i.reply
455		msg.identities = [{:name =>
456			'Soprani.ca Gateway to XMPP - Catapult',
457			:type => 'sms-ctplt', :category => 'gateway'}]
458		msg.features = ["jabber:iq:register",
459			"jabber:iq:gateway", "jabber:iq:private",
460			"http://jabber.org/protocol/disco#info",
461			"http://jabber.org/protocol/commands",
462			"http://jabber.org/protocol/muc"]
463		write_to_stream msg
464	end
465
466	iq '/iq/ns:query', :ns => 'jabber:iq:register' do |i, qn|
467		puts "IQ: #{i.inspect}"
468
469		if i.type == :set
470			xn = qn.children.find { |v| v.element_name == "x" }
471
472			user_id = ''
473			api_token = ''
474			api_secret = ''
475			phone_num = ''
476
477			if xn.nil?
478				user_id = qn.children.find {
479					|v| v.element_name == "nick" }
480				api_token = qn.children.find {
481					|v| v.element_name == "username" }
482				api_secret = qn.children.find {
483					|v| v.element_name == "password" }
484				phone_num = qn.children.find {
485					|v| v.element_name == "phone" }
486			else
487				for field in xn.children
488					if field.element_name == "field"
489						val = field.children.find { |v|
490						v.element_name == "value" }
491
492						case field['var']
493						when 'nick'
494							user_id = val.text
495						when 'username'
496							api_token = val.text
497						when 'password'
498							api_secret = val.text
499						when 'phone'
500							phone_num = val.text
501						else
502							# TODO: error
503							puts "?: " +field['var']
504						end
505					end
506				end
507			end
508
509			if phone_num[0] != '+'
510				# TODO: add text re number not (yet) supported
511				write_to_stream error_msg(i.reply, qn, :cancel,
512					'item-not-found')
513				next
514			end
515
516			uri = URI.parse('https://api.catapult.inetwork.com')
517			http = Net::HTTP.new(uri.host, uri.port)
518			http.use_ssl = true
519			request = Net::HTTP::Get.new('/v1/users/' + user_id +
520				'/phoneNumbers/' + phone_num)
521			request.basic_auth api_token, api_secret
522			response = http.request(request)
523
524			puts 'API response: ' + response.to_s + ' with code ' +
525				response.code + ', body "' + response.body + '"'
526
527			if response.code == '200'
528				params = JSON.parse response.body
529				if params['numberState'] == 'enabled'
530					jid_key = "catapult_jid-" + phone_num
531
532					bare_jid = i.from.to_s.split('/', 2)[0]
533					cred_key = "catapult_cred-" + bare_jid
534
535					# TODO: pre-validate ARGV[5] is integer
536					conn = Hiredis::Connection.new
537					conn.connect(ARGV[4], ARGV[5].to_i)
538
539					# TODO: use SETNX instead
540					conn.write ["EXISTS", jid_key]
541					if conn.read == 1
542						conn.disconnect
543
544						# TODO: add txt re num exists
545						write_to_stream error_msg(
546							i.reply, qn, :cancel,
547							'conflict')
548						next
549					end
550
551					conn.write ["EXISTS", cred_key]
552					if conn.read == 1
553						conn.disconnect
554
555						# TODO: add txt re already exist
556						write_to_stream error_msg(
557							i.reply, qn, :cancel,
558							'conflict')
559						next
560					end
561
562					conn.write ["SET", jid_key, bare_jid]
563					if conn.read != 1
564						conn.disconnect
565
566						# TODO: catch/relay RuntimeError
567						# TODO: add txt re push failure
568						write_to_stream error_msg(
569							i.reply, qn, :cancel,
570							'internal-server-error')
571						next
572					end
573
574					conn.write ["RPUSH",cred_key,user_id]
575					conn.write ["RPUSH",cred_key,api_token]
576					conn.write ["RPUSH",cred_key,api_secret]
577					conn.write ["RPUSH",cred_key,phone_num]
578
579					# TODO: confirm cred_key list size == 4
580
581					for n in 1..4 do
582						# TODO: catch/relay RuntimeError
583						result = conn.read
584						if result != n
585							conn.disconnect
586
587							write_to_stream(
588							error_msg(
589							i.reply, qn, :cancel,
590							'internal-server-error')
591							)
592							next
593						end
594					end
595					conn.disconnect
596
597					write_to_stream i.reply
598				else
599					# TODO: add text re number disabled
600					write_to_stream error_msg(i.reply, qn,
601						:modify, 'not-acceptable')
602				end
603			elsif response.code == '401'
604				# TODO: add text re bad credentials
605				write_to_stream error_msg(i.reply, qn, :auth,
606					'not-authorized')
607			elsif response.code == '404'
608				# TODO: add text re number not found or disabled
609				write_to_stream error_msg(i.reply, qn, :cancel,
610					'item-not-found')
611			else
612				# TODO: add text re misc error, and mention code
613				write_to_stream error_msg(i.reply, qn, :modify,
614					'not-acceptable')
615			end
616
617		elsif i.type == :get
618			orig = i.reply
619
620			msg = Nokogiri::XML::Node.new 'query',orig.document
621			msg['xmlns'] = 'jabber:iq:register'
622			n1 = Nokogiri::XML::Node.new 'instructions',msg.document
623			n1.content= "Enter the information from your Account " +
624				"page as well as the Phone Number\nin your " +
625				"account you want to use (ie. '+12345678901')" +
626				".\nUser Id is nick, API Token is username, " +
627				"API Secret is password, Phone Number is phone"+
628				".\n\nThe source code for this gateway is at " +
629				"https://github.com/ossguy/sgx-catapult ." +
630				"\nCopyright (C) 2017  Denver Gingerich and " +
631				"others, licensed under AGPLv3+."
632			n2 = Nokogiri::XML::Node.new 'nick',msg.document
633			n3 = Nokogiri::XML::Node.new 'username',msg.document
634			n4 = Nokogiri::XML::Node.new 'password',msg.document
635			n5 = Nokogiri::XML::Node.new 'phone',msg.document
636			msg.add_child(n1)
637			msg.add_child(n2)
638			msg.add_child(n3)
639			msg.add_child(n4)
640			msg.add_child(n5)
641
642			x = Blather::Stanza::X.new :form, [
643				{:required => true, :type => :"text-single",
644				:label => 'User Id', :var => 'nick'},
645				{:required => true, :type => :"text-single",
646				:label => 'API Token', :var => 'username'},
647				{:required => true, :type => :"text-private",
648				:label => 'API Secret', :var => 'password'},
649				{:required => true, :type => :"text-single",
650				:label => 'Phone Number', :var => 'phone'}
651			]
652			x.title= 'Register for ' +
653				'Soprani.ca Gateway to XMPP - Catapult'
654			x.instructions= "Enter the details from your Account " +
655				"page as well as the Phone Number\nin your " +
656				"account you want to use (ie. '+12345678901')" +
657				".\n\nThe source code for this gateway is at " +
658				"https://github.com/ossguy/sgx-catapult ." +
659				"\nCopyright (C) 2017  Denver Gingerich and " +
660				"others, licensed under AGPLv3+."
661			msg.add_child(x)
662
663			orig.add_child(msg)
664			puts "RESPONSE2: #{orig.inspect}"
665			write_to_stream orig
666			puts "SENT"
667		end
668	end
669
670	subscription(:request?) do |s|
671		# TODO: are these the best to return?  really need '!' here?
672		#write_to_stream s.approve!
673		#write_to_stream s.request!
674	end
675end
676
677[:INT, :TERM].each do |sig|
678	trap(sig) {
679		puts 'Shutting down gateway...'
680		SGXcatapult.shutdown
681		puts 'Gateway has terminated.'
682
683		EM.stop
684	}
685end
686
687class ReceiptMessage < Blather::Stanza
688	def self.new(to = nil)
689		node = super :message
690		node.to = to
691		node
692	end
693end
694
695class WebhookHandler < Goliath::API
696	def send_media(from, to, media_url)
697		# we assume media_url is of the form (always the case so far):
698		#  https://api.catapult.inetwork.com/v1/users/[uid]/media/[file]
699
700		# the caller must guarantee that 'to' is a bare JID
701		proxy_url = ARGV[8] + to + '/' + media_url.split('/', 8)[7]
702
703		puts 'ORIG_URL: ' + media_url
704		puts 'PROX_URL: ' + proxy_url
705
706		# put URL in the body (so Conversations will still see it)...
707		msg = Blather::Stanza::Message.new(to, proxy_url)
708		msg.from = from
709
710		# ...but also provide URL in XEP-0066 (OOB) fashion
711		# TODO: confirm client supports OOB or don't send this
712		x = Nokogiri::XML::Node.new 'x', msg.document
713		x['xmlns'] = 'jabber:x:oob'
714
715		urln = Nokogiri::XML::Node.new 'url', msg.document
716		urlc = Nokogiri::XML::Text.new proxy_url, msg.document
717
718		urln.add_child(urlc)
719		x.add_child(urln)
720		msg.add_child(x)
721
722		SGXcatapult.write(msg)
723	end
724
725	def response(env)
726		puts 'ENV: ' + env.to_s
727		body = Rack::Request.new(env).body.read
728		puts 'BODY: ' + body
729		params = JSON.parse body
730
731		users_num = ''
732		others_num = ''
733		if params['direction'] == 'in'
734			users_num = params['to']
735			others_num = params['from']
736		elsif params['direction'] == 'out'
737			users_num = params['from']
738			others_num = params['to']
739		else
740			# TODO: exception or similar
741			puts "big problem: '" + params['direction'] + "'"
742			return [200, {}, "OK"]
743		end
744
745		jid_key = "catapult_jid-" + users_num
746
747		if others_num[0] != '+'
748			# TODO: check that others_num actually a shortcode first
749			others_num = others_num +
750				';phone-context=ca-us.phone-context.soprani.ca'
751		end
752
753		conn = Hiredis::Connection.new
754		conn.connect(ARGV[4], ARGV[5].to_i)
755
756		conn.write ["EXISTS", jid_key]
757		if conn.read == 0
758			conn.disconnect
759
760			puts "jid_key (#{jid_key}) DNE; Catapult misconfigured?"
761
762			# TODO: likely not appropriate; give error to Catapult?
763			# TODO: add text re credentials not being registered
764			#write_to_stream error_msg(m.reply, m.body, :auth,
765			#	'registration-required')
766			return [200, {}, "OK"]
767		end
768
769		conn.write ["GET", jid_key]
770		bare_jid = conn.read
771		conn.disconnect
772
773		msg = ''
774		case params['direction']
775		when 'in'
776			text = ''
777			case params['eventType']
778			when 'sms'
779				text = params['text']
780			when 'mms'
781				has_media = false
782				params['media'].each do |media_url|
783					if not media_url.end_with?('.smil',
784						'.txt', '.xml')
785
786						has_media = true
787						send_media(others_num + '@' +
788							ARGV[0],
789							bare_jid, media_url)
790					end
791				end
792
793				if params['text'].empty?
794					if not has_media
795						text = '[suspected group msg ' +
796							'with no text (odd)]'
797					end
798				else
799					if has_media
800						# TODO: write/use a caption XEP
801						text = params['text']
802					else
803						text = '[suspected group msg ' +
804							'(recipient list not ' +
805							'available) with ' +
806							'following text] ' +
807							params['text']
808					end
809				end
810
811				# ie. if text param non-empty or had no media
812				if not text.empty?
813					msg = Blather::Stanza::Message.new(
814						bare_jid, text)
815					msg.from = others_num + '@' + ARGV[0]
816					SGXcatapult.write(msg)
817				end
818
819				return [200, {}, "OK"]
820			else
821				text = "unknown type (#{params['eventType']})" +
822					" with text: " + params['text']
823
824				# TODO log/notify of this properly
825				puts text
826			end
827
828			msg = Blather::Stanza::Message.new(bare_jid, text)
829		else # per prior switch, this is:  params['direction'] == 'out'
830			msg = ReceiptMessage.new(bare_jid)
831
832			# TODO: put in member/instance variable
833			uuid_gen = UUID.new
834			msg['id'] = uuid_gen.generate
835
836			case params['deliveryState']
837			when 'not-delivered'
838				# TODO: add text re deliveryDescription reason
839				msg = SGXcatapult.error_msg(msg, nil, :cancel,
840					'service-unavailable')
841				return [200, {}, "OK"]
842			when 'delivered'
843				# TODO: send only when requested per XEP-0184
844				rcvd = Nokogiri::XML::Node.new 'received',
845					msg.document
846				rcvd['xmlns'] = 'urn:xmpp:receipts'
847				rcvd['id'] = params['tag']
848				msg.add_child(rcvd)
849			when 'waiting'
850				# can't really do anything with it; nice to know
851				puts "message with id #{params['tag']} waiting"
852				return [200, {}, "OK"]
853			else
854				# TODO: notify somehow of unknown state receivd?
855				puts "message with id #{params['tag']} has " +
856					"other state #{params['deliveryState']}"
857				return [200, {}, "OK"]
858			end
859
860			puts "RESPONSE4: #{msg.inspect}"
861		end
862
863		msg.from = others_num + '@' + ARGV[0]
864		SGXcatapult.write(msg)
865
866		[200, {}, "OK"]
867	end
868end
869
870EM.run do
871	SGXcatapult.run
872
873	server = Goliath::Server.new('0.0.0.0', ARGV[7].to_i)
874	server.api = WebhookHandler.new
875	server.app = Goliath::Rack::Builder.build(server.api.class, server.api)
876	server.logger = Log4r::Logger.new('goliath')
877	server.logger.add(Log4r::StdoutOutputter.new('console'))
878	server.logger.level = Log4r::INFO
879	server.start
880end