butler 1.8.1 → 1.8.2

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 (71) hide show
  1. data/CHANGELOG.txt +212 -0
  2. data/{README → README.txt} +0 -0
  3. data/Rakefile +16 -11
  4. data/bin/botcontrol +35 -14
  5. data/data/butler/dialogs/create.rb +29 -40
  6. data/data/butler/dialogs/create_config.rb +1 -1
  7. data/data/butler/dialogs/dir.rb +13 -0
  8. data/data/butler/dialogs/en/create.yaml +24 -10
  9. data/data/butler/dialogs/en/dir.yaml +5 -0
  10. data/data/butler/dialogs/en/help.yaml +28 -11
  11. data/data/butler/dialogs/en/quickcreate.yaml +14 -0
  12. data/data/butler/dialogs/help.rb +16 -4
  13. data/data/butler/dialogs/quickcreate.rb +49 -0
  14. data/data/butler/plugins/core/access.rb +211 -0
  15. data/data/butler/plugins/core/logout.rb +11 -11
  16. data/data/butler/plugins/core/plugins.rb +23 -41
  17. data/data/butler/plugins/dev/bleakhouse.rb +46 -0
  18. data/data/butler/plugins/games/roll.rb +1 -1
  19. data/data/butler/plugins/operator/deop.rb +15 -20
  20. data/data/butler/plugins/operator/devoice.rb +14 -20
  21. data/data/butler/plugins/operator/limit.rb +56 -21
  22. data/data/butler/plugins/operator/op.rb +15 -20
  23. data/data/butler/plugins/operator/voice.rb +15 -20
  24. data/data/butler/plugins/service/define.rb +3 -3
  25. data/data/butler/plugins/service/more.rb +40 -0
  26. data/data/butler/plugins/util/cycle.rb +1 -1
  27. data/data/butler/plugins/util/load.rb +5 -5
  28. data/data/butler/plugins/util/pong.rb +3 -2
  29. data/lib/access/privilege.rb +17 -0
  30. data/lib/access/role.rb +33 -2
  31. data/lib/access/savable.rb +6 -0
  32. data/lib/access/yamlbase.rb +1 -2
  33. data/lib/butler/bot.rb +40 -7
  34. data/lib/butler/debuglog.rb +17 -0
  35. data/lib/butler/dialog.rb +1 -1
  36. data/lib/butler/irc/{channels.rb → channellist.rb} +2 -2
  37. data/lib/butler/irc/client.rb +60 -79
  38. data/lib/butler/irc/client/filter.rb +12 -0
  39. data/lib/butler/irc/client/listener.rb +55 -0
  40. data/lib/butler/irc/client/listenerlist.rb +69 -0
  41. data/lib/butler/irc/hostmask.rb +31 -16
  42. data/lib/butler/irc/message.rb +3 -3
  43. data/lib/butler/irc/parser.rb +2 -2
  44. data/lib/butler/irc/parser/rfc2812.rb +2 -6
  45. data/lib/butler/irc/socket.rb +12 -6
  46. data/lib/butler/irc/string.rb +4 -0
  47. data/lib/butler/irc/user.rb +0 -6
  48. data/lib/butler/irc/{users.rb → userlist.rb} +2 -2
  49. data/lib/butler/irc/whois.rb +6 -0
  50. data/lib/butler/plugin.rb +48 -14
  51. data/lib/butler/plugin/configproxy.rb +20 -0
  52. data/lib/butler/plugin/mapper.rb +308 -24
  53. data/lib/butler/plugin/matcher.rb +3 -1
  54. data/lib/butler/plugin/more.rb +65 -0
  55. data/lib/butler/plugin/onhandlers.rb +4 -4
  56. data/lib/butler/plugin/trigger.rb +4 -2
  57. data/lib/butler/plugins.rb +1 -1
  58. data/lib/butler/session.rb +11 -0
  59. data/lib/butler/version.rb +1 -1
  60. data/lib/cloptions.rb +1 -1
  61. data/lib/diagnostics.rb +20 -0
  62. data/lib/dialogline.rb +1 -1
  63. data/lib/durations.rb +19 -6
  64. data/lib/event.rb +8 -5
  65. data/lib/installer.rb +10 -3
  66. data/lib/ostructfixed.rb +11 -0
  67. data/lib/ruby/kernel/daemonize.rb +1 -2
  68. data/test/butler/plugin/mapper.rb +46 -0
  69. metadata +28 -11
  70. data/CHANGELOG +0 -44
  71. data/data/butler/plugins/core/privilege.rb +0 -103
@@ -0,0 +1,12 @@
1
+ class Butler
2
+ module IRC
3
+ class Client
4
+ module Filter # :nodoc:
5
+ attr_accessor :listener
6
+ def unsubscribe
7
+ listener.unsubscribe
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,55 @@
1
+ #--
2
+ # Copyright 2007 by Stefan Rusterholz.
3
+ # All rights reserved.
4
+ # See LICENSE.txt for permissions.
5
+ #++
6
+
7
+
8
+
9
+ class Butler
10
+ module IRC
11
+ class Client
12
+
13
+ # Created by Butler::IRC::Client#subscribe() and similar methods
14
+ class Listener
15
+ AllSymbols = [nil].freeze
16
+ include Comparable
17
+
18
+ attr_reader :priority
19
+ attr_reader :symbols
20
+ attr_accessor :container
21
+
22
+ def initialize(symbols=nil, priority=0, *args, &callback)
23
+ @priority = priority
24
+ @symbols = (symbols ? Array(symbols) : AllSymbols).freeze
25
+ @args = args
26
+ @callback = callback
27
+ @container = nil
28
+ end
29
+
30
+ # will remove this listener from the clients dispatcher forever
31
+ def unsubscribe
32
+ @container.unsubscribe(self)
33
+ end
34
+
35
+ # Comparison is done by priority, higher priority comes first, so
36
+ # Listener.new(nil, -100) > Listener.new(nil, 100)
37
+ def <=>(other)
38
+ other.priority <=> @priority
39
+ end
40
+
41
+ # set the priority of this listener
42
+ # see Butler::IRC::Client#subscribe() for infos about priority
43
+ def priority=(value)
44
+ @priority = Integer(value)
45
+ @container.mutated(self)
46
+ end
47
+
48
+ # Invoke the listener, always passes self as first argument
49
+ def call(*args)
50
+ @callback.call(self, *(args+@args))
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,69 @@
1
+ #--
2
+ # Copyright 2007 by Stefan Rusterholz.
3
+ # All rights reserved.
4
+ # See LICENSE.txt for permissions.
5
+ #++
6
+
7
+
8
+
9
+ require 'thread'
10
+
11
+
12
+
13
+ class Butler
14
+ module IRC
15
+ class Client
16
+ class ListenerList
17
+ include Enumerable
18
+
19
+ attr_reader :mutex
20
+
21
+ def initialize
22
+ @mutex = Mutex.new
23
+ @all = {}
24
+ @per_symbol = Hash.new { |h,k| h[k] = [] }
25
+ end
26
+
27
+ def each(&block)
28
+ @all.each_key(&block)
29
+ end
30
+
31
+ def synchronized_each_for(symbol, &block)
32
+ @mutex.synchronize {
33
+ @per_symbol[symbol]+@per_symbol[nil]
34
+ }.each(&block)
35
+ end
36
+
37
+ def subscribe(listener)
38
+ @mutex.synchronize {
39
+ raise "#{listener} already subscribed" if @all[listener]
40
+ listener.container = self
41
+ @all[listener] = true
42
+ listener.symbols.each { |s|
43
+ @per_symbol[s] << listener
44
+ @per_symbol[s] = @per_symbol[s].sort_by { |l| -l.priority }
45
+ }
46
+ }
47
+ end
48
+
49
+ def unsubscribe(listener)
50
+ @mutex.synchronize {
51
+ @all.delete(listener)
52
+ listener.symbols.each { |s|
53
+ @per_symbol[s].delete(listener)
54
+ @per_symbol.delete(s) if @per_symbol[s].empty?
55
+ }
56
+ }
57
+ end
58
+
59
+ def mutated(listener)
60
+ @mutex.synchronize {
61
+ listener.symbols.each { |symbol|
62
+ @per_symbol[s] = @per_symbol[s].sort_by { |l| -l.priority }
63
+ }
64
+ }
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -6,48 +6,63 @@
6
6
 
7
7
 
8
8
 
9
- require 'butler/irc/users'
10
-
11
-
12
-
13
9
  class Butler
14
10
  module IRC
15
11
  # Provides methods to see if hostmasks match
16
12
  class Hostmask
13
+
14
+ # List of characters to escape in hostmasks and with what to replace
17
15
  Filter = [
18
16
  [/([\\\[\]{}^`.-])/, '\\\\\1'],
19
17
  [/\?/, "."],
20
18
  [/\*/, ".*?"],
21
19
  ]
22
20
 
23
- # FIXME, not yet correct, escaped * and ?
21
+ # Create a hostmask from a hostmask-string, e.g. "nick!user@host.tld"
22
+ # Also see Butler::IRC::User#hostmask
24
23
  def initialize(mask)
25
- @mask = mask.dup.freeze
26
- filtered = Filter.inject('[]{}^`.-') { |s,(a,b)| s.gsub(a,b) }
27
- #string.gsub(/[\\\x00-\x1f]/) { |match| ("\\%02x" % match[0]) } #implement later
28
- @regex = Regexp.new("\A#{filtered}\z")
24
+ @mask = mask.freeze
25
+
26
+ # FIXME, not yet correct, escaped * and ?
27
+ filtered = Filter.inject(@mask) { |s,(a,b)| s.gsub(a,b) }
28
+ # FIXME implement later
29
+ #string.gsub(/[\\\x00-\x1f]/) { |match| ("\\%02x" % match[0]) }
30
+ n,u,h = filtered.split(/[!@]/)
31
+ @regex = Regexp.new("\A(#{n})!(#{u})@(#{h})\z")
29
32
  end
30
33
 
31
- def matches?(mask)
32
- mask = mask.hostmask if mask.kind_of?(User)
33
- return !@regex.match(mask.to_str).nil?
34
+ # Match a hostmask or anything that responds to #hostmask or #to_str
35
+ # Sets $1-$3 to nick, user and host if matched.
36
+ def =~(mask)
37
+ mask = mask.hostmask if mask.respond_to?(:hostmask)
38
+ !!(@regex =~ mask.to_str)
34
39
  end
35
-
40
+
41
+ # Match a hostmask or anything that responds to #hostmask or #to_str
42
+ # Returns a MatchData instance with 3 captures (nick, user, host)
36
43
  def match(mask)
37
44
  mask = mask.hostmask if mask.kind_of?(User)
38
45
  @regex.match(mask.to_str)
39
46
  end
40
-
47
+ alias === match
48
+
49
+ # return the mask-string
41
50
  def to_str
42
51
  @mask
43
52
  end
44
53
  alias to_s to_str
45
- end
54
+
55
+ def inspect # :nodoc:
56
+ "#<Hostmask #{@mask}>"
57
+ end
58
+ end # Hostmask
46
59
 
47
60
  class User
61
+ # Return the users hostmask, uses wildcards for unknown parts
62
+ # FIXME enable wildcard enforcement
48
63
  def hostmask
49
64
  return Hostmask.new("#{@nick||'*'}!#{@user||'*'}@#{@host||'*'}")
50
65
  end
51
66
  end
52
67
  end
53
- end
68
+ end
@@ -8,16 +8,16 @@
8
8
 
9
9
  #require 'ruby/string' # we need the transcode! method
10
10
 
11
+
12
+
11
13
  class Butler
12
14
  module IRC
13
15
 
14
16
  # Butler::IRC::Message represents messages received by the server.
15
17
  # It provides convenience methods that allow to access information about
16
18
  # those messages easier, e.g. who (as Butler::IRC::User object) sent the
17
- # message in which channel (IRC::Channel object) with what text.
19
+ # message in which channel (Butler::IRC::Channel object) with what text.
18
20
  # Raw message and raw parsed data are still available though.
19
- #
20
- # =FIXME
21
21
  class Message
22
22
  # the command-symbol, see COMMANDS (e.g. :PRIVMSG, :JOIN, ...)
23
23
  attr_reader :symbol
@@ -6,12 +6,12 @@
6
6
 
7
7
 
8
8
 
9
- require 'butler/irc/channels'
9
+ require 'butler/irc/channellist'
10
10
  require 'butler/irc/hostmask'
11
11
  require 'butler/irc/message'
12
12
  require 'butler/irc/parser/commands'
13
13
  require 'butler/irc/string'
14
- require 'butler/irc/users'
14
+ require 'butler/irc/userlist'
15
15
  require 'ruby/exception/detailed'
16
16
  require 'ruby/hash/zip'
17
17
 
@@ -29,8 +29,7 @@ add("kill", :KILL, /^(\S*) (\S*) (.*)/, [:channel, :for, :text]) { |message
29
29
  parser.channels.delete_user(message.for, :kill)
30
30
  end
31
31
  }
32
- # FIXME, really ':?' or just plain ':'?
33
- add("mode", :MODE, /^(\S*) :?(.*)/, [:for, :arguments]) { |message, parser|
32
+ add("mode", :MODE, /^(\S*) (.*)/, [:for, :arguments]) { |message, parser|
34
33
  modifiers = message[:arguments].split(" ")
35
34
  modes = modifiers.shift.split("")
36
35
  flags = {"o" => User::Flags::OP, "v" => User::Flags::VOICE, "u" => User::Flags::UOP}
@@ -190,10 +189,7 @@ add("317", :RPL_WHOISIDLE, /^(\S+) (\S+) ([^:]+) :(.*)/, [:for, :nick, :values,
190
189
  }
191
190
  }
192
191
  add("318", :RPL_ENDOFWHOIS)
193
- add("319", :RPL_WHOISCHANNELS, /^(\S+) (\S+) :(.*)/, [:for, :nick, :channels]) { |message, parser|
194
- # FIXME
195
- message.alter_member(:channels, message.channels.split(" ").map { |channel| parser.channels.create(channel) })
196
- }
192
+ add("319", :RPL_WHOISCHANNELS, /^(\S+) (\S+) :(.*)/, [:for, :nick, :channels]) # only add channels shared with butler to a user - that happens elsewhere already
197
193
  add("321", :RPL_LISTSTART)
198
194
  add("322", :RPL_LIST, /^(\S+) (\S+) (\d+) :(.*)/, [:for, :channelname, :usercount, :topic])
199
195
  add("323", :RPL_LISTEND)
@@ -6,11 +6,12 @@
6
6
 
7
7
 
8
8
 
9
- require 'thread'
10
- require 'socket'
11
- require 'ostructfixed'
9
+ require 'diagnostics'
12
10
  require 'log/comfort'
11
+ require 'ostructfixed'
13
12
  require 'ruby/string/chunks'
13
+ require 'socket'
14
+ require 'thread'
14
15
 
15
16
 
16
17
 
@@ -56,7 +57,7 @@ class Butler
56
57
  VERSION = "1.0.0"
57
58
 
58
59
  include Log::Comfort
59
-
60
+
60
61
  # server the instance is linked with
61
62
  attr_reader :server
62
63
  # port used for connection
@@ -107,7 +108,7 @@ class Butler
107
108
  @limit[key] = options.delete(key) if options.has_key?(key)
108
109
  }
109
110
  @mutex = Mutex.new
110
- @socket = nil
111
+ @socket = Diagnostics.new(self, :write => [NoMethodError, "Must connect first to write to the socket"])
111
112
  @connected = false
112
113
  raise ArgumentError, "Unknown arguments: #{options.keys.inspect}" unless options.empty?
113
114
  end
@@ -205,6 +206,7 @@ class Butler
205
206
  end
206
207
 
207
208
  # FIXME, figure out what the server supports, possibly requires it
209
+ # to be moved to Butler::IRC::Client (to allow ghosting, nickchange, identify)
208
210
  def ghost(nickname, password)
209
211
  write("NS :GHOST #{nickname} #{password}")
210
212
  end
@@ -273,7 +275,6 @@ class Butler
273
275
  end
274
276
 
275
277
  # part specified channels
276
- # FIXME, better way to implement the reason? use a block (yay)?
277
278
  # returns the channels parted from.
278
279
  def part(reason=nil, *channels)
279
280
  if channels.empty?
@@ -310,6 +311,11 @@ class Butler
310
311
  write("KICK #{channel} #{user} :#{reason}")
311
312
  end
312
313
 
314
+ # send a mode command to a channel
315
+ def mode(channel, mode)
316
+ write("MODE #{channel} #{mode}")
317
+ end
318
+
313
319
  # Give Op to user in channel
314
320
  # User can be a nick or IRC::User, either one or an array.
315
321
  def op(channel, *users)
@@ -45,6 +45,10 @@ class String
45
45
  def valid_user?
46
46
  strip_user_prefixes.valid_nickname?
47
47
  end
48
+
49
+ def same_nick?(other)
50
+ strip_user_prefixes.downcase == other.to_str.strip_user_prefixes.downcase
51
+ end
48
52
 
49
53
  # removes indicators from nicknames and channelnames
50
54
  def strip_user_prefixes
@@ -166,7 +166,6 @@ class Butler
166
166
  !common_channels(with_other_user).empty?
167
167
  end
168
168
 
169
- # FIXME
170
169
  # add a channel to the user (should only be used by Butler::IRC::Parser)
171
170
  def add_channel(channel, reason)
172
171
  @channels[channel.to_str.downcase] ||= 0
@@ -176,7 +175,6 @@ class Butler
176
175
  self
177
176
  end
178
177
 
179
- # FIXME
180
178
  # remove a channel from the user (should only be used by Butler::IRC::Parser)
181
179
  def delete_channel(channel, reason)
182
180
  @channels.delete(channel.to_str.downcase)
@@ -185,28 +183,24 @@ class Butler
185
183
  end
186
184
  end
187
185
 
188
- # FIXME
189
186
  def add_flags(channel, flags)
190
187
  channel = channel.to_str.downcase
191
188
  raise ArgumentError, "User #{self} is not listed in #{channel}" unless @channels.include?(channel)
192
189
  @channels[channel] |= flags
193
190
  end
194
191
 
195
- # FIXME
196
192
  def delete_flags(channel, flags)
197
193
  channel = channel.to_str.downcase
198
194
  raise ArgumentError, "User #{self} is not listed in #{channel}" unless @channels.include?(channel)
199
195
  @channels[channel] &= ~flags
200
196
  end
201
197
 
202
- # FIXME
203
198
  def quit
204
199
  @channels.each { |channel, _| delete_channel(channel, :quit) }
205
200
  self.status = :offline
206
201
  @users.delete(self, :quit)
207
202
  end
208
203
 
209
- # FIXME
210
204
  def kill
211
205
  @channels.each { |channel, _| delete_channel(channel, :kill) }
212
206
  self.status = :offline
@@ -6,7 +6,7 @@
6
6
 
7
7
 
8
8
 
9
- require 'butler/irc/channels'
9
+ require 'butler/irc/channellist'
10
10
  require 'butler/irc/string'
11
11
  require 'butler/irc/user'
12
12
  require 'thread'
@@ -16,7 +16,7 @@ require 'thread'
16
16
  class Butler
17
17
  module IRC
18
18
  # Enumerable: all known & visible users
19
- class Users
19
+ class UserList
20
20
  include Enumerable
21
21
 
22
22
  # the user that represents the clients user
@@ -0,0 +1,6 @@
1
+ class Butler
2
+ module IRC
3
+ # Reply for Butler::IRC::Client#whois
4
+ Whois = Struct.new(:exists, :nick, :user, :host, :real, :registered, :channels, :server, :idle, :signon)
5
+ end
6
+ end
@@ -10,6 +10,7 @@ require 'butler/plugin/configproxy'
10
10
  require 'butler/plugin/onhandlers'
11
11
  require 'butler/plugin/mapper'
12
12
  require 'butler/plugin/matcher'
13
+ require 'butler/plugin/more'
13
14
  require 'butler/plugin/trigger'
14
15
  require 'log/comfort'
15
16
  require 'ostructfixed'
@@ -36,15 +37,16 @@ class Butler
36
37
  # this method is called to initialize the plugin-class,
37
38
  # do not override
38
39
  def load_plugin(butler, base, path) # :nodoc:
39
- @butler = butler
40
- @base = base.dup.freeze
41
- @name = File.basename(base).freeze
42
- @commands = []
43
- @listener = []
44
- @config = ConfigProxy.new(@butler.config, "plugin/#{name}")
40
+ @butler = butler
41
+ @base = base.dup.freeze
42
+ @name = File.basename(base).freeze
43
+ @commands = []
44
+ @listener = []
45
+ @mapping_type = Hash.new { |h,k| MappingTypes[k] }
46
+ @config = ConfigProxy.new(@butler.config, "plugin/#{name}")
45
47
 
46
48
  if File.directory?(path) then
47
- raise "Not supported yet" # FIXME
49
+ raise "Not supported yet"
48
50
  @path = OpenStruct.new(
49
51
  :data => (@path+"/data").freeze,
50
52
  :strings => (@path+"/strings").freeze,
@@ -104,12 +106,16 @@ class Butler
104
106
  new(message).usage(data)
105
107
  end
106
108
 
107
- def on_load
109
+ def on_load(*args)
110
+ end
111
+
112
+ def on_login(*args)
108
113
  end
109
114
 
110
115
  def trigger(commands)
111
116
  commands = { "en" => commands } unless commands.kind_of?(Hash)
112
117
  commands.each { |lang, command|
118
+ raise "Invalid trigger, language must be a string" unless String === lang
113
119
  trigger = Trigger.new(self, lang, command)
114
120
  @butler.add_command(trigger)
115
121
  @commands << trigger
@@ -119,6 +125,7 @@ class Butler
119
125
  def map(meth, expressions)
120
126
  expressions = { "en" => expressions } unless expressions.kind_of?(Hash)
121
127
  expressions.each { |lang, expression|
128
+ raise "Invalid map, language must be a string, method a symbol and expression a string" unless String === lang and String === expression and Symbol === meth
122
129
  mapper = Mapper.new(self, meth, lang, expression)
123
130
  @butler.add_command(mapper)
124
131
  @commands << mapper
@@ -142,8 +149,14 @@ class Butler
142
149
  @listener << listener
143
150
  listener
144
151
  end
152
+
153
+ def on_disconnect(*args)
154
+ end
155
+
156
+ def on_quit(*args)
157
+ end
145
158
 
146
- def on_unload
159
+ def on_unload(*args)
147
160
  end
148
161
 
149
162
  def unload_plugin
@@ -205,14 +218,35 @@ class Butler
205
218
  # and translates it using vars as variables for the string interpolation.
206
219
  # The string is subsequently mirc_formatted (see String#mirc_formatted).
207
220
  # Besides that it works like Message#answer.
208
- def answer(string, vars={})
209
- string = localize(string, vars) if string.kind_of?(Symbol)
210
- @message.answer(string.mirc_formatted)
221
+ # Another feature of answer is, that it is 'moreified', that means if your answer-text is longer
222
+ # than 300 chars, butler will display 'more...' at the end and using the plugin 'more' the user
223
+ # can see the rest of the text.
224
+ def answer(text, vars={})
225
+ text = localize(text, vars) if text.kind_of?(Symbol)
226
+ @message.from.session["more"] = More.new(
227
+ @message,
228
+ nil,
229
+ text.mirc_formatted
230
+ )
231
+ @message.answer("#{@message.from.to_s+': ' if @message.public?}#{@message.from.session['more'].show}")
211
232
  end
233
+
234
+ # Same as answer, but 'more' gets a lead prefixed (see Butler::Plugin::More for more info)
235
+ def answer_with_lead(lead, string, vars={})
236
+ text = localize(text, vars) if text.kind_of?(Symbol)
237
+ lead = localize(lead, vars) if text.kind_of?(Symbol)
238
+ @message.from.session["more"] = More.new(
239
+ @message,
240
+ lead.mirc_formatted,
241
+ text.mirc_formatted
242
+ )
243
+ @message.answer("#{@message.from.to_s+': ' if @message.public?}#{@message.from.session['more'].show}")
244
+ end
245
+
212
246
 
213
247
  # == About
214
248
  # Sends a privmsg (localized and formatted) to any number of recipients
215
- # (Users or Channels)
249
+ # (UserList or ChannelList)
216
250
  #
217
251
  # == Synopsis
218
252
  # privmsg(:greet, "#some_channel", :from => @butler.myself.nick)
@@ -230,7 +264,7 @@ class Butler
230
264
 
231
265
  # == About
232
266
  # Sends a notice (localized and formatted) to any number of recipients
233
- # (Users or Channels)
267
+ # (UserList or ChannelList)
234
268
  #
235
269
  # == Synopsis
236
270
  # notice(:greet, "#some_channel", :from => @butler.myself.nick)