cinch 1.1.3 → 2.0.0.pre.1

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 (83) hide show
  1. data/LICENSE +1 -0
  2. data/README.md +3 -3
  3. data/docs/bot_options.md +435 -0
  4. data/docs/changes.md +440 -0
  5. data/docs/common_mistakes.md +35 -0
  6. data/docs/common_tasks.md +47 -0
  7. data/docs/encodings.md +67 -0
  8. data/docs/events.md +272 -0
  9. data/docs/logging.md +5 -0
  10. data/docs/migrating.md +267 -0
  11. data/docs/readme.md +18 -0
  12. data/examples/plugins/custom_prefix.rb +1 -1
  13. data/examples/plugins/dice_roll.rb +38 -0
  14. data/examples/plugins/lambdas.rb +1 -1
  15. data/examples/plugins/memo.rb +16 -10
  16. data/examples/plugins/url_shorten.rb +1 -0
  17. data/lib/cinch.rb +5 -60
  18. data/lib/cinch/ban.rb +13 -7
  19. data/lib/cinch/bot.rb +228 -403
  20. data/lib/cinch/{cache_manager.rb → cached_list.rb} +5 -1
  21. data/lib/cinch/callback.rb +3 -0
  22. data/lib/cinch/channel.rb +119 -195
  23. data/lib/cinch/{channel_manager.rb → channel_list.rb} +6 -3
  24. data/lib/cinch/configuration.rb +73 -0
  25. data/lib/cinch/configuration/bot.rb +47 -0
  26. data/lib/cinch/configuration/dcc.rb +16 -0
  27. data/lib/cinch/configuration/plugins.rb +41 -0
  28. data/lib/cinch/configuration/sasl.rb +17 -0
  29. data/lib/cinch/configuration/ssl.rb +19 -0
  30. data/lib/cinch/configuration/storage.rb +37 -0
  31. data/lib/cinch/configuration/timeouts.rb +14 -0
  32. data/lib/cinch/constants.rb +531 -369
  33. data/lib/cinch/dcc.rb +12 -0
  34. data/lib/cinch/dcc/dccable_object.rb +37 -0
  35. data/lib/cinch/dcc/incoming.rb +1 -0
  36. data/lib/cinch/dcc/incoming/send.rb +131 -0
  37. data/lib/cinch/dcc/outgoing.rb +1 -0
  38. data/lib/cinch/dcc/outgoing/send.rb +115 -0
  39. data/lib/cinch/exceptions.rb +8 -1
  40. data/lib/cinch/formatting.rb +106 -0
  41. data/lib/cinch/handler.rb +104 -0
  42. data/lib/cinch/handler_list.rb +86 -0
  43. data/lib/cinch/helpers.rb +167 -10
  44. data/lib/cinch/irc.rb +525 -110
  45. data/lib/cinch/isupport.rb +11 -9
  46. data/lib/cinch/logger.rb +168 -0
  47. data/lib/cinch/logger/formatted_logger.rb +72 -55
  48. data/lib/cinch/logger/zcbot_logger.rb +9 -24
  49. data/lib/cinch/logger_list.rb +62 -0
  50. data/lib/cinch/mask.rb +19 -10
  51. data/lib/cinch/message.rb +94 -28
  52. data/lib/cinch/message_queue.rb +70 -28
  53. data/lib/cinch/mode_parser.rb +6 -1
  54. data/lib/cinch/network.rb +104 -0
  55. data/lib/cinch/{rubyext/queue.rb → open_ended_queue.rb} +8 -1
  56. data/lib/cinch/pattern.rb +24 -4
  57. data/lib/cinch/plugin.rb +352 -177
  58. data/lib/cinch/plugin_list.rb +35 -0
  59. data/lib/cinch/rubyext/float.rb +3 -0
  60. data/lib/cinch/rubyext/module.rb +7 -0
  61. data/lib/cinch/rubyext/string.rb +9 -0
  62. data/lib/cinch/sasl.rb +34 -0
  63. data/lib/cinch/sasl/dh_blowfish.rb +71 -0
  64. data/lib/cinch/sasl/diffie_hellman.rb +47 -0
  65. data/lib/cinch/sasl/mechanism.rb +6 -0
  66. data/lib/cinch/sasl/plain.rb +26 -0
  67. data/lib/cinch/storage.rb +62 -0
  68. data/lib/cinch/storage/null.rb +12 -0
  69. data/lib/cinch/storage/yaml.rb +96 -0
  70. data/lib/cinch/syncable.rb +13 -1
  71. data/lib/cinch/target.rb +144 -0
  72. data/lib/cinch/timer.rb +145 -0
  73. data/lib/cinch/user.rb +169 -225
  74. data/lib/cinch/{user_manager.rb → user_list.rb} +7 -2
  75. data/lib/cinch/utilities/deprecation.rb +12 -0
  76. data/lib/cinch/utilities/encoding.rb +54 -0
  77. data/lib/cinch/utilities/kernel.rb +13 -0
  78. data/lib/cinch/utilities/string.rb +13 -0
  79. data/lib/cinch/version.rb +4 -0
  80. metadata +88 -47
  81. data/lib/cinch/logger/logger.rb +0 -44
  82. data/lib/cinch/logger/null_logger.rb +0 -18
  83. data/lib/cinch/rubyext/infinity.rb +0 -1
@@ -0,0 +1,104 @@
1
+ module Cinch
2
+ # This class allows querying the IRC network for its name and used
3
+ # server software as well as certain non-standard behaviour.
4
+ #
5
+ # @since 2.0.0
6
+ class Network
7
+ # @return [Symbol] The name of the network. `:unknown` if the
8
+ # network couldn't be detected.
9
+ attr_reader :name
10
+
11
+ # @api private
12
+ attr_writer :name
13
+
14
+ # @return [Symbol] The server software used by the network.
15
+ # `:unknown` if the software couldn't be detected.
16
+ attr_reader :ircd
17
+
18
+ # @api private
19
+ attr_writer :ircd
20
+
21
+ # @return [Array<Symbol>] All client capabilities supported by the
22
+ # network.
23
+ attr_reader :capabilities
24
+
25
+ # @api private
26
+ attr_writer :capabilities
27
+
28
+ # @param [Symbol] name
29
+ # @param [Symbol] ircd
30
+ # @api private
31
+ # @note The user should not create instances of this class but use
32
+ # {IRC#network} instead.
33
+ def initialize(name, ircd)
34
+ @name = name
35
+ @ircd = ircd
36
+ @capabilities = []
37
+ end
38
+
39
+ # @return [String, nil] The mode used for getting the list of
40
+ # channel owners, if any
41
+ def owner_list_mode
42
+ return "q" if @ircd == :unreal
43
+ end
44
+
45
+ # @return [String, nil] The mode used for getting the list of
46
+ # channel quiets, if any
47
+ def quiet_list_mode
48
+ return "q" if @ircd == :"ircd-seven"
49
+ end
50
+
51
+ # @return [Boolean] Does WHOIS only support one argument?
52
+ def whois_only_one_argument?
53
+ @name == :jtv
54
+ end
55
+
56
+ # @return [Boolean] True if connected to NgameTV
57
+ def ngametv?
58
+ @name == :ngametv
59
+ end
60
+
61
+ # @return [Boolean] True if connected to JTV
62
+ def jtv?
63
+ @name == :jtv
64
+ end
65
+
66
+ # @return [Boolean] True if we do not know which network we are
67
+ # connected to
68
+ def unknown_network?
69
+ @name == :unknown
70
+ end
71
+
72
+ # @return [Boolean] True if we do not know which software the
73
+ # server is running
74
+ def unknown_ircd?
75
+ @ircd == :unknown
76
+ end
77
+
78
+ # Note for the default_* methods: Always make sure to return a
79
+ # value for when no network/ircd was detected so that MessageQueue
80
+ # doesn't break.
81
+
82
+ # @return [Number] The `messages per second` value that best suits
83
+ # the current network
84
+ def default_messages_per_second
85
+ case @network
86
+ when :freenode
87
+ 0.7
88
+ else
89
+ 0.5
90
+ end
91
+ end
92
+
93
+ # @return [Number] The `server queue size` value that best suits
94
+ # the current network
95
+ def default_server_queue_size
96
+ case @network
97
+ when :quakenet
98
+ 40
99
+ else
100
+ 10
101
+ end
102
+ end
103
+ end
104
+ end
@@ -1,5 +1,12 @@
1
1
  require "thread"
2
- class Queue
2
+
3
+ # Like Ruby's Queue class, but allowing both pushing and unshifting
4
+ # objects.
5
+ #
6
+ # @api private
7
+ class OpenEndedQueue < Queue
8
+ # @param [Object] obj
9
+ # @return [void]
3
10
  def unshift(obj)
4
11
  t = nil
5
12
  @mutex.synchronize{
@@ -1,16 +1,25 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  module Cinch
3
3
  # @api private
4
+ # @since 1.1.0
4
5
  class Pattern
5
6
  # @param [String, Regexp, NilClass, Proc, #to_s] obj The object to
6
7
  # convert to a regexp
7
8
  # @return [Regexp, nil]
8
- def self.obj_to_r(obj)
9
+ def self.obj_to_r(obj, anchor = nil)
9
10
  case obj
10
11
  when Regexp, NilClass
11
12
  return obj
12
13
  else
13
- return Regexp.new(Regexp.escape(obj.to_s))
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
14
23
  end
15
24
  end
16
25
 
@@ -22,6 +31,15 @@ module Cinch
22
31
  end
23
32
  end
24
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
+
25
43
  attr_reader :prefix
26
44
  attr_reader :suffix
27
45
  attr_reader :pattern
@@ -30,14 +48,16 @@ module Cinch
30
48
  end
31
49
 
32
50
  def to_r(msg = nil)
33
- prefix = Pattern.obj_to_r(Pattern.resolve_proc(@prefix, msg))
34
- suffix = Pattern.obj_to_r(Pattern.resolve_proc(@suffix, msg))
35
51
  pattern = Pattern.resolve_proc(@pattern, msg)
36
52
 
37
53
  case pattern
38
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)
39
57
  /#{prefix}#{pattern}#{suffix}/
40
58
  else
59
+ prefix = Pattern.obj_to_r(Pattern.resolve_proc(@prefix, msg))
60
+ suffix = Pattern.obj_to_r(Pattern.resolve_proc(@suffix, msg))
41
61
  /^#{prefix}#{Pattern.obj_to_r(pattern)}#{suffix}$/
42
62
  end
43
63
  end
@@ -1,17 +1,167 @@
1
+ # TODO more details in "message dropped" debug output
1
2
  module Cinch
3
+ # This class represents the core of the plugin functionality of
4
+ # Cinch. It provides both the methods for users to write their own
5
+ # plugins as well as for the Cinch framework to use them.
6
+ #
7
+ # The {ClassMethods} module, which will get included automatically
8
+ # in all classes that include `Cinch::Plugin`, includes all class
9
+ # methods that the user will use for creating plugins.
10
+ #
11
+ # Most of the instance methods are for use by the Cinch framework
12
+ # and part of the private API, but some will also be used by plugin
13
+ # authors, mainly {#config}, {#synchronize}, {#storage} and {#bot}.
2
14
  module Plugin
3
15
  include Helpers
4
16
 
17
+ # The ClassMethods module includes all methods that the user will
18
+ # need for creating plugins for the Cinch framework: Setting
19
+ # options (see {#set} and the attributes) as well as methods for
20
+ # configuring the actual pattern matching ({#match}, {#listen_to}).
21
+ #
22
+ # Furthermore, the attributes allow for programmatically
23
+ # inspecting plugins.
24
+ #
25
+ # @attr plugin_name
5
26
  module ClassMethods
6
- # @api private
7
- Match = Struct.new(:pattern, :use_prefix, :use_suffix, :method)
8
- # @api private
27
+ # @return [Hash<Symbol<:pre, :post> => Array<Hook>>] All hooks
28
+ attr_reader :hooks
29
+
30
+ # @return [Array<Symbol<:message, :channel, :private>>] The list of events to react on
31
+ attr_accessor :reacting_on
32
+
33
+ # The name of the plugin.
34
+ # @overload plugin_name
35
+ # @return [String, nil]
36
+ # @overload plugin_name=(new_name)
37
+ # @param [String, nil] new_name
38
+ # @return [String]
39
+ # @return [String, nil] The name of the plugin
40
+ attr_reader :plugin_name
41
+
42
+ # @return [String]
43
+ def plugin_name=(new_name)
44
+ if new_name.nil? && self.name
45
+ @plugin_name = self.name.split("::").last.downcase
46
+ else
47
+ @plugin_name = new_name
48
+ end
49
+ end
50
+
51
+ # @return [Array<Matcher>] All matchers
52
+ attr_reader :matchers
53
+
54
+ # @return [Array<Listener>] All listeners
55
+ attr_reader :listeners
56
+
57
+ # @return [Array<Timer>] All timers
58
+ attr_reader :timers
59
+
60
+ # @return [Array<String>] All CTCPs
61
+ attr_reader :ctcps
62
+
63
+ # @return [String, nil] The help message
64
+ attr_accessor :help
65
+
66
+ # @return [String, Regexp, Proc] The prefix
67
+ attr_accessor :prefix
68
+
69
+ # @return [String, Regexp, Proc] The suffix
70
+ attr_accessor :suffix
71
+
72
+ # @return [Array<Symbol>] Required plugin options
73
+ attr_accessor :required_options
74
+
75
+ # Represents a Matcher as created by {#match}.
76
+ #
77
+ # @attr [String, Regexp, Proc] pattern
78
+ # @attr [Boolean] use_prefix
79
+ # @attr [Boolean] use_suffix
80
+ # @attr [Symbol] method
81
+ # @attr [Symbol] group
82
+ Matcher = Struct.new(:pattern, :use_prefix, :use_suffix, :method, :group, :prefix, :suffix, :reacting_on)
83
+
84
+ # Represents a Listener as created by {#listen_to}.
85
+ #
86
+ # @attr [Symbol] event
87
+ # @attr [Symbol] method
9
88
  Listener = Struct.new(:event, :method)
10
- # @api private
11
- Timer = Struct.new(:interval, :method, :threaded, :registered)
12
- # @api private
89
+
90
+ # Represents a Timer as created by {#timer}.
91
+ #
92
+ # @note This is not the same as a {Cinch::Timer} object, which
93
+ # will allow controlling and inspecting actually running
94
+ # timers. This class only describes a Timer that still has to
95
+ # be created.
96
+ #
97
+ # @attr [Number] interval
98
+ # @attr [Symbol] method
99
+ # @attr [Hash] options
100
+ # @attr [Boolean] registered
101
+ Timer = Struct.new(:interval, :options, :registered)
102
+
103
+ # Represents a Hook as created by {#hook}.
104
+ #
105
+ # @attr [Symbol] type
106
+ # @attr [Array<Symbol>] for
107
+ # @attr [Symbol] method
13
108
  Hook = Struct.new(:type, :for, :method)
14
109
 
110
+ # @api private
111
+ def self.extended(by)
112
+ by.instance_exec do
113
+ @matchers = []
114
+ @ctcps = []
115
+ @listeners = []
116
+ @timers = []
117
+ @help = nil
118
+ @hooks = Hash.new{|h, k| h[k] = []}
119
+ @prefix = nil
120
+ @suffix = nil
121
+ @reacting_on = :message
122
+ @required_options = []
123
+ self.plugin_name = nil
124
+ end
125
+ end
126
+
127
+ # Set options.
128
+ #
129
+ # Available options:
130
+ #
131
+ # - {#help}
132
+ # - {#plugin_name}
133
+ # - {#prefix}
134
+ # - {#reacting_on}
135
+ # - {#required_options}
136
+ # - {#suffix}
137
+ #
138
+ # @overload set(key, value)
139
+ # @param [Symbol] key The option's name
140
+ # @param [Object] value
141
+ # @return [void]
142
+ # @overload set(options)
143
+ # @param [Hash<Symbol => Object>] options The options, as key => value associations
144
+ # @return [void]
145
+ # @example
146
+ # set(:help => "the help message",
147
+ # :prefix => "^")
148
+ # @return [void]
149
+ # @since 2.0.0
150
+ def set(*args)
151
+ case args.size
152
+ when 1
153
+ # {:key => value, ...}
154
+ args.first.each do |key, value|
155
+ self.send("#{key}=", value)
156
+ end
157
+ when 2
158
+ # key, value
159
+ self.send("#{args.first}=", args.last)
160
+ else
161
+ raise ArgumentError # TODO proper error message
162
+ end
163
+ end
164
+
15
165
  # Set a match pattern.
16
166
  #
17
167
  # @param [Regexp, String] pattern A pattern
@@ -19,11 +169,19 @@ module Cinch
19
169
  # @option options [Boolean] :use_prefix (true) If true, the
20
170
  # plugin prefix will automatically be prepended to the
21
171
  # pattern.
22
- # @return [void]
172
+ # @option options [Boolean] :use_suffix (true) If true, the
173
+ # plugin suffix will automatically be appended to the
174
+ # pattern.
175
+ # @option options [Symbol] :group (nil) The group the match belongs to.
176
+ # @return [Matcher]
177
+ # @todo Document match/listener grouping
178
+ # @todo document new options
23
179
  def match(pattern, options = {})
24
- options = {:use_prefix => true, :use_suffix => true, :method => :execute}.merge(options)
25
- @__cinch_matches ||= []
26
- @__cinch_matches << Match.new(pattern, options[:use_prefix], options[:use_suffix], options[:method])
180
+ options = {:use_prefix => true, :use_suffix => true, :method => :execute, :group => nil, :prefix => nil, :suffix => nil, :reacting_on => nil}.merge(options)
181
+ matcher = Matcher.new(pattern, *options.values_at(:use_prefix, :use_suffix, :method, :group, :prefix, :suffix, :reacting_on))
182
+ @matchers << matcher
183
+
184
+ matcher
27
185
  end
28
186
 
29
187
  # Events to listen to.
@@ -37,72 +195,34 @@ module Cinch
37
195
  # - :message (both channel and private messages)
38
196
  # - :error (IRC errors)
39
197
  # - :ctcp (ctcp requests)
198
+ # - :action (actions, aka /me)
40
199
  #
41
200
  # @param [Hash] options
42
201
  # @option options [Symbol] :method (:listen) The method to
43
202
  # execute
44
- # @return [void]
203
+ # @return [Array<Listener]
45
204
  def listen_to(*types)
46
205
  options = {:method => :listen}
47
206
  if types.last.is_a?(Hash)
48
207
  options.merge!(types.pop)
49
208
  end
50
209
 
51
- @__cinch_listeners ||= []
210
+ listeners = types.map {|type| Listener.new(type, options[:method])}
211
+ @listeners.concat listeners
52
212
 
53
- types.each do |type|
54
- @__cinch_listeners << Listener.new(type, options[:method])
55
- end
213
+ listeners
56
214
  end
57
215
 
216
+ # @version 1.1.1
58
217
  def ctcp(command)
59
- (@__cinch_ctcps ||= []) << command.to_s.upcase
60
- end
61
-
62
- # Define a help message which will be returned on "&lt;prefix&gt;help
63
- # &lt;pluginname&gt;".
64
- #
65
- # @param [String] message
66
- # @return [void]
67
- def help(message)
68
- @__cinch_help_message = message
69
- end
70
-
71
- # Set the plugin prefix.
72
- #
73
- # @param [String] prefix
74
- # @return [void]
75
- def prefix(prefix = nil, &block)
76
- raise ArgumentError if prefix.nil? && block.nil?
77
- @__cinch_prefix = prefix || block
78
- end
79
-
80
- # Set the plugin suffix.
81
- #
82
- # @param [String] suffix
83
- # @return [void]
84
- def suffix(suffix = nil, &block)
85
- raise ArgumentError if suffix.nil? && block.nil?
86
- @__cinch_suffix = suffix || block
218
+ @ctcps << command.to_s.upcase
87
219
  end
88
220
 
89
-
90
-
91
- # Set which kind of messages to react on (i.e. call {#execute})
92
- #
93
- # @param [Symbol<:message, :channel, :private>] target React to all,
94
- # only public or only private messages?
221
+ # Set which kind of messages to react on for matchers.
222
+ # @param [Symbol<:message, :channel, :private>] event Which event to react on
95
223
  # @return [void]
96
- def react_on(target)
97
- @__cinch_react_on = target
98
- end
99
-
100
- # Define the plugin name.
101
- #
102
- # @param [String] name
103
- # @return [void]
104
- def plugin(name)
105
- @__cinch_name = name
224
+ def react_on(event)
225
+ self.reacting_on = event
106
226
  end
107
227
 
108
228
  # @example
@@ -110,14 +230,27 @@ module Cinch
110
230
  # def some_method
111
231
  # Channel("#cinch-bots").send(Time.now.to_s)
112
232
  # end
233
+ #
113
234
  # @param [Number] interval Interval in seconds
114
- # @option options [Symbol] :method (:timer) Method to call
115
- # @option options [Boolean] :threaded (true) Call method in a thread?
116
- # @return [void]
235
+ # @option options [Symbol] :method (:timer) Method to call (only
236
+ # if no proc is provided)
237
+ # @option options [Number] :shots (Float::INFINITY) How often
238
+ # should the timer fire?
239
+ # @option options [Boolean] :threaded (true) Call method in a
240
+ # thread?
241
+ # @option options [Boolean] :start_automatically (true) If true,
242
+ # the timer will automatically start after the bot finished
243
+ # connecting.
244
+ # @option options [Boolean] :stop_automaticall (true) If true,
245
+ # the timer will automatically stop when the bot disconnects.
246
+ # @return [Timer]
247
+ # @since 1.1.0
117
248
  def timer(interval, options = {})
118
249
  options = {:method => :timer, :threaded => true}.merge(options)
119
- @__cinch_timers ||= []
120
- @__cinch_timers << Timer.new(interval, options[:method], options[:threaded], false)
250
+ timer = Timer.new(interval, options, false)
251
+ @timers << timer
252
+
253
+ timer
121
254
  end
122
255
 
123
256
  # Defines a hook which will be run before or after a handler is
@@ -128,27 +261,23 @@ module Cinch
128
261
  # @option options [Array<:match, :listen_to, :ctcp>] :for ([:match, :listen_to, :ctcp])
129
262
  # Which kinds of events to run the hook for.
130
263
  # @option options [Symbol] :method (true) The method to execute.
131
- # @return [void]
264
+ # @return [Hook]
265
+ # @since 1.1.0
132
266
  def hook(type, options = {})
133
267
  options = {:for => [:match, :listen_to, :ctcp], :method => :hook}.merge(options)
134
- __hooks(type) << Hook.new(type, options[:for], options[:method])
135
- end
268
+ hook = Hook.new(type, options[:for], options[:method])
269
+ __hooks(type) << hook
136
270
 
137
- # @return [String]
138
- # @api private
139
- def __plugin_name
140
- @__cinch_name || self.name.split("::").last.downcase
271
+ hook
141
272
  end
142
273
 
143
274
  # @return [Hash]
144
275
  # @api private
145
276
  def __hooks(type = nil, events = nil)
146
- @__cinch_hooks ||= Hash.new{|h,k| h[k] = []}
147
-
148
277
  if type.nil?
149
- hooks = @__cinch_hooks
278
+ hooks = @hooks
150
279
  else
151
- hooks = @__cinch_hooks[type]
280
+ hooks = @hooks[type]
152
281
  end
153
282
 
154
283
  if events.nil?
@@ -162,133 +291,178 @@ module Cinch
162
291
  end
163
292
  end
164
293
 
165
- # @return [void]
294
+ # @return [Boolean] True if processing should continue
166
295
  # @api private
167
- def __register_with_bot(bot, instance)
168
- plugin_name = __plugin_name
169
-
170
- (@__cinch_listeners || []).each do |listener|
171
- bot.debug "[plugin] #{plugin_name}: Registering listener for type `#{listener.event}`"
172
- bot.on(listener.event, [], instance) do |message, plugin, *args|
173
- if plugin.respond_to?(listener.method)
174
- plugin.class.__hooks(:pre, :listen_to).each {|hook| plugin.__send__(hook.method, message)}
175
- plugin.__send__(listener.method, message, *args)
176
- plugin.class.__hooks(:post, :listen_to).each {|hook| plugin.__send__(hook.method, message)}
177
- end
178
- end
179
- end
296
+ def call_hooks(type, event, instance, args)
297
+ stop = __hooks(type, event).find { |hook|
298
+ # stop after the first hook that returns false
299
+ instance.__send__(hook.method, *args) == false
300
+ }
180
301
 
181
- if (@__cinch_matches ||= []).empty?
182
- @__cinch_matches << Match.new(plugin_name, true, true, :execute)
183
- end
302
+ !stop
303
+ end
184
304
 
185
- prefix = @__cinch_prefix || bot.config.plugins.prefix
186
- suffix = @__cinch_suffix || bot.config.plugins.suffix
187
-
188
- @__cinch_matches.each do |pattern|
189
- _prefix = pattern.use_prefix ? prefix : nil
190
- _suffix = pattern.use_suffix ? suffix : nil
191
-
192
- pattern_to_register = Pattern.new(_prefix, pattern.pattern, _suffix)
193
- react_on = @__cinch_react_on || :message
194
-
195
- bot.debug "[plugin] #{plugin_name}: Registering executor with pattern `#{pattern_to_register.inspect}`, reacting on `#{react_on}`"
196
-
197
- bot.on(react_on, pattern_to_register, instance, pattern) do |message, plugin, pattern, *args|
198
- if plugin.respond_to?(pattern.method)
199
- method = plugin.method(pattern.method)
200
- arity = method.arity - 1
201
- if arity > 0
202
- args = args[0..arity - 1]
203
- elsif arity == 0
204
- args = []
205
- end
206
- plugin.class.__hooks(:pre, :match).each {|hook| plugin.__send__(hook.method, message)}
207
- method.call(message, *args)
208
- plugin.class.__hooks(:post, :match).each {|hook| plugin.__send__(hook.method, message)}
209
- end
210
- end
211
- end
305
+ # @param [Bot] bot
306
+ # @return [Array<Symbol>, nil]
307
+ # @since 2.0.0
308
+ # @api private
309
+ def check_for_missing_options(bot)
310
+ @required_options.select { |option|
311
+ !bot.config.plugins.options[self].has_key?(option)
312
+ }
313
+ end
314
+ end
212
315
 
213
- (@__cinch_ctcps || []).each do |ctcp|
214
- bot.debug "[plugin] #{plugin_name}: Registering CTCP `#{ctcp}`"
215
- bot.on(:ctcp, ctcp, instance, ctcp) do |message, plugin, ctcp, *args|
216
- plugin.class.__hooks(:pre, :ctcp).each {|hook| plugin.__send__(hook.method, message)}
217
- plugin.__send__("ctcp_#{ctcp.downcase}", message, *args)
218
- plugin.class.__hooks(:post, :ctcp).each {|hook| plugin.__send__(hook.method, message)}
316
+ def __register_listeners
317
+ self.class.listeners.each do |listener|
318
+ @bot.loggers.debug "[plugin] #{self.class.plugin_name}: Registering listener for type `#{listener.event}`"
319
+ new_handler = Handler.new(@bot, listener.event, Pattern.new(nil, //, nil)) do |message, *args|
320
+ if self.class.call_hooks(:pre, :listen_to, self, [message])
321
+ __send__(listener.method, message, *args)
322
+ self.class.call_hooks(:post, :listen_to, self, [message])
323
+ else
324
+ @bot.loggers.debug "[plugin] #{self.class.plugin_name}: Dropping message due to hook"
219
325
  end
220
326
  end
221
327
 
222
- (@__cinch_timers || []).each do |timer|
223
- bot.debug "[plugin] #{__plugin_name}: Registering timer with interval `#{timer.interval}` for method `#{timer.method}`"
224
- bot.on :connect do
225
- next if timer.registered
226
- timer.registered = true
227
- Thread.new do
228
- bot.debug "registering timer..."
229
- loop do
230
- sleep timer.interval
231
- if instance.respond_to?(timer.method)
232
- l = lambda {
233
- begin
234
- instance.__send__(timer.method)
235
- rescue => e
236
- bot.logger.log_exception(e)
237
- end
238
- }
239
-
240
- if timer.threaded
241
- Thread.new do
242
- l.call
243
- end
244
- else
245
- l.call
246
- end
247
- end
248
- end
249
- end
328
+ @handlers << new_handler
329
+ @bot.handlers.register(new_handler)
330
+ end
331
+ end
332
+ private :__register_listeners
333
+
334
+ def __register_ctcps
335
+ self.class.ctcps.each do |ctcp|
336
+ @bot.loggers.debug "[plugin] #{self.class.plugin_name}: Registering CTCP `#{ctcp}`"
337
+ new_handler = Handler.new(@bot, :ctcp, Pattern.generate(:ctcp, ctcp)) do |message, *args|
338
+ if self.class.call_hooks(:pre, :ctcp, self, [message])
339
+ __send__("ctcp_#{ctcp.downcase}", message, *args)
340
+ self.class.call_hooks(:post, :ctcp, self, [message])
341
+ else
342
+ @bot.loggers.debug "[plugin] #{self.class.plugin_name}: Dropping message due to hook"
250
343
  end
251
344
  end
252
345
 
253
- if @__cinch_help_message
254
- bot.debug "[plugin] #{plugin_name}: Registering help message"
255
- help_pattern = Pattern.new(prefix, "help #{plugin_name}", suffix)
256
- bot.on(:message, help_pattern, @__cinch_help_message) do |message, help_message|
257
- message.reply(help_message)
346
+ @handlers << new_handler
347
+ @bot.handlers.register(new_handler)
348
+ end
349
+ end
350
+ private :__register_ctcps
351
+
352
+ def __register_timers
353
+ @timers = self.class.timers.map {|timer_struct|
354
+ @bot.loggers.debug "[plugin] #{self.class.plugin_name}: Registering timer with interval `#{timer_struct.interval}` for method `#{timer_struct.options[:method]}`"
355
+
356
+ block = self.method(timer_struct.options[:method])
357
+ options = timer_struct.options.merge(interval: timer_struct.interval)
358
+ Cinch::Timer.new(@bot, options, &block)
359
+ }
360
+ end
361
+ private :__register_timers
362
+
363
+ def __register_matchers
364
+ prefix = self.class.prefix || @bot.config.plugins.prefix
365
+ suffix = self.class.suffix || @bot.config.plugins.suffix
366
+
367
+ self.class.matchers.each do |matcher|
368
+ _prefix = matcher.use_prefix ? matcher.prefix || prefix : nil
369
+ _suffix = matcher.use_suffix ? matcher.suffix || suffix : nil
370
+
371
+ pattern_to_register = Pattern.new(_prefix, matcher.pattern, _suffix)
372
+ react_on = matcher.reacting_on || self.class.reacting_on || :message
373
+
374
+ @bot.loggers.debug "[plugin] #{self.class.plugin_name}: Registering executor with pattern `#{pattern_to_register.inspect}`, reacting on `#{react_on}`"
375
+
376
+ new_handler = Handler.new(@bot, react_on, pattern_to_register, group: matcher.group) do |message, *args|
377
+ method = method(matcher.method)
378
+ arity = method.arity - 1
379
+ if arity > 0
380
+ args = args[0..arity - 1]
381
+ elsif arity == 0
382
+ args = []
258
383
  end
384
+ if self.class.call_hooks(:pre, :match, self, [message])
385
+ method.call(message, *args)
386
+ self.class.call_hooks(:post, :match, self, [message])
387
+ else
388
+ @bot.loggers.debug "[plugin] #{self.class.plugin_name}: Dropping message due to hook"
389
+ end
390
+ end
391
+ @handlers << new_handler
392
+ @bot.handlers.register(new_handler)
393
+ end
394
+ end
395
+ private :__register_matchers
396
+
397
+ def __register_help
398
+ prefix = self.class.prefix || @bot.config.plugins.prefix
399
+ suffix = self.class.suffix || @bot.config.plugins.suffix
400
+ if self.class.help
401
+ @bot.loggers.debug "[plugin] #{self.class.plugin_name}: Registering help message"
402
+ help_pattern = Pattern.new(prefix, "help #{self.class.plugin_name}", suffix)
403
+ new_handler = Handler.new(@bot, :message, help_pattern) do |message|
404
+ message.reply(self.class.help)
259
405
  end
406
+
407
+ @handlers << new_handler
408
+ @bot.handlers.register(new_handler)
409
+ end
410
+ end
411
+ private :__register_help
412
+
413
+ # @return [void]
414
+ # @api private
415
+ def __register
416
+ missing = self.class.check_for_missing_options(@bot)
417
+ unless missing.empty?
418
+ @bot.loggers.warn "[plugin] #{self.class.plugin_name}: Could not register plugin because the following options are not set: #{missing.join(", ")}"
419
+ return
260
420
  end
421
+
422
+ __register_listeners
423
+ __register_matchers
424
+ __register_ctcps
425
+ __register_timers
426
+ __register_help
261
427
  end
262
428
 
263
429
  # @return [Bot]
264
430
  attr_reader :bot
431
+
432
+ # @return [Array<Handler>] handlers
433
+ attr_reader :handlers
434
+
435
+ # @return [Array<Cinch::Timer>]
436
+ attr_reader :timers
437
+
438
+ # @return [Storage] The per-plugin persistent storage
439
+ attr_reader :storage
440
+
265
441
  # @api private
266
442
  def initialize(bot)
267
443
  @bot = bot
268
- self.class.__register_with_bot(bot, self)
444
+ @handlers = []
445
+ @timers = []
446
+ @storage = bot.config.storage.backend.new(@bot.config.storage, self)
447
+ __register
269
448
  end
270
449
 
271
- # (see Bot#synchronize)
272
- def synchronize(*args, &block)
273
- @bot.synchronize(*args, &block)
274
- end
450
+ # @since 2.0.0
451
+ def unregister
452
+ @bot.loggers.debug "[plugin] #{self.class.plugin_name}: Unloading plugin"
453
+ @timers.each do |timer|
454
+ timer.stop
455
+ end
275
456
 
276
- # This method will be executed whenever an event the plugin
277
- # {Plugin::ClassMethods#listen_to listens to} occurs.
278
- #
279
- # @abstract
280
- # @return [void]
281
- # @see Plugin::ClassMethods#listen_to
282
- def listen(*args)
457
+ handlers.each do |handler|
458
+ handler.stop
459
+ handler.unregister
460
+ end
283
461
  end
284
462
 
285
- # This method will be executed whenever a message matches the
286
- # {Plugin::ClassMethods#match match pattern} of the plugin.
287
- #
288
- # @abstract
289
- # @return [void]
290
- # @see Plugin::ClassMethods#match
291
- def execute(*args)
463
+ # (see Bot#synchronize)
464
+ def synchronize(*args, &block)
465
+ @bot.synchronize(*args, &block)
292
466
  end
293
467
 
294
468
  # Provides access to plugin-specific options.
@@ -298,6 +472,7 @@ module Cinch
298
472
  @bot.config.plugins.options[self.class] || {}
299
473
  end
300
474
 
475
+ # @api private
301
476
  def self.included(by)
302
477
  by.extend ClassMethods
303
478
  end