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