cakewalk 3.0.0

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 (99) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +1 -0
  3. data/LICENSE +22 -0
  4. data/README.md +169 -0
  5. data/docs/bot_options.md +454 -0
  6. data/docs/changes.md +541 -0
  7. data/docs/common_mistakes.md +60 -0
  8. data/docs/common_tasks.md +57 -0
  9. data/docs/encodings.md +69 -0
  10. data/docs/events.md +273 -0
  11. data/docs/getting_started.md +184 -0
  12. data/docs/logging.md +90 -0
  13. data/docs/migrating.md +267 -0
  14. data/docs/plugins.md +4 -0
  15. data/docs/readme.md +20 -0
  16. data/examples/basic/autovoice.rb +32 -0
  17. data/examples/basic/google.rb +35 -0
  18. data/examples/basic/hello.rb +15 -0
  19. data/examples/basic/join_part.rb +34 -0
  20. data/examples/basic/memo.rb +39 -0
  21. data/examples/basic/msg.rb +16 -0
  22. data/examples/basic/seen.rb +36 -0
  23. data/examples/basic/urban_dict.rb +35 -0
  24. data/examples/basic/url_shorten.rb +35 -0
  25. data/examples/plugins/autovoice.rb +37 -0
  26. data/examples/plugins/custom_prefix.rb +23 -0
  27. data/examples/plugins/dice_roll.rb +38 -0
  28. data/examples/plugins/google.rb +36 -0
  29. data/examples/plugins/hello.rb +22 -0
  30. data/examples/plugins/hooks.rb +36 -0
  31. data/examples/plugins/join_part.rb +42 -0
  32. data/examples/plugins/lambdas.rb +35 -0
  33. data/examples/plugins/last_nick.rb +24 -0
  34. data/examples/plugins/msg.rb +22 -0
  35. data/examples/plugins/multiple_matches.rb +32 -0
  36. data/examples/plugins/own_events.rb +37 -0
  37. data/examples/plugins/seen.rb +45 -0
  38. data/examples/plugins/timer.rb +22 -0
  39. data/examples/plugins/url_shorten.rb +33 -0
  40. data/lib/cakewalk/ban.rb +50 -0
  41. data/lib/cakewalk/bot.rb +479 -0
  42. data/lib/cakewalk/cached_list.rb +19 -0
  43. data/lib/cakewalk/callback.rb +20 -0
  44. data/lib/cakewalk/channel.rb +463 -0
  45. data/lib/cakewalk/channel_list.rb +29 -0
  46. data/lib/cakewalk/configuration/bot.rb +48 -0
  47. data/lib/cakewalk/configuration/dcc.rb +16 -0
  48. data/lib/cakewalk/configuration/plugins.rb +41 -0
  49. data/lib/cakewalk/configuration/sasl.rb +19 -0
  50. data/lib/cakewalk/configuration/ssl.rb +19 -0
  51. data/lib/cakewalk/configuration/timeouts.rb +14 -0
  52. data/lib/cakewalk/configuration.rb +73 -0
  53. data/lib/cakewalk/constants.rb +533 -0
  54. data/lib/cakewalk/dcc/dccable_object.rb +37 -0
  55. data/lib/cakewalk/dcc/incoming/send.rb +147 -0
  56. data/lib/cakewalk/dcc/incoming.rb +1 -0
  57. data/lib/cakewalk/dcc/outgoing/send.rb +122 -0
  58. data/lib/cakewalk/dcc/outgoing.rb +1 -0
  59. data/lib/cakewalk/dcc.rb +12 -0
  60. data/lib/cakewalk/exceptions.rb +46 -0
  61. data/lib/cakewalk/formatting.rb +125 -0
  62. data/lib/cakewalk/handler.rb +118 -0
  63. data/lib/cakewalk/handler_list.rb +90 -0
  64. data/lib/cakewalk/helpers.rb +231 -0
  65. data/lib/cakewalk/irc.rb +913 -0
  66. data/lib/cakewalk/isupport.rb +98 -0
  67. data/lib/cakewalk/log_filter.rb +21 -0
  68. data/lib/cakewalk/logger/formatted_logger.rb +97 -0
  69. data/lib/cakewalk/logger/zcbot_logger.rb +22 -0
  70. data/lib/cakewalk/logger.rb +168 -0
  71. data/lib/cakewalk/logger_list.rb +85 -0
  72. data/lib/cakewalk/mask.rb +69 -0
  73. data/lib/cakewalk/message.rb +391 -0
  74. data/lib/cakewalk/message_queue.rb +107 -0
  75. data/lib/cakewalk/mode_parser.rb +76 -0
  76. data/lib/cakewalk/network.rb +89 -0
  77. data/lib/cakewalk/open_ended_queue.rb +26 -0
  78. data/lib/cakewalk/pattern.rb +65 -0
  79. data/lib/cakewalk/plugin.rb +515 -0
  80. data/lib/cakewalk/plugin_list.rb +38 -0
  81. data/lib/cakewalk/rubyext/float.rb +3 -0
  82. data/lib/cakewalk/rubyext/module.rb +26 -0
  83. data/lib/cakewalk/rubyext/string.rb +33 -0
  84. data/lib/cakewalk/sasl/dh_blowfish.rb +71 -0
  85. data/lib/cakewalk/sasl/diffie_hellman.rb +47 -0
  86. data/lib/cakewalk/sasl/mechanism.rb +6 -0
  87. data/lib/cakewalk/sasl/plain.rb +26 -0
  88. data/lib/cakewalk/sasl.rb +34 -0
  89. data/lib/cakewalk/syncable.rb +83 -0
  90. data/lib/cakewalk/target.rb +199 -0
  91. data/lib/cakewalk/timer.rb +145 -0
  92. data/lib/cakewalk/user.rb +488 -0
  93. data/lib/cakewalk/user_list.rb +87 -0
  94. data/lib/cakewalk/utilities/deprecation.rb +16 -0
  95. data/lib/cakewalk/utilities/encoding.rb +37 -0
  96. data/lib/cakewalk/utilities/kernel.rb +13 -0
  97. data/lib/cakewalk/version.rb +4 -0
  98. data/lib/cakewalk.rb +5 -0
  99. metadata +140 -0
@@ -0,0 +1,65 @@
1
+ # -*- coding: utf-8 -*-
2
+ module Cakewalk
3
+ # @api private
4
+ # @since 1.1.0
5
+ class Pattern
6
+ # @param [String, Regexp, NilClass, Proc, #to_s] obj The object to
7
+ # convert to a regexp
8
+ # @return [Regexp, nil]
9
+ def self.obj_to_r(obj, anchor = nil)
10
+ case obj
11
+ when Regexp, NilClass
12
+ return obj
13
+ else
14
+ escaped = Regexp.escape(obj.to_s)
15
+ case anchor
16
+ when :start
17
+ return Regexp.new("^" + escaped)
18
+ when :end
19
+ return Regexp.new(escaped + "$")
20
+ when nil
21
+ return Regexp.new(Regexp.escape(obj.to_s))
22
+ end
23
+ end
24
+ end
25
+
26
+ def self.resolve_proc(obj, msg = nil)
27
+ if obj.is_a?(Proc)
28
+ return resolve_proc(obj.call(msg), msg)
29
+ else
30
+ return obj
31
+ end
32
+ end
33
+
34
+ def self.generate(type, argument)
35
+ case type
36
+ when :ctcp
37
+ Pattern.new(/^/, /#{Regexp.escape(argument.to_s)}(?:$| .+)/, nil)
38
+ else
39
+ raise ArgumentError, "Unsupported type: #{type.inspect}"
40
+ end
41
+ end
42
+
43
+ attr_reader :prefix
44
+ attr_reader :suffix
45
+ attr_reader :pattern
46
+ def initialize(prefix, pattern, suffix)
47
+ @prefix, @pattern, @suffix = prefix, pattern, suffix
48
+ end
49
+
50
+ def to_r(msg = nil)
51
+ pattern = Pattern.resolve_proc(@pattern, msg)
52
+
53
+ case pattern
54
+ when Regexp, NilClass
55
+ prefix = Pattern.obj_to_r(Pattern.resolve_proc(@prefix, msg), :start)
56
+ suffix = Pattern.obj_to_r(Pattern.resolve_proc(@suffix, msg), :end)
57
+ /#{prefix}#{pattern}#{suffix}/
58
+ else
59
+ prefix = Pattern.obj_to_r(Pattern.resolve_proc(@prefix, msg))
60
+ suffix = Pattern.obj_to_r(Pattern.resolve_proc(@suffix, msg))
61
+ /^#{prefix}#{Pattern.obj_to_r(pattern)}#{suffix}$/
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,515 @@
1
+ require "cakewalk/helpers"
2
+
3
+ module Cakewalk
4
+ # This class represents the core of the plugin functionality of
5
+ # Cakewalk. It provides both the methods for users to write their own
6
+ # plugins as well as for the Cakewalk framework to use them.
7
+ #
8
+ # The {ClassMethods} module, which will get included automatically
9
+ # in all classes that include `Cakewalk::Plugin`, includes all class
10
+ # methods that the user will use for creating plugins.
11
+ #
12
+ # Most of the instance methods are for use by the Cakewalk framework
13
+ # and part of the private API, but some will also be used by plugin
14
+ # authors, mainly {#config}, {#synchronize} and {#bot}.
15
+ module Plugin
16
+ include Helpers
17
+
18
+ # The ClassMethods module includes all methods that the user will
19
+ # need for creating plugins for the Cakewalk framework: Setting
20
+ # options (see {#set} and the attributes) as well as methods for
21
+ # configuring the actual pattern matching ({#match}, {#listen_to}).
22
+ #
23
+ # Furthermore, the attributes allow for programmatically
24
+ # inspecting plugins.
25
+ #
26
+ # @attr plugin_name
27
+ module ClassMethods
28
+ # @return [Hash{:pre, :post => Array<Hook>}] All hooks
29
+ attr_reader :hooks
30
+
31
+ # @return [Array<:message, :channel, :private>] The list of events to react on
32
+ attr_accessor :react_on
33
+
34
+ # The name of the plugin.
35
+ # @overload plugin_name
36
+ # @return [String, nil]
37
+ # @overload plugin_name=(new_name)
38
+ # @param [String, nil] new_name
39
+ # @return [String]
40
+ # @return [String, nil] The name of the plugin
41
+ attr_reader :plugin_name
42
+
43
+ # @return [String]
44
+ def plugin_name=(new_name)
45
+ if new_name.nil? && self.name
46
+ @plugin_name = self.name.split("::").last.downcase
47
+ else
48
+ @plugin_name = new_name
49
+ end
50
+ end
51
+
52
+ # @return [Array<Matcher>] All matchers
53
+ attr_reader :matchers
54
+
55
+ # @return [Array<Listener>] All listeners
56
+ attr_reader :listeners
57
+
58
+ # @return [Array<Timer>] All timers
59
+ attr_reader :timers
60
+
61
+ # @return [Array<String>] All CTCPs
62
+ attr_reader :ctcps
63
+
64
+ # @return [String, nil] The help message
65
+ attr_accessor :help
66
+
67
+ # @return [String, Regexp, Proc] The prefix
68
+ attr_accessor :prefix
69
+
70
+ # @return [String, Regexp, Proc] The suffix
71
+ attr_accessor :suffix
72
+
73
+ # @return [Array<Symbol>] Required plugin options
74
+ attr_accessor :required_options
75
+
76
+ # Represents a Matcher as created by {#match}.
77
+ #
78
+ # @attr [String, Regexp, Proc] pattern
79
+ # @attr [Boolean] use_prefix
80
+ # @attr [Boolean] use_suffix
81
+ # @attr [Symbol] method
82
+ # @attr [Symbol] group
83
+ Matcher = Struct.new(:pattern,
84
+ :use_prefix,
85
+ :use_suffix,
86
+ :method,
87
+ :group,
88
+ :prefix,
89
+ :suffix,
90
+ :react_on,
91
+ :strip_colors)
92
+
93
+ # Represents a Listener as created by {#listen_to}.
94
+ #
95
+ # @attr [Symbol] event
96
+ # @attr [Symbol] method
97
+ Listener = Struct.new(:event, :method)
98
+
99
+ # Represents a Timer as created by {#timer}.
100
+ #
101
+ # @note This is not the same as a {Cakewalk::Timer} object, which
102
+ # will allow controlling and inspecting actually running
103
+ # timers. This class only describes a Timer that still has to
104
+ # be created.
105
+ #
106
+ # @attr [Numeric] interval
107
+ # @attr [Symbol] method
108
+ # @attr [Hash] options
109
+ # @attr [Boolean] registered
110
+ Timer = Struct.new(:interval, :options, :registered)
111
+
112
+ # Represents a Hook as created by {#hook}.
113
+ #
114
+ # @attr [Symbol] type
115
+ # @attr [Array<Symbol>] for
116
+ # @attr [Symbol] method
117
+ Hook = Struct.new(:type, :for, :method, :group)
118
+
119
+ # @api private
120
+ def self.extended(by)
121
+ by.instance_exec do
122
+ @matchers = []
123
+ @ctcps = []
124
+ @listeners = []
125
+ @timers = []
126
+ @help = nil
127
+ @hooks = Hash.new{|h, k| h[k] = []}
128
+ @prefix = nil
129
+ @suffix = nil
130
+ @react_on = :message
131
+ @required_options = []
132
+ self.plugin_name = nil
133
+ end
134
+ end
135
+
136
+ # Set options.
137
+ #
138
+ # Available options:
139
+ #
140
+ # - {#help}
141
+ # - {#plugin_name}
142
+ # - {#prefix}
143
+ # - {#react_on}
144
+ # - {#required_options}
145
+ # - {#suffix}
146
+ #
147
+ # @overload set(key, value)
148
+ # @param [Symbol] key The option's name
149
+ # @param [Object] value
150
+ # @return [void]
151
+ # @overload set(options)
152
+ # @param [Hash{Symbol => Object}] options The options, as key => value associations
153
+ # @return [void]
154
+ # @example
155
+ # set(:help => "the help message",
156
+ # :prefix => "^")
157
+ # @return [void]
158
+ # @since 2.0.0
159
+ def set(*args)
160
+ case args.size
161
+ when 1
162
+ # {:key => value, ...}
163
+ args.first.each do |key, value|
164
+ self.send("#{key}=", value)
165
+ end
166
+ when 2
167
+ # key, value
168
+ self.send("#{args.first}=", args.last)
169
+ else
170
+ raise ArgumentError # TODO proper error message
171
+ end
172
+ end
173
+
174
+ # Set a match pattern.
175
+ #
176
+ # @param [Regexp, String] pattern A pattern
177
+ # @option options [Symbol] :method (:execute) The method to execute
178
+ # @option options [Boolean] :use_prefix (true) If true, the
179
+ # plugin prefix will automatically be prepended to the
180
+ # pattern.
181
+ # @option options [Boolean] :use_suffix (true) If true, the
182
+ # plugin suffix will automatically be appended to the
183
+ # pattern.
184
+ # @option options [String, Regexp, Proc] prefix (nil) A prefix
185
+ # overwriting the per-plugin prefix.
186
+ # @option options [String, Regexp, Proc] suffix (nil) A suffix
187
+ # overwriting the per-plugin suffix.
188
+ # @option options [Symbol, Fixnum] react_on (:message) The
189
+ # {file:docs/events.md event} to react on.
190
+ # @option options [Symbol] :group (nil) The group the match belongs to.
191
+ # @option options [Boolean] :strip_colors (false) Strip colors
192
+ # from message before attempting match
193
+ # @return [Matcher]
194
+ # @todo Document match/listener grouping
195
+ def match(pattern, options = {})
196
+ options = {
197
+ :use_prefix => true,
198
+ :use_suffix => true,
199
+ :method => :execute,
200
+ :group => nil,
201
+ :prefix => nil,
202
+ :suffix => nil,
203
+ :react_on => nil,
204
+ :strip_colors => false,
205
+ }.merge(options)
206
+ if options[:react_on]
207
+ options[:react_on] = options[:react_on].to_s.to_sym
208
+ end
209
+ matcher = Matcher.new(pattern, *options.values_at(:use_prefix,
210
+ :use_suffix,
211
+ :method,
212
+ :group,
213
+ :prefix,
214
+ :suffix,
215
+ :react_on,
216
+ :strip_colors))
217
+ @matchers << matcher
218
+
219
+ matcher
220
+ end
221
+
222
+ # Events to listen to.
223
+ # @overload listen_to(*types, options = {})
224
+ # @param [String, Symbol, Integer] *types Events to listen to. Available
225
+ # events are all IRC commands in lowercase as symbols, all numeric
226
+ # replies and all events listed in the {file:docs/events.md list of events}.
227
+ # @param [Hash] options
228
+ # @option options [Symbol] :method (:listen) The method to
229
+ # execute
230
+ # @return [Array<Listener>]
231
+ def listen_to(*types)
232
+ options = {:method => :listen}
233
+ if types.last.is_a?(Hash)
234
+ options.merge!(types.pop)
235
+ end
236
+
237
+ listeners = types.map {|type| Listener.new(type.to_s.to_sym, options[:method])}
238
+ @listeners.concat listeners
239
+
240
+ listeners
241
+ end
242
+
243
+ # @version 1.1.1
244
+ def ctcp(command)
245
+ @ctcps << command.to_s.upcase
246
+ end
247
+
248
+ # @example
249
+ # timer 5, method: :some_method
250
+ # def some_method
251
+ # Channel("#cakewalk-bots").send(Time.now.to_s)
252
+ # end
253
+ #
254
+ # @param [Numeric] interval Interval in seconds
255
+ # @option options [Symbol] :method (:timer) Method to call (only
256
+ # if no proc is provided)
257
+ # @option options [Integer] :shots (Float::INFINITY) How often
258
+ # should the timer fire?
259
+ # @option options [Boolean] :threaded (true) Call method in a
260
+ # thread?
261
+ # @option options [Boolean] :start_automatically (true) If true,
262
+ # the timer will automatically start after the bot finished
263
+ # connecting.
264
+ # @option options [Boolean] :stop_automaticall (true) If true,
265
+ # the timer will automatically stop when the bot disconnects.
266
+ # @return [Timer]
267
+ # @since 1.1.0
268
+ def timer(interval, options = {})
269
+ options = {:method => :timer, :threaded => true}.merge(options)
270
+ timer = Timer.new(interval, options, false)
271
+ @timers << timer
272
+
273
+ timer
274
+ end
275
+
276
+ # Defines a hook which will be run before or after a handler is
277
+ # executed, depending on the value of `type`.
278
+ #
279
+ # @param [:pre, :post] type Run the hook before or after
280
+ # a handler?
281
+ # @option options [Array<:match, :listen_to, :ctcp>] :for ([:match, :listen_to, :ctcp])
282
+ # Which kinds of events to run the hook for.
283
+ # @option options [Symbol] :method (:hook) The method to execute.
284
+ # @option options [Symbol] :group (nil) The match group to
285
+ # execute the hook for. Hooks belonging to the `nil` group
286
+ # will execute for all matches.
287
+ # @return [Hook]
288
+ # @since 1.1.0
289
+ def hook(type, options = {})
290
+ options = {:for => [:match, :listen_to, :ctcp], :method => :hook, :group => nil}.merge(options)
291
+ hook = Hook.new(type, options[:for], options[:method], options[:group])
292
+ __hooks(type) << hook
293
+
294
+ hook
295
+ end
296
+
297
+ # @return [Hash]
298
+ # @api private
299
+ def __hooks(type = nil, events = nil, group = nil)
300
+ if type.nil?
301
+ hooks = @hooks
302
+ else
303
+ hooks = @hooks[type]
304
+ end
305
+
306
+ if events.nil?
307
+ return hooks
308
+ else
309
+ events = [*events]
310
+ if hooks.is_a?(Hash)
311
+ hooks = hooks.map { |k, v| v }
312
+ end
313
+ hooks = hooks.select { |hook| (events & hook.for).size > 0 }
314
+ end
315
+
316
+ return hooks.select { |hook| hook.group.nil? || hook.group == group }
317
+ end
318
+
319
+ # @return [Boolean] True if processing should continue
320
+ # @api private
321
+ def call_hooks(type, event, group, instance, args)
322
+ stop = __hooks(type, event, group).find { |hook|
323
+ # stop after the first hook that returns false
324
+ if hook.method.respond_to?(:call)
325
+ instance.instance_exec(*args, &hook.method) == false
326
+ else
327
+ instance.__send__(hook.method, *args) == false
328
+ end
329
+ }
330
+
331
+ !stop
332
+ end
333
+
334
+ # @param [Bot] bot
335
+ # @return [Array<Symbol>, nil]
336
+ # @since 2.0.0
337
+ # @api private
338
+ def check_for_missing_options(bot)
339
+ @required_options.select { |option|
340
+ !bot.config.plugins.options[self].has_key?(option)
341
+ }
342
+ end
343
+ end
344
+
345
+ def __register_listeners
346
+ self.class.listeners.each do |listener|
347
+ @bot.loggers.debug "[plugin] #{self.class.plugin_name}: Registering listener for type `#{listener.event}`"
348
+ new_handler = Handler.new(@bot, listener.event, Pattern.new(nil, //, nil)) do |message, *args|
349
+ if self.class.call_hooks(:pre, :listen_to, nil, self, [message])
350
+ __send__(listener.method, message, *args)
351
+ self.class.call_hooks(:post, :listen_to, nil, self, [message])
352
+ else
353
+ @bot.loggers.debug "[plugin] #{self.class.plugin_name}: Dropping message due to hook"
354
+ end
355
+ end
356
+
357
+ @handlers << new_handler
358
+ @bot.handlers.register(new_handler)
359
+ end
360
+ end
361
+ private :__register_listeners
362
+
363
+ def __register_ctcps
364
+ self.class.ctcps.each do |ctcp|
365
+ @bot.loggers.debug "[plugin] #{self.class.plugin_name}: Registering CTCP `#{ctcp}`"
366
+ new_handler = Handler.new(@bot, :ctcp, Pattern.generate(:ctcp, ctcp)) do |message, *args|
367
+ if self.class.call_hooks(:pre, :ctcp, nil, self, [message])
368
+ __send__("ctcp_#{ctcp.downcase}", message, *args)
369
+ self.class.call_hooks(:post, :ctcp, nil, self, [message])
370
+ else
371
+ @bot.loggers.debug "[plugin] #{self.class.plugin_name}: Dropping message due to hook"
372
+ end
373
+ end
374
+
375
+ @handlers << new_handler
376
+ @bot.handlers.register(new_handler)
377
+ end
378
+ end
379
+ private :__register_ctcps
380
+
381
+ def __register_timers
382
+ @timers = self.class.timers.map {|timer_struct|
383
+ @bot.loggers.debug "[plugin] #{self.class.plugin_name}: Registering timer with interval `#{timer_struct.interval}` for method `#{timer_struct.options[:method]}`"
384
+
385
+ block = self.method(timer_struct.options[:method])
386
+ options = timer_struct.options.merge(interval: timer_struct.interval)
387
+ Cakewalk::Timer.new(@bot, options, &block)
388
+ }
389
+ end
390
+ private :__register_timers
391
+
392
+ def __register_matchers
393
+ prefix = self.class.prefix || @bot.config.plugins.prefix
394
+ suffix = self.class.suffix || @bot.config.plugins.suffix
395
+
396
+ self.class.matchers.each do |matcher|
397
+ _prefix = matcher.use_prefix ? matcher.prefix || prefix : nil
398
+ _suffix = matcher.use_suffix ? matcher.suffix || suffix : nil
399
+
400
+ pattern_to_register = Pattern.new(_prefix, matcher.pattern, _suffix)
401
+ react_on = matcher.react_on || self.class.react_on || :message
402
+
403
+ @bot.loggers.debug "[plugin] #{self.class.plugin_name}: Registering executor with pattern `#{pattern_to_register.inspect}`, reacting on `#{react_on}`"
404
+
405
+ new_handler = Handler.new(@bot,
406
+ react_on,
407
+ pattern_to_register,
408
+ group: matcher.group,
409
+ strip_colors: matcher.strip_colors) do |message, *args|
410
+ method = method(matcher.method)
411
+ arity = method.arity - 1
412
+ if arity > 0
413
+ args = args[0..arity - 1]
414
+ elsif arity == 0
415
+ args = []
416
+ end
417
+ if self.class.call_hooks(:pre, :match, matcher.group, self, [message])
418
+ method.call(message, *args)
419
+ self.class.call_hooks(:post, :match, matcher.group, self, [message])
420
+ else
421
+ @bot.loggers.debug "[plugin] #{self.class.plugin_name}: Dropping message due to hook"
422
+ end
423
+ end
424
+ @handlers << new_handler
425
+ @bot.handlers.register(new_handler)
426
+ end
427
+ end
428
+ private :__register_matchers
429
+
430
+ def __register_help
431
+ prefix = self.class.prefix || @bot.config.plugins.prefix
432
+ suffix = self.class.suffix || @bot.config.plugins.suffix
433
+ if self.class.help
434
+ @bot.loggers.debug "[plugin] #{self.class.plugin_name}: Registering help message"
435
+ help_pattern = Pattern.new(prefix, "help #{self.class.plugin_name}", suffix)
436
+ new_handler = Handler.new(@bot, :message, help_pattern) do |message|
437
+ message.reply(self.class.help)
438
+ end
439
+
440
+ @handlers << new_handler
441
+ @bot.handlers.register(new_handler)
442
+ end
443
+ end
444
+ private :__register_help
445
+
446
+ # @return [void]
447
+ # @api private
448
+ def __register
449
+ missing = self.class.check_for_missing_options(@bot)
450
+ unless missing.empty?
451
+ @bot.loggers.warn "[plugin] #{self.class.plugin_name}: Could not register plugin because the following options are not set: #{missing.join(", ")}"
452
+ return
453
+ end
454
+
455
+ __register_listeners
456
+ __register_matchers
457
+ __register_ctcps
458
+ __register_timers
459
+ __register_help
460
+ end
461
+
462
+ # @return [Bot]
463
+ attr_reader :bot
464
+
465
+ # @return [Array<Handler>] handlers
466
+ attr_reader :handlers
467
+
468
+ # @return [Array<Cakewalk::Timer>]
469
+ attr_reader :timers
470
+
471
+ # @api private
472
+ def initialize(bot)
473
+ @bot = bot
474
+ @handlers = []
475
+ @timers = []
476
+ __register
477
+ end
478
+
479
+ # @since 2.0.0
480
+ def unregister
481
+ @bot.loggers.debug "[plugin] #{self.class.plugin_name}: Unloading plugin"
482
+ @timers.each do |timer|
483
+ timer.stop
484
+ end
485
+
486
+ handlers.each do |handler|
487
+ handler.stop
488
+ handler.unregister
489
+ end
490
+ end
491
+
492
+ # (see Bot#synchronize)
493
+ def synchronize(name, &block)
494
+ @bot.synchronize(name, &block)
495
+ end
496
+
497
+ # Provides access to plugin-specific options.
498
+ #
499
+ # @return [Hash] A hash of options
500
+ def config
501
+ @bot.config.plugins.options[self.class] || {}
502
+ end
503
+
504
+ def shared
505
+ @bot.config.shared
506
+ end
507
+
508
+ # @api private
509
+ def self.included(by)
510
+ by.extend ClassMethods
511
+ end
512
+ end
513
+ end
514
+
515
+ # TODO more details in "message dropped" debug output
@@ -0,0 +1,38 @@
1
+ module Cakewalk
2
+ # @since 2.0.0
3
+ class PluginList < Array
4
+ def initialize(bot)
5
+ @bot = bot
6
+ super()
7
+ end
8
+
9
+ # @param [Class<Plugin>] plugin
10
+ def register_plugin(plugin)
11
+ self << plugin.new(@bot)
12
+ end
13
+
14
+ # @param [Array<Class<Plugin>>] plugins
15
+ def register_plugins(plugins)
16
+ plugins.each { |plugin| register_plugin(plugin) }
17
+ end
18
+
19
+ # @since 2.0.0
20
+ def unregister_plugin(plugin)
21
+ plugin.unregister
22
+ delete(plugin)
23
+ end
24
+
25
+ # @since 2.0.0
26
+ def unregister_plugins(plugins)
27
+ if plugins == self
28
+ plugins = self.dup
29
+ end
30
+ plugins.each { |plugin| unregister_plugin(plugin) }
31
+ end
32
+
33
+ # @since 2.0.0
34
+ def unregister_all
35
+ unregister_plugins(self)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,3 @@
1
+ unless Float.const_defined?(:INFINITY)
2
+ Float::INFINITY = 1.0/0.0
3
+ end
@@ -0,0 +1,26 @@
1
+ # Extensions to Ruby's Module class.
2
+ class Module
3
+ # Like `attr_reader`, but for defining a synchronized attribute
4
+ # reader.
5
+ #
6
+ # @api private
7
+ def synced_attr_reader(attribute)
8
+ undef_method(attribute)
9
+ define_method(attribute) do
10
+ attr(attribute)
11
+ end
12
+
13
+ define_method("#{attribute}_unsynced") do
14
+ attr(attribute, false, true)
15
+ end
16
+ end
17
+
18
+ # Like `attr_accessor`, but for defining a synchronized attribute
19
+ # accessor
20
+ #
21
+ # @api private
22
+ def synced_attr_accessor(attr)
23
+ synced_attr_reader(attr)
24
+ attr_accessor(attr)
25
+ end
26
+ end
@@ -0,0 +1,33 @@
1
+ # Extensions to Ruby's String class.
2
+ class String
3
+ # Like `String#downcase`, but respecting different IRC casemaps.
4
+ #
5
+ # @param [:rfc1459, :"strict-rfc1459", :ascii] mapping
6
+ # @return [String]
7
+ def irc_downcase(mapping)
8
+ case mapping
9
+ when :rfc1459
10
+ self.tr("A-Z[]\\\\^", "a-z{}|~")
11
+ when :"strict-rfc1459"
12
+ self.tr("A-Z[]\\\\", "a-z{}|")
13
+ else
14
+ # when :ascii or unknown/nil
15
+ self.tr("A-Z", "a-z")
16
+ end
17
+ end
18
+
19
+ # Like `String#upcase`, but respecting different IRC casemaps.
20
+ #
21
+ # @param [:rfc1459, :"strict-rfc1459", :ascii] mapping
22
+ # @return [String]
23
+ def irc_upcase(mapping)
24
+ case mapping
25
+ when :ascii
26
+ self.tr("a-z", "A-Z")
27
+ when :rfc1459
28
+ self.tr("a-z{}|~", "A-Z[]\\\\^")
29
+ when :"strict-rfc1459"
30
+ self.tr("a-z{}|", "A-Z[]\\\\")
31
+ end
32
+ end
33
+ end