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