rbot 0.9.9

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.
Files changed (76) hide show
  1. data/AUTHORS +16 -0
  2. data/COPYING +21 -0
  3. data/ChangeLog +418 -0
  4. data/INSTALL +8 -0
  5. data/README +44 -0
  6. data/REQUIREMENTS +34 -0
  7. data/TODO +5 -0
  8. data/Usage_en.txt +129 -0
  9. data/bin/rbot +81 -0
  10. data/data/rbot/contrib/plugins/figlet.rb +20 -0
  11. data/data/rbot/contrib/plugins/ri.rb +83 -0
  12. data/data/rbot/contrib/plugins/stats.rb +232 -0
  13. data/data/rbot/contrib/plugins/vandale.rb +49 -0
  14. data/data/rbot/languages/dutch.lang +73 -0
  15. data/data/rbot/languages/english.lang +75 -0
  16. data/data/rbot/languages/french.lang +39 -0
  17. data/data/rbot/languages/german.lang +67 -0
  18. data/data/rbot/plugins/autoop.rb +68 -0
  19. data/data/rbot/plugins/autorejoin.rb +16 -0
  20. data/data/rbot/plugins/cal.rb +15 -0
  21. data/data/rbot/plugins/dice.rb +81 -0
  22. data/data/rbot/plugins/eightball.rb +19 -0
  23. data/data/rbot/plugins/excuse.rb +470 -0
  24. data/data/rbot/plugins/fish.rb +61 -0
  25. data/data/rbot/plugins/fortune.rb +22 -0
  26. data/data/rbot/plugins/freshmeat.rb +98 -0
  27. data/data/rbot/plugins/google.rb +51 -0
  28. data/data/rbot/plugins/host.rb +14 -0
  29. data/data/rbot/plugins/httpd.rb.disabled +35 -0
  30. data/data/rbot/plugins/insult.rb +258 -0
  31. data/data/rbot/plugins/karma.rb +85 -0
  32. data/data/rbot/plugins/lart.rb +181 -0
  33. data/data/rbot/plugins/math.rb +122 -0
  34. data/data/rbot/plugins/nickserv.rb +89 -0
  35. data/data/rbot/plugins/nslookup.rb +43 -0
  36. data/data/rbot/plugins/opme.rb +19 -0
  37. data/data/rbot/plugins/quakeauth.rb +51 -0
  38. data/data/rbot/plugins/quotes.rb +321 -0
  39. data/data/rbot/plugins/remind.rb +228 -0
  40. data/data/rbot/plugins/roshambo.rb +54 -0
  41. data/data/rbot/plugins/rot13.rb +10 -0
  42. data/data/rbot/plugins/roulette.rb +147 -0
  43. data/data/rbot/plugins/rss.rb.disabled +414 -0
  44. data/data/rbot/plugins/seen.rb +89 -0
  45. data/data/rbot/plugins/slashdot.rb +94 -0
  46. data/data/rbot/plugins/spell.rb +36 -0
  47. data/data/rbot/plugins/tube.rb +71 -0
  48. data/data/rbot/plugins/url.rb +88 -0
  49. data/data/rbot/plugins/weather.rb +649 -0
  50. data/data/rbot/plugins/wserver.rb +71 -0
  51. data/data/rbot/plugins/xmlrpc.rb.disabled +52 -0
  52. data/data/rbot/templates/keywords.rbot +4 -0
  53. data/data/rbot/templates/lart/larts +98 -0
  54. data/data/rbot/templates/lart/praises +5 -0
  55. data/data/rbot/templates/levels.rbot +30 -0
  56. data/data/rbot/templates/users.rbot +1 -0
  57. data/lib/rbot/auth.rb +203 -0
  58. data/lib/rbot/channel.rb +54 -0
  59. data/lib/rbot/config.rb +363 -0
  60. data/lib/rbot/dbhash.rb +112 -0
  61. data/lib/rbot/httputil.rb +141 -0
  62. data/lib/rbot/ircbot.rb +808 -0
  63. data/lib/rbot/ircsocket.rb +185 -0
  64. data/lib/rbot/keywords.rb +433 -0
  65. data/lib/rbot/language.rb +69 -0
  66. data/lib/rbot/message.rb +256 -0
  67. data/lib/rbot/messagemapper.rb +262 -0
  68. data/lib/rbot/plugins.rb +291 -0
  69. data/lib/rbot/post-install.rb +8 -0
  70. data/lib/rbot/rbotconfig.rb +36 -0
  71. data/lib/rbot/registry.rb +271 -0
  72. data/lib/rbot/rfc2812.rb +1104 -0
  73. data/lib/rbot/timer.rb +201 -0
  74. data/lib/rbot/utils.rb +83 -0
  75. data/setup.rb +1360 -0
  76. metadata +129 -0
@@ -0,0 +1,69 @@
1
+ module Irc
2
+ module Language
3
+
4
+ class Language
5
+ BotConfig.register BotConfigEnumValue.new('core.language',
6
+ :default => "english", :wizard => true,
7
+ :values => Proc.new{|bot|
8
+ Dir.new(Config::datadir + "/languages").collect {|f|
9
+ f =~ /\.lang$/ ? f.gsub(/\.lang$/, "") : nil
10
+ }.compact
11
+ },
12
+ :on_change => Proc.new {|bot, v| bot.lang.set_language v},
13
+ :desc => "Which language file the bot should use")
14
+
15
+ def initialize(language)
16
+ set_language language
17
+ end
18
+
19
+ def set_language(language)
20
+ file = Config::datadir + "/languages/#{language}.lang"
21
+ unless(FileTest.exist?(file))
22
+ raise "no such language: #{language} (no such file #{file})"
23
+ end
24
+ @language = language
25
+ @file = file
26
+ scan
27
+ end
28
+
29
+ def scan
30
+ @strings = Hash.new
31
+ current_key = nil
32
+ IO.foreach(@file) {|l|
33
+ next if l =~ /^$/
34
+ next if l =~ /^\s*#/
35
+ if(l =~ /^(\S+):$/)
36
+ @strings[$1] = Array.new
37
+ current_key = $1
38
+ elsif(l =~ /^\s*(.*)$/)
39
+ @strings[current_key] << $1
40
+ end
41
+ }
42
+ end
43
+
44
+ def rescan
45
+ scan
46
+ end
47
+
48
+ def get(key)
49
+ if(@strings.has_key?(key))
50
+ return @strings[key][rand(@strings[key].length)]
51
+ else
52
+ raise "undefined language key"
53
+ end
54
+ end
55
+
56
+ def save
57
+ File.open(@file, "w") {|file|
58
+ @strings.each {|key,val|
59
+ file.puts "#{key}:"
60
+ val.each_value {|v|
61
+ file.puts " #{v}"
62
+ }
63
+ }
64
+ }
65
+ end
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,256 @@
1
+ module Irc
2
+
3
+ # base user message class, all user messages derive from this
4
+ # (a user message is defined as having a source hostmask, a target
5
+ # nick/channel and a message part)
6
+ class BasicUserMessage
7
+
8
+ # associated bot
9
+ attr_reader :bot
10
+
11
+ # when the message was received
12
+ attr_reader :time
13
+
14
+ # hostmask of message source
15
+ attr_reader :source
16
+
17
+ # nick of message source
18
+ attr_reader :sourcenick
19
+
20
+ # url part of message source
21
+ attr_reader :sourceaddress
22
+
23
+ # nick/channel message was sent to
24
+ attr_reader :target
25
+
26
+ # contents of the message
27
+ attr_accessor :message
28
+
29
+ # has the message been replied to/handled by a plugin?
30
+ attr_accessor :replied
31
+
32
+ # instantiate a new Message
33
+ # bot:: associated bot class
34
+ # source:: hostmask of the message source
35
+ # target:: nick/channel message is destined for
36
+ # message:: message part
37
+ def initialize(bot, source, target, message)
38
+ @time = Time.now
39
+ @bot = bot
40
+ @source = source
41
+ @address = false
42
+ @target = target
43
+ @message = BasicUserMessage.stripcolour message
44
+ @replied = false
45
+
46
+ # split source into consituent parts
47
+ if source =~ /^((\S+)!(\S+))$/
48
+ @sourcenick = $2
49
+ @sourceaddress = $3
50
+ end
51
+
52
+ if target && target.downcase == @bot.nick.downcase
53
+ @address = true
54
+ end
55
+
56
+ end
57
+
58
+ # returns true if the message was addressed to the bot.
59
+ # This includes any private message to the bot, or any public message
60
+ # which looks like it's addressed to the bot, e.g. "bot: foo", "bot, foo",
61
+ # a kick message when bot was kicked etc.
62
+ def address?
63
+ return @address
64
+ end
65
+
66
+ # has this message been replied to by a plugin?
67
+ def replied?
68
+ return @replied
69
+ end
70
+
71
+ # strip mIRC colour escapes from a string
72
+ def BasicUserMessage.stripcolour(string)
73
+ return "" unless string
74
+ ret = string.gsub(/\cC\d\d?(?:,\d\d?)?/, "")
75
+ #ret.tr!("\x00-\x1f", "")
76
+ ret
77
+ end
78
+
79
+ end
80
+
81
+ # class for handling IRC user messages. Includes some utilities for handling
82
+ # the message, for example in plugins.
83
+ # The +message+ member will have any bot addressing "^bot: " removed
84
+ # (address? will return true in this case)
85
+ class UserMessage < BasicUserMessage
86
+
87
+ # for plugin messages, the name of the plugin invoked by the message
88
+ attr_reader :plugin
89
+
90
+ # for plugin messages, the rest of the message, with the plugin name
91
+ # removed
92
+ attr_reader :params
93
+
94
+ # convenience member. Who to reply to (i.e. would be sourcenick for a
95
+ # privately addressed message, or target (the channel) for a publicly
96
+ # addressed message
97
+ attr_reader :replyto
98
+
99
+ # channel the message was in, nil for privately addressed messages
100
+ attr_reader :channel
101
+
102
+ # for PRIVMSGs, true if the message was a CTCP ACTION (CTCP stuff
103
+ # will be stripped from the message)
104
+ attr_reader :action
105
+
106
+ # instantiate a new UserMessage
107
+ # bot:: associated bot class
108
+ # source:: hostmask of the message source
109
+ # target:: nick/channel message is destined for
110
+ # message:: message part
111
+ def initialize(bot, source, target, message)
112
+ super(bot, source, target, message)
113
+ @target = target
114
+ @private = false
115
+ @plugin = nil
116
+ @action = false
117
+
118
+ if target.downcase == @bot.nick.downcase
119
+ @private = true
120
+ @address = true
121
+ @channel = nil
122
+ @replyto = @sourcenick
123
+ else
124
+ @replyto = @target
125
+ @channel = @target
126
+ end
127
+
128
+ # check for option extra addressing prefixes, e.g "|search foo", or
129
+ # "!version" - first match wins
130
+ bot.addressing_prefixes.each {|mprefix|
131
+ if @message.gsub!(/^#{Regexp.escape(mprefix)}\s*/, "")
132
+ @address = true
133
+ break
134
+ end
135
+ }
136
+
137
+ # even if they used above prefixes, we allow for silly people who
138
+ # combine all possible types, e.g. "|rbot: hello", or
139
+ # "/msg rbot rbot: hello", etc
140
+ if @message.gsub!(/^\s*#{bot.nick}\s*([:;,>]|\s)\s*/, "")
141
+ @address = true
142
+ end
143
+
144
+ if(@message =~ /^\001ACTION\s(.+)\001/)
145
+ @message = $1
146
+ @action = true
147
+ end
148
+
149
+ # free splitting for plugins
150
+ @params = @message.dup
151
+ if @params.gsub!(/^\s*(\S+)[\s$]*/, "")
152
+ @plugin = $1.downcase
153
+ @params = nil unless @params.length > 0
154
+ end
155
+ end
156
+
157
+ # returns true for private messages, e.g. "/msg bot hello"
158
+ def private?
159
+ return @private
160
+ end
161
+
162
+ # returns true if the message was in a channel
163
+ def public?
164
+ return !@private
165
+ end
166
+
167
+ def action?
168
+ return @action
169
+ end
170
+
171
+ # convenience method to reply to a message, useful in plugins. It's the
172
+ # same as doing:
173
+ # <tt>@bot.say m.replyto, string</tt>
174
+ # So if the message is private, it will reply to the user. If it was
175
+ # in a channel, it will reply in the channel.
176
+ def reply(string)
177
+ @bot.say @replyto, string
178
+ @replied = true
179
+ end
180
+
181
+ # convenience method to reply "okay" in the current language to the
182
+ # message
183
+ def okay
184
+ @bot.say @replyto, @bot.lang.get("okay")
185
+ end
186
+
187
+ end
188
+
189
+ # class to manage IRC PRIVMSGs
190
+ class PrivMessage < UserMessage
191
+ end
192
+
193
+ # class to manage IRC NOTICEs
194
+ class NoticeMessage < UserMessage
195
+ end
196
+
197
+ # class to manage IRC KICKs
198
+ # +address?+ can be used as a shortcut to see if the bot was kicked,
199
+ # basically, +target+ was kicked from +channel+ by +source+ with +message+
200
+ class KickMessage < BasicUserMessage
201
+ # channel user was kicked from
202
+ attr_reader :channel
203
+
204
+ def initialize(bot, source, target, channel, message="")
205
+ super(bot, source, target, message)
206
+ @channel = channel
207
+ end
208
+ end
209
+
210
+ # class to pass IRC Nick changes in. @message contains the old nickame,
211
+ # @sourcenick contains the new one.
212
+ class NickMessage < BasicUserMessage
213
+ def initialize(bot, source, oldnick, newnick)
214
+ super(bot, source, oldnick, newnick)
215
+ end
216
+ end
217
+
218
+ class QuitMessage < BasicUserMessage
219
+ def initialize(bot, source, target, message="")
220
+ super(bot, source, target, message)
221
+ end
222
+ end
223
+
224
+ class TopicMessage < BasicUserMessage
225
+ # channel topic
226
+ attr_reader :topic
227
+ # topic set at (unixtime)
228
+ attr_reader :timestamp
229
+ # topic set on channel
230
+ attr_reader :channel
231
+
232
+ def initialize(bot, source, channel, timestamp, topic="")
233
+ super(bot, source, channel, topic)
234
+ @topic = topic
235
+ @timestamp = timestamp
236
+ @channel = channel
237
+ end
238
+ end
239
+
240
+ # class to manage channel joins
241
+ class JoinMessage < BasicUserMessage
242
+ # channel joined
243
+ attr_reader :channel
244
+ def initialize(bot, source, channel, message="")
245
+ super(bot, source, channel, message)
246
+ @channel = channel
247
+ # in this case sourcenick is the nick that could be the bot
248
+ @address = (sourcenick.downcase == @bot.nick.downcase)
249
+ end
250
+ end
251
+
252
+ # class to manage channel parts
253
+ # same as a join, but can have a message too
254
+ class PartMessage < JoinMessage
255
+ end
256
+ end
@@ -0,0 +1,262 @@
1
+ module Irc
2
+
3
+ # +MessageMapper+ is a class designed to reduce the amount of regexps and
4
+ # string parsing plugins and bot modules need to do, in order to process
5
+ # and respond to messages.
6
+ #
7
+ # You add templates to the MessageMapper which are examined by the handle
8
+ # method when handling a message. The templates tell the mapper which
9
+ # method in its parent class (your class) to invoke for that message. The
10
+ # string is split, optionally defaulted and validated before being passed
11
+ # to the matched method.
12
+ #
13
+ # A template such as "foo :option :otheroption" will match the string "foo
14
+ # bar baz" and, by default, result in method +foo+ being called, if
15
+ # present, in the parent class. It will receive two parameters, the
16
+ # Message (derived from BasicUserMessage) and a Hash containing
17
+ # {:option => "bar", :otheroption => "baz"}
18
+ # See the #map method for more details.
19
+ class MessageMapper
20
+ # used to set the method name used as a fallback for unmatched messages.
21
+ # The default fallback is a method called "usage".
22
+ attr_writer :fallback
23
+
24
+ # parent:: parent class which will receive mapped messages
25
+ #
26
+ # create a new MessageMapper with parent class +parent+. This class will
27
+ # receive messages from the mapper via the handle() method.
28
+ def initialize(parent)
29
+ @parent = parent
30
+ @templates = Array.new
31
+ @fallback = 'usage'
32
+ end
33
+
34
+ # args:: hash format containing arguments for this template
35
+ #
36
+ # map a template string to an action. example:
37
+ # map 'myplugin :parameter1 :parameter2'
38
+ # (other examples follow). By default, maps a matched string to an
39
+ # action with the name of the first word in the template. The action is
40
+ # a method which takes a message and a parameter hash for arguments.
41
+ #
42
+ # The :action => 'method_name' option can be used to override this
43
+ # default behaviour. Example:
44
+ # map 'myplugin :parameter1 :parameter2', :action => 'mymethod'
45
+ #
46
+ # By default whether a handler is fired depends on an auth check. The
47
+ # first component of the string is used for the auth check, unless
48
+ # overridden via the :auth => 'auth_name' option.
49
+ #
50
+ # Static parameters (not prefixed with ':' or '*') must match the
51
+ # respective component of the message exactly. Example:
52
+ # map 'myplugin :foo is :bar'
53
+ # will only match messages of the form "myplugin something is
54
+ # somethingelse"
55
+ #
56
+ # Dynamic parameters can be specified by a colon ':' to match a single
57
+ # component (whitespace seperated), or a * to such up all following
58
+ # parameters into an array. Example:
59
+ # map 'myplugin :parameter1 *rest'
60
+ #
61
+ # You can provide defaults for dynamic components using the :defaults
62
+ # parameter. If a component has a default, then it is optional. e.g:
63
+ # map 'myplugin :foo :bar', :defaults => {:bar => 'qux'}
64
+ # would match 'myplugin param param2' and also 'myplugin param'. In the
65
+ # latter case, :bar would be provided from the default.
66
+ #
67
+ # Components can be validated before being allowed to match, for
68
+ # example if you need a component to be a number:
69
+ # map 'myplugin :param', :requirements => {:param => /^\d+$/}
70
+ # will only match strings of the form 'myplugin 1234' or some other
71
+ # number.
72
+ #
73
+ # Templates can be set not to match public or private messages using the
74
+ # :public or :private boolean options.
75
+ #
76
+ # Further examples:
77
+ #
78
+ # # match 'karmastats' and call my stats() method
79
+ # map 'karmastats', :action => 'stats'
80
+ # # match 'karma' with an optional 'key' and call my karma() method
81
+ # map 'karma :key', :defaults => {:key => false}
82
+ # # match 'karma for something' and call my karma() method
83
+ # map 'karma for :key'
84
+ #
85
+ # # two matches, one for public messages in a channel, one for
86
+ # # private messages which therefore require a channel argument
87
+ # map 'urls search :channel :limit :string', :action => 'search',
88
+ # :defaults => {:limit => 4},
89
+ # :requirements => {:limit => /^\d+$/},
90
+ # :public => false
91
+ # plugin.map 'urls search :limit :string', :action => 'search',
92
+ # :defaults => {:limit => 4},
93
+ # :requirements => {:limit => /^\d+$/},
94
+ # :private => false
95
+ #
96
+ def map(*args)
97
+ @templates << Template.new(*args)
98
+ end
99
+
100
+ def each
101
+ @templates.each {|tmpl| yield tmpl}
102
+ end
103
+ def last
104
+ @templates.last
105
+ end
106
+
107
+ # m:: derived from BasicUserMessage
108
+ #
109
+ # examine the message +m+, comparing it with each map()'d template to
110
+ # find and process a match. Templates are examined in the order they
111
+ # were map()'d - first match wins.
112
+ #
113
+ # returns +true+ if a match is found including fallbacks, +false+
114
+ # otherwise.
115
+ def handle(m)
116
+ return false if @templates.empty?
117
+ failures = []
118
+ @templates.each do |tmpl|
119
+ options, failure = tmpl.recognize(m)
120
+ if options.nil?
121
+ failures << [tmpl, failure]
122
+ else
123
+ action = tmpl.options[:action] ? tmpl.options[:action] : tmpl.items[0]
124
+ next unless @parent.respond_to?(action)
125
+ auth = tmpl.options[:auth] ? tmpl.options[:auth] : tmpl.items[0]
126
+ debug "checking auth for #{auth}"
127
+ if m.bot.auth.allow?(auth, m.source, m.replyto)
128
+ debug "template match found and auth'd: #{action.inspect} #{options.inspect}"
129
+ @parent.send(action, m, options)
130
+ return true
131
+ end
132
+ debug "auth failed for #{auth}"
133
+ # if it's just an auth failure but otherwise the match is good,
134
+ # don't try any more handlers
135
+ return false
136
+ end
137
+ end
138
+ debug failures.inspect
139
+ debug "no handler found, trying fallback"
140
+ if @fallback != nil && @parent.respond_to?(@fallback)
141
+ if m.bot.auth.allow?(@fallback, m.source, m.replyto)
142
+ @parent.send(@fallback, m, {})
143
+ return true
144
+ end
145
+ end
146
+ return false
147
+ end
148
+
149
+ end
150
+
151
+ class Template
152
+ attr_reader :defaults # The defaults hash
153
+ attr_reader :options # The options hash
154
+ attr_reader :items
155
+ def initialize(template, hash={})
156
+ raise ArgumentError, "Second argument must be a hash!" unless hash.kind_of?(Hash)
157
+ @defaults = hash[:defaults].kind_of?(Hash) ? hash.delete(:defaults) : {}
158
+ @requirements = hash[:requirements].kind_of?(Hash) ? hash.delete(:requirements) : {}
159
+ self.items = template
160
+ @options = hash
161
+ end
162
+ def items=(str)
163
+ items = str.split(/\s+/).collect {|c| (/^(:|\*)(\w+)$/ =~ c) ? (($1 == ':' ) ? $2.intern : "*#{$2}".intern) : c} if str.kind_of?(String) # split and convert ':xyz' to symbols
164
+ items.shift if items.first == ""
165
+ items.pop if items.last == ""
166
+ @items = items
167
+
168
+ if @items.first.kind_of? Symbol
169
+ raise ArgumentError, "Illegal template -- first component cannot be dynamic\n #{str.inspect}"
170
+ end
171
+
172
+ # Verify uniqueness of each component.
173
+ @items.inject({}) do |seen, item|
174
+ if item.kind_of? Symbol
175
+ raise ArgumentError, "Illegal template -- duplicate item #{item}\n #{str.inspect}" if seen.key? item
176
+ seen[item] = true
177
+ end
178
+ seen
179
+ end
180
+ end
181
+
182
+ # Recognize the provided string components, returning a hash of
183
+ # recognized values, or [nil, reason] if the string isn't recognized.
184
+ def recognize(m)
185
+ components = m.message.split(/\s+/)
186
+ options = {}
187
+
188
+ @items.each do |item|
189
+ if /^\*/ =~ item.to_s
190
+ if components.empty?
191
+ value = @defaults.has_key?(item) ? @defaults[item].clone : []
192
+ else
193
+ value = components.clone
194
+ end
195
+ components = []
196
+ def value.to_s() self.join(' ') end
197
+ options[item.to_s.sub(/^\*/,"").intern] = value
198
+ elsif item.kind_of? Symbol
199
+ value = components.shift || @defaults[item]
200
+ if passes_requirements?(item, value)
201
+ options[item] = value
202
+ else
203
+ if @defaults.has_key?(item)
204
+ options[item] = @defaults[item]
205
+ # push the test-failed component back on the stack
206
+ components.unshift value
207
+ else
208
+ return nil, requirements_for(item)
209
+ end
210
+ end
211
+ else
212
+ return nil, "No value available for component #{item.inspect}" if components.empty?
213
+ component = components.shift
214
+ return nil, "Value for component #{item.inspect} doesn't match #{component}" if component != item
215
+ end
216
+ end
217
+
218
+ return nil, "Unused components were left: #{components.join '/'}" unless components.empty?
219
+
220
+ return nil, "template is not configured for private messages" if @options.has_key?(:private) && !@options[:private] && m.private?
221
+ return nil, "template is not configured for public messages" if @options.has_key?(:public) && !@options[:public] && !m.private?
222
+
223
+ options.delete_if {|k, v| v.nil?} # Remove nil values.
224
+ return options, nil
225
+ end
226
+
227
+ def inspect
228
+ when_str = @requirements.empty? ? "" : " when #{@requirements.inspect}"
229
+ default_str = @defaults.empty? ? "" : " || #{@defaults.inspect}"
230
+ "<#{self.class.to_s} #{@items.collect{|c| c.kind_of?(String) ? c : c.inspect}.join(' ').inspect}#{default_str}#{when_str}>"
231
+ end
232
+
233
+ # Verify that the given value passes this template's requirements
234
+ def passes_requirements?(name, value)
235
+ return @defaults.key?(name) && @defaults[name].nil? if value.nil? # Make sure it's there if it should be
236
+
237
+ case @requirements[name]
238
+ when nil then true
239
+ when Regexp then
240
+ value = value.to_s
241
+ match = @requirements[name].match(value)
242
+ match && match[0].length == value.length
243
+ else
244
+ @requirements[name] == value.to_s
245
+ end
246
+ end
247
+
248
+ def requirements_for(name)
249
+ name = name.to_s.sub(/^\*/,"").intern if (/^\*/ =~ name.inspect)
250
+ presence = (@defaults.key?(name) && @defaults[name].nil?)
251
+ requirement = case @requirements[name]
252
+ when nil then nil
253
+ when Regexp then "match #{@requirements[name].inspect}"
254
+ else "be equal to #{@requirements[name].inspect}"
255
+ end
256
+ if presence && requirement then "#{name} must be present and #{requirement}"
257
+ elsif presence || requirement then "#{name} must #{requirement || 'be present'}"
258
+ else "#{name} has no requirements"
259
+ end
260
+ end
261
+ end
262
+ end