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