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