sgx-bwmsgsv2.rb

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