Class: SMS::App

Inherits:
Thing show all
Defined in:
lib/rubysms/application.rb

Constant Summary collapse

NAMED_PRIORITY =
{
	:highest => 100,
	:high    => 90,
	:normal  => 50,
	:low     => 10,
	:lowest  => 0
}

Instance Attribute Summary

Attributes inherited from Thing

#label, #router

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Thing

#outgoing, #start, #stop

Class Method Details

.method_added(meth) ⇒ Object



276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/rubysms/application.rb', line 276

def method_added(meth)
	if @serve
		@services = []\
			unless @services
		
		# add this method, along with the last stored
		# regex, to the map of services for this app.
		# the default 'incoming' method will iterate
		# the regexen, and redirect the message to
		# the method linked here
		@services.push([meth, @serve])
		@serve = nil
	end
end

.priority(priority = nil) ⇒ Object

Sets or returns the priority of this application class. Returning this value isn’t tremendously useful by itself, and mostly exists for the sake of completeness, and to be called by Application#priority. The value returned is obtained by finding the first ancestor of this class which has a @priority (yes, it looks inside other classes instance variables. I’m sorry.), and converts it to a number via the SMS::App::NAMED_PRIORITY constant.

class One < SMS::App
  priority :high
end

class Two < One
end

class Three < Two
  priority 36
end

One.priority   => 90 # set via NAMED_PRIORITY
Two.priority   => 90 # inherited from One
Three.priority => 36 # set literally


88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/rubysms/application.rb', line 88

def self.priority(priority=nil)

	# set the priority of this class if an argument
	# were provided, and allow execution to continue
	# to check it's validity
	unless priority.nil?
		@priority = priority
	end
	
	# find the first ancestor with a priority
	# (Class.ancestors *includes* self)
	self.ancestors.each do |klass|
		if klass.instance_variable_defined?(:@priority)
			prio = klass.instance_variable_get(:@priority)
			
			# literal numbers are okay, although
			# that probably isn't such a good idea
			if prio.is_a?(Numeric)
				return prio
			
			# if this class has a named priority,
			# resolve and return it's value
			elsif prio.is_a?(Symbol)
				if NAMED_PRIORITY.has_key?(prio)
					return NAMED_PRIORITY[prio]
				
				# don't allow invalid named priorites.
				# i can't think of a use case, especially
				# since the constant can be monkey-patched
				# if it's really necessary
				else
					raise(
						NameError,
						"Invalid named priority #{prio.inspect} " +\
						"of {klass}. Valid named priorties are: " +\
						NAMED_PRIORITY.keys.join(", "))
				end
			end
		end
	end
	
	# no ancestor has a priority, so assume
	# that this app is of "normal" priority
	return NAMED_PRIORITY[:normal]
end

.serve(regex) ⇒ Object



272
273
274
# File 'lib/rubysms/application.rb', line 272

def serve(regex)
	@serve = regex
end

.serve!(*backends) ⇒ Object

Creates and starts a router to serve only this application. Handy during development.

This method accepts an arbitrary number of backends, each of which can be provided in numerous ways. This is kind of hard to wrap one’s head around, but makes us super flexible. TODO: this magic will all be moved to the

    router, one day, so multiple apps
    can take advantage of it.

# start the default backends
# (one http, and one drb)
App.serve!

# just the http backend
App.serve!(:HTTP)

# the http backend... with configuration option(s)!
# (in this case, a port). it's got to be an array,
# so we know that we're referring to one single
# backend here, not two "HTTP" and "8080" backends
App.serve!([:HTTP, 8080])

# two GSM backends on separate ports
App.serve!([:GSM, "/dev/ttyS0"], [:GSM, "/dev/ttyS1"])

You may notice that these arguments resemble the config options from the Malawi RapidSMS project… this is not a co-incidence.



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/rubysms/application.rb', line 45

def self.serve!(*backends)
	
	# if no backends were explicitly requested,
	# default to the HTTP + DRB offline backends
	backends = [:HTTP, :DRB] if\
		backends.empty?
	
	# create a router, and attach each new backend
	# in turn. because ruby's *splat operator is so
	# clever, each _backend_ can be provided in many
	# ways - see this method's docstring.
	router = SMS::Router.new
	backends.each do |backend|
		router.add_backend(*backend)
	end
	
	router.add_app(self.new)
	router.serve_forever
end

Instance Method Details

#assemble(*parts) ⇒ Object



225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/rubysms/application.rb', line 225

def assemble(*parts)
	
	# the last element can be an array,
	# which contains arguments to sprintf
	args = parts[-1].is_a?(Array)? parts.pop : []
	
	# resolve each remaining part
	# via self#messge, which can
	# (should?) be overloaded
	parts.collect do |msg|
		message(msg)
	end.join("") % args
end

#incoming(msg) ⇒ Object



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/rubysms/application.rb', line 142

def incoming(msg)
	if services = self.class.instance_variable_get(:@services)
		
		# duplicate the message text before hacking it
		# into pieces, so we don't alter the original
		text = msg.text.dup
		
		# lock threads while handling this message, so we don't have
		# to worry about being interrupted by other incoming messages
		# (in theory, this shouldn't be a problem, but it turns out
		# to be a frequent source of bugs)
		Thread.exclusive do
			services.each do |service|
				method, pattern, priority = *service
				
				# if the pattern is a string, then assume that
				# it's a case-insensitive simple trigger - it's
				# a common enough use-case to warrant an exception
				if pattern.is_a?(String)
					pattern = /\A#{pattern}\Z/i
				end
				
				# if this pattern looks like a regex,
				# attempt to match the incoming message
				if pattern.respond_to?(:match)
					if m = pattern.match(text)
						
						# we have a match! attempt to
						# dispatch it to the receiver
						dispatch_to(method, msg, m.captures)
					
						# the method accepted the text, but it may not be interested
						# in the whole message. so crop off just the part that matched
						text.sub!(pattern, "")
					
						# stop processing if we have
						# dealt with all of the text
						return true unless text =~ /\S/
					
						# there is text remaining, so
						# (re-)start iterating services
						# (jumps back to services.each)
						retry
					end
		
				# the special :anything pattern can be used
				# as a default service. once this is hit, we
				# are done processing the entire message
				elsif pattern == :anything
					dispatch_to(method, msg, [text])
					return true
				
				# we don't understand what this pattern
				# is, or how it ended up in @services.
				# no big deal, but log it anyway, since
				# it indicates that *something* is awry
				else
					log "Invalid pattern: #{pattern.inspect}", :warn
				end
			end#each
			
			
		end#exclusive
	end#if
end

#message(msg) ⇒ Object



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/rubysms/application.rb', line 208

def message(msg)
	if msg.is_a? Symbol
		begin
			self.class.const_get(:Messages)[msg]
	
		# something went wrong, but i don't
		# particularly care what, right now.
		# log it, and carry on regardless
		rescue StandardError
			log "Invalid message #{msg.inspect} for #{self.class}", :warn
			"<#{msg}>"
		end
	else
		msg
	end
end

#priorityObject



138
139
140
# File 'lib/rubysms/application.rb', line 138

def priority
	@priority or self.class.priority
end

#priority=(level) ⇒ Object



134
135
136
# File 'lib/rubysms/application.rb', line 134

def priority=(level)
	@priority = level
end