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.
- checksums.yaml +7 -0
- data/.yardopts +1 -0
- data/LICENSE +22 -0
- data/README.md +180 -0
- data/docs/bot_options.md +454 -0
- data/docs/changes.md +541 -0
- data/docs/common_mistakes.md +60 -0
- data/docs/common_tasks.md +57 -0
- data/docs/encodings.md +69 -0
- data/docs/events.md +273 -0
- data/docs/getting_started.md +184 -0
- data/docs/logging.md +90 -0
- data/docs/migrating.md +267 -0
- data/docs/plugins.md +4 -0
- data/docs/readme.md +20 -0
- data/examples/basic/autovoice.rb +32 -0
- data/examples/basic/google.rb +35 -0
- data/examples/basic/hello.rb +15 -0
- data/examples/basic/join_part.rb +34 -0
- data/examples/basic/memo.rb +39 -0
- data/examples/basic/msg.rb +16 -0
- data/examples/basic/seen.rb +36 -0
- data/examples/basic/urban_dict.rb +35 -0
- data/examples/basic/url_shorten.rb +35 -0
- data/examples/plugins/autovoice.rb +37 -0
- data/examples/plugins/custom_prefix.rb +23 -0
- data/examples/plugins/dice_roll.rb +38 -0
- data/examples/plugins/google.rb +36 -0
- data/examples/plugins/hello.rb +22 -0
- data/examples/plugins/hooks.rb +36 -0
- data/examples/plugins/join_part.rb +42 -0
- data/examples/plugins/lambdas.rb +35 -0
- data/examples/plugins/last_nick.rb +24 -0
- data/examples/plugins/msg.rb +22 -0
- data/examples/plugins/multiple_matches.rb +32 -0
- data/examples/plugins/own_events.rb +37 -0
- data/examples/plugins/seen.rb +45 -0
- data/examples/plugins/timer.rb +22 -0
- data/examples/plugins/url_shorten.rb +33 -0
- data/lib/cinch.rb +5 -0
- data/lib/cinch/ban.rb +50 -0
- data/lib/cinch/bot.rb +479 -0
- data/lib/cinch/cached_list.rb +19 -0
- data/lib/cinch/callback.rb +20 -0
- data/lib/cinch/channel.rb +463 -0
- data/lib/cinch/channel_list.rb +29 -0
- data/lib/cinch/configuration.rb +73 -0
- data/lib/cinch/configuration/bot.rb +48 -0
- data/lib/cinch/configuration/dcc.rb +16 -0
- data/lib/cinch/configuration/plugins.rb +41 -0
- data/lib/cinch/configuration/sasl.rb +19 -0
- data/lib/cinch/configuration/ssl.rb +19 -0
- data/lib/cinch/configuration/timeouts.rb +14 -0
- data/lib/cinch/constants.rb +533 -0
- data/lib/cinch/dcc.rb +12 -0
- data/lib/cinch/dcc/dccable_object.rb +37 -0
- data/lib/cinch/dcc/incoming.rb +1 -0
- data/lib/cinch/dcc/incoming/send.rb +147 -0
- data/lib/cinch/dcc/outgoing.rb +1 -0
- data/lib/cinch/dcc/outgoing/send.rb +122 -0
- data/lib/cinch/exceptions.rb +46 -0
- data/lib/cinch/formatting.rb +125 -0
- data/lib/cinch/handler.rb +118 -0
- data/lib/cinch/handler_list.rb +90 -0
- data/lib/cinch/helpers.rb +231 -0
- data/lib/cinch/irc.rb +924 -0
- data/lib/cinch/isupport.rb +98 -0
- data/lib/cinch/log_filter.rb +21 -0
- data/lib/cinch/logger.rb +168 -0
- data/lib/cinch/logger/formatted_logger.rb +97 -0
- data/lib/cinch/logger/zcbot_logger.rb +22 -0
- data/lib/cinch/logger_list.rb +85 -0
- data/lib/cinch/mask.rb +69 -0
- data/lib/cinch/message.rb +392 -0
- data/lib/cinch/message_queue.rb +107 -0
- data/lib/cinch/mode_parser.rb +76 -0
- data/lib/cinch/network.rb +104 -0
- data/lib/cinch/open_ended_queue.rb +26 -0
- data/lib/cinch/pattern.rb +65 -0
- data/lib/cinch/plugin.rb +515 -0
- data/lib/cinch/plugin_list.rb +38 -0
- data/lib/cinch/rubyext/float.rb +3 -0
- data/lib/cinch/rubyext/module.rb +26 -0
- data/lib/cinch/rubyext/string.rb +33 -0
- data/lib/cinch/sasl.rb +34 -0
- data/lib/cinch/sasl/dh_blowfish.rb +71 -0
- data/lib/cinch/sasl/diffie_hellman.rb +47 -0
- data/lib/cinch/sasl/mechanism.rb +6 -0
- data/lib/cinch/sasl/plain.rb +26 -0
- data/lib/cinch/syncable.rb +83 -0
- data/lib/cinch/target.rb +199 -0
- data/lib/cinch/timer.rb +145 -0
- data/lib/cinch/user.rb +488 -0
- data/lib/cinch/user_list.rb +87 -0
- data/lib/cinch/utilities/deprecation.rb +16 -0
- data/lib/cinch/utilities/encoding.rb +37 -0
- data/lib/cinch/utilities/kernel.rb +13 -0
- data/lib/cinch/version.rb +4 -0
- 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
|
data/lib/cinch/plugin.rb
ADDED
@@ -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
|