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