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