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