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