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