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