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 write_to_stream i.reply
590 EMPromise.reject(:done)
591 end
592 else
593 creds_from_registration_query(qn)
594 end
595 }.then { |user_id, api_token, api_secret, phone_num|
596 if phone_num[0] == '+'
597 [user_id, api_token, api_secret, phone_num]
598 else
599 # TODO: add text re number not (yet) supported
600 EMPromise.reject([:cancel, 'item-not-found'])
601 end
602 }.then { |user_id, api_token, api_secret, phone_num|
603 # TODO: find way to verify #{phone_num}, too
604 call_catapult(
605 api_token,
606 api_secret,
607 :get,
608 "api/v2/users/#{user_id}/media"
609 ).then { |response|
610 JSON.parse(response)
611 # TODO: confirm response is array - could be empty
612
613 puts "register got str #{response.to_s[0..999]}"
614
615 check_then_register(
616 i,
617 user_id,
618 api_token,
619 api_secret,
620 phone_num
621 )
622 }
623 }.catch { |e|
624 EMPromise.reject(case e
625 when 401
626 # TODO: add text re bad credentials
627 [:auth, 'not-authorized']
628 when 404
629 # TODO: add text re number not found or disabled
630 [:cancel, 'item-not-found']
631 when Integer
632 [:modify, 'not-acceptable']
633 else
634 e
635 end)
636 }
637 end
638
639 def self.registration_form(orig, existing_number=nil)
640 msg = Nokogiri::XML::Node.new 'query', orig.document
641 msg['xmlns'] = 'jabber:iq:register'
642
643 if existing_number
644 msg.add_child(
645 Nokogiri::XML::Node.new(
646 'registered', msg.document
647 )
648 )
649 end
650
651 # TODO: update "User Id" x2 below (to "accountId"?), and others?
652 n1 = Nokogiri::XML::Node.new(
653 'instructions', msg.document
654 )
655 n1.content = "Enter the information from your Account "\
656 "page as well as the Phone Number\nin your "\
657 "account you want to use (ie. '+12345678901')"\
658 ".\nUser Id is nick, API Token is username, "\
659 "API Secret is password, Phone Number is phone"\
660 ".\n\nThe source code for this gateway is at "\
661 "https://gitlab.com/soprani.ca/sgx-bwmsgsv2 ."\
662 "\nCopyright (C) 2017-2020 Denver Gingerich "\
663 "and others, licensed under AGPLv3+."
664 n2 = Nokogiri::XML::Node.new 'nick', msg.document
665 n3 = Nokogiri::XML::Node.new 'username', msg.document
666 n4 = Nokogiri::XML::Node.new 'password', msg.document
667 n5 = Nokogiri::XML::Node.new 'phone', msg.document
668 n5.content = existing_number.to_s
669 msg.add_child(n1)
670 msg.add_child(n2)
671 msg.add_child(n3)
672 msg.add_child(n4)
673 msg.add_child(n5)
674
675 x = Blather::Stanza::X.new :form, [
676 {
677 required: true, type: :"text-single",
678 label: 'User Id', var: 'nick'
679 },
680 {
681 required: true, type: :"text-single",
682 label: 'API Token', var: 'username'
683 },
684 {
685 required: true, type: :"text-private",
686 label: 'API Secret', var: 'password'
687 },
688 {
689 required: true, type: :"text-single",
690 label: 'Phone Number', var: 'phone',
691 value: existing_number.to_s
692 }
693 ]
694 x.title = 'Register for '\
695 'Soprani.ca Gateway to XMPP - Bandwidth API V2'
696 x.instructions = "Enter the details from your Account "\
697 "page as well as the Phone Number\nin your "\
698 "account you want to use (ie. '+12345678901')"\
699 ".\n\nThe source code for this gateway is at "\
700 "https://gitlab.com/soprani.ca/sgx-bwmsgsv2 ."\
701 "\nCopyright (C) 2017-2020 Denver Gingerich "\
702 "and others, licensed under AGPLv3+."
703 msg.add_child(x)
704
705 orig.add_child(msg)
706
707 return orig
708 end
709
710 iq '/iq/ns:query', ns: 'jabber:iq:register' do |i, qn|
711 puts "IQ: #{i.inspect}"
712
713 case i.type
714 when :set
715 process_registration(i, qn)
716 when :get
717 bare_jid = i.from.stripped
718 @registration_repo.find(bare_jid).then { |creds|
719 reply = registration_form(i.reply, creds.last)
720 puts "RESPONSE2: #{reply.inspect}"
721 write_to_stream reply
722 }
723 else
724 # Unknown IQ, ignore for now
725 EMPromise.reject(:done)
726 end.catch { |e|
727 if e.is_a?(Array) && e.length == 2
728 write_to_stream error_msg(i.reply, qn, *e)
729 elsif e != :done
730 EMPromise.reject(e)
731 end
732 }.catch(&method(:panic))
733 end
734
735 iq type: [:get, :set] do |iq|
736 write_to_stream(Blather::StanzaError.new(
737 iq,
738 'feature-not-implemented',
739 :cancel
740 ))
741 end
742end
743
744class ReceiptMessage < Blather::Stanza
745 def self.new(to=nil)
746 node = super :message
747 node.to = to
748 node
749 end
750end
751
752class WebhookHandler < Goliath::API
753 use Sentry::Rack::CaptureExceptions
754 use Goliath::Rack::Params
755
756 def response(env)
757 @registration_repo = RegistrationRepo.new
758 # TODO: add timestamp grab here, and MUST include ./tai version
759
760 puts 'ENV: ' + env.reject { |k| k == 'params' }.to_s
761
762 if params.empty?
763 puts 'PARAMS empty!'
764 return [200, {}, "OK"]
765 end
766
767 if env['REQUEST_URI'] != '/'
768 puts 'BADREQUEST1: non-/ request "' +
769 env['REQUEST_URI'] + '", method "' +
770 env['REQUEST_METHOD'] + '"'
771 return [200, {}, "OK"]
772 end
773
774 if env['REQUEST_METHOD'] != 'POST'
775 puts 'BADREQUEST2: non-POST request; URI: "' +
776 env['REQUEST_URI'] + '", method "' +
777 env['REQUEST_METHOD'] + '"'
778 return [200, {}, "OK"]
779 end
780
781 # TODO: process each message in list, not just first one
782 jparams = params['_json'][0]['message']
783
784 type = params['_json'][0]['type']
785
786 users_num = ''
787 others_num = ''
788 if jparams['direction'] == 'in'
789 users_num = jparams['owner']
790 others_num = jparams['from']
791 elsif jparams['direction'] == 'out'
792 users_num = jparams['from']
793 others_num = jparams['owner']
794 else
795 # TODO: exception or similar
796 puts "big prob: '" + jparams['direction'] + "'" + body
797 return [200, {}, "OK"]
798 end
799
800 puts 'BODY - messageId: ' + jparams['id'] +
801 ', eventType: ' + type +
802 ', time: ' + jparams['time'] +
803 ', direction: ' + jparams['direction'] +
804 #', state: ' + jparams['state'] +
805 ', deliveryState: ' + (jparams['deliveryState'] ?
806 jparams['deliveryState'] : 'NONE') +
807 ', errorCode: ' + (jparams['errorCode'] ?
808 jparams['errorCode'] : 'NONE') +
809 ', description: ' + (jparams['description'] ?
810 jparams['description'] : 'NONE') +
811 ', tag: ' + (jparams['tag'] ? jparams['tag'] : 'NONE') +
812 ', media: ' + (jparams['media'] ?
813 jparams['media'].to_s : 'NONE')
814
815 if others_num[0] != '+'
816 # TODO: check that others_num actually a shortcode first
817 others_num +=
818 ';phone-context=ca-us.phone-context.soprani.ca'
819 end
820
821 bare_jid = @registration_repo.find_jid(users_num).sync
822
823 if !bare_jid
824 puts "jid_key for (#{users_num}) DNE; BW API misconfigured?"
825
826 # TODO: likely not appropriate; give error to BW API?
827 # TODO: add text re credentials not being registered
828 #write_to_stream error_msg(m.reply, m.body, :auth,
829 # 'registration-required')
830 return [200, {}, "OK"]
831 end
832
833 msg = nil
834 case jparams['direction']
835 when 'in'
836 text = ''
837 case type
838 when 'sms'
839 text = jparams['text']
840 when 'mms'
841 has_media = false
842
843 if jparams['text'].empty?
844 if not has_media
845 text = '[suspected group msg '\
846 'with no text (odd)]'
847 end
848 else
849 text = if has_media
850 # TODO: write/use a caption XEP
851 jparams['text']
852 else
853 '[suspected group msg '\
854 '(recipient list not '\
855 'available) with '\
856 'following text] ' +
857 jparams['text']
858 end
859 end
860
861 # ie. if text param non-empty or had no media
862 if not text.empty?
863 msg = Blather::Stanza::Message.new(
864 bare_jid, text)
865 msg.from = others_num + '@' + ARGV[0]
866 SGXbwmsgsv2.write(msg)
867 end
868
869 return [200, {}, "OK"]
870 when 'message-received'
871 # TODO: handle group chat, and fix above
872 text = jparams['text']
873
874 if jparams['to'].length > 1
875 msg = Blather::Stanza::Message.new(
876 Blather::JID.new(bare_jid).domain,
877 text
878 )
879
880 addrs = Nokogiri::XML::Node.new(
881 'addresses', msg.document)
882 addrs['xmlns'] = 'http://jabber.org/' \
883 'protocol/address'
884
885 addr1 = Nokogiri::XML::Node.new(
886 'address', msg.document)
887 addr1['type'] = 'to'
888 addr1['jid'] = bare_jid
889 addrs.add_child(addr1)
890
891 jparams['to'].each do |receiver|
892 if receiver == users_num
893 # already there in addr1
894 next
895 end
896
897 addrn = Nokogiri::XML::Node.new(
898 'address', msg.document)
899 addrn['type'] = 'to'
900 addrn['uri'] = "sms:#{receiver}"
901 addrn['delivered'] = 'true'
902 addrs.add_child(addrn)
903 end
904
905 msg.add_child(addrs)
906
907 # TODO: delete
908 puts "RESPONSE9: #{msg.inspect}"
909 end
910
911 Array(jparams['media']).each do |media_url|
912 unless media_url.end_with?(
913 '.smil', '.txt', '.xml'
914 )
915 has_media = true
916 SGXbwmsgsv2.send_media(
917 others_num + '@' +
918 ARGV[0],
919 bare_jid, media_url,
920 nil, nil, msg
921 )
922 end
923 end
924 else
925 text = "unknown type (#{type})"\
926 " with text: " + jparams['text']
927
928 # TODO: log/notify of this properly
929 puts text
930 end
931
932 if not msg
933 msg = Blather::Stanza::Message.new(bare_jid, text)
934 end
935 else # per prior switch, this is: jparams['direction'] == 'out'
936 tag_parts = jparams['tag'].split(/ /, 2)
937 id = WEBrick::HTTPUtils.unescape(tag_parts[0])
938 resourcepart = WEBrick::HTTPUtils.unescape(tag_parts[1])
939
940 # TODO: remove this hack
941 if jparams['to'].length > 1
942 puts "WARN! group no rcpt: #{users_num}"
943 return [200, {}, "OK"]
944 end
945
946 case type
947 when 'message-failed'
948 # create a bare message like the one user sent
949 msg = Blather::Stanza::Message.new(
950 others_num + '@' + ARGV[0])
951 msg.from = bare_jid + '/' + resourcepart
952 msg['id'] = id
953
954 # TODO: add 'errorCode' and/or 'description' val
955 # create an error reply to the bare message
956 msg = msg.as_error(
957 'recipient-unavailable',
958 :wait,
959 jparams['description']
960 )
961
962 # TODO: make prettier: this should be done above
963 others_num = params['_json'][0]['to']
964 when 'message-delivered'
965
966 msg = ReceiptMessage.new(bare_jid)
967
968 # TODO: put in member/instance variable
969 msg['id'] = SecureRandom.uuid
970
971 # TODO: send only when requested per XEP-0184
972 rcvd = Nokogiri::XML::Node.new(
973 'received',
974 msg.document
975 )
976 rcvd['xmlns'] = 'urn:xmpp:receipts'
977 rcvd['id'] = id
978 msg.add_child(rcvd)
979
980 # TODO: make prettier: this should be done above
981 others_num = params['_json'][0]['to']
982 else
983 # TODO: notify somehow of unknown state receivd?
984 puts "message with id #{id} has "\
985 "other type #{type}"
986 return [200, {}, "OK"]
987 end
988
989 puts "RESPONSE4: #{msg.inspect}"
990 end
991
992 msg.from = others_num + '@' + ARGV[0]
993 SGXbwmsgsv2.write(msg)
994
995 [200, {}, "OK"]
996 rescue Exception => e
997 Sentry.capture_exception(e)
998 puts 'Shutting down gateway due to exception 013: ' + e.message
999 SGXbwmsgsv2.shutdown
1000 puts 'Gateway has terminated.'
1001 EM.stop
1002 end
1003end
1004
1005at_exit do
1006 $stdout.sync = true
1007
1008 puts "Soprani.ca/SMS Gateway for XMPP - Bandwidth API V2\n"\
1009 "==>> last commit of this version is " + `git rev-parse HEAD` + "\n"
1010
1011 if ARGV.size != 7
1012 puts "Usage: sgx-bwmsgsv2.rb <component_jid> "\
1013 "<component_password> <server_hostname> "\
1014 "<server_port> <application_id> "\
1015 "<http_listen_port> <mms_proxy_prefix_url>"
1016 exit 0
1017 end
1018
1019 t = Time.now
1020 puts "LOG %d.%09d: starting...\n\n" % [t.to_i, t.nsec]
1021
1022 EM.run do
1023 REDIS = EM::Hiredis.connect
1024
1025 SGXbwmsgsv2.run
1026
1027 # required when using Prosody otherwise disconnects on 6-hour inactivity
1028 EM.add_periodic_timer(3600) do
1029 msg = Blather::Stanza::Iq::Ping.new(:get, 'localhost')
1030 msg.from = ARGV[0]
1031 SGXbwmsgsv2.write(msg)
1032 end
1033
1034 server = Goliath::Server.new('0.0.0.0', ARGV[5].to_i)
1035 server.api = WebhookHandler.new
1036 server.app = Goliath::Rack::Builder.build(server.api.class, server.api)
1037 server.logger = Log4r::Logger.new('goliath')
1038 server.logger.add(Log4r::StdoutOutputter.new('console'))
1039 server.logger.level = Log4r::INFO
1040 server.start do
1041 ["INT", "TERM"].each do |sig|
1042 trap(sig) do
1043 EM.defer do
1044 puts 'Shutting down gateway...'
1045 SGXbwmsgsv2.shutdown
1046
1047 puts 'Gateway has terminated.'
1048 EM.stop
1049 end
1050 end
1051 end
1052 end
1053 end
1054end