1#!/usr/bin/env ruby
2#
3# Copyright (C) 2017 Denver Gingerich <denver@ossguy.com>
4# Copyright (C) 2017 Stephen Paul Weber <singpolyma@singpolyma.net>
5#
6# This file is part of sgx-catapult.
7#
8# sgx-catapult 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-catapult 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-catapult. If not, see <http://www.gnu.org/licenses/>.
20
21require 'blather/client/dsl'
22require 'json'
23require 'net/http'
24require 'redis/connection/hiredis'
25require 'time'
26require 'uri'
27require 'uuid'
28
29require 'goliath/api'
30require 'goliath/server'
31require 'log4r'
32
33puts "Soprani.ca/SMS Gateway for XMPP - Catapult v0.016"
34
35if ARGV.size != 9 then
36 puts "Usage: sgx-catapult.rb <component_jid> <component_password> " +
37 "<server_hostname> <server_port> " +
38 "<redis_hostname> <redis_port> <delivery_receipt_url> " +
39 "<http_listen_port> <mms_proxy_prefix_url>"
40 exit 0
41end
42
43module SGXcatapult
44 extend Blather::DSL
45
46 @jingle_sids = Hash.new
47 @jingle_fnames = Hash.new
48 @partial_data = Hash.new
49 @uuid_gen = UUID.new
50
51 def self.run
52 client.run
53 end
54
55 # so classes outside this module can write messages, too
56 def self.write(stanza)
57 client.write(stanza)
58 end
59
60 def self.error_msg(orig, query_node, type, name, text = nil)
61 if not query_node.nil?
62 orig.add_child(query_node)
63 orig.type = :error
64 end
65
66 error = Nokogiri::XML::Node.new 'error', orig.document
67 error['type'] = type
68 orig.add_child(error)
69
70 suberr = Nokogiri::XML::Node.new name, orig.document
71 suberr['xmlns'] = 'urn:ietf:params:xml:ns:xmpp-stanzas'
72 error.add_child(suberr)
73
74 # TODO: add some explanatory xml:lang='en' text (see text param)
75 puts "RESPONSE3: #{orig.inspect}"
76 return orig
77 end
78
79 setup ARGV[0], ARGV[1], ARGV[2], ARGV[3]
80
81 message :chat?, :body do |m|
82 num_dest = m.to.to_s.split('@', 2)[0]
83
84 if num_dest[0] != '+'
85 # check to see if a valid shortcode context is specified
86 num_and_context = num_dest.split(';', 2)
87 if num_and_context[1] and num_and_context[1] ==
88 'phone-context=ca-us.phone-context.soprani.ca'
89
90 # TODO: check if num_dest is fully numeric
91 num_dest = num_and_context[0]
92 else
93 # TODO: text re num not (yet) supportd/implmentd
94 write_to_stream error_msg(m.reply, m.body,
95 :cancel, 'item-not-found')
96 next
97 end
98 end
99
100 bare_jid = m.from.to_s.split('/', 2)[0]
101 cred_key = "catapult_cred-" + bare_jid
102
103 conn = Hiredis::Connection.new
104 conn.connect(ARGV[4], ARGV[5].to_i)
105
106 conn.write ["EXISTS", cred_key]
107 if conn.read == 0
108 conn.disconnect
109
110 # TODO: add text re credentials not being registered
111 write_to_stream error_msg(m.reply, m.body, :auth,
112 'registration-required')
113 next
114 end
115
116 conn.write ["LRANGE", cred_key, 0, 3]
117 user_id, api_token, api_secret, users_num = conn.read
118 conn.disconnect
119
120 uri = URI.parse('https://api.catapult.inetwork.com')
121 http = Net::HTTP.new(uri.host, uri.port)
122 http.use_ssl = true
123 request = Net::HTTP::Post.new('/v1/users/' + user_id +
124 '/messages')
125 request.basic_auth api_token, api_secret
126 request.add_field('Content-Type', 'application/json')
127 request.body = JSON.dump({
128 'from' => users_num,
129 'to' => num_dest,
130 'text' => m.body,
131 'tag' => m.id, # TODO: message has it?
132 'receiptRequested' => 'all',
133 'callbackUrl' => ARGV[6]
134 })
135 response = http.request(request)
136
137 puts 'API response to send: ' + response.to_s + ' with code ' +
138 response.code + ', body "' + response.body + '"'
139
140 if response.code != '201'
141 # TODO: add text re unexpected code; mention code number
142 write_to_stream error_msg(m.reply, m.body, :cancel,
143 'internal-server-error')
144 next
145 end
146 end
147
148 def self.user_cap_identities()
149 [{:category => 'client', :type => 'sms'}]
150 end
151
152 def self.user_cap_features()
153 [
154 "urn:xmpp:receipts",
155 "urn:xmpp:jingle:1", "urn:xmpp:jingle:transports:ibb:1",
156
157 # TODO: eventually add more efficient file transfer mechanisms
158 #"urn:xmpp:jingle:transports:s5b:1",
159
160 # TODO: MUST add all relevant/reasonable vers of file-transfer
161 #"urn:xmpp:jingle:apps:file-transfer:4"
162 "urn:xmpp:jingle:apps:file-transfer:3"
163 ]
164 end
165
166 presence :subscribe? do |p|
167 puts "PRESENCE1: #{p.inspect}"
168
169 msg = Blather::Stanza::Presence.new
170 msg.to = p.from
171 msg.from = p.to
172 msg.type = :subscribed
173
174 puts "RESPONSE5: #{msg.inspect}"
175 write_to_stream msg
176 end
177
178 presence :probe? do |p|
179 puts 'PRESENCE2: ' + p.inspect
180
181 caps = Blather::Stanza::Capabilities.new
182 # TODO: user a better node URI (?)
183 caps.node = 'http://catapult.sgx.soprani.ca/'
184 caps.identities = user_cap_identities()
185 caps.features = user_cap_features()
186
187 msg = caps.c
188 msg.to = p.from
189 msg.from = p.to.to_s + '/sgx'
190
191 puts 'RESPONSE6: ' + msg.inspect
192 write_to_stream msg
193 end
194
195 iq '/iq/ns:jingle', :ns => 'urn:xmpp:jingle:1' do |i, jn|
196 puts "IQj: #{i.inspect}"
197
198 if jn[0]['action'] == 'transport-accept'
199 puts "REPLY0: #{i.reply.inspect}"
200 write_to_stream i.reply
201 next
202 elsif jn[0]['action'] == 'session-terminate'
203 # TODO: unexpected (usually we do this; handle?)
204 puts "TERMINATED"
205 next
206 elsif jn[0]['action'] == 'transport-info'
207 # TODO: unexpected, but should handle in a nice way
208 puts "FAIL!!!"
209 next
210 elsif i.type == :error
211 # TODO: do something, maybe terminating the connection
212 puts 'ERROR!!!'
213 next
214 end
215
216 # TODO: should probably confirm we got session-initiate here
217
218 write_to_stream i.reply
219 puts "RESPONSE8: #{i.reply.inspect}"
220
221 msg = Blather::Stanza::Iq.new :set
222 msg.to = i.from
223 msg.from = i.to
224
225 cn = jn.children.find { |v| v.element_name == "content" }
226 puts 'CN-name: ' + cn['name']
227 puts 'JN-sid: ' + jn[0]['sid']
228
229 ibb_found = false
230 last_sid = ''
231 for child in cn.children
232 if child.element_name == 'transport'
233 puts 'TPORT: ' + child.namespace.href
234 last_sid = child['sid']
235 if 'urn:xmpp:jingle:transports:ibb:1' ==
236 child.namespace.href
237
238 ibb_found = true
239 break
240 end
241 end
242 end
243
244 j = Nokogiri::XML::Node.new 'jingle',msg.document
245 j['xmlns'] = 'urn:xmpp:jingle:1'
246 j['sid'] = jn[0]['sid']
247 msg.add_child(j)
248
249 content = Nokogiri::XML::Node.new 'content',msg.document
250 content['name'] = cn['name']
251 content['creator'] = 'initiator'
252 j.add_child(content)
253
254 transport = Nokogiri::XML::Node.new 'transport',msg.document
255 # TODO: make block-size more variable and/or dependent on sender
256 transport['block-size'] = '4096'
257 transport['xmlns'] = 'urn:xmpp:jingle:transports:ibb:1'
258 if ibb_found
259 transport['sid'] = last_sid
260 j['action'] = 'session-accept'
261 j['responder'] = i.from
262
263 dsc = Nokogiri::XML::Node.new 'description',msg.document
264 dsc['xmlns'] = 'urn:xmpp:jingle:apps:file-transfer:3'
265 content.add_child(dsc)
266 else
267 # for Conversations - it tries s5b even if caps ibb-only
268 transport['sid'] = @uuid_gen.generate
269 j['action'] = 'transport-replace'
270 j['initiator'] = i.from
271 end
272 content.add_child(transport)
273
274 @jingle_sids[transport['sid']] = jn[0]['sid']
275
276 # TODO: save <date> as well? Gajim sends, Conversations does not
277 # TODO: save/validate <size> with eventual full received length
278 fname = cn.children.find { |v| v.element_name == "description"
279 }.children.find { |w| w.element_name == "offer"
280 }.children.find { |x| x.element_name == "file"
281 }.children.find { |y| y.element_name == "name" }
282 @jingle_fnames[transport['sid']] = fname.text
283
284 puts "RESPONSE9: #{msg.inspect}"
285 write_to_stream msg
286 end
287
288 iq '/iq/ns:open', :ns =>
289 'http://jabber.org/protocol/ibb' do |i, on|
290
291 puts "IQo: #{i.inspect}"
292
293 @partial_data[on[0]['sid']] = ''
294 write_to_stream i.reply
295 end
296
297 iq '/iq/ns:data', :ns =>
298 'http://jabber.org/protocol/ibb' do |i, dn|
299
300 @partial_data[dn[0]['sid']] += Base64.decode64(dn[0].text)
301 write_to_stream i.reply
302 end
303
304 iq '/iq/ns:close', :ns =>
305 'http://jabber.org/protocol/ibb' do |i, cn|
306
307 puts "IQc: #{i.inspect}"
308 write_to_stream i.reply
309
310 # TODO: refactor below so that "message :chat?" uses same code
311 num_dest = i.to.to_s.split('@', 2)[0]
312
313 if num_dest[0] != '+'
314 # check to see if a valid shortcode context is specified
315 num_and_context = num_dest.split(';', 2)
316 if num_and_context[1] and num_and_context[1] ==
317 'phone-context=ca-us.phone-context.soprani.ca'
318
319 # TODO: check if num_dest is fully numeric
320 num_dest = num_and_context[0]
321 else
322 # TODO: text re num not (yet) supportd/implmentd
323 write_to_stream error_msg(i.reply, nil,
324 :cancel, 'item-not-found')
325 next
326 end
327 end
328
329 bare_jid = i.from.to_s.split('/', 2)[0]
330 cred_key = "catapult_cred-" + bare_jid
331
332 # TODO: connect at start of program instead
333 conn = Hiredis::Connection.new
334 conn.connect(ARGV[4], ARGV[5].to_i)
335
336 conn.write ["EXISTS", cred_key]
337 if conn.read == 0
338 conn.disconnect
339
340 # TODO: add text re credentials not being registered
341 write_to_stream error_msg(i.reply, nil, :auth,
342 'registration-required')
343 next
344 end
345
346 conn.write ["LRANGE", cred_key, 0, 3]
347 user_id, api_token, api_secret, users_num = conn.read
348 conn.disconnect
349
350 # upload cached data to server (before success reply)
351 media_name = Time.now.utc.iso8601 + '_' + @uuid_gen.generate +
352 '_' + @jingle_fnames[cn[0]['sid']]
353 puts 'name to save: ' + media_name
354
355 uri = URI.parse('https://api.catapult.inetwork.com')
356 http = Net::HTTP.new(uri.host, uri.port)
357 http.use_ssl = true
358 request = Net::HTTP::Put.new('/v1/users/' + user_id +
359 '/media/' + media_name)
360 request.basic_auth api_token, api_secret
361 request.body = @partial_data[cn[0]['sid']]
362 response = http.request(request)
363
364 puts 'eAPI response to send: ' + response.to_s + ' with code ' +
365 response.code + ', body "' + response.body + '"'
366
367 if response.code != '200'
368 # TODO: add text re unexpected code; mention code number
369 write_to_stream error_msg(i.reply, nil, :cancel,
370 'internal-server-error')
371 next
372 end
373
374 uri = URI.parse('https://api.catapult.inetwork.com')
375 http = Net::HTTP.new(uri.host, uri.port)
376 http.use_ssl = true
377 request = Net::HTTP::Post.new('/v1/users/' + user_id +
378 '/messages')
379 request.basic_auth api_token, api_secret
380 request.add_field('Content-Type', 'application/json')
381 request.body = JSON.dump({
382 'from' => users_num,
383 'to' => num_dest,
384 'text' => '',
385 'media' => [
386 'https://api.catapult.inetwork.com/v1/users/' +
387 user_id + '/media/' + media_name],
388 'tag' => i.id # TODO: message has it?
389 # TODO: add back when Bandwidth AP supports it (?); now:
390 # "The ''messages'' resource property
391 # ''receiptRequested'' is not supported for MMS"
392 #'receiptRequested' => 'all',
393 #'callbackUrl' => ARGV[6]
394 })
395 response = http.request(request)
396
397 puts 'mAPI response to send: ' + response.to_s + ' with code ' +
398 response.code + ', body "' + response.body + '"'
399
400 if response.code != '201'
401 # TODO: add text re unexpected code; mention code number
402 write_to_stream error_msg(i.reply, nil, :cancel,
403 'internal-server-error')
404 next
405 end
406
407 @partial_data[cn[0]['sid']] = ''
408
409 # received the complete file so now close the stream
410 msg = Blather::Stanza::Iq.new :set
411 msg.to = i.from
412 msg.from = i.to
413
414 j = Nokogiri::XML::Node.new 'jingle',msg.document
415 j['xmlns'] = 'urn:xmpp:jingle:1'
416 j['action'] = 'session-terminate'
417 j['sid'] = @jingle_sids[cn[0]['sid']]
418 msg.add_child(j)
419
420 r = Nokogiri::XML::Node.new 'reason',msg.document
421 s = Nokogiri::XML::Node.new 'success',msg.document
422 r.add_child(s)
423 j.add_child(r)
424
425 puts 'RESPONSE1: ' + msg.inspect
426 write_to_stream msg
427 end
428
429 iq '/iq/ns:query', :ns =>
430 'http://jabber.org/protocol/disco#items' do |i, xpath_result|
431
432 write_to_stream i.reply
433 end
434
435 iq '/iq/ns:query', :ns =>
436 'http://jabber.org/protocol/disco#info' do |i, xpath_result|
437
438 if i.to.to_s.include? '@'
439 # TODO: confirm the node URL is expected using below
440 #puts "XR[node]: #{xpath_result[0]['node']}"
441
442 msg = i.reply
443 msg.identities = user_cap_identities()
444 msg.features = user_cap_features()
445
446 puts 'RESPONSE7: ' + msg.inspect
447 write_to_stream msg
448 next
449 end
450
451 msg = i.reply
452 msg.identities = [{:name =>
453 'Soprani.ca Gateway to XMPP - Catapult',
454 :type => 'sms-ctplt', :category => 'gateway'}]
455 msg.features = ["jabber:iq:register",
456 "jabber:iq:gateway", "jabber:iq:private",
457 "http://jabber.org/protocol/disco#info",
458 "http://jabber.org/protocol/commands",
459 "http://jabber.org/protocol/muc"]
460 write_to_stream msg
461 end
462
463 iq '/iq/ns:query', :ns => 'jabber:iq:register' do |i, qn|
464 puts "IQ: #{i.inspect}"
465
466 if i.type == :set
467 xn = qn.children.find { |v| v.element_name == "x" }
468
469 user_id = ''
470 api_token = ''
471 api_secret = ''
472 phone_num = ''
473
474 if xn.nil?
475 user_id = qn.children.find {
476 |v| v.element_name == "nick" }
477 api_token = qn.children.find {
478 |v| v.element_name == "username" }
479 api_secret = qn.children.find {
480 |v| v.element_name == "password" }
481 phone_num = qn.children.find {
482 |v| v.element_name == "phone" }
483 else
484 for field in xn.children
485 if field.element_name == "field"
486 val = field.children.find { |v|
487 v.element_name == "value" }
488
489 case field['var']
490 when 'nick'
491 user_id = val.text
492 when 'username'
493 api_token = val.text
494 when 'password'
495 api_secret = val.text
496 when 'phone'
497 phone_num = val.text
498 else
499 # TODO: error
500 puts "?: " +field['var']
501 end
502 end
503 end
504 end
505
506 if phone_num[0] != '+'
507 # TODO: add text re number not (yet) supported
508 write_to_stream error_msg(i.reply, qn, :cancel,
509 'item-not-found')
510 next
511 end
512
513 uri = URI.parse('https://api.catapult.inetwork.com')
514 http = Net::HTTP.new(uri.host, uri.port)
515 http.use_ssl = true
516 request = Net::HTTP::Get.new('/v1/users/' + user_id +
517 '/phoneNumbers/' + phone_num)
518 request.basic_auth api_token, api_secret
519 response = http.request(request)
520
521 puts 'API response: ' + response.to_s + ' with code ' +
522 response.code + ', body "' + response.body + '"'
523
524 if response.code == '200'
525 params = JSON.parse response.body
526 if params['numberState'] == 'enabled'
527 num_key = "catapult_num-" + phone_num
528
529 bare_jid = i.from.to_s.split('/', 2)[0]
530 cred_key = "catapult_cred-" + bare_jid
531
532 # TODO: pre-validate ARGV[5] is integer
533 conn = Hiredis::Connection.new
534 conn.connect(ARGV[4], ARGV[5].to_i)
535
536 # TODO: use SETNX instead
537 conn.write ["EXISTS", num_key]
538 if conn.read == 1
539 conn.disconnect
540
541 # TODO: add txt re num exists
542 write_to_stream error_msg(
543 i.reply, qn, :cancel,
544 'conflict')
545 next
546 end
547
548 conn.write ["EXISTS", cred_key]
549 if conn.read == 1
550 conn.disconnect
551
552 # TODO: add txt re already exist
553 write_to_stream error_msg(
554 i.reply, qn, :cancel,
555 'conflict')
556 next
557 end
558
559 conn.write ["SET", num_key, bare_jid]
560 if conn.read != 1
561 conn.disconnect
562
563 # TODO: catch/relay RuntimeError
564 # TODO: add txt re push failure
565 write_to_stream error_msg(
566 i.reply, qn, :cancel,
567 'internal-server-error')
568 next
569 end
570
571 conn.write ["RPUSH",cred_key,user_id]
572 conn.write ["RPUSH",cred_key,api_token]
573 conn.write ["RPUSH",cred_key,api_secret]
574 conn.write ["RPUSH",cred_key,phone_num]
575
576 # TODO: confirm cred_key list size == 4
577
578 for n in 1..4 do
579 # TODO: catch/relay RuntimeError
580 result = conn.read
581 if result != n
582 conn.disconnect
583
584 write_to_stream(
585 error_msg(
586 i.reply, qn, :cancel,
587 'internal-server-error')
588 )
589 next
590 end
591 end
592 conn.disconnect
593
594 write_to_stream i.reply
595 else
596 # TODO: add text re number disabled
597 write_to_stream error_msg(i.reply, qn,
598 :modify, 'not-acceptable')
599 end
600 elsif response.code == '401'
601 # TODO: add text re bad credentials
602 write_to_stream error_msg(i.reply, qn, :auth,
603 'not-authorized')
604 elsif response.code == '404'
605 # TODO: add text re number not found or disabled
606 write_to_stream error_msg(i.reply, qn, :cancel,
607 'item-not-found')
608 else
609 # TODO: add text re misc error, and mention code
610 write_to_stream error_msg(i.reply, qn, :modify,
611 'not-acceptable')
612 end
613
614 elsif i.type == :get
615 orig = i.reply
616
617 msg = Nokogiri::XML::Node.new 'query',orig.document
618 msg['xmlns'] = 'jabber:iq:register'
619 n1 = Nokogiri::XML::Node.new 'instructions',msg.document
620 n1.content= "Enter the information from your Account " +
621 "page as well as the Phone Number\nin your " +
622 "account you want to use (ie. '+12345678901')" +
623 ".\nUser Id is nick, API Token is username, " +
624 "API Secret is password, Phone Number is phone"+
625 ".\n\nThe source code for this gateway is at " +
626 "https://github.com/ossguy/sgx-catapult ." +
627 "\nCopyright (C) 2017 Denver Gingerich and " +
628 "others, licensed under AGPLv3+."
629 n2 = Nokogiri::XML::Node.new 'nick',msg.document
630 n3 = Nokogiri::XML::Node.new 'username',msg.document
631 n4 = Nokogiri::XML::Node.new 'password',msg.document
632 n5 = Nokogiri::XML::Node.new 'phone',msg.document
633 msg.add_child(n1)
634 msg.add_child(n2)
635 msg.add_child(n3)
636 msg.add_child(n4)
637 msg.add_child(n5)
638
639 x = Blather::Stanza::X.new :form, [
640 {:required => true, :type => :"text-single",
641 :label => 'User Id', :var => 'nick'},
642 {:required => true, :type => :"text-single",
643 :label => 'API Token', :var => 'username'},
644 {:required => true, :type => :"text-private",
645 :label => 'API Secret', :var => 'password'},
646 {:required => true, :type => :"text-single",
647 :label => 'Phone Number', :var => 'phone'}
648 ]
649 x.title= 'Register for ' +
650 'Soprani.ca Gateway to XMPP - Catapult'
651 x.instructions= "Enter the details from your Account " +
652 "page as well as the Phone Number\nin your " +
653 "account you want to use (ie. '+12345678901')" +
654 ".\n\nThe source code for this gateway is at " +
655 "https://github.com/ossguy/sgx-catapult ." +
656 "\nCopyright (C) 2017 Denver Gingerich and " +
657 "others, licensed under AGPLv3+."
658 msg.add_child(x)
659
660 orig.add_child(msg)
661 puts "RESPONSE2: #{orig.inspect}"
662 write_to_stream orig
663 puts "SENT"
664 end
665 end
666
667 subscription(:request?) do |s|
668 # TODO: are these the best to return? really need '!' here?
669 #write_to_stream s.approve!
670 #write_to_stream s.request!
671 end
672end
673
674[:INT, :TERM].each do |sig|
675 trap(sig) {
676 puts 'Shutting down gateway...'
677 SGXcatapult.shutdown
678 puts 'Gateway has terminated.'
679
680 EM.stop
681 }
682end
683
684class ReceiptMessage < Blather::Stanza
685 def self.new(to = nil)
686 node = super :message
687 node.to = to
688 node
689 end
690end
691
692class WebhookHandler < Goliath::API
693 def send_media(from, to, media_url)
694 # we assume media_url is of the form (always the case so far):
695 # https://api.catapult.inetwork.com/v1/users/[uid]/media/[file]
696
697 # the caller must guarantee that 'to' is a bare JID
698 proxy_url = ARGV[8] + to + '/' + media_url.split('/', 8)[7]
699
700 puts 'ORIG_URL: ' + media_url
701 puts 'PROX_URL: ' + proxy_url
702
703 # put URL in the body (so Conversations will still see it)...
704 msg = Blather::Stanza::Message.new(to, proxy_url)
705 msg.from = from
706
707 # ...but also provide URL in XEP-0066 (OOB) fashion
708 # TODO: confirm client supports OOB or don't send this
709 x = Nokogiri::XML::Node.new 'x', msg.document
710 x['xmlns'] = 'jabber:x:oob'
711
712 urln = Nokogiri::XML::Node.new 'url', msg.document
713 urlc = Nokogiri::XML::Text.new proxy_url, msg.document
714
715 urln.add_child(urlc)
716 x.add_child(urln)
717 msg.add_child(x)
718
719 SGXcatapult.write(msg)
720 end
721
722 def response(env)
723 puts 'ENV: ' + env.to_s
724 body = Rack::Request.new(env).body.read
725 puts 'BODY: ' + body
726 params = JSON.parse body
727
728 users_num = ''
729 others_num = ''
730 if params['direction'] == 'in'
731 users_num = params['to']
732 others_num = params['from']
733 elsif params['direction'] == 'out'
734 users_num = params['from']
735 others_num = params['to']
736 else
737 # TODO: exception or similar
738 puts "big problem: '" + params['direction'] + "'"
739 return [200, {}, "OK"]
740 end
741
742 num_key = "catapult_num-" + users_num
743
744 if others_num[0] != '+'
745 # TODO: check that others_num actually a shortcode first
746 others_num = others_num +
747 ';phone-context=ca-us.phone-context.soprani.ca'
748 end
749
750 conn = Hiredis::Connection.new
751 conn.connect(ARGV[4], ARGV[5].to_i)
752
753 conn.write ["EXISTS", num_key]
754 if conn.read == 0
755 conn.disconnect
756
757 puts "num_key (#{num_key}) DNE; Catapult misconfigured?"
758
759 # TODO: likely not appropriate; give error to Catapult?
760 # TODO: add text re credentials not being registered
761 #write_to_stream error_msg(m.reply, m.body, :auth,
762 # 'registration-required')
763 return [200, {}, "OK"]
764 end
765
766 conn.write ["GET", num_key]
767 bare_jid = conn.read
768 conn.disconnect
769
770 msg = ''
771 case params['direction']
772 when 'in'
773 text = ''
774 case params['eventType']
775 when 'sms'
776 text = params['text']
777 when 'mms'
778 has_media = false
779 params['media'].each do |media_url|
780 if not media_url.end_with?('.smil',
781 '.txt', '.xml')
782
783 has_media = true
784 send_media(others_num + '@' +
785 ARGV[0],
786 bare_jid, media_url)
787 end
788 end
789
790 if params['text'].empty?
791 if not has_media
792 text = '[suspected group msg ' +
793 'with no text (odd)]'
794 end
795 else
796 if has_media
797 # TODO: write/use a caption XEP
798 text = params['text']
799 else
800 text = '[suspected group msg ' +
801 '(recipient list not ' +
802 'available) with ' +
803 'following text] ' +
804 params['text']
805 end
806 end
807
808 # ie. if text param non-empty or had no media
809 if not text.empty?
810 msg = Blather::Stanza::Message.new(
811 bare_jid, text)
812 msg.from = others_num + '@' + ARGV[0]
813 SGXcatapult.write(msg)
814 end
815
816 return [200, {}, "OK"]
817 else
818 text = "unknown type (#{params['eventType']})" +
819 " with text: " + params['text']
820
821 # TODO log/notify of this properly
822 puts text
823 end
824
825 msg = Blather::Stanza::Message.new(bare_jid, text)
826 else # per prior switch, this is: params['direction'] == 'out'
827 msg = ReceiptMessage.new(bare_jid)
828
829 # TODO: put in member/instance variable
830 uuid_gen = UUID.new
831 msg['id'] = uuid_gen.generate
832
833 case params['deliveryState']
834 when 'not-delivered'
835 # TODO: add text re deliveryDescription reason
836 msg = SGXcatapult.error_msg(msg, nil, :cancel,
837 'service-unavailable')
838 return [200, {}, "OK"]
839 when 'delivered'
840 # TODO: send only when requested per XEP-0184
841 rcvd = Nokogiri::XML::Node.new 'received',
842 msg.document
843 rcvd['xmlns'] = 'urn:xmpp:receipts'
844 rcvd['id'] = params['tag']
845 msg.add_child(rcvd)
846 when 'waiting'
847 # can't really do anything with it; nice to know
848 puts "message with id #{params['tag']} waiting"
849 return [200, {}, "OK"]
850 else
851 # TODO: notify somehow of unknown state receivd?
852 puts "message with id #{params['tag']} has " +
853 "other state #{params['deliveryState']}"
854 return [200, {}, "OK"]
855 end
856
857 puts "RESPONSE4: #{msg.inspect}"
858 end
859
860 msg.from = others_num + '@' + ARGV[0]
861 SGXcatapult.write(msg)
862
863 [200, {}, "OK"]
864 end
865end
866
867EM.run do
868 SGXcatapult.run
869
870 server = Goliath::Server.new('0.0.0.0', ARGV[7].to_i)
871 server.api = WebhookHandler.new
872 server.app = Goliath::Rack::Builder.build(server.api.class, server.api)
873 server.logger = Log4r::Logger.new('goliath')
874 server.logger.add(Log4r::StdoutOutputter.new('console'))
875 server.logger.level = Log4r::INFO
876 server.start
877end