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