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