sgx-bwmsgsv2.rb

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