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