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