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.
- 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
|