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 puts "received <remove/> - ignoring for now..."
601 EMPromise.reject(:done)
602 else
603 creds_from_registration_query(qn)
604 end
605 }.then { |user_id, api_token, api_secret, phone_num|
606 if phone_num[0] == '+'
607 [user_id, api_token, api_secret, phone_num]
608 else
609 # TODO: add text re number not (yet) supported
610 EMPromise.reject([:cancel, 'item-not-found'])
611 end
612 }.then { |user_id, api_token, api_secret, phone_num|
613 # TODO: find way to verify #{phone_num}, too
614 call_catapult(
615 api_token,
616 api_secret,
617 :get,
618 "api/v2/users/#{user_id}/media"
619 ).then { |response|
620 JSON.parse(response)
621 # TODO: confirm response is array - could be empty
622
623 puts "register got str #{response.to_s[0..999]}"
624
625 check_then_register(
626 i,
627 user_id,
628 api_token,
629 api_secret,
630 phone_num
631 )
632 }
633 }.catch { |e|
634 EMPromise.reject(case e
635 when 401
636 # TODO: add text re bad credentials
637 [:auth, 'not-authorized']
638 when 404
639 # TODO: add text re number not found or disabled
640 [:cancel, 'item-not-found']
641 when Integer
642 [:modify, 'not-acceptable']
643 else
644 e
645 end)
646 }
647 end
648
649 def self.registration_form(orig, existing_number=nil)
650 msg = Nokogiri::XML::Node.new 'query', orig.document
651 msg['xmlns'] = 'jabber:iq:register'
652
653 if existing_number
654 msg.add_child(
655 Nokogiri::XML::Node.new(
656 'registered', msg.document
657 )
658 )
659 end
660
661 # TODO: update "User Id" x2 below (to "accountId"?), and others?
662 n1 = Nokogiri::XML::Node.new(
663 'instructions', msg.document
664 )
665 n1.content = "Enter the information from your Account "\
666 "page as well as the Phone Number\nin your "\
667 "account you want to use (ie. '+12345678901')"\
668 ".\nUser Id is nick, API Token is username, "\
669 "API Secret is password, Phone Number is phone"\
670 ".\n\nThe source code for this gateway is at "\
671 "https://gitlab.com/soprani.ca/sgx-bwmsgsv2 ."\
672 "\nCopyright (C) 2017-2020 Denver Gingerich "\
673 "and others, licensed under AGPLv3+."
674 n2 = Nokogiri::XML::Node.new 'nick', msg.document
675 n3 = Nokogiri::XML::Node.new 'username', msg.document
676 n4 = Nokogiri::XML::Node.new 'password', msg.document
677 n5 = Nokogiri::XML::Node.new 'phone', msg.document
678 n5.content = existing_number.to_s
679 msg.add_child(n1)
680 msg.add_child(n2)
681 msg.add_child(n3)
682 msg.add_child(n4)
683 msg.add_child(n5)
684
685 x = Blather::Stanza::X.new :form, [
686 {
687 required: true, type: :"text-single",
688 label: 'User Id', var: 'nick'
689 },
690 {
691 required: true, type: :"text-single",
692 label: 'API Token', var: 'username'
693 },
694 {
695 required: true, type: :"text-private",
696 label: 'API Secret', var: 'password'
697 },
698 {
699 required: true, type: :"text-single",
700 label: 'Phone Number', var: 'phone',
701 value: existing_number.to_s
702 }
703 ]
704 x.title = 'Register for '\
705 'Soprani.ca Gateway to XMPP - Bandwidth API V2'
706 x.instructions = "Enter the details from your Account "\
707 "page as well as the Phone Number\nin your "\
708 "account you want to use (ie. '+12345678901')"\
709 ".\n\nThe source code for this gateway is at "\
710 "https://gitlab.com/soprani.ca/sgx-bwmsgsv2 ."\
711 "\nCopyright (C) 2017-2020 Denver Gingerich "\
712 "and others, licensed under AGPLv3+."
713 msg.add_child(x)
714
715 orig.add_child(msg)
716
717 return orig
718 end
719
720 iq '/iq/ns:query', ns: 'jabber:iq:register' do |i, qn|
721 puts "IQ: #{i.inspect}"
722
723 case i.type
724 when :set
725 process_registration(i, qn)
726 when :get
727 bare_jid = i.from.stripped
728 @registration_repo.find(bare_jid).then { |creds|
729 reply = registration_form(i.reply, creds.last)
730 puts "RESPONSE2: #{reply.inspect}"
731 write_to_stream reply
732 }
733 else
734 # Unknown IQ, ignore for now
735 EMPromise.reject(:done)
736 end.catch { |e|
737 if e.is_a?(Array) && e.length == 2
738 write_to_stream error_msg(i.reply, qn, *e)
739 elsif e != :done
740 EMPromise.reject(e)
741 end
742 }.catch(&method(:panic))
743 end
744
745 iq type: [:get, :set] do |iq|
746 write_to_stream(Blather::StanzaError.new(
747 iq,
748 'feature-not-implemented',
749 :cancel
750 ))
751 end
752end
753
754class ReceiptMessage < Blather::Stanza
755 def self.new(to=nil)
756 node = super :message
757 node.to = to
758 node
759 end
760end
761
762class WebhookHandler < Goliath::API
763 use Goliath::Rack::Params
764
765 @registration_repo = RegistrationRepo.new
766
767 def response(env)
768 # TODO: add timestamp grab here, and MUST include ./tai version
769
770 puts 'ENV: ' + env.reject { |k| k == 'params' }.to_s
771
772 if params.empty?
773 puts 'PARAMS empty!'
774 return [200, {}, "OK"]
775 end
776
777 if env['REQUEST_URI'] != '/'
778 puts 'BADREQUEST1: non-/ request "' +
779 env['REQUEST_URI'] + '", method "' +
780 env['REQUEST_METHOD'] + '"'
781 return [200, {}, "OK"]
782 end
783
784 if env['REQUEST_METHOD'] != 'POST'
785 puts 'BADREQUEST2: non-POST request; URI: "' +
786 env['REQUEST_URI'] + '", method "' +
787 env['REQUEST_METHOD'] + '"'
788 return [200, {}, "OK"]
789 end
790
791 # TODO: process each message in list, not just first one
792 jparams = params['_json'][0]['message']
793
794 type = params['_json'][0]['type']
795
796 users_num = ''
797 others_num = ''
798 if jparams['direction'] == 'in'
799 users_num = jparams['owner']
800 others_num = jparams['from']
801 elsif jparams['direction'] == 'out'
802 users_num = jparams['from']
803 others_num = jparams['owner']
804 else
805 # TODO: exception or similar
806 puts "big prob: '" + jparams['direction'] + "'" + body
807 return [200, {}, "OK"]
808 end
809
810 puts 'BODY - messageId: ' + jparams['id'] +
811 ', eventType: ' + type +
812 ', time: ' + jparams['time'] +
813 ', direction: ' + jparams['direction'] +
814 #', state: ' + jparams['state'] +
815 ', deliveryState: ' + (jparams['deliveryState'] ?
816 jparams['deliveryState'] : 'NONE') +
817 ', errorCode: ' + (jparams['errorCode'] ?
818 jparams['errorCode'] : 'NONE') +
819 ', description: ' + (jparams['description'] ?
820 jparams['description'] : 'NONE') +
821 ', tag: ' + (jparams['tag'] ? jparams['tag'] : 'NONE') +
822 ', media: ' + (jparams['media'] ?
823 jparams['media'].to_s : 'NONE')
824
825 if others_num[0] != '+'
826 # TODO: check that others_num actually a shortcode first
827 others_num +=
828 ';phone-context=ca-us.phone-context.soprani.ca'
829 end
830
831 bare_jid = @registration_repo.find_jid(users_num).sync
832
833 if !bare_jid
834 puts "jid_key (#{jid_key}) DNE; BW API misconfigured?"
835
836 # TODO: likely not appropriate; give error to BW API?
837 # TODO: add text re credentials not being registered
838 #write_to_stream error_msg(m.reply, m.body, :auth,
839 # 'registration-required')
840 return [200, {}, "OK"]
841 end
842
843 msg = nil
844 case jparams['direction']
845 when 'in'
846 text = ''
847 case type
848 when 'sms'
849 text = jparams['text']
850 when 'mms'
851 has_media = false
852
853 if jparams['text'].empty?
854 if not has_media
855 text = '[suspected group msg '\
856 'with no text (odd)]'
857 end
858 else
859 text = if has_media
860 # TODO: write/use a caption XEP
861 jparams['text']
862 else
863 '[suspected group msg '\
864 '(recipient list not '\
865 'available) with '\
866 'following text] ' +
867 jparams['text']
868 end
869 end
870
871 # ie. if text param non-empty or had no media
872 if not text.empty?
873 msg = Blather::Stanza::Message.new(
874 bare_jid, text)
875 msg.from = others_num + '@' + ARGV[0]
876 SGXbwmsgsv2.write(msg)
877 end
878
879 return [200, {}, "OK"]
880 when 'message-received'
881 # TODO: handle group chat, and fix above
882 text = jparams['text']
883
884 if jparams['to'].length > 1
885 msg = Blather::Stanza::Message.new(
886 Blather::JID.new(bare_jid).domain,
887 text
888 )
889
890 addrs = Nokogiri::XML::Node.new(
891 'addresses', msg.document)
892 addrs['xmlns'] = 'http://jabber.org/' \
893 'protocol/address'
894
895 addr1 = Nokogiri::XML::Node.new(
896 'address', msg.document)
897 addr1['type'] = 'to'
898 addr1['jid'] = bare_jid
899 addrs.add_child(addr1)
900
901 jparams['to'].each do |receiver|
902 if receiver == users_num
903 # already there in addr1
904 next
905 end
906
907 addrn = Nokogiri::XML::Node.new(
908 'address', msg.document)
909 addrn['type'] = 'to'
910 addrn['uri'] = "sms:#{receiver}"
911 addrn['delivered'] = 'true'
912 addrs.add_child(addrn)
913 end
914
915 msg.add_child(addrs)
916
917 # TODO: delete
918 puts "RESPONSE9: #{msg.inspect}"
919 end
920
921 Array(jparams['media']).each do |media_url|
922 unless media_url.end_with?(
923 '.smil', '.txt', '.xml'
924 )
925 has_media = true
926 SGXbwmsgsv2.send_media(
927 others_num + '@' +
928 ARGV[0],
929 bare_jid, media_url,
930 nil, nil, msg
931 )
932 end
933 end
934 else
935 text = "unknown type (#{type})"\
936 " with text: " + jparams['text']
937
938 # TODO: log/notify of this properly
939 puts text
940 end
941
942 if not msg
943 msg = Blather::Stanza::Message.new(bare_jid, text)
944 end
945 else # per prior switch, this is: jparams['direction'] == 'out'
946 tag_parts = jparams['tag'].split(/ /, 2)
947 id = WEBrick::HTTPUtils.unescape(tag_parts[0])
948 resourcepart = WEBrick::HTTPUtils.unescape(tag_parts[1])
949
950 # TODO: remove this hack
951 if jparams['to'].length > 1
952 puts "WARN! group no rcpt: #{users_num}"
953 return [200, {}, "OK"]
954 end
955
956 case type
957 when 'message-failed'
958 # create a bare message like the one user sent
959 msg = Blather::Stanza::Message.new(
960 others_num + '@' + ARGV[0])
961 msg.from = bare_jid + '/' + resourcepart
962 msg['id'] = id
963
964 # TODO: add 'errorCode' and/or 'description' val
965 # create an error reply to the bare message
966 msg = msg.as_error(
967 'recipient-unavailable',
968 :wait,
969 jparams['description']
970 )
971
972 # TODO: make prettier: this should be done above
973 others_num = params['_json'][0]['to']
974 when 'message-delivered'
975
976 msg = ReceiptMessage.new(bare_jid)
977
978 # TODO: put in member/instance variable
979 msg['id'] = SecureRandom.uuid
980
981 # TODO: send only when requested per XEP-0184
982 rcvd = Nokogiri::XML::Node.new(
983 'received',
984 msg.document
985 )
986 rcvd['xmlns'] = 'urn:xmpp:receipts'
987 rcvd['id'] = id
988 msg.add_child(rcvd)
989
990 # TODO: make prettier: this should be done above
991 others_num = params['_json'][0]['to']
992 else
993 # TODO: notify somehow of unknown state receivd?
994 puts "message with id #{id} has "\
995 "other type #{type}"
996 return [200, {}, "OK"]
997 end
998
999 puts "RESPONSE4: #{msg.inspect}"
1000 end
1001
1002 msg.from = others_num + '@' + ARGV[0]
1003 SGXbwmsgsv2.write(msg)
1004
1005 [200, {}, "OK"]
1006 rescue Exception => e
1007 puts 'Shutting down gateway due to exception 013: ' + e.message
1008 SGXbwmsgsv2.shutdown
1009 puts 'Gateway has terminated.'
1010 EM.stop
1011 end
1012end
1013
1014at_exit do
1015 $stdout.sync = true
1016
1017 puts "Soprani.ca/SMS Gateway for XMPP - Bandwidth API V2\n"\
1018 "==>> last commit of this version is " + `git rev-parse HEAD` + "\n"
1019
1020 if ARGV.size != 7 and ARGV.size != 8
1021 puts "Usage: sgx-bwmsgsv2.rb <component_jid> "\
1022 "<component_password> <server_hostname> "\
1023 "<server_port> <application_id> "\
1024 "<http_listen_port> <mms_proxy_prefix_url> [V1_creds_file]"
1025 exit 0
1026 end
1027
1028 t = Time.now
1029 puts "LOG %d.%09d: starting...\n\n" % [t.to_i, t.nsec]
1030
1031 EM.run do
1032 REDIS = EM::Hiredis.connect
1033
1034 SGXbwmsgsv2.run
1035
1036 # required when using Prosody otherwise disconnects on 6-hour inactivity
1037 EM.add_periodic_timer(3600) do
1038 msg = Blather::Stanza::Iq::Ping.new(:get, 'localhost')
1039 msg.from = ARGV[0]
1040 SGXbwmsgsv2.write(msg)
1041 end
1042
1043 server = Goliath::Server.new('0.0.0.0', ARGV[5].to_i)
1044 server.api = WebhookHandler.new
1045 server.app = Goliath::Rack::Builder.build(server.api.class, server.api)
1046 server.logger = Log4r::Logger.new('goliath')
1047 server.logger.add(Log4r::StdoutOutputter.new('console'))
1048 server.logger.level = Log4r::INFO
1049 server.start do
1050 ["INT", "TERM"].each do |sig|
1051 trap(sig) do
1052 EM.defer do
1053 puts 'Shutting down gateway...'
1054 SGXbwmsgsv2.shutdown
1055
1056 puts 'Gateway has terminated.'
1057 EM.stop
1058 end
1059 end
1060 end
1061 end
1062 end
1063end