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 'multihashes'
27require 'securerandom'
28require "sentry-ruby"
29require 'time'
30require 'uri'
31require 'webrick'
32
33require 'goliath/api'
34require 'goliath/server'
35require 'log4r'
36
37require 'em_promise'
38
39require_relative 'lib/bandwidth_error'
40require_relative 'lib/registration_repo'
41
42Sentry.init
43
44# List of supported MIME types from Bandwidth - https://support.bandwidth.com/hc/en-us/articles/360014128994-What-MMS-file-types-are-supported-
45MMS_MIME_TYPES = [
46 "application/json",
47 "application/ogg",
48 "application/pdf",
49 "application/rtf",
50 "application/zip",
51 "application/x-tar",
52 "application/xml",
53 "application/gzip",
54 "application/x-bzip2",
55 "application/x-gzip",
56 "application/smil",
57 "application/javascript",
58 "audio/mp4",
59 "audio/mpeg",
60 "audio/ogg",
61 "audio/flac",
62 "audio/webm",
63 "audio/wav",
64 "audio/amr",
65 "audio/3gpp",
66 "image/bmp",
67 "image/gif",
68 "image/jpeg",
69 "image/pjpeg",
70 "image/png",
71 "image/svg+xml",
72 "image/tiff",
73 "image/webp",
74 "image/x-icon",
75 "text/css",
76 "text/csv",
77 "text/calendar",
78 "text/plain",
79 "text/javascript",
80 "text/vcard",
81 "text/vnd.wap.wml",
82 "text/xml",
83 "video/avi",
84 "video/mp4",
85 "video/mpeg",
86 "video/ogg",
87 "video/quicktime",
88 "video/webm",
89 "video/x-ms-wmv",
90 "video/x-flv"
91]
92
93def panic(e)
94 Sentry.capture_exception(e)
95 puts "Shutting down gateway due to exception: #{e.message}"
96 puts e.backtrace
97 SGXbwmsgsv2.shutdown
98 puts 'Gateway has terminated.'
99 EM.stop
100end
101
102EM.error_handler(&method(:panic))
103
104def extract_shortcode(dest)
105 num, context = dest.split(';', 2)
106 num if context == 'phone-context=ca-us.phone-context.soprani.ca'
107end
108
109def anonymous_tel?(dest)
110 dest.split(';', 2)[1] == 'phone-context=anonymous.phone-context.soprani.ca'
111end
112
113class SGXClient < Blather::Client
114 def register_handler(type, *guards, &block)
115 super(type, *guards) { |*args| wrap_handler(*args, &block) }
116 end
117
118 def register_handler_before(type, *guards, &block)
119 check_handler(type, guards)
120 handler = lambda { |*args| wrap_handler(*args, &block) }
121
122 @handlers[type] ||= []
123 @handlers[type].unshift([guards, handler])
124 end
125
126protected
127
128 def wrap_handler(*args)
129 v = yield(*args)
130 v = v.sync if ENV['ENV'] == 'test' && v.is_a?(Promise)
131 v.catch(&method(:panic)) if v.is_a?(Promise)
132 true # Do not run other handlers unless throw :pass
133 rescue Exception => e
134 panic(e)
135 end
136end
137
138# TODO: keep in sync with jmp-acct_bot.rb, and eventually put in common location
139module CatapultSettingFlagBits
140 VOICEMAIL_TRANSCRIPTION_DISABLED = 0
141 MMS_ON_OOB_URL = 1
142end
143
144module SGXbwmsgsv2
145 extend Blather::DSL
146
147 @registration_repo = RegistrationRepo.new
148 @client = SGXClient.new
149 @gateway_features = [
150 "http://jabber.org/protocol/disco#info",
151 "http://jabber.org/protocol/address/",
152 "jabber:iq:register"
153 ]
154
155 def self.run
156 # TODO: read/save ARGV[7] creds to local variables
157 client.run
158 end
159
160 # so classes outside this module can write messages, too
161 def self.write(stanza)
162 client.write(stanza)
163 end
164
165 def self.before_handler(type, *guards, &block)
166 client.register_handler_before(type, *guards, &block)
167 end
168
169 def self.send_media(from, to, media_url, desc=nil, subject=nil, m=nil)
170 # we assume media_url is one of these (always the case so far):
171 # https://messaging.bandwidth.com/api/v2/users/[u]/media/[path]
172
173 puts 'ORIG_URL: ' + media_url
174 usr = to
175 if media_url.start_with?('https://messaging.bandwidth.com/api/v2/users/')
176 pth = media_url.split('/', 9)[8]
177 # the caller must guarantee that 'to' is a bare JID
178 media_url = ARGV[6] + WEBrick::HTTPUtils.escape(usr) + '/' + pth
179 puts 'PROX_URL: ' + media_url
180 end
181
182 msg = m ? m.copy : Blather::Stanza::Message.new(to, "")
183 msg.from = from
184 msg.subject = subject if subject
185
186 # provide URL in XEP-0066 (OOB) fashion
187 x = Nokogiri::XML::Node.new 'x', msg.document
188 x['xmlns'] = 'jabber:x:oob'
189
190 urln = Nokogiri::XML::Node.new 'url', msg.document
191 urlc = Nokogiri::XML::Text.new media_url, msg.document
192 urln.add_child(urlc)
193 x.add_child(urln)
194
195 if desc
196 descn = Nokogiri::XML::Node.new('desc', msg.document)
197 descc = Nokogiri::XML::Text.new(desc, msg.document)
198 descn.add_child(descc)
199 x.add_child(descn)
200 end
201
202 msg.add_child(x)
203
204 write(msg)
205 rescue Exception => e
206 panic(e)
207 end
208
209 setup ARGV[0], ARGV[1], ARGV[2], ARGV[3], nil, nil, async: true
210
211 def self.pass_on_message(m, users_num, jid)
212 # setup delivery receipt; similar to a reply
213 rcpt = ReceiptMessage.new(m.from.stripped)
214 rcpt.from = m.to
215
216 # pass original message (before sending receipt)
217 m.to = jid
218 m.from = "#{users_num}@#{ARGV[0]}"
219
220 puts 'XRESPONSE0: ' + m.inspect
221 write_to_stream m
222
223 # send a delivery receipt back to the sender
224 # TODO: send only when requested per XEP-0184
225 # TODO: pass receipts from target if supported
226
227 # TODO: put in member/instance variable
228 rcpt['id'] = SecureRandom.uuid
229 rcvd = Nokogiri::XML::Node.new 'received', rcpt.document
230 rcvd['xmlns'] = 'urn:xmpp:receipts'
231 rcvd['id'] = m.id
232 rcpt.add_child(rcvd)
233
234 puts 'XRESPONSE1: ' + rcpt.inspect
235 write_to_stream rcpt
236 end
237
238 def self.call_catapult(
239 token, secret, m, pth, body=nil,
240 head={}, code=[200], respond_with=:body
241 )
242 # pth looks like one of:
243 # "api/v2/users/#{user_id}/[endpoint_name]"
244
245 url_prefix = ''
246
247 # TODO: need to make a separate thing for voice.bw.c eventually
248 if pth.start_with? 'api/v2/users'
249 url_prefix = 'https://messaging.bandwidth.com/'
250 end
251
252 EM::HttpRequest.new(
253 url_prefix + pth
254 ).public_send(
255 m,
256 head: {
257 'Authorization' => [token, secret]
258 }.merge(head),
259 body: body
260 ).then { |http|
261 puts "API response to send: #{http.response} with code"\
262 " response.code #{http.response_header.status}"
263
264 if code.include?(http.response_header.status)
265 case respond_with
266 when :body
267 http.response
268 when :headers
269 http.response_header
270 else
271 http
272 end
273 else
274 EMPromise.reject(
275 BandwidthError.for(http.response_header.status, http.response)
276 )
277 end
278 }
279 end
280
281 def self.to_catapult_possible_oob(s, num_dest, user_id, token, secret,
282 usern)
283 un = s.at("oob|x > oob|url", oob: "jabber:x:oob")
284 unless un
285 puts "MMSOOB: no url node found so process as normal"
286 return to_catapult(s, nil, num_dest, user_id, token,
287 secret, usern)
288 end
289 puts "MMSOOB: found a url node - checking if to make MMS..."
290
291 body = s.respond_to?(:body) ? s.body : ''
292 EM::HttpRequest.new(un.text, tls: { verify_peer: true }).head.then { |http|
293 # If content is too large, or MIME type is not supported, place the link inside the body and do not send MMS.
294 if http.response_header["CONTENT_LENGTH"].to_i > 3500000 ||
295 !MMS_MIME_TYPES.include?(http.response_header["CONTENT_TYPE"])
296 unless body.include?(un.text)
297 s.body = body.empty? ? un.text : "#{body}\n#{un.text}"
298 end
299 to_catapult(s, nil, num_dest, user_id, token, secret, usern)
300 else # If size is less than ~3.5MB, strip the link from the body and attach media in the body.
301 # some clients send URI in both body & <url/> so delete
302 s.body = body.sub(/\s*#{Regexp.escape(un.text)}\s*$/, '')
303
304 puts "MMSOOB: url text is '#{un.text}'"
305 puts "MMSOOB: the body is '#{body.to_s.strip}'"
306
307 puts "MMSOOB: sending MMS since found OOB & user asked"
308 to_catapult(s, un.text, num_dest, user_id, token, secret, usern)
309 end
310 }
311 end
312
313 def self.to_catapult(s, murl, num_dest, user_id, token, secret, usern)
314 body = s.respond_to?(:body) ? s.body.to_s : ''
315 if murl.to_s.empty? && body.strip.empty?
316 return EMPromise.reject(
317 [:modify, 'policy-violation']
318 )
319 end
320
321 segment_size = body.ascii_only? ? 160 : 70
322 if !murl && ENV["MMS_PATH"] && body.length > segment_size*3
323 file = Multibases.pack(
324 'base58btc',
325 Multihashes.encode(Digest::SHA256.digest(body), "sha2-256")
326 ).to_s
327 File.open("#{ENV['MMS_PATH']}/#{file}", "w") { |fh| fh.write body }
328 murl = "#{ENV['MMS_URL']}/#{file}"
329 body = ""
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.dig('_json', 0, 'message')
770 type = params.dig('_json', 0, 'type')
771
772 return [400, {}, "Missing params\n"] unless jparams && type
773
774 users_num, others_num = if jparams['direction'] == 'in'
775 [jparams['owner'], jparams['from']]
776 elsif jparams['direction'] == 'out'
777 [jparams['from'], jparams['owner']]
778 else
779 puts "big prob: '#{jparams['direction']}'"
780 return [400, {}, "OK"]
781 end
782
783 return [400, {}, "Missing params\n"] unless users_num && others_num
784 return [400, {}, "Missing params\n"] unless jparams['to'].is_a?(Array)
785
786 puts "BODY - messageId: #{jparams['id']}" \
787 ", eventType: #{type}" \
788 ", time: #{jparams['time']}" \
789 ", direction: #{jparams['direction']}" \
790 ", deliveryState: #{jparams['deliveryState'] || 'NONE'}" \
791 ", errorCode: #{jparams['errorCode'] || 'NONE'}" \
792 ", description: #{jparams['description'] || 'NONE'}" \
793 ", tag: #{jparams['tag'] || 'NONE'}" \
794 ", media: #{jparams['media'] || 'NONE'}"
795
796 if others_num[0] != '+'
797 # TODO: check that others_num actually a shortcode first
798 others_num +=
799 ';phone-context=ca-us.phone-context.soprani.ca'
800 end
801
802 bare_jid = @registration_repo.find_jid(users_num).sync
803
804 if !bare_jid
805 puts "jid_key for (#{users_num}) DNE; BW API misconfigured?"
806
807 return [403, {}, "Customer not found\n"]
808 end
809
810 msg = nil
811 case jparams['direction']
812 when 'in'
813 text = ''
814 case type
815 when 'sms'
816 text = jparams['text']
817 when 'mms'
818 has_media = false
819
820 if jparams['text'].empty?
821 if not has_media
822 text = '[suspected group msg '\
823 'with no text (odd)]'
824 end
825 else
826 text = if has_media
827 # TODO: write/use a caption XEP
828 jparams['text']
829 else
830 '[suspected group msg '\
831 '(recipient list not '\
832 'available) with '\
833 'following text] ' +
834 jparams['text']
835 end
836 end
837
838 # ie. if text param non-empty or had no media
839 if not text.empty?
840 msg = Blather::Stanza::Message.new(
841 bare_jid, text)
842 msg.from = others_num + '@' + ARGV[0]
843 SGXbwmsgsv2.write(msg)
844 end
845
846 return [200, {}, "OK"]
847 when 'message-received'
848 # TODO: handle group chat, and fix above
849 text = jparams['text']
850
851 if jparams['to'].length > 1
852 msg = Blather::Stanza::Message.new(
853 Blather::JID.new(bare_jid).domain,
854 text
855 )
856
857 addrs = Nokogiri::XML::Node.new(
858 'addresses', msg.document)
859 addrs['xmlns'] = 'http://jabber.org/' \
860 'protocol/address'
861
862 addr1 = Nokogiri::XML::Node.new(
863 'address', msg.document)
864 addr1['type'] = 'to'
865 addr1['jid'] = bare_jid
866 addrs.add_child(addr1)
867
868 jparams['to'].each do |receiver|
869 if receiver == users_num
870 # already there in addr1
871 next
872 end
873
874 addrn = Nokogiri::XML::Node.new(
875 'address', msg.document)
876 addrn['type'] = 'to'
877 addrn['uri'] = "sms:#{receiver}"
878 addrn['delivered'] = 'true'
879 addrs.add_child(addrn)
880 end
881
882 msg.add_child(addrs)
883
884 # TODO: delete
885 puts "RESPONSE9: #{msg.inspect}"
886 end
887
888 Array(jparams['media']).each do |media_url|
889 unless media_url.end_with?(
890 '.smil', '.txt', '.xml'
891 )
892 has_media = true
893 SGXbwmsgsv2.send_media(
894 others_num + '@' +
895 ARGV[0],
896 bare_jid, media_url,
897 nil, nil, msg
898 )
899 end
900 end
901 else
902 text = "unknown type (#{type})"\
903 " with text: " + jparams['text']
904
905 # TODO: log/notify of this properly
906 puts text
907 end
908
909 if not msg
910 msg = Blather::Stanza::Message.new(bare_jid, text)
911 end
912 else # per prior switch, this is: jparams['direction'] == 'out'
913 tag_parts = jparams['tag'].split(/ /, 2)
914 id = WEBrick::HTTPUtils.unescape(tag_parts[0])
915 resourcepart = WEBrick::HTTPUtils.unescape(tag_parts[1])
916
917 # TODO: remove this hack
918 if jparams['to'].length > 1
919 puts "WARN! group no rcpt: #{users_num}"
920 return [200, {}, "OK"]
921 end
922
923 case type
924 when 'message-failed'
925 # create a bare message like the one user sent
926 msg = Blather::Stanza::Message.new(
927 others_num + '@' + ARGV[0])
928 msg.from = bare_jid + '/' + resourcepart
929 msg['id'] = id
930
931 # TODO: add 'errorCode' and/or 'description' val
932 # create an error reply to the bare message
933 msg = msg.as_error(
934 'recipient-unavailable',
935 :wait,
936 jparams['description']
937 )
938
939 # TODO: make prettier: this should be done above
940 others_num = params['_json'][0]['to']
941 when 'message-delivered'
942
943 msg = ReceiptMessage.new(bare_jid)
944
945 # TODO: put in member/instance variable
946 msg['id'] = SecureRandom.uuid
947
948 # TODO: send only when requested per XEP-0184
949 rcvd = Nokogiri::XML::Node.new(
950 'received',
951 msg.document
952 )
953 rcvd['xmlns'] = 'urn:xmpp:receipts'
954 rcvd['id'] = id
955 msg.add_child(rcvd)
956
957 # TODO: make prettier: this should be done above
958 others_num = params['_json'][0]['to']
959 else
960 # TODO: notify somehow of unknown state receivd?
961 puts "message with id #{id} has "\
962 "other type #{type}"
963 return [200, {}, "OK"]
964 end
965
966 puts "RESPONSE4: #{msg.inspect}"
967 end
968
969 msg.from = others_num + '@' + ARGV[0]
970 SGXbwmsgsv2.write(msg)
971
972 [200, {}, "OK"]
973 rescue Exception => e
974 Sentry.capture_exception(e)
975 puts 'Shutting down gateway due to exception 013: ' + e.message
976 SGXbwmsgsv2.shutdown
977 puts 'Gateway has terminated.'
978 EM.stop
979 end
980end
981
982at_exit do
983 $stdout.sync = true
984
985 puts "Soprani.ca/SMS Gateway for XMPP - Bandwidth API V2\n"\
986 "==>> last commit of this version is " + `git rev-parse HEAD` + "\n"
987
988 if ARGV.size != 7
989 puts "Usage: sgx-bwmsgsv2.rb <component_jid> "\
990 "<component_password> <server_hostname> "\
991 "<server_port> <application_id> "\
992 "<http_listen_port> <mms_proxy_prefix_url>"
993 exit 0
994 end
995
996 t = Time.now
997 puts "LOG %d.%09d: starting...\n\n" % [t.to_i, t.nsec]
998
999 EM.run do
1000 REDIS = EM::Hiredis.connect
1001
1002 SGXbwmsgsv2.run
1003
1004 # required when using Prosody otherwise disconnects on 6-hour inactivity
1005 EM.add_periodic_timer(3600) do
1006 msg = Blather::Stanza::Iq::Ping.new(:get, 'localhost')
1007 msg.from = ARGV[0]
1008 SGXbwmsgsv2.write(msg)
1009 end
1010
1011 server = Goliath::Server.new('0.0.0.0', ARGV[5].to_i)
1012 server.api = WebhookHandler.new
1013 server.app = Goliath::Rack::Builder.build(server.api.class, server.api)
1014 server.logger = Log4r::Logger.new('goliath')
1015 server.logger.add(Log4r::StdoutOutputter.new('console'))
1016 server.logger.level = Log4r::INFO
1017 server.start do
1018 ["INT", "TERM"].each do |sig|
1019 trap(sig) do
1020 EM.defer do
1021 puts 'Shutting down gateway...'
1022 SGXbwmsgsv2.shutdown
1023
1024 puts 'Gateway has terminated.'
1025 EM.stop
1026 end
1027 end
1028 end
1029 end
1030 end
1031end unless ENV['ENV'] == 'test'