adammck-rubysms 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +50 -0
- data/bin/rubysms-drb-client +168 -0
- data/lib/drb-client.glade +90 -0
- data/lib/rubysms.rb +39 -0
- data/lib/rubysms/application.rb +207 -0
- data/lib/rubysms/backend.rb +61 -0
- data/lib/rubysms/backend/cellphone.ico +0 -0
- data/lib/rubysms/backend/drb.rb +49 -0
- data/lib/rubysms/backend/gsm.rb +116 -0
- data/lib/rubysms/backend/http.rb +414 -0
- data/lib/rubysms/errors.rb +17 -0
- data/lib/rubysms/logger.rb +69 -0
- data/lib/rubysms/message/incoming.rb +58 -0
- data/lib/rubysms/message/outgoing.rb +40 -0
- data/lib/rubysms/person.rb +47 -0
- data/lib/rubysms/router.rb +198 -0
- data/lib/rubysms/thing.rb +38 -0
- data/rubysms.gemspec +47 -0
- metadata +99 -0
@@ -0,0 +1,69 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim: noet
|
3
|
+
|
4
|
+
module SMS
|
5
|
+
class Logger
|
6
|
+
def initialize(stream=$stdout)
|
7
|
+
@stream = stream
|
8
|
+
end
|
9
|
+
|
10
|
+
def event(str, type=:info)
|
11
|
+
|
12
|
+
# arrays or strings are fine. quack!
|
13
|
+
str = str.join("\n") if str.respond_to?(:join)
|
14
|
+
|
15
|
+
# each item in the log is prefixed by a four-char
|
16
|
+
# coloured prefix block, to help scanning by eye
|
17
|
+
prefix_text = LogPrefix[type] || type.to_s
|
18
|
+
prefix = colored(prefix_text, type)
|
19
|
+
|
20
|
+
# the first line of the message is indented by
|
21
|
+
# the prefix, so indent subsequent lines by an
|
22
|
+
# equal amount of space, to keep them lined up
|
23
|
+
indent = colored((" " * prefix_text.length), type) + " "
|
24
|
+
@stream.puts prefix + " " + str.to_s.gsub("\n", "\n#{indent}")
|
25
|
+
|
26
|
+
# flush immediately, to prevent
|
27
|
+
# the log being backend up
|
28
|
+
@stream.flush
|
29
|
+
end
|
30
|
+
|
31
|
+
def event_with_time(str, *rest)
|
32
|
+
event("#{time_log(Time.now)} #{str}", *rest)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Returns a short timestamp suitable
|
38
|
+
# for embedding in the screen log.
|
39
|
+
def time_log(dt=nil)
|
40
|
+
dt = DateTime.now unless dt
|
41
|
+
dt.strftime("%I:%M%p")
|
42
|
+
end
|
43
|
+
|
44
|
+
def colored(str, color)
|
45
|
+
|
46
|
+
# resolve named colors
|
47
|
+
# to their ANSI color
|
48
|
+
color = LogColors[color]\
|
49
|
+
if color.is_a? Symbol
|
50
|
+
|
51
|
+
# return the ugly ANSI string
|
52
|
+
"\e[#{color};37;1m#{str}\e[0m"
|
53
|
+
end
|
54
|
+
|
55
|
+
LogColors = {
|
56
|
+
:init => 46,
|
57
|
+
:info => 40,
|
58
|
+
:warn => 41,
|
59
|
+
:err => 41,
|
60
|
+
:in => 45,
|
61
|
+
:out => 45 }
|
62
|
+
|
63
|
+
LogPrefix = {
|
64
|
+
:info => " ",
|
65
|
+
:err => "FAIL",
|
66
|
+
:in => " << ",
|
67
|
+
:out => " >> " }
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim: noet
|
3
|
+
|
4
|
+
|
5
|
+
module SMS
|
6
|
+
class Incoming
|
7
|
+
attr_reader :sent, :received, :text
|
8
|
+
attr_reader :backend, :sender, :responses
|
9
|
+
|
10
|
+
def initialize(backend, sender, sent, text)
|
11
|
+
|
12
|
+
# move all arguments into read-only
|
13
|
+
# attributes. ugly, but Struct only
|
14
|
+
# supports read/write attrs
|
15
|
+
@backend = backend
|
16
|
+
@sent = sent
|
17
|
+
@text = text
|
18
|
+
|
19
|
+
# Sets @sender, transforming _sender_ into an SMS::Person if
|
20
|
+
# it isn't already (to enable persistance between :Outgoing
|
21
|
+
# and/or SMS::Incoming objects)
|
22
|
+
@sender = sender.is_a?(SMS::Person) ? sender : SMS::Person.fetch(backend, sender)
|
23
|
+
|
24
|
+
# assume that the message was
|
25
|
+
# received right now, since we
|
26
|
+
# don't have an incoming buffer
|
27
|
+
@received = Time.now
|
28
|
+
|
29
|
+
# initialize a place for responses
|
30
|
+
# to this message to live, to be
|
31
|
+
# extracted (for logging?) later
|
32
|
+
@responses = []
|
33
|
+
end
|
34
|
+
|
35
|
+
# Creates an SMS::Outgoing object, adds it to _@responses_, and links
|
36
|
+
# it back to this SMS::Incoming object via Outgoing#in_response_to.
|
37
|
+
# IMPORTANT: This method doesn't actually SEND the message, it just
|
38
|
+
# creates it - use Incoming#respond to create an send in one call.
|
39
|
+
# This is most useful when you want to quickly create a response,
|
40
|
+
# modify it a bit, and send it.
|
41
|
+
def create_response(response_text)
|
42
|
+
og = SMS::Outgoing.new(backend, sender, response_text)
|
43
|
+
og.in_response_to = self
|
44
|
+
@responses.push(og)
|
45
|
+
og
|
46
|
+
end
|
47
|
+
|
48
|
+
# Same as Incoming#respond, but also sends the message.
|
49
|
+
def respond(response_text)
|
50
|
+
create_response(response_text).send!
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns the phone number of the sender of this message.
|
54
|
+
def phone_number
|
55
|
+
sender.phone_number
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim: noet
|
3
|
+
|
4
|
+
|
5
|
+
module SMS
|
6
|
+
class Outgoing
|
7
|
+
attr_accessor :text, :in_response_to
|
8
|
+
attr_reader :backend, :recipient, :sent
|
9
|
+
|
10
|
+
def initialize(backend, recipient=nil, text=nil)
|
11
|
+
|
12
|
+
# move all arguments into instance
|
13
|
+
# vars, to be accessed by accessors
|
14
|
+
@backend = backend
|
15
|
+
@text = text
|
16
|
+
|
17
|
+
# Sets @recipient, transforming _recipient_ into an SMS::Person if
|
18
|
+
# it isn't already (to enable persistance between :Outgoing and/or
|
19
|
+
# SMS::Incoming objects)
|
20
|
+
@recipient = recipient.is_a?(SMS::Person) ? recipient : SMS::Person.fetch(backend, recipient)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Sends the message via _@backend_ NOW, and
|
24
|
+
# prevents any further modifications to self,
|
25
|
+
# to avoid the object misrepresenting reality.
|
26
|
+
def send!
|
27
|
+
backend.send_sms(self)
|
28
|
+
@sent = Time.now
|
29
|
+
|
30
|
+
# once sent, allow no
|
31
|
+
# more modifications
|
32
|
+
freeze
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the phone number of the recipient of this message.
|
36
|
+
def phone_number
|
37
|
+
recipient.phone_number
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim: noet
|
3
|
+
|
4
|
+
module SMS
|
5
|
+
class Person < Hash
|
6
|
+
@@people = {}
|
7
|
+
|
8
|
+
def self.fetch(backend, key)
|
9
|
+
|
10
|
+
# if a people hash doesn't exist for
|
11
|
+
# this backend yet, create one now
|
12
|
+
unless @@people.has_key?(backend)
|
13
|
+
@@people[backend] = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
# if a person object doesn't already exist for this key,
|
17
|
+
# create one (to be fetched again next time around)
|
18
|
+
unless @@people[backend].has_key?(key)
|
19
|
+
p = SMS::Person.new(backend, key)
|
20
|
+
@@people[backend][key] = p
|
21
|
+
end
|
22
|
+
|
23
|
+
# return the persistant person,
|
24
|
+
# which may or may not be new
|
25
|
+
@@people[backend][key]
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
attr_reader :backend, :key
|
30
|
+
|
31
|
+
def initialize(backend, key)
|
32
|
+
@backend = backend
|
33
|
+
@key = key
|
34
|
+
end
|
35
|
+
|
36
|
+
# Return this person is a vaguely useful
|
37
|
+
# way that doesn't look too out of place.
|
38
|
+
def to_s
|
39
|
+
"#<SMS::Person backend=#{backend.label}, key=#{key}>"
|
40
|
+
end
|
41
|
+
|
42
|
+
# wat
|
43
|
+
def phone_number
|
44
|
+
key
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim: noet
|
3
|
+
|
4
|
+
module SMS
|
5
|
+
class Router
|
6
|
+
attr_reader :apps, :backends
|
7
|
+
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@log = Logger.new(STDOUT)
|
11
|
+
@backends = []
|
12
|
+
@apps = []
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
# proxy methods to pass events
|
17
|
+
# to the logger with the pretty
|
18
|
+
|
19
|
+
def log(*args)
|
20
|
+
@log.event(*args)
|
21
|
+
end
|
22
|
+
|
23
|
+
def log_with_time(*args)
|
24
|
+
@log.event_with_time(*args)
|
25
|
+
end
|
26
|
+
|
27
|
+
def log_exception(error, prefix_message=nil)
|
28
|
+
msgs = [error.class, error.message]
|
29
|
+
|
30
|
+
# if a prefix was provided (to give a litle
|
31
|
+
# more info on what went wrong), prepend it
|
32
|
+
# to the output with a blank line
|
33
|
+
unless prefix_message.nil?
|
34
|
+
msg.shift prefix_message, ""
|
35
|
+
end
|
36
|
+
|
37
|
+
# add each line until the current frame is within
|
38
|
+
# rubysms (the remainder will just be from gems)
|
39
|
+
catch(:done) do
|
40
|
+
error.backtrace.each do |line|
|
41
|
+
if line =~ /^#{SMS::Root}/
|
42
|
+
throw :done
|
43
|
+
end
|
44
|
+
|
45
|
+
# still within the application,
|
46
|
+
# so add the frame to the log
|
47
|
+
msgs.push(" " + line)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
@log.event msgs, :warn
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
# Starts listening for incoming messages
|
56
|
+
# on all backends, and never returns.
|
57
|
+
def serve_forever
|
58
|
+
|
59
|
+
# (attempt to) start up each
|
60
|
+
# backend in a separate thread
|
61
|
+
@backends.each do |b|
|
62
|
+
Thread.new do
|
63
|
+
b.start
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# applications don't need their own
|
68
|
+
# thread (they're notified in serial),
|
69
|
+
# but do have a #start method
|
70
|
+
@apps.each { |a| a.start }
|
71
|
+
|
72
|
+
# catch interrupts and display a nice message (rather than
|
73
|
+
# a backtrace). to avoid seeing control characters (^C) in
|
74
|
+
# the output, disable the "echoctl" option in your terminal
|
75
|
+
# (i added "stty -echoctl" to my .bashrc)
|
76
|
+
trap("INT") do
|
77
|
+
log "Shutting down", :init
|
78
|
+
|
79
|
+
# fire the "stop" method of
|
80
|
+
# each application and backend
|
81
|
+
# before terminating the process
|
82
|
+
(@backends + @apps).each do |inst|
|
83
|
+
inst.stop
|
84
|
+
end
|
85
|
+
|
86
|
+
exit
|
87
|
+
end
|
88
|
+
|
89
|
+
# block until ctrl+c
|
90
|
+
while true do
|
91
|
+
sleep 5
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Accepts an SMS::Backend::Base or SMS::App instance,
|
96
|
+
# which is stored until _serve_forever_ is called.
|
97
|
+
# DEPRECATED because it's confusing and magical.
|
98
|
+
def add(something)
|
99
|
+
log "Router#add is deprecated; use " +\
|
100
|
+
"#add_backend and #add_app", :warn
|
101
|
+
|
102
|
+
if something.is_a? SMS::Backend::Base
|
103
|
+
@backends.push(something)
|
104
|
+
|
105
|
+
elsif something.is_a? SMS::App
|
106
|
+
@apps.push(something)
|
107
|
+
|
108
|
+
else
|
109
|
+
raise RuntimeError,
|
110
|
+
"Router#add doesn't know what " +\
|
111
|
+
"to do with a #{something.klass}"
|
112
|
+
end
|
113
|
+
|
114
|
+
# store a reference back to this router in
|
115
|
+
# the app or backend, so it can talk back
|
116
|
+
something.router = self
|
117
|
+
end
|
118
|
+
|
119
|
+
# Adds an SMS application (which is usually an instance of a subclass
|
120
|
+
# of SMS::App, but anything's fine, so long as it quacks the right way)
|
121
|
+
# to this router, which will be started once _serve_forever_ is called.
|
122
|
+
def add_app(app)
|
123
|
+
@apps.push(app)
|
124
|
+
app.router = self
|
125
|
+
end
|
126
|
+
|
127
|
+
# Adds an SMS backend (which MUST be is_a?(SMS::Backend::Base), for now),
|
128
|
+
# or a symbol representing a loadable SMS backend, which is passed on to
|
129
|
+
# SMS::Backend.create (along with *args) to be required and initialized.
|
130
|
+
# This only really works with built-in backends, for now, but is useful
|
131
|
+
# for initializing those:
|
132
|
+
#
|
133
|
+
# # start serving with a single
|
134
|
+
# # http backend on port 9000
|
135
|
+
# router = SMS::Router.new
|
136
|
+
# router.add_backend(:HTTP, 9000)
|
137
|
+
# router.serve_forever
|
138
|
+
#
|
139
|
+
# # start serving on two gsm
|
140
|
+
# # modems with pin numbers
|
141
|
+
# router = SMS::Router.new
|
142
|
+
# router.add_backend(:GSM, "/dev/ttyS0", 1234)
|
143
|
+
# router.add_backend(:GSM, "/dev/ttyS1", 5678)
|
144
|
+
# router.serve_forever
|
145
|
+
def add_backend(backend, *args)
|
146
|
+
|
147
|
+
# if a backend object was given, add it to this router
|
148
|
+
# TODO: this modifies the argument just slightly. would
|
149
|
+
# it be better to duplicate the object first?
|
150
|
+
if backend.is_a?(SMS::Backend::Base)
|
151
|
+
@backends.push(backend)
|
152
|
+
backend.router = self
|
153
|
+
|
154
|
+
# if it's a named backend, spawn it (along
|
155
|
+
# with the optional arguments) and recurse
|
156
|
+
elsif backend.is_a?(Symbol)
|
157
|
+
add_backend SMS::Backend.create(backend, *args)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Relays a given incoming message from a
|
162
|
+
# specific backend to all applications.
|
163
|
+
def incoming(msg)
|
164
|
+
log_with_time "[#{msg.backend.label}] #{msg.sender.key}: #{msg.text} (#{msg.text.length})", :in
|
165
|
+
|
166
|
+
# notify each application of the message.
|
167
|
+
# they may or may not respond to it
|
168
|
+
@apps.each do |app|
|
169
|
+
begin
|
170
|
+
app.incoming msg
|
171
|
+
|
172
|
+
# something went boom in the app
|
173
|
+
# log it, and continue with the next
|
174
|
+
rescue StandardError => err
|
175
|
+
log_exception(err)
|
176
|
+
|
177
|
+
# if msg.responses.empty?
|
178
|
+
# msg.respond("Sorry, there was an error while processing your message.")
|
179
|
+
# end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Notifies each application of an outgoing message, and
|
185
|
+
# logs it. Should be called by all backends prior to sending.
|
186
|
+
def outgoing(msg)
|
187
|
+
log_with_time "[#{msg.backend.label}] #{msg.recipient.key}: #{msg.text} (#{msg.text.length})", :out
|
188
|
+
log("Outgoing message exceeds 140 characters", :warn) if msg.text.length > 140
|
189
|
+
cancelled = false
|
190
|
+
|
191
|
+
# notify each app of the outgoing sms
|
192
|
+
# note that the sending can still fail
|
193
|
+
@apps.each do |app|
|
194
|
+
app.outgoing msg
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim: noet
|
3
|
+
|
4
|
+
module SMS
|
5
|
+
class Thing
|
6
|
+
attr_accessor :router, :label
|
7
|
+
|
8
|
+
|
9
|
+
# stubs to avoid respond_to? or
|
10
|
+
# NoMethodError on subclasses
|
11
|
+
def incoming(*args); end
|
12
|
+
def outgoing(*args); end
|
13
|
+
def start(*args); end
|
14
|
+
def stop(*args); end
|
15
|
+
|
16
|
+
def label
|
17
|
+
@label or self.class.to_s.scan(/[a-z]+\Z/i).first
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
# proxy method(s) back to the router, so
|
24
|
+
# apps and backends can log things merrily
|
25
|
+
|
26
|
+
def log(*args)
|
27
|
+
router.log(*args)
|
28
|
+
end
|
29
|
+
|
30
|
+
def log_with_time(*args)
|
31
|
+
router.log_with_time(*args)
|
32
|
+
end
|
33
|
+
|
34
|
+
def log_exception(*args)
|
35
|
+
router.log_exception(*args)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|