cinch 1.1.3 → 2.0.0.pre.1
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.
- 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
|