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