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