grinch 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +1 -0
  3. data/LICENSE +22 -0
  4. data/README.md +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