grinch 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +1 -0
  3. data/LICENSE +22 -0
  4. data/README.md +180 -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/cinch.rb +5 -0
  41. data/lib/cinch/ban.rb +50 -0
  42. data/lib/cinch/bot.rb +479 -0
  43. data/lib/cinch/cached_list.rb +19 -0
  44. data/lib/cinch/callback.rb +20 -0
  45. data/lib/cinch/channel.rb +463 -0
  46. data/lib/cinch/channel_list.rb +29 -0
  47. data/lib/cinch/configuration.rb +73 -0
  48. data/lib/cinch/configuration/bot.rb +48 -0
  49. data/lib/cinch/configuration/dcc.rb +16 -0
  50. data/lib/cinch/configuration/plugins.rb +41 -0
  51. data/lib/cinch/configuration/sasl.rb +19 -0
  52. data/lib/cinch/configuration/ssl.rb +19 -0
  53. data/lib/cinch/configuration/timeouts.rb +14 -0
  54. data/lib/cinch/constants.rb +533 -0
  55. data/lib/cinch/dcc.rb +12 -0
  56. data/lib/cinch/dcc/dccable_object.rb +37 -0
  57. data/lib/cinch/dcc/incoming.rb +1 -0
  58. data/lib/cinch/dcc/incoming/send.rb +147 -0
  59. data/lib/cinch/dcc/outgoing.rb +1 -0
  60. data/lib/cinch/dcc/outgoing/send.rb +122 -0
  61. data/lib/cinch/exceptions.rb +46 -0
  62. data/lib/cinch/formatting.rb +125 -0
  63. data/lib/cinch/handler.rb +118 -0
  64. data/lib/cinch/handler_list.rb +90 -0
  65. data/lib/cinch/helpers.rb +231 -0
  66. data/lib/cinch/irc.rb +924 -0
  67. data/lib/cinch/isupport.rb +98 -0
  68. data/lib/cinch/log_filter.rb +21 -0
  69. data/lib/cinch/logger.rb +168 -0
  70. data/lib/cinch/logger/formatted_logger.rb +97 -0
  71. data/lib/cinch/logger/zcbot_logger.rb +22 -0
  72. data/lib/cinch/logger_list.rb +85 -0
  73. data/lib/cinch/mask.rb +69 -0
  74. data/lib/cinch/message.rb +392 -0
  75. data/lib/cinch/message_queue.rb +107 -0
  76. data/lib/cinch/mode_parser.rb +76 -0
  77. data/lib/cinch/network.rb +104 -0
  78. data/lib/cinch/open_ended_queue.rb +26 -0
  79. data/lib/cinch/pattern.rb +65 -0
  80. data/lib/cinch/plugin.rb +515 -0
  81. data/lib/cinch/plugin_list.rb +38 -0
  82. data/lib/cinch/rubyext/float.rb +3 -0
  83. data/lib/cinch/rubyext/module.rb +26 -0
  84. data/lib/cinch/rubyext/string.rb +33 -0
  85. data/lib/cinch/sasl.rb +34 -0
  86. data/lib/cinch/sasl/dh_blowfish.rb +71 -0
  87. data/lib/cinch/sasl/diffie_hellman.rb +47 -0
  88. data/lib/cinch/sasl/mechanism.rb +6 -0
  89. data/lib/cinch/sasl/plain.rb +26 -0
  90. data/lib/cinch/syncable.rb +83 -0
  91. data/lib/cinch/target.rb +199 -0
  92. data/lib/cinch/timer.rb +145 -0
  93. data/lib/cinch/user.rb +488 -0
  94. data/lib/cinch/user_list.rb +87 -0
  95. data/lib/cinch/utilities/deprecation.rb +16 -0
  96. data/lib/cinch/utilities/encoding.rb +37 -0
  97. data/lib/cinch/utilities/kernel.rb +13 -0
  98. data/lib/cinch/version.rb +4 -0
  99. metadata +140 -0
@@ -0,0 +1,107 @@
1
+ # -*- coding: utf-8 -*-
2
+ require "cinch/open_ended_queue"
3
+
4
+ module Cinch
5
+ # This class manages all outgoing messages, applying rate throttling
6
+ # and fair distribution.
7
+ #
8
+ # @api private
9
+ class MessageQueue
10
+ def initialize(socket, bot)
11
+ @socket = socket
12
+ @bot = bot
13
+
14
+ @queues = {:generic => OpenEndedQueue.new}
15
+ @queues_to_process = Queue.new
16
+ @queued_queues = Set.new
17
+
18
+ @mutex = Mutex.new
19
+
20
+ @time_since_last_send = nil
21
+
22
+ @log = []
23
+ end
24
+
25
+ # @return [void]
26
+ def queue(message)
27
+ command, target, _ = message.split(" ", 3)
28
+
29
+ queue = nil
30
+ case command
31
+ when "PRIVMSG", "NOTICE"
32
+ @mutex.synchronize do
33
+ # we are assuming that each message has only one target,
34
+ # which will be true as long as the user does not send raw
35
+ # messages.
36
+ #
37
+ # this assumption is also reflected in the computation of
38
+ # passed time and processed messages, since our score does
39
+ # not take weights into account.
40
+ queue = @queues[target] ||= OpenEndedQueue.new
41
+ end
42
+ else
43
+ queue = @queues[:generic]
44
+ end
45
+ queue << message
46
+
47
+ @mutex.synchronize do
48
+ unless @queued_queues.include?(queue)
49
+ @queued_queues << queue
50
+ @queues_to_process << queue
51
+ end
52
+ end
53
+ end
54
+
55
+ # @return [void]
56
+ def process!
57
+ loop do
58
+ wait
59
+ process_one
60
+ end
61
+ end
62
+
63
+ private
64
+ def wait
65
+ if @log.size > 1
66
+ mps = @bot.config.messages_per_second || @bot.irc.network.default_messages_per_second
67
+ max_queue_size = @bot.config.server_queue_size || @bot.irc.network.default_server_queue_size
68
+
69
+ time_passed = @log.last - @log.first
70
+
71
+ messages_processed = (time_passed * mps).floor
72
+ effective_size = @log.size - messages_processed
73
+
74
+ if effective_size <= 0
75
+ @log.clear
76
+ elsif effective_size >= max_queue_size
77
+ sleep 1.0/mps
78
+ end
79
+ end
80
+ end
81
+
82
+ def process_one
83
+ queue = @queues_to_process.pop
84
+ message = queue.pop.to_s.each_line.first.chomp
85
+
86
+ if queue.empty?
87
+ @mutex.synchronize do
88
+ @queued_queues.delete(queue)
89
+ end
90
+ else
91
+ @queues_to_process << queue
92
+ end
93
+
94
+ begin
95
+ to_send = Cinch::Utilities::Encoding.encode_outgoing(message, @bot.config.encoding)
96
+ @socket.write(to_send + "\r\n")
97
+ @log << Time.now
98
+ @bot.loggers.outgoing(message)
99
+
100
+ @time_since_last_send = Time.now
101
+ rescue IOError
102
+ @bot.loggers.error "Could not send message (connectivity problems): #{message}"
103
+ end
104
+ end
105
+
106
+ end # class MessageQueue
107
+ end
@@ -0,0 +1,76 @@
1
+ require "cinch/exceptions"
2
+
3
+ module Cinch
4
+ # @api private
5
+ # @since 1.1.0
6
+ module ModeParser
7
+ ErrEmptyString = "Empty mode string"
8
+ MalformedError = Struct.new(:modes)
9
+ EmptySequenceError = Struct.new(:modes)
10
+ NotEnoughParametersError = Struct.new(:op)
11
+ TooManyParametersError = Struct.new(:modes, :params)
12
+
13
+ # @param [String] modes The mode string as sent by the server
14
+ # @param [Array<String>] params Parameters belonging to the modes
15
+ # @param [Hash{:add, :remove => Array<String>}] param_modes
16
+ # A mapping describing which modes require parameters
17
+ # @return [(Array<(Symbol<:add, :remove>, String<char>, String<param>), foo)]
18
+ def self.parse_modes(modes, params, param_modes = {})
19
+ if modes.size == 0
20
+ return nil, ErrEmptyString
21
+ # raise Exceptions::InvalidModeString, 'Empty mode string'
22
+ end
23
+
24
+ if modes[0] !~ /[+-]/
25
+ return nil, MalformedError.new(modes)
26
+ # raise Exceptions::InvalidModeString, "Malformed modes string: %s" % modes
27
+ end
28
+
29
+ changes = []
30
+
31
+ direction = nil
32
+ count = -1
33
+
34
+ modes.each_char do |ch|
35
+ if ch =~ /[+-]/
36
+ if count == 0
37
+ return changes, EmptySequenceError.new(modes)
38
+ # raise Exceptions::InvalidModeString, 'Empty mode sequence: %s' % modes
39
+ end
40
+
41
+ direction = case ch
42
+ when "+"
43
+ :add
44
+ when "-"
45
+ :remove
46
+ end
47
+ count = 0
48
+ else
49
+ param = nil
50
+ if param_modes.has_key?(direction) && param_modes[direction].include?(ch)
51
+ if params.size > 0
52
+ param = params.shift
53
+ else
54
+ return changes, NotEnoughParametersError.new(ch)
55
+ # raise Exceptions::InvalidModeString, 'Not enough parameters: %s' % ch.inspect
56
+ end
57
+ end
58
+ changes << [direction, ch, param]
59
+ count += 1
60
+ end
61
+ end
62
+
63
+ if params.size > 0
64
+ return changes, TooManyParametersError.new(modes, params)
65
+ # raise Exceptions::InvalidModeString, 'Too many parameters: %s %s' % [modes, params]
66
+ end
67
+
68
+ if count == 0
69
+ return changes, EmptySequenceError.new(modes)
70
+ # raise Exceptions::InvalidModeString, 'Empty mode sequence: %s' % modes
71
+ end
72
+
73
+ return changes, nil
74
+ end
75
+ end
76
+ end
@@ -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 || @ircd == :inspircd
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 [Numeric] The `messages per second` value that best suits
83
+ # the current network
84
+ def default_messages_per_second
85
+ case @name
86
+ when :freenode
87
+ 0.7
88
+ else
89
+ 0.5
90
+ end
91
+ end
92
+
93
+ # @return [Integer] The `server queue size` value that best suits
94
+ # the current network
95
+ def default_server_queue_size
96
+ case @name
97
+ when :quakenet
98
+ 40
99
+ else
100
+ 10
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,26 @@
1
+ require "thread"
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]
10
+ def unshift(obj)
11
+ t = nil
12
+ @mutex.synchronize{
13
+ @que.unshift obj
14
+ begin
15
+ t = @waiting.shift
16
+ t.wakeup if t
17
+ rescue ThreadError
18
+ retry
19
+ end
20
+ }
21
+ begin
22
+ t.run if t
23
+ rescue ThreadError
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,65 @@
1
+ # -*- coding: utf-8 -*-
2
+ module Cinch
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 "cinch/helpers"
2
+
3
+ module Cinch
4
+ # This class represents the core of the plugin functionality of
5
+ # Cinch. It provides both the methods for users to write their own
6
+ # plugins as well as for the Cinch framework to use them.
7
+ #
8
+ # The {ClassMethods} module, which will get included automatically
9
+ # in all classes that include `Cinch::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 Cinch 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 Cinch 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 {Cinch::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("#cinch-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
+ Cinch::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<Cinch::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