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