adammck-rubysms 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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