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