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,17 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: noet
3
+
4
+
5
+ module SMS
6
+ class InvalidBackend < Exception
7
+ end
8
+
9
+ class CancelIncoming < StandardError
10
+ end
11
+
12
+ class CancelOutgoing < StandardError
13
+ end
14
+
15
+ class Respond < Interrupt
16
+ end
17
+ end
@@ -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