adammck-rubysms 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: noet
3
+
4
+
5
+ module SMS::Backend
6
+
7
+ # TODO: doc
8
+ def self.create(klass, label=nil, *args)
9
+
10
+ # if a class name was passed (rather
11
+ # than a real class), attempt to load
12
+ # the ruby source, and resolve the name
13
+ unless klass.is_a?(Class)
14
+ begin
15
+ src = File.dirname(__FILE__) +\
16
+ "/backend/#{klass.to_s.downcase}.rb"
17
+ require src
18
+
19
+ # if the backend couldn't be required, re-
20
+ # raise the error with a more useful message
21
+ rescue LoadError
22
+ raise LoadError.new(
23
+ "Couldn't load #{klass.inspect} " +\
24
+ "backend from: #{src}")
25
+ end
26
+
27
+ begin
28
+ klass = SMS::Backend.const_get(klass)
29
+
30
+ # if the constant couldn't be found,
31
+ # re-raise with a more useful message
32
+ rescue NameError
33
+ raise LoadError.new(
34
+ "Loaded #{klass.inspect} backend from " +\
35
+ "#{src}, but the SMS::Backend::#{klass} "+\
36
+ "class was not defined")
37
+ end
38
+ end
39
+
40
+ # create an instance of this backend,
41
+ # passing along the (optional) arguments
42
+ inst = klass.new(*args)
43
+
44
+ # apply the label, if one were provided.
45
+ # if not, the backend will provide its own
46
+ inst.label = label unless\
47
+ label.nil?
48
+
49
+ inst
50
+ end
51
+
52
+ class Base < SMS::Thing
53
+
54
+ # This method should be called (via super)
55
+ # by all backends before sending a message,
56
+ # so it can be logged, and apps are notified
57
+ def send_sms(msg)
58
+ router.outgoing(msg)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: noet
3
+
4
+
5
+ require "drb.rb"
6
+
7
+
8
+ module SMS::Backend
9
+ class DRB < Base
10
+ DRB_PORT = 1370
11
+
12
+ def start
13
+ begin
14
+
15
+ # start the DRb service, listening for connections
16
+ # from the RubySMS virtual device (virtual-device.rb)
17
+ drb = DRb.start_service("druby://localhost:#{DRB_PORT}", self)
18
+ log ["Started DRb Offline Backend", "URI: #{drb.uri}"], :init
19
+
20
+ # a hash to store incoming
21
+ # connections from drb clients
22
+ @injectors = {}
23
+ end
24
+ end
25
+
26
+ def send_sms(msg)
27
+ to = msg.phone_number
28
+ super
29
+
30
+ # if this is the first time that we
31
+ # have communicated with this DRb
32
+ # client, then initialize the object
33
+ unless @injectors.include?(to)
34
+ drbo = DRbObject.new_with_uri("druby://localhost:#{DRB_PORT}#{to}")
35
+ @injectors[to] = drbo
36
+ end
37
+
38
+ @injectors[to].incoming(msg.text)
39
+ end
40
+
41
+ # called from another ruby process, via
42
+ # drb, to simulate an incoming sms message
43
+ def incoming(sender, text)
44
+ router.incoming(
45
+ SMS::Incoming.new(
46
+ self, sender, Time.now, text))
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: noet
3
+
4
+
5
+ # attempt to load rubygsm using relative paths first,
6
+ # so we can easily run on the trunk by cloning from
7
+ # github. the dir structure should look something like:
8
+ #
9
+ # projects
10
+ # - rubygsms
11
+ # - rubygsm
12
+ begin
13
+ dev_dir = "#{dir}/../../../rubygsm"
14
+ dev_path = "#{dev_dir}/lib/rubygsm.rb"
15
+ require File.expand_path(dev_path)
16
+
17
+ rescue LoadError
18
+ begin
19
+
20
+ # couldn't load via relative
21
+ # path, so try loading the gem
22
+ require "rubygems"
23
+ require "rubygsm"
24
+
25
+ rescue LoadError
26
+
27
+ # nothing worked, so re-raise
28
+ # with more useful information
29
+ raise LoadError.new(
30
+ "Couldn't load RubyGSM relatively (tried: " +\
31
+ "#{dev_path.inspect}) or via RubyGems")
32
+
33
+ end
34
+ end
35
+
36
+
37
+ module SMS::Backend
38
+
39
+ # Provides an interface between RubyGSM and RubySMS,
40
+ # which allows RubySMS to send real SMS in an abstract
41
+ # fashion, which can be replicated by other backends.
42
+ # This backend is probably the thinnest layer between
43
+ # applications and the network, since the backend API
44
+ # (first implemented here) was based on RubyGSM.
45
+ class GSM < Base
46
+
47
+ # just store the arguments until the
48
+ # backend is ready to be started
49
+ def initialize(port=:auto, pin=nil)
50
+ @port = port
51
+ @pin = nil
52
+ end
53
+
54
+ def start
55
+
56
+ # lock the threads during modem initialization,
57
+ # simply to avoid the screen log being mixed up
58
+ Thread.exclusive do
59
+ begin
60
+ @gsm = ::Gsm::Modem.new(@port)
61
+ @gsm.use_pin(@pin) unless @pin.nil?
62
+ @gsm.receive method(:incoming)
63
+
64
+ #bands = @gsm.bands_available.join(", ")
65
+ #log "Using GSM Band: #{@gsm.band}MHz"
66
+ #log "Modem supports: #{bands}"
67
+
68
+ log "Waiting for GSM network..."
69
+ str = @gsm.wait_for_network
70
+ log "Signal strength is: #{str}"
71
+
72
+ # couldn't open the port. this usually means
73
+ # that the modem isn't plugged in to it...
74
+ rescue Errno::ENOENT, ArgumentError
75
+ log "Couldn't open #{@port}", :err
76
+ raise IOError
77
+
78
+ # something else went wrong
79
+ # while initializing the modem
80
+ rescue ::Gsm::Modem::Error => err
81
+ log ["Couldn't initialize the modem",
82
+ "RubyGSM Says: #{err.desc}"], :err
83
+ raise RuntimeError
84
+ end
85
+
86
+ # rubygsm didn't blow up?!
87
+ log "Started GSM Backend", :init
88
+ end
89
+ end
90
+
91
+ def send_sms(msg)
92
+ super
93
+
94
+ # send the message to the modem via rubygsm, and log
95
+ # if it failed. TODO: needs moar info from rubygsm
96
+ # on *why* sending failed
97
+ unless @gsm.send_sms(msg.recipient.phone_number, msg.text)
98
+ log "Message sending FAILED", :warn
99
+ end
100
+ end
101
+
102
+ # called back by rubygsm when an incoming
103
+ # message arrives, which we will pass on
104
+ # to rubysms to dispatch to applications
105
+ def incoming(msg)
106
+
107
+ # NOTE: the msg argument is a GSM::Incoming
108
+ # object from RubyGSM, NOT the more useful
109
+ # SMS::Incoming object from RubySMS
110
+
111
+ router.incoming(
112
+ SMS::Incoming.new(
113
+ self, msg.sender, msg.sent, msg.text))
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,414 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: noet
3
+
4
+
5
+ require "rubygems"
6
+ require "rack"
7
+
8
+
9
+ module SMS::Backend
10
+
11
+ # Provides a low-tech HTML webUI to inject mock SMS messages into
12
+ # RubySMS, and receive responses. This is usually used during app
13
+ # development, to provide a cross-platform method of simulating
14
+ # a two-way conversation with the SMS backend(s). Note, though,
15
+ # that there is no technical difference between the SMS::Incoming
16
+ # and SMS::Outgoing objects created by this backend, and those
17
+ # created by "real" incoming messages via the GSM backend.
18
+ #
19
+ # The JSON API used internally by this backend also be used by
20
+ # other HTML applications to communicate with RubySMS, but that
21
+ # is quite obscure, and isn't very well documented yet. Also, it
22
+ # sporadically changes without warning. May The Force be with you.
23
+ class HTTP < Base
24
+ HTTP_PORT = 1270
25
+ MT_URL = "http://ajax.googleapis.com/ajax/libs/mootools/1.2.1/mootools-yui-compressed.js"
26
+ attr_reader :msg_log
27
+
28
+ def initialize(port=HTTP_PORT, mootools_url=MT_URL)
29
+ @app = RackApp.new(self, mootools_url)
30
+ @port = port
31
+
32
+ # initialize the log, which returns empty
33
+ # arrays (new session) for unknown keys
34
+ # to avoid initializing sessions all over
35
+ @msg_log = {}
36
+ end
37
+
38
+ # Starts a thread-blocking Mongrel to serve
39
+ # SMS::Backend::HTTP::RackApp, and never returns.
40
+ def start
41
+
42
+ # add a screen log message, which is kind of
43
+ # a lie, because we haven't started anything yet
44
+ uri = "http://localhost:#{@port}"
45
+ log ["Started HTTP Offline Backend", "URI: #{uri}"], :init
46
+
47
+ # this is goodbye
48
+ Rack::Handler::Mongrel.run(
49
+ @app, :Port=>@port)
50
+ end
51
+
52
+ # outgoing message from RubySMS (probably
53
+ # in response to an incoming, but maybe a
54
+ # blast or other unsolicited message). do
55
+ # nothing except add it to the log, for it
56
+ # to be picked up next time someone looks
57
+ def send_sms(msg)
58
+ s = msg.recipient.phone_number
59
+ t = msg.text
60
+
61
+ # allow RubySMS to notify the router
62
+ # this is a giant ugly temporary hack
63
+ super
64
+
65
+ # init the message log for
66
+ # this session if necessary
67
+ @msg_log[s] = []\
68
+ unless @msg_log.has_key?(s)
69
+
70
+ # add the outgoing message to the log
71
+ msg_id = @msg_log[s].push\
72
+ [t.object_id.abs.to_s, "out", t]
73
+ end
74
+
75
+
76
+
77
+
78
+ # This simple Rack application handles the few
79
+ # HTTP requests that this backend will serve:
80
+ #
81
+ # GET / -- redirect to a random blank session
82
+ # GET /123456.json -- export session 123456 as JSON data
83
+ # GET /123456 -- view session 123456 (actually a
84
+ # static HTML page which fetches
85
+ # the data via javascript+json)
86
+ # POST /123456/send -- add a message to session 123456
87
+ class RackApp
88
+ def initialize(http_backend, mootools_url)
89
+ @backend = http_backend
90
+ @mt_url = mootools_url
91
+
92
+ # generate the html to be returned by replacing the
93
+ # variables in the constant with our instance vars
94
+ @html = HTML.sub(/%(\w+)%/) do
95
+ instance_variable_get("@#{$1}")
96
+ end
97
+ end
98
+
99
+ def call(env)
100
+ req = Rack::Request.new(env)
101
+ path = req.path_info
102
+
103
+ if req.get?
104
+
105
+ # serve GET /
106
+ # for requests not containing a session id, generate a random
107
+ # new one (between 111111 and 999999) and redirect back to it
108
+ if path == "/"
109
+ while true
110
+
111
+ # randomize a session id, and stop looping if
112
+ # it's empty - this is just to avoid accidentally
113
+ # jumping into someone elses session (although
114
+ # that's allowed, if explicly requested)
115
+ new_session = (111111 + rand(888888)).to_s
116
+ break unless @backend.msg_log.has_key?(new_session)
117
+ end
118
+
119
+ return [
120
+ 301,
121
+ {"location" => "/#{new_session}"},
122
+ "Redirecting to session #{new_session}"]
123
+
124
+ # serve GET /123456
125
+ elsif m = path.match(/^\/\d{6}$/)
126
+
127
+ # just so render the static HTML content (the
128
+ # log contents are rendered via JSON, above)
129
+ return [200, {"content-type" => "text/html"}, @html]
130
+
131
+ # serve GET /123456.json
132
+ elsif m = path.match(/^\/(\d{6})\.json$/)
133
+ msgs = @backend.msg_log[m.captures[0]] || []
134
+
135
+ return [
136
+ 200,
137
+ {"content-type" => "application/json"},
138
+ "[" + (msgs.collect { |msg| msg.inspect }.join(", ")) + "]"]
139
+
140
+ # serve GET /favicon.ico
141
+ # as if YOU'VE never wasted
142
+ # a minute on frivolous crap
143
+ elsif path == "/favicon.ico"
144
+ icon = File.dirname(__FILE__) + "/cellphone.ico"
145
+ return [200, {"content-type" => "image/x-ico"}, File.read(icon)]
146
+ end
147
+
148
+ # serve POST /123456/send
149
+ elsif (m = path.match(/^\/(\d{6})\/send$/)) && req.post?
150
+ t = req.POST["msg"]
151
+ s = m.captures[0]
152
+
153
+ # init the message log for
154
+ # this session if necessary
155
+ @backend.msg_log[s] = []\
156
+ unless @backend.msg_log.has_key?(s)
157
+
158
+ # log the incoming message, so it shows
159
+ # up in the two-way "conversation"
160
+ msg_id = @backend.msg_log[s].push\
161
+ [t.object_id.abs.to_s, "in", t]
162
+
163
+ # push the incoming message
164
+ # into RubySMS, to distribute
165
+ # to each application
166
+ @backend.router.incoming(
167
+ SMS::Incoming.new(
168
+ @backend, s, Time.now, t))
169
+
170
+ # acknowledge POST with
171
+ # the new message ID
172
+ return [
173
+ 200,
174
+ {"content-type" => "text/plain" },
175
+ t.object_id.abs.to_s]
176
+ end
177
+
178
+ # nothing else is valid. not 404, because it might be
179
+ # an invalid method, and i can't be arsed right now.
180
+ [500, {"content-type" => "text/plain" }, "FAIL."]
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ SMS::Backend::HTTP::HTML = <<EOF
187
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
188
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
189
+ <html>
190
+ <head>
191
+ <title>RubySMS Virtual Device</title>
192
+ <script id="mt" type="text/javascript" src="%mt_url%"></script>
193
+ <style type="text/css">
194
+
195
+ /* remove m+p from most elements,
196
+ * without resetting form elements */
197
+ body, h1, #log, #log li, form {
198
+ margin: 0;
199
+ padding: 0;
200
+ }
201
+
202
+ body {
203
+ line-height: 1;
204
+ font: 8pt sans-serif;
205
+ background: #eef;
206
+ padding: 2em;
207
+ }
208
+
209
+ body.framed {
210
+ background: transparent;
211
+ padding: 0;
212
+ }
213
+
214
+ body.framed #wrapper {
215
+ position: absolute;
216
+ width: 100%;
217
+ bottom: 0;
218
+ left: 0;
219
+
220
+ }
221
+
222
+ #wrapper div {
223
+ padding: 0.5em;
224
+ background: #33a7d2;
225
+ }
226
+
227
+ h1 {
228
+ font-size: 100%;
229
+ color: #fff;
230
+ }
231
+
232
+ #log {
233
+ height: 14em;
234
+ overflow-y: scroll;
235
+ font-family: monospace;
236
+ background: #fff;
237
+ margin: 0.5em 0;
238
+ }
239
+
240
+ #log li {
241
+ line-height: 1.4;
242
+ list-style: none;
243
+ white-space: pre;
244
+ padding: 0.5em;
245
+ }
246
+
247
+ #log li.in {
248
+ color: #888;
249
+ border-top: 1px dotted #000;
250
+ }
251
+
252
+ /* don't show the divider
253
+ * at the top of the log */
254
+ #log li:first-child.in {
255
+ border-top: 0; }
256
+
257
+ #log li.out {
258
+ color: #000;
259
+ background: #f8f8f8;
260
+ }
261
+
262
+ /* messages prior to the latest
263
+ * are dim, to enhance readability */
264
+ #log li.old {
265
+ border-top: 0;
266
+ color: #ddd;
267
+ }
268
+
269
+ #log li.error {
270
+ color: #f00;
271
+ }
272
+
273
+ form { }
274
+
275
+ form input {
276
+ -moz-box-sizing: border-box;
277
+ width: 100%;
278
+ }
279
+ </style>
280
+ </head>
281
+ <body>
282
+ <div id="wrapper">
283
+ <div>
284
+ <h1>RubySMS Virtual Device</h1>
285
+
286
+ <ul id="log">
287
+ </ul>
288
+
289
+ <form id="send" method="post">
290
+ <input type="text" id="msg" name="msg" />
291
+ <!--<input type="submit" value="Send" />-->
292
+ </form>
293
+ </div>
294
+ </div>
295
+
296
+ <script type="text/javascript">
297
+ /* if mootools wasn't loaded (ie, the internet at this shitty
298
+ * african hotel is broken again), just throw up a warning */
299
+ if(typeof(MooTools) == "undefined") {
300
+ var err = [
301
+ "Couldn't load MooTools from: " + document.getElementById("mt").src,
302
+ "This interface will not work without it, because I'm a lazy programmer. Sorry."
303
+ ].join("\\n");
304
+ document.getElementById("log").innerHTML = '<li class="error">' + err + '</li>';
305
+
306
+ } else {
307
+ window.addEvent("domready", function() {
308
+
309
+ /* if this window is not the top-level
310
+ * window (ie, it has been included in
311
+ * an iframe), then add a body class
312
+ * to style things slightly differently */
313
+ if (window != top) {
314
+ $(document.body).addClass("framed");
315
+ }
316
+
317
+ // extract the session id from the URI
318
+ var session_id = location.pathname.replace(/[^0-9]/g, "");
319
+
320
+ /* for storing the timeout, so we
321
+ * can ensure that only one fetch
322
+ * is running at a time */
323
+ var timeout = null;
324
+
325
+ // the scrolling message log
326
+ var log = $("log");
327
+
328
+ /* function to be called when it is time
329
+ * to update the log by polling the server */
330
+ var update = function(msg_id) {
331
+ $clear(timeout);
332
+
333
+ new Request.JSON({
334
+ "method": "get",
335
+ "url": "/" + session_id + ".json",
336
+ "onSuccess": function(json) {
337
+ var dimmed_old = false;
338
+
339
+ json.each(function(msg) {
340
+ var msg_id = "msg-" + msg[0];
341
+
342
+ /* iterate the items returned by the JSON request, and append
343
+ * any new messages to the message log, in order of receipt */
344
+ if ($(msg_id) == null) {
345
+
346
+ /* before adding new messages, add a class
347
+ * to the existing messages, to dim them */
348
+ if (!dimmed_old) {
349
+ log.getElements("li").addClass("old");
350
+ dimmed_old = true;
351
+ }
352
+
353
+ /* create the new element, and inject it into
354
+ * the log (msg[1] contains "in" or "out"). */
355
+ new Element("li", {
356
+ "text": ((msg[2] == "") ? "<blank>" : msg[2]),
357
+ "class": msg[1],
358
+ "id": msg_id
359
+ }).inject(log);
360
+ }
361
+ });
362
+
363
+ /* if the update function was called in response
364
+ * to an outgoing message (via the #send.onComplete
365
+ * event, below), a msg_id will have been returned
366
+ * by the POST request. this msg should now be in
367
+ * the log, so scroll to it, so we can quickly see
368
+ * the response */
369
+ if (msg_id != null) {
370
+ var msg_el = $("msg-" + msg_id);
371
+ if (msg_el != null) {
372
+
373
+ log.scrollTo(0, msg_el.getPosition(log)["y"]);
374
+ }
375
+
376
+ /* if it was called independantly, just scroll to
377
+ * the bottom of the log (Infinity doesn't work!) */
378
+ } else {
379
+ log.scrollTo(0, 9999);
380
+ }
381
+
382
+ /* call again in 30 seconds, to check for
383
+ * unsolicited messages once in a while */
384
+ timeout = update.delay(30000);
385
+ }
386
+ }).send();
387
+ };
388
+
389
+ /* when a message is posted via AJAX,
390
+ * reload the load to include it */
391
+ $("send").set("send", {
392
+ "url": "/" + session_id + "/send",
393
+ "onComplete": update
394
+
395
+ /* submit the form via ajax,
396
+ * and cancel the full-page */
397
+ }).addEvent("submit", function(ev) {
398
+ this.send();
399
+ ev.stop();
400
+
401
+ /* clear the text entry field to
402
+ * make way for the next message */
403
+ $("msg").value = "";
404
+ });
405
+
406
+ /* update the log now, in case there
407
+ * is already anything in the log */
408
+ update();
409
+ });
410
+ }
411
+ </script>
412
+ </body>
413
+ </html>
414
+ EOF