cinch 1.1.3 → 2.0.0.pre.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +1 -0
- data/README.md +3 -3
- data/docs/bot_options.md +435 -0
- data/docs/changes.md +440 -0
- data/docs/common_mistakes.md +35 -0
- data/docs/common_tasks.md +47 -0
- data/docs/encodings.md +67 -0
- data/docs/events.md +272 -0
- data/docs/logging.md +5 -0
- data/docs/migrating.md +267 -0
- data/docs/readme.md +18 -0
- data/examples/plugins/custom_prefix.rb +1 -1
- data/examples/plugins/dice_roll.rb +38 -0
- data/examples/plugins/lambdas.rb +1 -1
- data/examples/plugins/memo.rb +16 -10
- data/examples/plugins/url_shorten.rb +1 -0
- data/lib/cinch.rb +5 -60
- data/lib/cinch/ban.rb +13 -7
- data/lib/cinch/bot.rb +228 -403
- data/lib/cinch/{cache_manager.rb → cached_list.rb} +5 -1
- data/lib/cinch/callback.rb +3 -0
- data/lib/cinch/channel.rb +119 -195
- data/lib/cinch/{channel_manager.rb → channel_list.rb} +6 -3
- data/lib/cinch/configuration.rb +73 -0
- data/lib/cinch/configuration/bot.rb +47 -0
- data/lib/cinch/configuration/dcc.rb +16 -0
- data/lib/cinch/configuration/plugins.rb +41 -0
- data/lib/cinch/configuration/sasl.rb +17 -0
- data/lib/cinch/configuration/ssl.rb +19 -0
- data/lib/cinch/configuration/storage.rb +37 -0
- data/lib/cinch/configuration/timeouts.rb +14 -0
- data/lib/cinch/constants.rb +531 -369
- 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 +131 -0
- data/lib/cinch/dcc/outgoing.rb +1 -0
- data/lib/cinch/dcc/outgoing/send.rb +115 -0
- data/lib/cinch/exceptions.rb +8 -1
- data/lib/cinch/formatting.rb +106 -0
- data/lib/cinch/handler.rb +104 -0
- data/lib/cinch/handler_list.rb +86 -0
- data/lib/cinch/helpers.rb +167 -10
- data/lib/cinch/irc.rb +525 -110
- data/lib/cinch/isupport.rb +11 -9
- data/lib/cinch/logger.rb +168 -0
- data/lib/cinch/logger/formatted_logger.rb +72 -55
- data/lib/cinch/logger/zcbot_logger.rb +9 -24
- data/lib/cinch/logger_list.rb +62 -0
- data/lib/cinch/mask.rb +19 -10
- data/lib/cinch/message.rb +94 -28
- data/lib/cinch/message_queue.rb +70 -28
- data/lib/cinch/mode_parser.rb +6 -1
- data/lib/cinch/network.rb +104 -0
- data/lib/cinch/{rubyext/queue.rb → open_ended_queue.rb} +8 -1
- data/lib/cinch/pattern.rb +24 -4
- data/lib/cinch/plugin.rb +352 -177
- data/lib/cinch/plugin_list.rb +35 -0
- data/lib/cinch/rubyext/float.rb +3 -0
- data/lib/cinch/rubyext/module.rb +7 -0
- data/lib/cinch/rubyext/string.rb +9 -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/storage.rb +62 -0
- data/lib/cinch/storage/null.rb +12 -0
- data/lib/cinch/storage/yaml.rb +96 -0
- data/lib/cinch/syncable.rb +13 -1
- data/lib/cinch/target.rb +144 -0
- data/lib/cinch/timer.rb +145 -0
- data/lib/cinch/user.rb +169 -225
- data/lib/cinch/{user_manager.rb → user_list.rb} +7 -2
- data/lib/cinch/utilities/deprecation.rb +12 -0
- data/lib/cinch/utilities/encoding.rb +54 -0
- data/lib/cinch/utilities/kernel.rb +13 -0
- data/lib/cinch/utilities/string.rb +13 -0
- data/lib/cinch/version.rb +4 -0
- metadata +88 -47
- data/lib/cinch/logger/logger.rb +0 -44
- data/lib/cinch/logger/null_logger.rb +0 -18
- data/lib/cinch/rubyext/infinity.rb +0 -1
data/lib/cinch/dcc.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require "cinch/dcc/outgoing"
|
2
|
+
require "cinch/dcc/incoming"
|
3
|
+
|
4
|
+
module Cinch
|
5
|
+
# Cinch supports the following DCC commands:
|
6
|
+
#
|
7
|
+
# - SEND (both {DCC::Incoming::Send incoming} and
|
8
|
+
# {DCC::Outgoing::Send outgoing})
|
9
|
+
# @since 2.0.0
|
10
|
+
module DCC
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Cinch
|
2
|
+
module DCC
|
3
|
+
# This module describes the required interface for objects that should
|
4
|
+
# be sendable via DCC.
|
5
|
+
#
|
6
|
+
# @note `File` conforms to this interface.
|
7
|
+
# @since 2.0.0
|
8
|
+
# @abstract
|
9
|
+
module DCCableObject
|
10
|
+
# Return the next `number` bytes of the object.
|
11
|
+
#
|
12
|
+
# @param [Number] number Read `number` bytes at most
|
13
|
+
# @return [String] The read data
|
14
|
+
# @return [nil] If no more data can be read
|
15
|
+
def read(number)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Seek to a specific position.
|
19
|
+
#
|
20
|
+
# @param [Number] position The position in bytes to seek to
|
21
|
+
# @return [void]
|
22
|
+
def seek(position)
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [String] A string representing the object's path or name.
|
26
|
+
#
|
27
|
+
# @note This is only required if calling {User#dcc_send} with only
|
28
|
+
# one argument
|
29
|
+
def path
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return [Number] The total size of the data, in bytes.
|
33
|
+
def size
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require "cinch/dcc/incoming/send"
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require "socket"
|
2
|
+
require "ipaddr"
|
3
|
+
|
4
|
+
module Cinch
|
5
|
+
module DCC
|
6
|
+
module Incoming
|
7
|
+
# DCC SEND is a protocol for transferring files, usually found
|
8
|
+
# in IRC. While the handshake, i.e. the details of the file
|
9
|
+
# transfer, are transferred over IRC, the actual file transfer
|
10
|
+
# happens directly between two clients. As such it doesn't put
|
11
|
+
# stress on the IRC server.
|
12
|
+
#
|
13
|
+
# When someone tries to send a file to the bot, the `:dcc_send`
|
14
|
+
# event will be triggered, in which the DCC request can be
|
15
|
+
# inspected and optionally accepted.
|
16
|
+
#
|
17
|
+
# The event handler receives the plain message object as well as
|
18
|
+
# an instance of this class. That instance contains information
|
19
|
+
# about {#filename the suggested file name} (in a sanitized way)
|
20
|
+
# and allows for checking the origin.
|
21
|
+
#
|
22
|
+
# It is advised to reject transfers that seem to originate from
|
23
|
+
# a {#from_private_ip? private IP} or {#from_localhost? the
|
24
|
+
# local IP itself} unless that is expected. Otherwise, specially
|
25
|
+
# crafted requests could cause the bot to connect to internal
|
26
|
+
# services.
|
27
|
+
#
|
28
|
+
# Finally, the file transfer can be {#accept accepted} and
|
29
|
+
# written to any object that implements a `#<<` method, which
|
30
|
+
# includes File objects as well as plain strings.
|
31
|
+
#
|
32
|
+
# @example Saving a transfer to a temporary file
|
33
|
+
# require "tempfile"
|
34
|
+
#
|
35
|
+
# listen_to :dcc_send, method: :incoming_dcc
|
36
|
+
# def incoming_dcc(m, dcc)
|
37
|
+
# if dcc.from_private_ip? || dcc.from_localhost?
|
38
|
+
# @bot.loggers.debug "Not accepting potentially dangerous file transfer"
|
39
|
+
# return
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# t = Tempfile.new(dcc.filename)
|
43
|
+
# dcc.accept(t)
|
44
|
+
# t.close
|
45
|
+
# end
|
46
|
+
class Send
|
47
|
+
# @private
|
48
|
+
PRIVATE_NETS = [IPAddr.new("fc00::/7"),
|
49
|
+
IPAddr.new("10.0.0.0/8"),
|
50
|
+
IPAddr.new("172.16.0.0/12"),
|
51
|
+
IPAddr.new("192.168.0.0/16")]
|
52
|
+
|
53
|
+
# @private
|
54
|
+
LOCAL_NETS = [IPAddr.new("127.0.0.0/8"),
|
55
|
+
IPAddr.new("::1/128")]
|
56
|
+
|
57
|
+
# @return [User]
|
58
|
+
attr_reader :user
|
59
|
+
|
60
|
+
# @return [String]
|
61
|
+
attr_reader :filename
|
62
|
+
|
63
|
+
# @return [Number]
|
64
|
+
attr_reader :size
|
65
|
+
|
66
|
+
# @return [String]
|
67
|
+
attr_reader :ip
|
68
|
+
|
69
|
+
# @return [Number]
|
70
|
+
attr_reader :port
|
71
|
+
|
72
|
+
# @param [Hash] opts
|
73
|
+
# @option opts [User] user
|
74
|
+
# @option opts [String] filename
|
75
|
+
# @option opts [Number] size
|
76
|
+
# @option opts [String] ip
|
77
|
+
# @option opts [Number] port
|
78
|
+
# @api private
|
79
|
+
def initialize(opts)
|
80
|
+
@user, @filename, @size, @ip, @port = opts.values_at(:user, :filename, :size, :ip, :port)
|
81
|
+
end
|
82
|
+
|
83
|
+
# @return [String] The basename of the file name, with
|
84
|
+
# (back)slashes removed.
|
85
|
+
def filename
|
86
|
+
File.basename(File.expand_path(@filename)).delete("/\\")
|
87
|
+
end
|
88
|
+
|
89
|
+
# This method is used for accepting a DCC SEND offer. It
|
90
|
+
# expects an object to save the result to (usually an instance
|
91
|
+
# of IO or String).
|
92
|
+
#
|
93
|
+
# @param [#<<] io The object to write the data to.
|
94
|
+
# @return [void]
|
95
|
+
# @note This method blocks.
|
96
|
+
# @example Saving to a file
|
97
|
+
# f = File.open("/tmp/foo", "w")
|
98
|
+
# dcc.accept(f)
|
99
|
+
# f.close
|
100
|
+
#
|
101
|
+
# @example Saving to a string
|
102
|
+
# s = ""
|
103
|
+
# dcc.accept(s)
|
104
|
+
def accept(io)
|
105
|
+
socket = TCPSocket.new(@ip, @port)
|
106
|
+
total = 0
|
107
|
+
while buf = socket.read(1024)
|
108
|
+
total += buf.bytesize
|
109
|
+
|
110
|
+
socket.write [total].pack("N")
|
111
|
+
io << buf
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# @return [Boolean] True if the DCC originates from a private ip
|
116
|
+
# @see #from_localhost?
|
117
|
+
def from_private_ip?
|
118
|
+
ip = IPAddr.new(@ip)
|
119
|
+
PRIVATE_NETS.any? {|n| n.include?(ip)}
|
120
|
+
end
|
121
|
+
|
122
|
+
# @return [Boolean] True if the DCC originates from localhost
|
123
|
+
# @see #from_private_ip?
|
124
|
+
def from_localhost?
|
125
|
+
ip = IPAddr.new(@ip)
|
126
|
+
LOCAL_NETS.any? {|n| n.include?(ip)}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require "cinch/dcc/outgoing/send"
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require "socket"
|
2
|
+
require "ipaddr"
|
3
|
+
require "timeout"
|
4
|
+
|
5
|
+
module Cinch
|
6
|
+
module DCC
|
7
|
+
module Outgoing
|
8
|
+
# DCC SEND is a protocol for transferring files, usually found
|
9
|
+
# in IRC. While the handshake, i.e. the details of the file
|
10
|
+
# transfer, are transferred over IRC, the actual file transfer
|
11
|
+
# happens directly between two clients. As such it doesn't put
|
12
|
+
# stress on the IRC server.
|
13
|
+
#
|
14
|
+
# Cinch allows sending files by either using
|
15
|
+
# {Cinch::User#dcc_send}, which takes care of all parameters as
|
16
|
+
# well as setting up resume support, or by creating instances of
|
17
|
+
# this class directly. The latter will only be useful to people
|
18
|
+
# working on the Cinch code itself.
|
19
|
+
#
|
20
|
+
# {Cinch::User#dcc_send} expects an object to send as well as
|
21
|
+
# optionaly a file name, which is sent to the receiver as a
|
22
|
+
# suggestion where to save the file. If no file name is
|
23
|
+
# provided, the method will use the object's `#path` method to
|
24
|
+
# determine it.
|
25
|
+
#
|
26
|
+
# Any object that implements {DCC::DCCableObject} can be sent,
|
27
|
+
# but sending files will probably be the most common case.
|
28
|
+
#
|
29
|
+
# If you're behind a NAT it is necessary to explicitly set the
|
30
|
+
# external IP using the {file:bot_options.md#dccownip dcc.own_ip
|
31
|
+
# option}.
|
32
|
+
#
|
33
|
+
# @example Sending a file to a user
|
34
|
+
# match "send me something"
|
35
|
+
# def execute(m)
|
36
|
+
# m.user.dcc_send(open("/tmp/cookies"))
|
37
|
+
# end
|
38
|
+
class Send
|
39
|
+
# @param [Hash] opts
|
40
|
+
# @option opts [User] receiver
|
41
|
+
# @option opts [String] filename
|
42
|
+
# @option opts [File] io
|
43
|
+
# @option opts [String] own_ip
|
44
|
+
def initialize(opts = {})
|
45
|
+
@receiver, @filename, @io, @own_ip = opts.values_at(:receiver, :filename, :io, :own_ip)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Start the server
|
49
|
+
#
|
50
|
+
# @return [void]
|
51
|
+
def start_server
|
52
|
+
@socket = TCPServer.new(0)
|
53
|
+
@socket.listen(1)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Send the handshake to the user.
|
57
|
+
#
|
58
|
+
# @return [void]
|
59
|
+
def send_handshake
|
60
|
+
handshake = "\001DCC SEND %s %d %d %d\001" % [@filename, IPAddr.new(@own_ip).to_i, port, @io.size]
|
61
|
+
@receiver.send(handshake)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Listen for an incoming connection.
|
65
|
+
#
|
66
|
+
# This starts listening for an incoming connection to the server
|
67
|
+
# started by {#start_server}. After a client successfully
|
68
|
+
# connected, the server socket will be closed and the file
|
69
|
+
# transferred to the client.
|
70
|
+
#
|
71
|
+
# @raise [Timeout::Error] Raised if the receiver did not connect
|
72
|
+
# within 30 seconds
|
73
|
+
# @return [void]
|
74
|
+
# @note This method blocks.
|
75
|
+
def listen
|
76
|
+
begin
|
77
|
+
fd = nil
|
78
|
+
Timeout.timeout(30) do
|
79
|
+
fd, _ = @socket.accept
|
80
|
+
send_data(fd)
|
81
|
+
fd.close
|
82
|
+
end
|
83
|
+
ensure
|
84
|
+
@socket.close
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Seek to `pos` in the data.
|
89
|
+
#
|
90
|
+
# @param [Number] pos
|
91
|
+
# @return [void]
|
92
|
+
# @api private
|
93
|
+
def seek(pos)
|
94
|
+
@io.seek(pos)
|
95
|
+
end
|
96
|
+
|
97
|
+
# @return [Number] The port used for the socket
|
98
|
+
def port
|
99
|
+
@port ||= @socket.addr[1]
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
def send_data(fd)
|
104
|
+
@io.advise(:sequential)
|
105
|
+
|
106
|
+
while chunk = @io.read(8096)
|
107
|
+
rs, ws = IO.select([fd], [fd])
|
108
|
+
rs.first.recv unless rs.empty?
|
109
|
+
ws.first.write(chunk) unless ws.empty?
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
data/lib/cinch/exceptions.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
module Cinch
|
2
|
+
# A collection of exceptions.
|
2
3
|
module Exceptions
|
3
4
|
# Generic error. Superclass for all Cinch-specific errors.
|
4
5
|
class Generic < ::StandardError
|
5
6
|
end
|
6
7
|
|
8
|
+
# Generic error when an argument is too long.
|
7
9
|
class ArgumentTooLong < Generic
|
8
10
|
end
|
9
11
|
|
@@ -19,15 +21,20 @@ module Cinch
|
|
19
21
|
class KickReasonTooLong < ArgumentTooLong
|
20
22
|
end
|
21
23
|
|
24
|
+
# Raised whenever Cinch discovers a feature it doesn't support
|
25
|
+
# yet.
|
22
26
|
class UnsupportedFeature < Generic
|
23
27
|
end
|
24
28
|
|
29
|
+
# Raised when Cinch discovers a user or channel mode, which it
|
30
|
+
# doesn't support yet.
|
25
31
|
class UnsupportedMode < Generic
|
26
32
|
def initialize(mode)
|
27
|
-
super "Cinch does not support the mode #{mode} yet."
|
33
|
+
super "Cinch does not support the mode '#{mode}' yet."
|
28
34
|
end
|
29
35
|
end
|
30
36
|
|
37
|
+
# Error stating that an invalid mode string was encountered.
|
31
38
|
class InvalidModeString < Generic
|
32
39
|
end
|
33
40
|
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Cinch
|
2
|
+
# @since 2.0.0
|
3
|
+
#
|
4
|
+
# List of valid colors
|
5
|
+
# =========================
|
6
|
+
# - aqua
|
7
|
+
# - black
|
8
|
+
# - blue
|
9
|
+
# - brown
|
10
|
+
# - green
|
11
|
+
# - grey
|
12
|
+
# - lime
|
13
|
+
# - orange
|
14
|
+
# - pink
|
15
|
+
# - purple
|
16
|
+
# - red
|
17
|
+
# - royal
|
18
|
+
# - silver
|
19
|
+
# - teal
|
20
|
+
# - white
|
21
|
+
# - yellow
|
22
|
+
#
|
23
|
+
# List of valid attributes
|
24
|
+
# ========================
|
25
|
+
# - bold
|
26
|
+
# - italic
|
27
|
+
# - reverse/reversed
|
28
|
+
# - underline/underlined
|
29
|
+
#
|
30
|
+
# Other
|
31
|
+
# =====
|
32
|
+
# - reset (Resets all formatting to the client's defaults)
|
33
|
+
module Formatting
|
34
|
+
# @private
|
35
|
+
Colors = {
|
36
|
+
:white => "00",
|
37
|
+
:black => "01",
|
38
|
+
:blue => "02",
|
39
|
+
:green => "03",
|
40
|
+
:red => "04",
|
41
|
+
:brown => "05",
|
42
|
+
:purple => "06",
|
43
|
+
:orange => "07",
|
44
|
+
:yellow => "08",
|
45
|
+
:lime => "09",
|
46
|
+
:teal => "10",
|
47
|
+
:aqua => "11",
|
48
|
+
:royal => "12",
|
49
|
+
:pink => "13",
|
50
|
+
:grey => "14",
|
51
|
+
:silver => "15",
|
52
|
+
}
|
53
|
+
|
54
|
+
# @private
|
55
|
+
Attributes = {
|
56
|
+
:bold => 2.chr,
|
57
|
+
:underlined => 31.chr,
|
58
|
+
:underline => 31.chr,
|
59
|
+
:reversed => 22.chr,
|
60
|
+
:reverse => 22.chr,
|
61
|
+
:italic => 22.chr,
|
62
|
+
:reset => 15.chr,
|
63
|
+
}
|
64
|
+
|
65
|
+
# @param [Array<Symbol>] *settings The colors and attributes to apply.
|
66
|
+
# When supplying two colors, the first will be used for the
|
67
|
+
# foreground and the second for the background.
|
68
|
+
# @param [String] string The string to format.
|
69
|
+
# @return [String] The formatted string
|
70
|
+
# @since 2.0.0
|
71
|
+
# @raise [ArgumentError] When passing more than two colors as arguments.
|
72
|
+
# @see Helpers#Format Helpers#Format for easier access to this method.
|
73
|
+
#
|
74
|
+
# @example Nested formatting, combining text styles and colors
|
75
|
+
# reply = Format(:underline, "Hello %s! Is your favourite color %s?" % [Format(:bold, "stranger"), Format(:red, "red")])
|
76
|
+
def self.format(*settings, string)
|
77
|
+
string = string.dup
|
78
|
+
|
79
|
+
attributes = settings.select {|k| Attributes.has_key?(k)}.map {|k| Attributes[k]}
|
80
|
+
colors = settings.select {|k| Colors.has_key?(k)}.map {|k| Colors[k]}
|
81
|
+
if colors.size > 2
|
82
|
+
raise ArgumentError, "At most two colors (foreground and background) might be specified"
|
83
|
+
end
|
84
|
+
|
85
|
+
attribute_string = attributes.join
|
86
|
+
color_string = if colors.empty?
|
87
|
+
""
|
88
|
+
else
|
89
|
+
"\x03#{colors.join(",")}"
|
90
|
+
end
|
91
|
+
|
92
|
+
prepend = attribute_string + color_string
|
93
|
+
append = Attributes[:reset]
|
94
|
+
|
95
|
+
# attributes act as toggles, so e.g. underline+underline = no
|
96
|
+
# underline. We thus have to delete all duplicate attributes
|
97
|
+
# from nested strings.
|
98
|
+
string.delete!(attribute_string)
|
99
|
+
|
100
|
+
# Replace the reset code of nested strings to continue the
|
101
|
+
# formattings of the outer string.
|
102
|
+
string.gsub!(/#{Attributes[:reset]}/, Attributes[:reset] + prepend)
|
103
|
+
return prepend + string + append
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module Cinch
|
2
|
+
# @since 2.0.0
|
3
|
+
class Handler
|
4
|
+
# @return [Bot]
|
5
|
+
attr_reader :bot
|
6
|
+
|
7
|
+
# @return [Symbol]
|
8
|
+
attr_reader :event
|
9
|
+
|
10
|
+
# @return [Pattern]
|
11
|
+
attr_reader :pattern
|
12
|
+
|
13
|
+
# @return [Array]
|
14
|
+
attr_reader :args
|
15
|
+
|
16
|
+
# @return [Proc]
|
17
|
+
attr_reader :block
|
18
|
+
|
19
|
+
# @return [Symbol]
|
20
|
+
attr_reader :group
|
21
|
+
|
22
|
+
# @return [ThreadGroup]
|
23
|
+
# @api private
|
24
|
+
attr_reader :thread_group
|
25
|
+
|
26
|
+
# @param [Bot] bot
|
27
|
+
# @param [Symbol] event
|
28
|
+
# @param [Pattern] pattern
|
29
|
+
# @param [Hash] options
|
30
|
+
# @option options [Symbol] :group (nil) Match group the h belongs
|
31
|
+
# to.
|
32
|
+
# @option options [Boolean] :execute_in_callback (false) Whether
|
33
|
+
# to execute the handler in an instance of {Callback}
|
34
|
+
# @option options [Array] :args ([]) Additional arguments to pass
|
35
|
+
# to the block
|
36
|
+
def initialize(bot, event, pattern, options = {}, &block)
|
37
|
+
options = {:group => nil, :execute_in_callback => false, :args => []}.merge(options)
|
38
|
+
@bot = bot
|
39
|
+
@event = event
|
40
|
+
@pattern = pattern
|
41
|
+
@group = options[:group]
|
42
|
+
@execute_in_callback = options[:execute_in_callback]
|
43
|
+
@args = options[:args]
|
44
|
+
@block = block
|
45
|
+
|
46
|
+
@thread_group = ThreadGroup.new
|
47
|
+
end
|
48
|
+
|
49
|
+
# Unregisters the handler.
|
50
|
+
#
|
51
|
+
# @return [void]
|
52
|
+
def unregister
|
53
|
+
@bot.handlers.unregister(self)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Stops execution of the handler. This means stopping and killing
|
57
|
+
# all associated threads.
|
58
|
+
#
|
59
|
+
# @return [void]
|
60
|
+
def stop
|
61
|
+
@bot.loggers.debug "[Stopping handler] Stopping all threads of handler #{self}: #{@thread_group.list.size} threads..."
|
62
|
+
@thread_group.list.each do |thread|
|
63
|
+
Thread.new do
|
64
|
+
@bot.loggers.debug "[Ending thread] Waiting 10 seconds for #{thread} to finish..."
|
65
|
+
thread.join(10)
|
66
|
+
@bot.loggers.debug "[Killing thread] Killing #{thread}"
|
67
|
+
thread.kill
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Executes the handler.
|
73
|
+
#
|
74
|
+
# @param [Message] message Message that caused the invocation
|
75
|
+
# @param [Array] captures Capture groups of the pattern that are
|
76
|
+
# being passed as arguments
|
77
|
+
# @return [void]
|
78
|
+
def call(message, captures, arguments)
|
79
|
+
bargs = captures + arguments
|
80
|
+
|
81
|
+
@thread_group.add Thread.new {
|
82
|
+
@bot.loggers.debug "[New thread] For #{self}: #{Thread.current} -- #{@thread_group.list.size} in total."
|
83
|
+
|
84
|
+
begin
|
85
|
+
if @execute_in_callback
|
86
|
+
@bot.callback.instance_exec(message, *@args, *bargs, &@block)
|
87
|
+
else
|
88
|
+
@block.call(message, *@args, *bargs)
|
89
|
+
end
|
90
|
+
rescue => e
|
91
|
+
@bot.loggers.exception(e)
|
92
|
+
ensure
|
93
|
+
@bot.loggers.debug "[Thread done] For #{self}: #{Thread.current} -- #{@thread_group.list.size - 1} remaining."
|
94
|
+
end
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
# @return [String]
|
99
|
+
def to_s
|
100
|
+
# TODO maybe add the number of running threads to the output?
|
101
|
+
"#<Cinch::Handler @event=#{@event.inspect} pattern=#{@pattern.inspect}>"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|