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