cinch 1.1.3 → 2.0.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
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