1#!/usr/bin/env ruby
  2#
  3# Copyright (C) 2017-2020  Denver Gingerich <denver@ossguy.com>
  4# Copyright (C) 2017  Stephen Paul Weber <singpolyma@singpolyma.net>
  5#
  6# This file is part of sgx-bwmsgsv2.
  7#
  8# sgx-bwmsgsv2 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-bwmsgsv2 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-bwmsgsv2.  If not, see <http://www.gnu.org/licenses/>.
 20
 21require 'blather/client/dsl'
 22require 'em-hiredis'
 23require 'em-http-request'
 24require 'json'
 25require 'securerandom'
 26require 'time'
 27require 'uri'
 28require 'webrick'
 29
 30require 'goliath/api'
 31require 'goliath/server'
 32require 'log4r'
 33
 34require_relative 'em_promise'
 35
 36def panic(e)
 37	puts "Shutting down gateway due to exception: #{e.message}"
 38	puts e.backtrace
 39	SGXbwmsgsv2.shutdown
 40	puts 'Gateway has terminated.'
 41	EM.stop
 42end
 43
 44def extract_shortcode(dest)
 45	num, context = dest.split(';', 2)
 46	num if context && context == 'phone-context=ca-us.phone-context.soprani.ca'
 47end
 48
 49def is_anonymous_tel?(dest)
 50	num, context = dest.split(';', 2)
 51	context && context == 'phone-context=anonymous.phone-context.soprani.ca'
 52end
 53
 54class SGXClient < Blather::Client
 55	def register_handler(type, *guards, &block)
 56		super(type, *guards) { |*args| wrap_handler(*args, &block) }
 57	end
 58
 59	def register_handler_before(type, *guards, &block)
 60		check_handler(type, guards)
 61		handler = lambda { |*args| wrap_handler(*args, &block) }
 62
 63		@handlers[type] ||= []
 64		@handlers[type].unshift([guards, handler])
 65	end
 66
 67protected
 68
 69	def wrap_handler(*args, &block)
 70		v = block.call(*args)
 71		v.catch(&method(:panic)) if v.is_a?(Promise)
 72		true # Do not run other handlers unless throw :pass
 73	rescue Exception => e
 74		panic(e)
 75	end
 76end
 77
 78# TODO: keep in sync with jmp-acct_bot.rb, and eventually put in common location
 79module CatapultSettingFlagBits
 80	VOICEMAIL_TRANSCRIPTION_DISABLED = 0
 81	MMS_ON_OOB_URL = 1
 82end
 83
 84module SGXbwmsgsv2
 85	extend Blather::DSL
 86
 87	@client = SGXClient.new
 88	@gateway_features = [
 89		"http://jabber.org/protocol/disco#info",
 90		"jabber:iq:register"
 91	]
 92
 93	def self.run
 94		client.run
 95	end
 96
 97	# so classes outside this module can write messages, too
 98	def self.write(stanza)
 99		client.write(stanza)
100	end
101
102	def self.before_handler(type, *guards, &block)
103		client.register_handler_before(type, *guards, &block)
104	end
105
106	def self.send_media(from, to, media_url, desc=nil, subject=nil)
107		# we assume media_url is of the form (always the case so far):
108		#  https://api.catapult.inetwork.com/v1/users/[uid]/media/[file]
109
110		# the caller must guarantee that 'to' is a bare JID
111		proxy_url = ARGV[6] + WEBrick::HTTPUtils.escape(to) + '/' +
112			media_url.split('/', 8)[7]
113
114		puts 'ORIG_URL: ' + media_url
115		puts 'PROX_URL: ' + proxy_url
116
117		# put URL in the body (so Conversations will still see it)...
118		msg = Blather::Stanza::Message.new(to, proxy_url)
119		msg.from = from
120		msg.subject = subject if subject
121
122		# ...but also provide URL in XEP-0066 (OOB) fashion
123		# TODO: confirm client supports OOB or don't send this
124		x = Nokogiri::XML::Node.new 'x', msg.document
125		x['xmlns'] = 'jabber:x:oob'
126
127		urln = Nokogiri::XML::Node.new 'url', msg.document
128		urlc = Nokogiri::XML::Text.new proxy_url, msg.document
129		urln.add_child(urlc)
130		x.add_child(urln)
131
132		if desc
133			descn = Nokogiri::XML::Node.new('desc', msg.document)
134			descc = Nokogiri::XML::Text.new(desc, msg.document)
135			descn.add_child(descc)
136			x.add_child(descn)
137		end
138
139		msg.add_child(x)
140
141		write(msg)
142	rescue Exception => e
143		panic(e)
144	end
145
146	def self.error_msg(orig, query_node, type, name, text=nil)
147		orig.type = :error
148
149		error = Nokogiri::XML::Node.new 'error', orig.document
150		error['type'] = type
151		orig.add_child(error)
152
153		suberr = Nokogiri::XML::Node.new name, orig.document
154		suberr['xmlns'] = 'urn:ietf:params:xml:ns:xmpp-stanzas'
155		error.add_child(suberr)
156
157		orig.add_child(query_node) if query_node
158
159		# TODO: add some explanatory xml:lang='en' text (see text param)
160		puts "RESPONSE3: #{orig.inspect}"
161		return orig
162	end
163
164	# workqueue_count MUST be 0 or else Blather uses threads!
165	setup ARGV[0], ARGV[1], ARGV[2], ARGV[3], nil, nil, workqueue_count: 0
166
167	def self.pass_on_message(m, users_num, jid)
168		# setup delivery receipt; similar to a reply
169		rcpt = ReceiptMessage.new(m.from.stripped)
170		rcpt.from = m.to
171
172		# pass original message (before sending receipt)
173		m.to = jid
174		m.from = "#{users_num}@#{ARGV[0]}"
175
176		puts 'XRESPONSE0: ' + m.inspect
177		write_to_stream m
178
179		# send a delivery receipt back to the sender
180		# TODO: send only when requested per XEP-0184
181		# TODO: pass receipts from target if supported
182
183		# TODO: put in member/instance variable
184		rcpt['id'] = SecureRandom.uuid
185		rcvd = Nokogiri::XML::Node.new 'received', rcpt.document
186		rcvd['xmlns'] = 'urn:xmpp:receipts'
187		rcvd['id'] = m.id
188		rcpt.add_child(rcvd)
189
190		puts 'XRESPONSE1: ' + rcpt.inspect
191		write_to_stream rcpt
192	end
193
194	def self.call_catapult(
195		token, secret, m, pth, body=nil,
196		head={}, code=[200], respond_with=:body
197	)
198		# TODO: need to make a separate thing for voice.bw.c eventually
199		EM::HttpRequest.new(
200			"https://messaging.bandwidth.com/#{pth}"
201		).public_send(
202			m,
203			head: {
204				'Authorization' => [token, secret]
205			}.merge(head),
206			body: body
207		).then { |http|
208			puts "API response to send: #{http.response} with code"\
209				" response.code #{http.response_header.status}"
210
211			if code.include?(http.response_header.status)
212				case respond_with
213				when :body
214					http.response
215				when :headers
216					http.response_header
217				else
218					http
219				end
220			else
221				EMPromise.reject(http.response_header.status)
222			end
223		}
224	end
225
226	def self.to_catapult_possible_oob(s, num_dest, user_id, token, secret,
227		usern)
228
229		xn = s.children.find { |v| v.element_name == "x" }
230		if not xn
231			to_catapult(s, nil, num_dest, user_id, token, secret,
232				usern)
233			return
234		end
235		puts "MMSOOB: found an x node - checking for url node..."
236
237		# TODO: also check for xmlns='jabber:x:oob' in <x/> - the below
238		#  is probably fine, though, as non-OOB <x><url/></x> unlikely
239
240		un = xn.children.find { |v| v.element_name == "url" }
241		if not un
242			puts "MMSOOB: no url node found so process as normal"
243			to_catapult(s, nil, num_dest, user_id, token, secret,
244				usern)
245			return
246		end
247		puts "MMSOOB: found a url node - checking if to make MMS..."
248
249		REDIS.getbit("catapult_setting_flags-#{s.from.stripped}",
250			CatapultSettingFlagBits::MMS_ON_OOB_URL).then { |oob_on|
251
252			puts "MMSOOB: found MMS_ON_OOB_URL value is '#{oob_on}'"
253			if 0 == oob_on
254				puts "MMSOOB: MMS_ON_OOB_URL off so no MMS send"
255				to_catapult(s, nil, num_dest, user_id, token,
256					secret, usern)
257				next
258			end
259
260			# TODO: check size of file at un.text and shrink if need
261
262			body = s.respond_to?(:body) ? s.body : ''
263			puts "MMSOOB: url text is '#{un.text}'"
264			puts "MMSOOB: the body is '#{body.to_s.strip}'"
265
266			# some clients send URI in both body & <url/> so delete
267			if un.text == body.to_s.strip
268				puts "MMSOOB: url matches body so deleting body"
269				s.body = ''
270			end
271
272			puts "MMSOOB: sending MMS since found OOB & user asked"
273			to_catapult(s, un.text, num_dest, user_id, token,
274				secret, usern)
275		}
276	end
277
278	def self.to_catapult(s, murl, num_dest, user_id, token, secret, usern)
279		body = s.respond_to?(:body) ? s.body : ''
280		if murl.to_s.empty? && body.to_s.strip.empty?
281			return EMPromise.reject(
282				[:modify, 'policy-violation']
283			)
284		end
285
286		extra = {}
287		if murl
288			extra = { media: murl }
289		end
290
291		call_catapult(
292			token,
293			secret,
294			:post,
295			"api/v2/users/#{user_id}/messages",
296			JSON.dump(extra.merge(
297				from: usern,
298				to:   num_dest,
299				text: body,
300				applicationId:  ARGV[4],
301				tag:
302					# callbacks need id and resourcepart
303					WEBrick::HTTPUtils.escape(s.id.to_s) +
304					' ' +
305					WEBrick::HTTPUtils.escape(
306						s.from.resource.to_s
307					)
308			)),
309			{'Content-Type' => 'application/json'},
310			[201]
311		).catch {
312			# TODO: add text; mention code number
313			EMPromise.reject(
314				[:cancel, 'internal-server-error']
315			)
316		}
317
318		t = Time.now
319		tai_timestamp = `./tai`.strip
320		tai_yyyymmdd = Time.at(tai_timestamp.to_i).strftime('%Y%m%d')
321		puts "SMU %d.%09d, %s: msg for %s sent on %s - incrementing\n" %
322			[t.to_i, t.nsec, tai_timestamp, usern, tai_yyyymmdd]
323
324		REDIS.incr('usage_messages-' + tai_yyyymmdd + '-' +
325			usern).then { |total|
326
327			t = Time.now
328			puts "SMU %d.%09d: total msgs for %s-%s now at %s\n" %
329				[t.to_i, t.nsec, tai_yyyymmdd, usern, total]
330		}
331	end
332
333	def self.validate_num(num)
334		EMPromise.resolve(num.to_s).then { |num_dest|
335			if num_dest =~ /\A\+?[0-9]+(?:;.*)?\Z/
336				next num_dest if num_dest[0] == '+'
337				shortcode = extract_shortcode(num_dest)
338				next shortcode if shortcode
339			end
340
341			if is_anonymous_tel?(num_dest)
342				EMPromise.reject([:cancel, 'gone'])
343			else
344				# TODO: text re num not (yet) supportd/implmentd
345				EMPromise.reject([:cancel, 'item-not-found'])
346			end
347		}
348	end
349
350	def self.fetch_catapult_cred_for(jid)
351		cred_key = "catapult_cred-#{jid.stripped}"
352		REDIS.lrange(cred_key, 0, 3).then { |creds|
353			if creds.length < 4
354				# TODO: add text re credentials not registered
355				EMPromise.reject(
356					[:auth, 'registration-required']
357				)
358			else
359				creds
360			end
361		}
362	end
363
364	message :error? do |m|
365		# TODO: report it somewhere/somehow - eat for now so no err loop
366		puts "EATERROR1: #{m.inspect}"
367	end
368
369	message :body do |m|
370		EMPromise.all([
371			validate_num(m.to.node),
372			fetch_catapult_cred_for(m.from)
373		]).then { |(num_dest, creds)|
374			jid_key = "catapult_jid-#{num_dest}"
375			REDIS.get(jid_key).then { |jid|
376				[jid, num_dest] + creds
377			}
378		}.then { |(jid, num_dest, *creds)|
379			# if destination user is in the system pass on directly
380			if jid
381				pass_on_message(m, creds.last, jid)
382			else
383				to_catapult_possible_oob(m, num_dest, *creds)
384			end
385		}.catch { |e|
386			if e.is_a?(Array) && e.length == 2
387				write_to_stream error_msg(m.reply, m.body, *e)
388			else
389				EMPromise.reject(e)
390			end
391		}
392	end
393
394	def self.user_cap_identities
395		[{category: 'client', type: 'sms'}]
396	end
397
398	def self.user_cap_features
399		[
400			"urn:xmpp:receipts"
401		]
402	end
403
404	def self.add_gateway_feature(feature)
405		@gateway_features << feature
406		@gateway_features.uniq!
407	end
408
409	subscription :request? do |p|
410		puts "PRESENCE1: #{p.inspect}"
411
412		# subscriptions are allowed from anyone - send reply immediately
413		msg = Blather::Stanza::Presence.new
414		msg.to = p.from
415		msg.from = p.to
416		msg.type = :subscribed
417
418		puts 'RESPONSE5a: ' + msg.inspect
419		write_to_stream msg
420
421		# send a <presence> immediately; not automatically probed for it
422		# TODO: refactor so no "presence :probe? do |p|" duplicate below
423		caps = Blather::Stanza::Capabilities.new
424		# TODO: user a better node URI (?)
425		caps.node = 'http://catapult.sgx.soprani.ca/'
426		caps.identities = user_cap_identities
427		caps.features = user_cap_features
428
429		msg = caps.c
430		msg.to = p.from
431		msg.from = p.to.to_s + '/sgx'
432
433		puts 'RESPONSE5b: ' + msg.inspect
434		write_to_stream msg
435
436		# need to subscribe back so Conversations displays images inline
437		msg = Blather::Stanza::Presence.new
438		msg.to = p.from.to_s.split('/', 2)[0]
439		msg.from = p.to.to_s.split('/', 2)[0]
440		msg.type = :subscribe
441
442		puts 'RESPONSE5c: ' + msg.inspect
443		write_to_stream msg
444	end
445
446	presence :probe? do |p|
447		puts 'PRESENCE2: ' + p.inspect
448
449		caps = Blather::Stanza::Capabilities.new
450		# TODO: user a better node URI (?)
451		caps.node = 'http://catapult.sgx.soprani.ca/'
452		caps.identities = user_cap_identities
453		caps.features = user_cap_features
454
455		msg = caps.c
456		msg.to = p.from
457		msg.from = p.to.to_s + '/sgx'
458
459		puts 'RESPONSE6: ' + msg.inspect
460		write_to_stream msg
461	end
462
463	iq '/iq/ns:query', ns:	'http://jabber.org/protocol/disco#info' do |i|
464		# respond to capabilities request for an sgx-bwmsgsv2 number JID
465		if i.to.node
466			# TODO: confirm the node URL is expected using below
467			#puts "XR[node]: #{xpath_result[0]['node']}"
468
469			msg = i.reply
470			msg.node = i.node
471			msg.identities = user_cap_identities
472			msg.features = user_cap_features
473
474			puts 'RESPONSE7: ' + msg.inspect
475			write_to_stream msg
476			next
477		end
478
479		# respond to capabilities request for sgx-bwmsgsv2 itself
480		msg = i.reply
481		msg.node = i.node
482		msg.identities = [{
483			name: 'Soprani.ca Gateway to XMPP - Bandwidth API V2',
484			type: 'sms', category: 'gateway'
485		}]
486		msg.features = @gateway_features
487		write_to_stream msg
488	end
489
490	def self.check_then_register(i, *creds)
491		jid_key = "catapult_jid-#{creds.last}"
492		bare_jid = i.from.stripped
493		cred_key = "catapult_cred-#{bare_jid}"
494
495		REDIS.get(jid_key).then { |existing_jid|
496			if existing_jid && existing_jid != bare_jid
497				# TODO: add/log text: credentials exist already
498				EMPromise.reject([:cancel, 'conflict'])
499			end
500		}.then {
501			REDIS.lrange(cred_key, 0, 3)
502		}.then { |existing_creds|
503			# TODO: add/log text: credentials exist already
504			if existing_creds.length == 4 && creds != existing_creds
505				EMPromise.reject([:cancel, 'conflict'])
506			elsif existing_creds.length < 4
507				REDIS.rpush(cred_key, *creds).then { |length|
508					if length != 4
509						EMPromise.reject([
510							:cancel,
511							'internal-server-error'
512						])
513					end
514				}
515			end
516		}.then {
517			# not necessary if existing_jid non-nil, easier this way
518			REDIS.set(jid_key, bare_jid)
519		}.then { |result|
520			if result != 'OK'
521				# TODO: add txt re push failure
522				EMPromise.reject(
523					[:cancel, 'internal-server-error']
524				)
525			end
526		}.then {
527			write_to_stream i.reply
528		}
529	end
530
531	def self.creds_from_registration_query(qn)
532		xn = qn.children.find { |v| v.element_name == "x" }
533
534		if xn
535			xn.children.each_with_object({}) do |field, h|
536				next if field.element_name != "field"
537				val = field.children.find { |v|
538					v.element_name == "value"
539				}
540
541				case field['var']
542				when 'nick'
543					h[:user_id] = val.text
544				when 'username'
545					h[:api_token] = val.text
546				when 'password'
547					h[:api_secret] = val.text
548				when 'phone'
549					h[:phone_num] = val.text
550				else
551					# TODO: error
552					puts "?: #{field['var']}"
553				end
554			end
555		else
556			qn.children.each_with_object({}) do |field, h|
557				case field.element_name
558				when "nick"
559					h[:user_id] = field.text
560				when "username"
561					h[:api_token] = field.text
562				when "password"
563					h[:api_secret] = field.text
564				when "phone"
565					h[:phone_num] = field.text
566				end
567			end
568		end.values_at(:user_id, :api_token, :api_secret, :phone_num)
569	end
570
571	def self.process_registration(i, qn)
572		EMPromise.resolve(
573			qn.children.find { |v| v.element_name == "remove" }
574		).then { |rn|
575			if rn
576				puts "received <remove/> - ignoring for now..."
577				EMPromise.reject(:done)
578			else
579				creds_from_registration_query(qn)
580			end
581		}.then { |user_id, api_token, api_secret, phone_num|
582			if phone_num[0] == '+'
583				[user_id, api_token, api_secret, phone_num]
584			else
585				# TODO: add text re number not (yet) supported
586				EMPromise.reject([:cancel, 'item-not-found'])
587			end
588		}.then { |user_id, api_token, api_secret, phone_num|
589			# TODO: find way to verify #{phone_num}, too
590			call_catapult(
591				api_token,
592				api_secret,
593				:get,
594				"api/v2/users/#{user_id}/media"
595			).then { |response|
596				params = JSON.parse(response)
597				# TODO: confirm params is array - could be empty
598
599				puts "register got str #{response.to_s[0..999]}"
600
601				check_then_register(
602					i,
603					user_id,
604					api_token,
605					api_secret,
606					phone_num
607				)
608			}
609		}.catch { |e|
610			EMPromise.reject(case e
611			when 401
612				# TODO: add text re bad credentials
613				[:auth, 'not-authorized']
614			when 404
615				# TODO: add text re number not found or disabled
616				[:cancel, 'item-not-found']
617			when Integer
618				[:modify, 'not-acceptable']
619			else
620				e
621			end)
622		}
623	end
624
625	def self.registration_form(orig, existing_number=nil)
626		msg = Nokogiri::XML::Node.new 'query', orig.document
627		msg['xmlns'] = 'jabber:iq:register'
628
629		if existing_number
630			msg.add_child(
631				Nokogiri::XML::Node.new(
632					'registered', msg.document
633				)
634			)
635		end
636
637		# TODO: update "User Id" x2 below (to "accountId"?), and others?
638		n1 = Nokogiri::XML::Node.new(
639			'instructions', msg.document
640		)
641		n1.content = "Enter the information from your Account "\
642			"page as well as the Phone Number\nin your "\
643			"account you want to use (ie. '+12345678901')"\
644			".\nUser Id is nick, API Token is username, "\
645			"API Secret is password, Phone Number is phone"\
646			".\n\nThe source code for this gateway is at "\
647			"https://gitlab.com/soprani.ca/sgx-bwmsgsv2 ."\
648			"\nCopyright (C) 2017-2020  Denver Gingerich "\
649			"and others, licensed under AGPLv3+."
650		n2 = Nokogiri::XML::Node.new 'nick', msg.document
651		n3 = Nokogiri::XML::Node.new 'username', msg.document
652		n4 = Nokogiri::XML::Node.new 'password', msg.document
653		n5 = Nokogiri::XML::Node.new 'phone', msg.document
654		n5.content = existing_number.to_s
655		msg.add_child(n1)
656		msg.add_child(n2)
657		msg.add_child(n3)
658		msg.add_child(n4)
659		msg.add_child(n5)
660
661		x = Blather::Stanza::X.new :form, [
662			{
663				required: true, type: :"text-single",
664				label: 'User Id', var: 'nick'
665			},
666			{
667				required: true, type: :"text-single",
668				label: 'API Token', var: 'username'
669			},
670			{
671				required: true, type: :"text-private",
672				label: 'API Secret', var: 'password'
673			},
674			{
675				required: true, type: :"text-single",
676				label: 'Phone Number', var: 'phone',
677				value: existing_number.to_s
678			}
679		]
680		x.title = 'Register for '\
681			'Soprani.ca Gateway to XMPP - Bandwidth API V2'
682		x.instructions = "Enter the details from your Account "\
683			"page as well as the Phone Number\nin your "\
684			"account you want to use (ie. '+12345678901')"\
685			".\n\nThe source code for this gateway is at "\
686			"https://gitlab.com/soprani.ca/sgx-bwmsgsv2 ."\
687			"\nCopyright (C) 2017-2020  Denver Gingerich "\
688			"and others, licensed under AGPLv3+."
689		msg.add_child(x)
690
691		orig.add_child(msg)
692
693		return orig
694	end
695
696	iq '/iq/ns:query', ns: 'jabber:iq:register' do |i, qn|
697		puts "IQ: #{i.inspect}"
698
699		case i.type
700		when :set
701			process_registration(i, qn)
702		when :get
703			bare_jid = i.from.stripped
704			cred_key = "catapult_cred-#{bare_jid}"
705			REDIS.lindex(cred_key, 3).then { |existing_number|
706				reply = registration_form(i.reply, existing_number)
707				puts "RESPONSE2: #{reply.inspect}"
708				write_to_stream reply
709			}
710		else
711			# Unknown IQ, ignore for now
712			EMPromise.reject(:done)
713		end.catch { |e|
714			if e.is_a?(Array) && e.length == 2
715				write_to_stream error_msg(i.reply, qn, *e)
716			elsif e != :done
717				EMPromise.reject(e)
718			end
719		}.catch(&method(:panic))
720	end
721
722	iq :get? do |i|
723		write_to_stream error_msg(i.reply, i.children, 'cancel', 'feature-not-implemented')
724	end
725
726	iq :set? do |i|
727		write_to_stream error_msg(i.reply, i.children, 'cancel', 'feature-not-implemented')
728	end
729end
730
731class ReceiptMessage < Blather::Stanza
732	def self.new(to=nil)
733		node = super :message
734		node.to = to
735		node
736	end
737end
738
739class WebhookHandler < Goliath::API
740	use Goliath::Rack::Params
741
742	def response(env)
743		puts 'ENV: ' + env.reject{ |k| k == 'params' }.to_s
744
745		users_num = ''
746		others_num = ''
747		if params['direction'] == 'in'
748			users_num = params['to']
749			others_num = params['from']
750		elsif params['direction'] == 'out'
751			users_num = params['from']
752			others_num = params['to']
753		else
754			# TODO: exception or similar
755			puts "big problem: '" + params['direction'] + "'" + body
756			return [200, {}, "OK"]
757		end
758
759		puts 'BODY - messageId: ' + params['messageId'] +
760			', eventType: ' + params['eventType'] +
761			', time: ' + params['time'] +
762			', direction: ' + params['direction'] +
763			', state: ' + params['state'] +
764			', deliveryState: ' + (params['deliveryState'] ?
765				params['deliveryState'] : 'NONE') +
766			', deliveryCode: ' + (params['deliveryCode'] ?
767				params['deliveryCode'] : 'NONE') +
768			', deliveryDesc: ' + (params['deliveryDescription'] ?
769				params['deliveryDescription'] : 'NONE') +
770			', tag: ' + (params['tag'] ? params['tag'] : 'NONE') +
771			', media: ' + (params['media'] ? params['media'].to_s :
772				'NONE')
773
774		if others_num[0] != '+'
775			# TODO: check that others_num actually a shortcode first
776			others_num +=
777				';phone-context=ca-us.phone-context.soprani.ca'
778		end
779
780		jid_key = "catapult_jid-#{users_num}"
781		bare_jid = REDIS.get(jid_key).promise.sync
782
783		if !bare_jid
784			puts "jid_key (#{jid_key}) DNE; BW API misconfigured?"
785
786			# TODO: likely not appropriate; give error to BW API?
787			# TODO: add text re credentials not being registered
788			#write_to_stream error_msg(m.reply, m.body, :auth,
789			#	'registration-required')
790			return [200, {}, "OK"]
791		end
792
793		msg = ''
794		case params['direction']
795		when 'in'
796			text = ''
797			case params['eventType']
798			when 'sms'
799				text = params['text']
800			when 'mms'
801				has_media = false
802				params['media'].each do |media_url|
803					if not media_url.end_with?(
804						'.smil', '.txt', '.xml'
805					)
806
807						has_media = true
808						SGXbwmsgsv2.send_media(
809							others_num + '@' +
810							ARGV[0],
811							bare_jid, media_url
812						)
813					end
814				end
815
816				if params['text'].empty?
817					if not has_media
818						text = '[suspected group msg '\
819							'with no text (odd)]'
820					end
821				else
822					text = if has_media
823						# TODO: write/use a caption XEP
824						params['text']
825					else
826						'[suspected group msg '\
827						'(recipient list not '\
828						'available) with '\
829						'following text] ' +
830						params['text']
831					end
832				end
833
834				# ie. if text param non-empty or had no media
835				if not text.empty?
836					msg = Blather::Stanza::Message.new(
837						bare_jid, text)
838					msg.from = others_num + '@' + ARGV[0]
839					SGXbwmsgsv2.write(msg)
840				end
841
842				return [200, {}, "OK"]
843			else
844				text = "unknown type (#{params['eventType']})"\
845					" with text: " + params['text']
846
847				# TODO: log/notify of this properly
848				puts text
849			end
850
851			msg = Blather::Stanza::Message.new(bare_jid, text)
852		else # per prior switch, this is:  params['direction'] == 'out'
853			tag_parts = params['tag'].split(/ /, 2)
854			id = WEBrick::HTTPUtils.unescape(tag_parts[0])
855			resourcepart = WEBrick::HTTPUtils.unescape(tag_parts[1])
856
857			case params['deliveryState']
858			when 'not-delivered'
859				# create a bare message like the one user sent
860				msg = Blather::Stanza::Message.new(
861					others_num + '@' + ARGV[0])
862				msg.from = bare_jid + '/' + resourcepart
863				msg['id'] = id
864
865				# create an error reply to the bare message
866				msg = Blather::StanzaError.new(
867					msg,
868					'recipient-unavailable',
869					:wait
870				).to_node
871			when 'delivered'
872				msg = ReceiptMessage.new(bare_jid)
873
874				# TODO: put in member/instance variable
875				msg['id'] = SecureRandom.uuid
876
877				# TODO: send only when requested per XEP-0184
878				rcvd = Nokogiri::XML::Node.new(
879					'received',
880					msg.document
881				)
882				rcvd['xmlns'] = 'urn:xmpp:receipts'
883				rcvd['id'] = id
884				msg.add_child(rcvd)
885			when 'waiting'
886				# can't really do anything with it; nice to know
887				puts "message with id #{id} waiting"
888				return [200, {}, "OK"]
889			else
890				# TODO: notify somehow of unknown state receivd?
891				puts "message with id #{id} 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		SGXbwmsgsv2.write(msg)
901
902		[200, {}, "OK"]
903
904	rescue Exception => e
905		puts 'Shutting down gateway due to exception 013: ' + e.message
906		SGXbwmsgsv2.shutdown
907		puts 'Gateway has terminated.'
908		EM.stop
909	end
910end
911
912at_exit do
913	$stdout.sync = true
914
915	puts "Soprani.ca/SMS Gateway for XMPP - Bandwidth API V2\n"\
916		"==>> last commit of this version is " + `git rev-parse HEAD` + "\n"
917
918	if ARGV.size != 7
919		puts "Usage: sgx-bwmsgsv2.rb <component_jid> "\
920			"<component_password> <server_hostname> "\
921			"<server_port> <application_id> "\
922			"<http_listen_port> <mms_proxy_prefix_url>"
923		exit 0
924	end
925
926	t = Time.now
927	puts "LOG %d.%09d: starting...\n\n" % [t.to_i, t.nsec]
928
929	EM.run do
930		REDIS = EM::Hiredis.connect
931
932		SGXbwmsgsv2.run
933
934		# required when using Prosody otherwise disconnects on 6-hour inactivity
935		EM.add_periodic_timer(3600) do
936			msg = Blather::Stanza::Iq::Ping.new(:get, 'localhost')
937			msg.from = ARGV[0]
938			SGXbwmsgsv2.write(msg)
939		end
940
941		server = Goliath::Server.new('0.0.0.0', ARGV[5].to_i)
942		server.api = WebhookHandler.new
943		server.app = Goliath::Rack::Builder.build(server.api.class, server.api)
944		server.logger = Log4r::Logger.new('goliath')
945		server.logger.add(Log4r::StdoutOutputter.new('console'))
946		server.logger.level = Log4r::INFO
947		server.start do
948			["INT", "TERM"].each do |sig|
949				trap(sig) do
950					EM.defer do
951						puts 'Shutting down gateway...'
952						SGXbwmsgsv2.shutdown
953
954						puts 'Gateway has terminated.'
955						EM.stop
956					end
957				end
958			end
959		end
960	end
961end