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