#!/usr/bin/env ruby
# frozen_string_literal: true

# Copyright (C) 2017-2020  Denver Gingerich <denver@ossguy.com>
# Copyright (C) 2017  Stephen Paul Weber <singpolyma@singpolyma.net>
#
# This file is part of sgx-bwmsgsv2.
#
# sgx-bwmsgsv2 is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# sgx-bwmsgsv2 is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with sgx-bwmsgsv2.  If not, see <http://www.gnu.org/licenses/>.

require 'blather/client/dsl'
require 'em-hiredis'
require 'em-http-request'
require 'json'
require 'securerandom'
require 'time'
require 'uri'
require 'webrick'

require 'goliath/api'
require 'goliath/server'
require 'log4r'

require 'em_promise'

require_relative 'lib/registration_repo'

def panic(e)
	puts "Shutting down gateway due to exception: #{e.message}"
	puts e.backtrace
	SGXbwmsgsv2.shutdown
	puts 'Gateway has terminated.'
	EM.stop
end

def extract_shortcode(dest)
	num, context = dest.split(';', 2)
	num if context == 'phone-context=ca-us.phone-context.soprani.ca'
end

def anonymous_tel?(dest)
	dest.split(';', 2)[1] == 'phone-context=anonymous.phone-context.soprani.ca'
end

class SGXClient < Blather::Client
	def register_handler(type, *guards, &block)
		super(type, *guards) { |*args| wrap_handler(*args, &block) }
	end

	def register_handler_before(type, *guards, &block)
		check_handler(type, guards)
		handler = lambda { |*args| wrap_handler(*args, &block) }

		@handlers[type] ||= []
		@handlers[type].unshift([guards, handler])
	end

protected

	def wrap_handler(*args)
		v = yield(*args)
		v.catch(&method(:panic)) if v.is_a?(Promise)
		true # Do not run other handlers unless throw :pass
	rescue Exception => e
		panic(e)
	end
end

# TODO: keep in sync with jmp-acct_bot.rb, and eventually put in common location
module CatapultSettingFlagBits
	VOICEMAIL_TRANSCRIPTION_DISABLED = 0
	MMS_ON_OOB_URL = 1
end

module SGXbwmsgsv2
	extend Blather::DSL

	@registration_repo = RegistrationRepo.new
	@client = SGXClient.new
	@gateway_features = [
		"http://jabber.org/protocol/disco#info",
		"http://jabber.org/protocol/address/",
		"jabber:iq:register"
	]

	def self.run
		# TODO: read/save ARGV[7] creds to local variables
		client.run
	end

	# so classes outside this module can write messages, too
	def self.write(stanza)
		client.write(stanza)
	end

	def self.before_handler(type, *guards, &block)
		client.register_handler_before(type, *guards, &block)
	end

	def self.send_media(from, to, media_url, desc=nil, subject=nil, m=nil)
		# we assume media_url is one of these (always the case so far):
		#  https://api.catapult.inetwork.com/v1/users/[uid]/media/[file]
		#  https://messaging.bandwidth.com/api/v2/users/[u]/media/[path]

		usr = to
		pth = ''
		if media_url.start_with?(
			'https://api.catapult.inetwork.com/v1/users/')

			# TODO: MUST fix this TERRIBLE hack
			# there must be a catapult_cred-<usr> key with V1 creds
			usr = 'v1'

			pth = media_url.split('/', 8)[7]

		elsif media_url.start_with?(
			'https://messaging.bandwidth.com/api/v2/users/')

			pth = media_url.split('/', 9)[8]
		else
			puts "ERROR2: unrecognized media_url: '#{media_url}'"
			return
		end

		# the caller must guarantee that 'to' is a bare JID
		proxy_url = ARGV[6] + WEBrick::HTTPUtils.escape(usr) + '/' + pth

		puts 'ORIG_URL: ' + media_url
		puts 'PROX_URL: ' + proxy_url

		# put URL in the body (so Conversations will still see it)...
		msg = Blather::Stanza::Message.new(to, proxy_url)
		if m
			msg = m.copy
			msg.body = proxy_url
		end
		msg.from = from
		msg.subject = subject if subject

		# ...but also provide URL in XEP-0066 (OOB) fashion
		# TODO: confirm client supports OOB or don't send this
		x = Nokogiri::XML::Node.new 'x', msg.document
		x['xmlns'] = 'jabber:x:oob'

		urln = Nokogiri::XML::Node.new 'url', msg.document
		urlc = Nokogiri::XML::Text.new proxy_url, msg.document
		urln.add_child(urlc)
		x.add_child(urln)

		if desc
			descn = Nokogiri::XML::Node.new('desc', msg.document)
			descc = Nokogiri::XML::Text.new(desc, msg.document)
			descn.add_child(descc)
			x.add_child(descn)
		end

		msg.add_child(x)

		write(msg)
	rescue Exception => e
		panic(e)
	end

	def self.error_msg(orig, query_node, type, name, _text=nil)
		orig.type = :error

		error = Nokogiri::XML::Node.new 'error', orig.document
		error['type'] = type
		orig.add_child(error)

		suberr = Nokogiri::XML::Node.new name, orig.document
		suberr['xmlns'] = 'urn:ietf:params:xml:ns:xmpp-stanzas'
		error.add_child(suberr)

		orig.add_child(query_node) if query_node

		# TODO: add some explanatory xml:lang='en' text (see text param)
		puts "RESPONSE3: #{orig.inspect}"
		return orig
	end

	# workqueue_count MUST be 0 or else Blather uses threads!
	setup ARGV[0], ARGV[1], ARGV[2], ARGV[3], nil, nil, workqueue_count: 0

	def self.pass_on_message(m, users_num, jid)
		# setup delivery receipt; similar to a reply
		rcpt = ReceiptMessage.new(m.from.stripped)
		rcpt.from = m.to

		# pass original message (before sending receipt)
		m.to = jid
		m.from = "#{users_num}@#{ARGV[0]}"

		puts 'XRESPONSE0: ' + m.inspect
		write_to_stream m

		# send a delivery receipt back to the sender
		# TODO: send only when requested per XEP-0184
		# TODO: pass receipts from target if supported

		# TODO: put in member/instance variable
		rcpt['id'] = SecureRandom.uuid
		rcvd = Nokogiri::XML::Node.new 'received', rcpt.document
		rcvd['xmlns'] = 'urn:xmpp:receipts'
		rcvd['id'] = m.id
		rcpt.add_child(rcvd)

		puts 'XRESPONSE1: ' + rcpt.inspect
		write_to_stream rcpt
	end

	def self.call_catapult(
		token, secret, m, pth, body=nil,
		head={}, code=[200], respond_with=:body
	)
		# pth looks like one of:
		#  "api/v2/users/#{user_id}/[endpoint_name]"
		#  "v1/users/#{user_id}/[endpoint_name]"

		url_prefix = ''

		# TODO: need to make a separate thing for voice.bw.c eventually
		if pth.start_with? 'api/v2/users'
			url_prefix = 'https://messaging.bandwidth.com/'
		elsif pth.start_with? 'v1/users'
			# begin hack for running V2 messages along with V1 voice
			url_prefix = 'https://api.catapult.inetwork.com/'
			# TODO: set token and secret to vals provided at startup
			# TODO: replace pth's user_id with user_id from ARGV[7]
			#  -> pth = "v1/users/#{new_id}/" + pth.split('/', 4)[3]
			# TODO: else error
		end

		EM::HttpRequest.new(
			url_prefix + pth
		).public_send(
			m,
			head: {
				'Authorization' => [token, secret]
			}.merge(head),
			body: body
		).then { |http|
			puts "API response to send: #{http.response} with code"\
				" response.code #{http.response_header.status}"

			if code.include?(http.response_header.status)
				case respond_with
				when :body
					http.response
				when :headers
					http.response_header
				else
					http
				end
			else
				EMPromise.reject(http.response_header.status)
			end
		}
	end

	def self.to_catapult_possible_oob(s, num_dest, user_id, token, secret,
		usern)
		un = s.at("oob|x > oob|url", oob: "jabber:x:oob")
		unless un
			puts "MMSOOB: no url node found so process as normal"
			return to_catapult(s, nil, num_dest, user_id, token,
				secret, usern)
		end
		puts "MMSOOB: found a url node - checking if to make MMS..."

		# TODO: check size of file at un.text and shrink if need

		body = s.respond_to?(:body) ? s.body : ''
		# some clients send URI in both body & <url/> so delete
		s.body = body.sub(/\s*#{Regexp.escape(un.text)}\s*$/, '')

		puts "MMSOOB: url text is '#{un.text}'"
		puts "MMSOOB: the body is '#{body.to_s.strip}'"

		puts "MMSOOB: sending MMS since found OOB & user asked"
		to_catapult(s, un.text, num_dest, user_id, token, secret, usern)
	end

	def self.to_catapult(s, murl, num_dest, user_id, token, secret, usern)
		body = s.respond_to?(:body) ? s.body : ''
		if murl.to_s.empty? && body.to_s.strip.empty?
			return EMPromise.reject(
				[:modify, 'policy-violation']
			)
		end

		extra = {}
		extra[:media] = murl if murl

		call_catapult(
			token,
			secret,
			:post,
			"api/v2/users/#{user_id}/messages",
			JSON.dump(extra.merge(
				from: usern,
				to:   num_dest,
				text: body,
				applicationId:  ARGV[4],
				tag:
					# callbacks need id and resourcepart
					WEBrick::HTTPUtils.escape(s.id.to_s) +
					' ' +
					WEBrick::HTTPUtils.escape(
						s.from.resource.to_s
					)
			)),
			{'Content-Type' => 'application/json'},
			[201]
		).catch {
			# TODO: add text; mention code number
			EMPromise.reject(
				[:cancel, 'internal-server-error']
			)
		}
	end

	def self.validate_num(m)
		# if sent to SGX domain use https://wiki.soprani.ca/SGX/GroupMMS
		if m.to == ARGV[0]
			an = m.children.find { |v| v.element_name == "addresses" }
			if not an
				return EMPromise.reject(
					[:cancel, 'item-not-found']
				)
			end
			puts "ADRXEP: found an addresses node - iterate addrs.."

			nums = []
			an.children.each do |e|
				num = ''
				type = ''
				e.attributes.each do |c|
					if c[0] == 'type'
						if c[1] != 'to'
							# TODO: error
						end
						type = c[1].to_s
					elsif c[0] == 'uri'
						if !c[1].to_s.start_with? 'sms:'
							# TODO: error
						end
						num = c[1].to_s[4..-1]
						# TODO: confirm num validates
						# TODO: else, error - unexpected name
					end
				end
				if num.empty? or type.empty?
					# TODO: error
				end
				nums << num
			end
			return nums
		end

		# if not sent to SGX domain, then assume destination is in 'to'
		EMPromise.resolve(m.to.node.to_s).then { |num_dest|
			if num_dest =~ /\A\+?[0-9]+(?:;.*)?\Z/
				next num_dest if num_dest[0] == '+'

				shortcode = extract_shortcode(num_dest)
				next shortcode if shortcode
			end

			if anonymous_tel?(num_dest)
				EMPromise.reject([:cancel, 'gone'])
			else
				# TODO: text re num not (yet) supportd/implmentd
				EMPromise.reject([:cancel, 'item-not-found'])
			end
		}
	end

	def self.fetch_catapult_cred_for(jid)
		@registration_repo.find(jid).then { |creds|
			if creds.length < 4
				# TODO: add text re credentials not registered
				EMPromise.reject(
					[:auth, 'registration-required']
				)
			else
				creds
			end
		}
	end

	message :error? do |m|
		# TODO: report it somewhere/somehow - eat for now so no err loop
		puts "EATERROR1: #{m.inspect}"
	end

	message :body do |m|
		EMPromise.all([
			validate_num(m),
			fetch_catapult_cred_for(m.from)
		]).then { |(num_dest, creds)|
			@registration_repo.find_jid(num_dest).then { |jid|
				[jid, num_dest] + creds
			}
		}.then { |(jid, num_dest, *creds)|
			if jid
				@registration_repo.find(jid).then { |other_user|
					[jid, num_dest] + creds + other_user.first
				}
			else
				[jid, num_dest] + creds + [nil]
			end
		}.then { |(jid, num_dest, *creds, other_user)|
			# if destination user is in the system pass on directly
			if other_user and not other_user.start_with? 'u-'
				pass_on_message(m, creds.last, jid)
			else
				to_catapult_possible_oob(m, num_dest, *creds)
			end
		}.catch { |e|
			if e.is_a?(Array) && e.length == 2
				write_to_stream error_msg(m.reply, m.body, *e)
			else
				EMPromise.reject(e)
			end
		}
	end

	def self.user_cap_identities
		[{category: 'client', type: 'sms'}]
	end

	# TODO: must re-add stuff so can do ad-hoc commands
	def self.user_cap_features
		["urn:xmpp:receipts"]
	end

	def self.add_gateway_feature(feature)
		@gateway_features << feature
		@gateway_features.uniq!
	end

	subscription :request? do |p|
		puts "PRESENCE1: #{p.inspect}"

		# subscriptions are allowed from anyone - send reply immediately
		msg = Blather::Stanza::Presence.new
		msg.to = p.from
		msg.from = p.to
		msg.type = :subscribed

		puts 'RESPONSE5a: ' + msg.inspect
		write_to_stream msg

		# send a <presence> immediately; not automatically probed for it
		# TODO: refactor so no "presence :probe? do |p|" duplicate below
		caps = Blather::Stanza::Capabilities.new
		# TODO: user a better node URI (?)
		caps.node = 'http://catapult.sgx.soprani.ca/'
		caps.identities = user_cap_identities
		caps.features = user_cap_features

		msg = caps.c
		msg.to = p.from
		msg.from = p.to.to_s + '/sgx'

		puts 'RESPONSE5b: ' + msg.inspect
		write_to_stream msg

		# need to subscribe back so Conversations displays images inline
		msg = Blather::Stanza::Presence.new
		msg.to = p.from.to_s.split('/', 2)[0]
		msg.from = p.to.to_s.split('/', 2)[0]
		msg.type = :subscribe

		puts 'RESPONSE5c: ' + msg.inspect
		write_to_stream msg
	end

	presence :probe? do |p|
		puts 'PRESENCE2: ' + p.inspect

		caps = Blather::Stanza::Capabilities.new
		# TODO: user a better node URI (?)
		caps.node = 'http://catapult.sgx.soprani.ca/'
		caps.identities = user_cap_identities
		caps.features = user_cap_features

		msg = caps.c
		msg.to = p.from
		msg.from = p.to.to_s + '/sgx'

		puts 'RESPONSE6: ' + msg.inspect
		write_to_stream msg
	end

	iq '/iq/ns:query', ns:	'http://jabber.org/protocol/disco#info' do |i|
		# TODO: return error if i.type is :set - if it is :reply or
		#  :error it should be ignored (as the below does currently);
		#  review specification to see how to handle other type values
		if i.type != :get
			puts 'DISCO iq rcvd, of non-get type "' + i.type.to_s +
				'" for message "' + i.inspect + '"; ignoring...'
			next
		end

		# respond to capabilities request for an sgx-bwmsgsv2 number JID
		if i.to.node
			# TODO: confirm the node URL is expected using below
			#puts "XR[node]: #{xpath_result[0]['node']}"

			msg = i.reply
			msg.node = i.node
			msg.identities = user_cap_identities
			msg.features = user_cap_features

			puts 'RESPONSE7: ' + msg.inspect
			write_to_stream msg
			next
		end

		# respond to capabilities request for sgx-bwmsgsv2 itself
		msg = i.reply
		msg.node = i.node
		msg.identities = [{
			name: 'Soprani.ca Gateway to XMPP - Bandwidth API V2',
			type: 'sms', category: 'gateway'
		}]
		msg.features = @gateway_features
		write_to_stream msg
	end

	def self.check_then_register(i, *creds)
		registration_repo
			.put(i.from, *creds)
			.catch_only(RegistrationRepo::Conflict) { |e|
				EMPromise.reject([:cancel, 'conflict', e.message])
			}.then {
				write_to_stream i.reply
			}
	end

	def self.creds_from_registration_query(qn)
		xn = qn.children.find { |v| v.element_name == "x" }

		if xn
			xn.children.each_with_object({}) do |field, h|
				next if field.element_name != "field"

				val = field.children.find { |v|
					v.element_name == "value"
				}

				case field['var']
				when 'nick'
					h[:user_id] = val.text
				when 'username'
					h[:api_token] = val.text
				when 'password'
					h[:api_secret] = val.text
				when 'phone'
					h[:phone_num] = val.text
				else
					# TODO: error
					puts "?: #{field['var']}"
				end
			end
		else
			qn.children.each_with_object({}) do |field, h|
				case field.element_name
				when "nick"
					h[:user_id] = field.text
				when "username"
					h[:api_token] = field.text
				when "password"
					h[:api_secret] = field.text
				when "phone"
					h[:phone_num] = field.text
				end
			end
		end.values_at(:user_id, :api_token, :api_secret, :phone_num)
	end

	def self.process_registration(i, qn)
		EMPromise.resolve(
			qn.children.find { |v| v.element_name == "remove" }
		).then { |rn|
			if rn
				@registration_repo.delete(i.from).then do
					EMPromise.reject(:done)
				end
			else
				creds_from_registration_query(qn)
			end
		}.then { |user_id, api_token, api_secret, phone_num|
			if phone_num[0] == '+'
				[user_id, api_token, api_secret, phone_num]
			else
				# TODO: add text re number not (yet) supported
				EMPromise.reject([:cancel, 'item-not-found'])
			end
		}.then { |user_id, api_token, api_secret, phone_num|
			# TODO: find way to verify #{phone_num}, too
			call_catapult(
				api_token,
				api_secret,
				:get,
				"api/v2/users/#{user_id}/media"
			).then { |response|
				JSON.parse(response)
				# TODO: confirm response is array - could be empty

				puts "register got str #{response.to_s[0..999]}"

				check_then_register(
					i,
					user_id,
					api_token,
					api_secret,
					phone_num
				)
			}
		}.catch { |e|
			EMPromise.reject(case e
			when 401
				# TODO: add text re bad credentials
				[:auth, 'not-authorized']
			when 404
				# TODO: add text re number not found or disabled
				[:cancel, 'item-not-found']
			when Integer
				[:modify, 'not-acceptable']
			else
				e
			end)
		}
	end

	def self.registration_form(orig, existing_number=nil)
		msg = Nokogiri::XML::Node.new 'query', orig.document
		msg['xmlns'] = 'jabber:iq:register'

		if existing_number
			msg.add_child(
				Nokogiri::XML::Node.new(
					'registered', msg.document
				)
			)
		end

		# TODO: update "User Id" x2 below (to "accountId"?), and others?
		n1 = Nokogiri::XML::Node.new(
			'instructions', msg.document
		)
		n1.content = "Enter the information from your Account "\
			"page as well as the Phone Number\nin your "\
			"account you want to use (ie. '+12345678901')"\
			".\nUser Id is nick, API Token is username, "\
			"API Secret is password, Phone Number is phone"\
			".\n\nThe source code for this gateway is at "\
			"https://gitlab.com/soprani.ca/sgx-bwmsgsv2 ."\
			"\nCopyright (C) 2017-2020  Denver Gingerich "\
			"and others, licensed under AGPLv3+."
		n2 = Nokogiri::XML::Node.new 'nick', msg.document
		n3 = Nokogiri::XML::Node.new 'username', msg.document
		n4 = Nokogiri::XML::Node.new 'password', msg.document
		n5 = Nokogiri::XML::Node.new 'phone', msg.document
		n5.content = existing_number.to_s
		msg.add_child(n1)
		msg.add_child(n2)
		msg.add_child(n3)
		msg.add_child(n4)
		msg.add_child(n5)

		x = Blather::Stanza::X.new :form, [
			{
				required: true, type: :"text-single",
				label: 'User Id', var: 'nick'
			},
			{
				required: true, type: :"text-single",
				label: 'API Token', var: 'username'
			},
			{
				required: true, type: :"text-private",
				label: 'API Secret', var: 'password'
			},
			{
				required: true, type: :"text-single",
				label: 'Phone Number', var: 'phone',
				value: existing_number.to_s
			}
		]
		x.title = 'Register for '\
			'Soprani.ca Gateway to XMPP - Bandwidth API V2'
		x.instructions = "Enter the details from your Account "\
			"page as well as the Phone Number\nin your "\
			"account you want to use (ie. '+12345678901')"\
			".\n\nThe source code for this gateway is at "\
			"https://gitlab.com/soprani.ca/sgx-bwmsgsv2 ."\
			"\nCopyright (C) 2017-2020  Denver Gingerich "\
			"and others, licensed under AGPLv3+."
		msg.add_child(x)

		orig.add_child(msg)

		return orig
	end

	iq '/iq/ns:query', ns: 'jabber:iq:register' do |i, qn|
		puts "IQ: #{i.inspect}"

		case i.type
		when :set
			process_registration(i, qn)
		when :get
			bare_jid = i.from.stripped
			@registration_repo.find(bare_jid).then { |creds|
				reply = registration_form(i.reply, creds.last)
				puts "RESPONSE2: #{reply.inspect}"
				write_to_stream reply
			}
		else
			# Unknown IQ, ignore for now
			EMPromise.reject(:done)
		end.catch { |e|
			if e.is_a?(Array) && e.length == 2
				write_to_stream error_msg(i.reply, qn, *e)
			elsif e != :done
				EMPromise.reject(e)
			end
		}.catch(&method(:panic))
	end

	iq type: [:get, :set] do |iq|
		write_to_stream(Blather::StanzaError.new(
			iq,
			'feature-not-implemented',
			:cancel
		))
	end
end

class ReceiptMessage < Blather::Stanza
	def self.new(to=nil)
		node = super :message
		node.to = to
		node
	end
end

class WebhookHandler < Goliath::API
	use Goliath::Rack::Params

	@registration_repo = RegistrationRepo.new

	def response(env)
		# TODO: add timestamp grab here, and MUST include ./tai version

		puts 'ENV: ' + env.reject { |k| k == 'params' }.to_s

		if params.empty?
			puts 'PARAMS empty!'
			return [200, {}, "OK"]
		end

		if env['REQUEST_URI'] != '/'
			puts 'BADREQUEST1: non-/ request "' +
				env['REQUEST_URI'] + '", method "' +
				env['REQUEST_METHOD'] + '"'
			return [200, {}, "OK"]
		end

		if env['REQUEST_METHOD'] != 'POST'
			puts 'BADREQUEST2: non-POST request; URI: "' +
				env['REQUEST_URI'] + '", method "' +
				env['REQUEST_METHOD'] + '"'
			return [200, {}, "OK"]
		end

		# TODO: process each message in list, not just first one
		jparams = params['_json'][0]['message']

		type = params['_json'][0]['type']

		users_num = ''
		others_num = ''
		if jparams['direction'] == 'in'
			users_num = jparams['owner']
			others_num = jparams['from']
		elsif jparams['direction'] == 'out'
			users_num = jparams['from']
			others_num = jparams['owner']
		else
			# TODO: exception or similar
			puts "big prob: '" + jparams['direction'] + "'" + body
			return [200, {}, "OK"]
		end

		puts 'BODY - messageId: ' + jparams['id'] +
			', eventType: ' + type +
			', time: ' + jparams['time'] +
			', direction: ' + jparams['direction'] +
			#', state: ' + jparams['state'] +
			', deliveryState: ' + (jparams['deliveryState'] ?
				jparams['deliveryState'] : 'NONE') +
			', errorCode: ' + (jparams['errorCode'] ?
				jparams['errorCode'] : 'NONE') +
			', description: ' + (jparams['description'] ?
				jparams['description'] : 'NONE') +
			', tag: ' + (jparams['tag'] ? jparams['tag'] : 'NONE') +
			', media: ' + (jparams['media'] ?
				jparams['media'].to_s : 'NONE')

		if others_num[0] != '+'
			# TODO: check that others_num actually a shortcode first
			others_num +=
				';phone-context=ca-us.phone-context.soprani.ca'
		end

		bare_jid = @registration_repo.find_jid(users_num).sync

		if !bare_jid
			puts "jid_key (#{jid_key}) DNE; BW API misconfigured?"

			# TODO: likely not appropriate; give error to BW API?
			# TODO: add text re credentials not being registered
			#write_to_stream error_msg(m.reply, m.body, :auth,
			#	'registration-required')
			return [200, {}, "OK"]
		end

		msg = nil
		case jparams['direction']
		when 'in'
			text = ''
			case type
			when 'sms'
				text = jparams['text']
			when 'mms'
				has_media = false

				if jparams['text'].empty?
					if not has_media
						text = '[suspected group msg '\
							'with no text (odd)]'
					end
				else
					text = if has_media
						# TODO: write/use a caption XEP
						jparams['text']
					else
						'[suspected group msg '\
						'(recipient list not '\
						'available) with '\
						'following text] ' +
						jparams['text']
					end
				end

				# ie. if text param non-empty or had no media
				if not text.empty?
					msg = Blather::Stanza::Message.new(
						bare_jid, text)
					msg.from = others_num + '@' + ARGV[0]
					SGXbwmsgsv2.write(msg)
				end

				return [200, {}, "OK"]
			when 'message-received'
				# TODO: handle group chat, and fix above
				text = jparams['text']

				if jparams['to'].length > 1
					msg = Blather::Stanza::Message.new(
						Blather::JID.new(bare_jid).domain,
						text
					)

					addrs = Nokogiri::XML::Node.new(
						'addresses', msg.document)
					addrs['xmlns'] = 'http://jabber.org/' \
						'protocol/address'

					addr1 = Nokogiri::XML::Node.new(
						'address', msg.document)
					addr1['type'] = 'to'
					addr1['jid'] = bare_jid
					addrs.add_child(addr1)

					jparams['to'].each do |receiver|
						if receiver == users_num
							# already there in addr1
							next
						end

						addrn = Nokogiri::XML::Node.new(
							'address', msg.document)
						addrn['type'] = 'to'
						addrn['uri'] = "sms:#{receiver}"
						addrn['delivered'] = 'true'
						addrs.add_child(addrn)
					end

					msg.add_child(addrs)

					# TODO: delete
					puts "RESPONSE9: #{msg.inspect}"
				end

				Array(jparams['media']).each do |media_url|
					unless media_url.end_with?(
						'.smil', '.txt', '.xml'
					)
						has_media = true
						SGXbwmsgsv2.send_media(
							others_num + '@' +
							ARGV[0],
							bare_jid, media_url,
							nil, nil, msg
						)
					end
				end
			else
				text = "unknown type (#{type})"\
					" with text: " + jparams['text']

				# TODO: log/notify of this properly
				puts text
			end

			if not msg
				msg = Blather::Stanza::Message.new(bare_jid, text)
			end
		else # per prior switch, this is:  jparams['direction'] == 'out'
			tag_parts = jparams['tag'].split(/ /, 2)
			id = WEBrick::HTTPUtils.unescape(tag_parts[0])
			resourcepart = WEBrick::HTTPUtils.unescape(tag_parts[1])

			# TODO: remove this hack
			if jparams['to'].length > 1
				puts "WARN! group no rcpt: #{users_num}"
				return [200, {}, "OK"]
			end

			case type
			when 'message-failed'
				# create a bare message like the one user sent
				msg = Blather::Stanza::Message.new(
					others_num + '@' + ARGV[0])
				msg.from = bare_jid + '/' + resourcepart
				msg['id'] = id

				# TODO: add 'errorCode' and/or 'description' val
				# create an error reply to the bare message
				msg = msg.as_error(
					'recipient-unavailable',
					:wait,
					jparams['description']
				)

				# TODO: make prettier: this should be done above
				others_num = params['_json'][0]['to']
			when 'message-delivered'

				msg = ReceiptMessage.new(bare_jid)

				# TODO: put in member/instance variable
				msg['id'] = SecureRandom.uuid

				# TODO: send only when requested per XEP-0184
				rcvd = Nokogiri::XML::Node.new(
					'received',
					msg.document
				)
				rcvd['xmlns'] = 'urn:xmpp:receipts'
				rcvd['id'] = id
				msg.add_child(rcvd)

				# TODO: make prettier: this should be done above
				others_num = params['_json'][0]['to']
			else
				# TODO: notify somehow of unknown state receivd?
				puts "message with id #{id} has "\
					"other type #{type}"
				return [200, {}, "OK"]
			end

			puts "RESPONSE4: #{msg.inspect}"
		end

		msg.from = others_num + '@' + ARGV[0]
		SGXbwmsgsv2.write(msg)

		[200, {}, "OK"]
	rescue Exception => e
		puts 'Shutting down gateway due to exception 013: ' + e.message
		SGXbwmsgsv2.shutdown
		puts 'Gateway has terminated.'
		EM.stop
	end
end

at_exit do
	$stdout.sync = true

	puts "Soprani.ca/SMS Gateway for XMPP - Bandwidth API V2\n"\
		"==>> last commit of this version is " + `git rev-parse HEAD` + "\n"

	if ARGV.size != 7 and ARGV.size != 8
		puts "Usage: sgx-bwmsgsv2.rb <component_jid> "\
			"<component_password> <server_hostname> "\
			"<server_port> <application_id> "\
			"<http_listen_port> <mms_proxy_prefix_url> [V1_creds_file]"
		exit 0
	end

	t = Time.now
	puts "LOG %d.%09d: starting...\n\n" % [t.to_i, t.nsec]

	EM.run do
		REDIS = EM::Hiredis.connect

		SGXbwmsgsv2.run

		# required when using Prosody otherwise disconnects on 6-hour inactivity
		EM.add_periodic_timer(3600) do
			msg = Blather::Stanza::Iq::Ping.new(:get, 'localhost')
			msg.from = ARGV[0]
			SGXbwmsgsv2.write(msg)
		end

		server = Goliath::Server.new('0.0.0.0', ARGV[5].to_i)
		server.api = WebhookHandler.new
		server.app = Goliath::Rack::Builder.build(server.api.class, server.api)
		server.logger = Log4r::Logger.new('goliath')
		server.logger.add(Log4r::StdoutOutputter.new('console'))
		server.logger.level = Log4r::INFO
		server.start do
			["INT", "TERM"].each do |sig|
				trap(sig) do
					EM.defer do
						puts 'Shutting down gateway...'
						SGXbwmsgsv2.shutdown

						puts 'Gateway has terminated.'
						EM.stop
					end
				end
			end
		end
	end
end
