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