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