ircinch 2.4.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/.standard.yml +3 -0
- data/CHANGELOG.md +298 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +23 -0
- data/README.md +195 -0
- data/Rakefile +14 -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 +14 -0
- data/examples/basic/join_part.rb +35 -0
- data/examples/basic/memo.rb +39 -0
- data/examples/basic/msg.rb +15 -0
- data/examples/basic/seen.rb +37 -0
- data/examples/basic/urban_dict.rb +36 -0
- data/examples/basic/url_shorten.rb +36 -0
- data/examples/plugins/autovoice.rb +37 -0
- data/examples/plugins/custom_prefix.rb +22 -0
- data/examples/plugins/dice_roll.rb +38 -0
- data/examples/plugins/google.rb +36 -0
- data/examples/plugins/hello.rb +21 -0
- data/examples/plugins/hooks.rb +34 -0
- data/examples/plugins/join_part.rb +41 -0
- data/examples/plugins/lambdas.rb +35 -0
- data/examples/plugins/last_nick.rb +24 -0
- data/examples/plugins/msg.rb +21 -0
- data/examples/plugins/multiple_matches.rb +32 -0
- data/examples/plugins/own_events.rb +37 -0
- data/examples/plugins/seen.rb +44 -0
- data/examples/plugins/timer.rb +22 -0
- data/examples/plugins/url_shorten.rb +34 -0
- data/ircinch.gemspec +43 -0
- data/lib/cinch/ban.rb +53 -0
- data/lib/cinch/bot.rb +476 -0
- data/lib/cinch/cached_list.rb +21 -0
- data/lib/cinch/callback.rb +22 -0
- data/lib/cinch/channel.rb +465 -0
- data/lib/cinch/channel_list.rb +31 -0
- data/lib/cinch/configuration/bot.rb +50 -0
- data/lib/cinch/configuration/dcc.rb +18 -0
- data/lib/cinch/configuration/plugins.rb +43 -0
- data/lib/cinch/configuration/sasl.rb +21 -0
- data/lib/cinch/configuration/ssl.rb +21 -0
- data/lib/cinch/configuration/timeouts.rb +16 -0
- data/lib/cinch/configuration.rb +75 -0
- data/lib/cinch/constants.rb +535 -0
- data/lib/cinch/dcc/dccable_object.rb +39 -0
- data/lib/cinch/dcc/incoming/send.rb +149 -0
- data/lib/cinch/dcc/incoming.rb +3 -0
- data/lib/cinch/dcc/outgoing/send.rb +123 -0
- data/lib/cinch/dcc/outgoing.rb +3 -0
- data/lib/cinch/dcc.rb +14 -0
- data/lib/cinch/exceptions.rb +48 -0
- data/lib/cinch/formatting.rb +127 -0
- data/lib/cinch/handler.rb +120 -0
- data/lib/cinch/handler_list.rb +92 -0
- data/lib/cinch/helpers.rb +230 -0
- data/lib/cinch/i_support.rb +100 -0
- data/lib/cinch/irc.rb +924 -0
- data/lib/cinch/log_filter.rb +23 -0
- data/lib/cinch/logger/formatted_logger.rb +100 -0
- data/lib/cinch/logger/zcbot_logger.rb +26 -0
- data/lib/cinch/logger.rb +171 -0
- data/lib/cinch/logger_list.rb +88 -0
- data/lib/cinch/mask.rb +69 -0
- data/lib/cinch/message.rb +397 -0
- data/lib/cinch/message_queue.rb +104 -0
- data/lib/cinch/mode_parser.rb +78 -0
- data/lib/cinch/network.rb +106 -0
- data/lib/cinch/open_ended_queue.rb +26 -0
- data/lib/cinch/pattern.rb +66 -0
- data/lib/cinch/plugin.rb +517 -0
- data/lib/cinch/plugin_list.rb +40 -0
- data/lib/cinch/rubyext/float.rb +5 -0
- data/lib/cinch/rubyext/module.rb +28 -0
- data/lib/cinch/rubyext/string.rb +35 -0
- data/lib/cinch/sasl/dh_blowfish.rb +73 -0
- data/lib/cinch/sasl/diffie_hellman.rb +50 -0
- data/lib/cinch/sasl/mechanism.rb +8 -0
- data/lib/cinch/sasl/plain.rb +29 -0
- data/lib/cinch/sasl.rb +36 -0
- data/lib/cinch/syncable.rb +83 -0
- data/lib/cinch/target.rb +199 -0
- data/lib/cinch/timer.rb +147 -0
- data/lib/cinch/user.rb +489 -0
- data/lib/cinch/user_list.rb +89 -0
- data/lib/cinch/utilities/deprecation.rb +18 -0
- data/lib/cinch/utilities/encoding.rb +39 -0
- data/lib/cinch/utilities/kernel.rb +15 -0
- data/lib/cinch/version.rb +6 -0
- data/lib/cinch.rb +7 -0
- data/lib/ircinch.rb +7 -0
- metadata +205 -0
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ipaddr"
|
4
|
+
require "socket"
|
5
|
+
|
6
|
+
module Cinch
|
7
|
+
module DCC
|
8
|
+
module Incoming
|
9
|
+
# DCC SEND is a protocol for transferring files, usually found
|
10
|
+
# in IRC. While the handshake, i.e. the details of the file
|
11
|
+
# transfer, are transferred over IRC, the actual file transfer
|
12
|
+
# happens directly between two clients. As such it doesn't put
|
13
|
+
# stress on the IRC server.
|
14
|
+
#
|
15
|
+
# When someone tries to send a file to the bot, the `:dcc_send`
|
16
|
+
# event will be triggered, in which the DCC request can be
|
17
|
+
# inspected and optionally accepted.
|
18
|
+
#
|
19
|
+
# The event handler receives the plain message object as well as
|
20
|
+
# an instance of this class. That instance contains information
|
21
|
+
# about {#filename the suggested file name} (in a sanitized way)
|
22
|
+
# and allows for checking the origin.
|
23
|
+
#
|
24
|
+
# It is advised to reject transfers that seem to originate from
|
25
|
+
# a {#from_private_ip? private IP} or {#from_localhost? the
|
26
|
+
# local IP itself} unless that is expected. Otherwise, specially
|
27
|
+
# crafted requests could cause the bot to connect to internal
|
28
|
+
# services.
|
29
|
+
#
|
30
|
+
# Finally, the file transfer can be {#accept accepted} and
|
31
|
+
# written to any object that implements a `#<<` method, which
|
32
|
+
# includes File objects as well as plain strings.
|
33
|
+
#
|
34
|
+
# @example Saving a transfer to a temporary file
|
35
|
+
# require "tempfile"
|
36
|
+
#
|
37
|
+
# listen_to :dcc_send, method: :incoming_dcc
|
38
|
+
# def incoming_dcc(m, dcc)
|
39
|
+
# if dcc.from_private_ip? || dcc.from_localhost?
|
40
|
+
# @bot.loggers.debug "Not accepting potentially dangerous file transfer"
|
41
|
+
# return
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# t = Tempfile.new(dcc.filename)
|
45
|
+
# dcc.accept(t)
|
46
|
+
# t.close
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# @attr_reader filename
|
50
|
+
class Send
|
51
|
+
# @private
|
52
|
+
PRIVATE_NETS = [IPAddr.new("fc00::/7"),
|
53
|
+
IPAddr.new("10.0.0.0/8"),
|
54
|
+
IPAddr.new("172.16.0.0/12"),
|
55
|
+
IPAddr.new("192.168.0.0/16")]
|
56
|
+
|
57
|
+
# @private
|
58
|
+
LOCAL_NETS = [IPAddr.new("127.0.0.0/8"),
|
59
|
+
IPAddr.new("::1/128")]
|
60
|
+
|
61
|
+
# @return [User]
|
62
|
+
attr_reader :user
|
63
|
+
|
64
|
+
# @return [Integer]
|
65
|
+
attr_reader :size
|
66
|
+
|
67
|
+
# @return [String]
|
68
|
+
attr_reader :ip
|
69
|
+
|
70
|
+
# @return [Fixnum]
|
71
|
+
attr_reader :port
|
72
|
+
|
73
|
+
# @param [Hash] opts
|
74
|
+
# @option opts [User] user
|
75
|
+
# @option opts [String] filename
|
76
|
+
# @option opts [Integer] size
|
77
|
+
# @option opts [String] ip
|
78
|
+
# @option opts [Fixnum] port
|
79
|
+
# @api private
|
80
|
+
def initialize(opts)
|
81
|
+
@user, @filename, @size, @ip, @port = opts.values_at(:user, :filename, :size, :ip, :port)
|
82
|
+
end
|
83
|
+
|
84
|
+
# @return [String] The basename of the file name, with
|
85
|
+
# (back)slashes removed.
|
86
|
+
def filename
|
87
|
+
File.basename(File.expand_path(@filename)).delete("/\\")
|
88
|
+
end
|
89
|
+
|
90
|
+
# This method is used for accepting a DCC SEND offer. It
|
91
|
+
# expects an object to save the result to (usually an instance
|
92
|
+
# of IO or String).
|
93
|
+
#
|
94
|
+
# @param [#<<] io The object to write the data to.
|
95
|
+
# @return [Boolean] True if the transfer finished
|
96
|
+
# successfully, false otherwise.
|
97
|
+
# @note This method blocks.
|
98
|
+
# @example Saving to a file
|
99
|
+
# f = File.open("/tmp/foo", "w")
|
100
|
+
# dcc.accept(f)
|
101
|
+
# f.close
|
102
|
+
#
|
103
|
+
# @example Saving to a string
|
104
|
+
# s = ""
|
105
|
+
# dcc.accept(s)
|
106
|
+
def accept(io)
|
107
|
+
socket = TCPSocket.new(@ip, @port)
|
108
|
+
total = 0
|
109
|
+
|
110
|
+
while (buf = socket.readpartial(8192))
|
111
|
+
total += buf.bytesize
|
112
|
+
|
113
|
+
begin
|
114
|
+
socket.write_nonblock [total].pack("N")
|
115
|
+
rescue Errno::EWOULDBLOCK, Errno::AGAIN
|
116
|
+
# Nobody cares about ACKs, really. And if the sender
|
117
|
+
# couldn't receive it at this point, they probably don't
|
118
|
+
# care, either.
|
119
|
+
end
|
120
|
+
io << buf
|
121
|
+
|
122
|
+
# Break here in case the sender doesn't close the
|
123
|
+
# connection on the final ACK.
|
124
|
+
break if total == @size
|
125
|
+
end
|
126
|
+
|
127
|
+
socket.close
|
128
|
+
true
|
129
|
+
rescue EOFError
|
130
|
+
false
|
131
|
+
end
|
132
|
+
|
133
|
+
# @return [Boolean] True if the DCC originates from a private ip
|
134
|
+
# @see #from_localhost?
|
135
|
+
def from_private_ip?
|
136
|
+
ip = IPAddr.new(@ip)
|
137
|
+
PRIVATE_NETS.any? { |n| n.include?(ip) }
|
138
|
+
end
|
139
|
+
|
140
|
+
# @return [Boolean] True if the DCC originates from localhost
|
141
|
+
# @see #from_private_ip?
|
142
|
+
def from_localhost?
|
143
|
+
ip = IPAddr.new(@ip)
|
144
|
+
LOCAL_NETS.any? { |n| n.include?(ip) }
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ipaddr"
|
4
|
+
require "socket"
|
5
|
+
require "timeout"
|
6
|
+
|
7
|
+
module Cinch
|
8
|
+
module DCC
|
9
|
+
module Outgoing
|
10
|
+
# DCC SEND is a protocol for transferring files, usually found
|
11
|
+
# in IRC. While the handshake, i.e. the details of the file
|
12
|
+
# transfer, are transferred over IRC, the actual file transfer
|
13
|
+
# happens directly between two clients. As such it doesn't put
|
14
|
+
# stress on the IRC server.
|
15
|
+
#
|
16
|
+
# Cinch allows sending files by either using
|
17
|
+
# {Cinch::User#dcc_send}, which takes care of all parameters as
|
18
|
+
# well as setting up resume support, or by creating instances of
|
19
|
+
# this class directly. The latter will only be useful to people
|
20
|
+
# working on the Cinch code itself.
|
21
|
+
#
|
22
|
+
# {Cinch::User#dcc_send} expects an object to send as well as
|
23
|
+
# optionaly a file name, which is sent to the receiver as a
|
24
|
+
# suggestion where to save the file. If no file name is
|
25
|
+
# provided, the method will use the object's `#path` method to
|
26
|
+
# determine it.
|
27
|
+
#
|
28
|
+
# Any object that implements {DCC::DCCableObject} can be sent,
|
29
|
+
# but sending files will probably be the most common case.
|
30
|
+
#
|
31
|
+
# If you're behind a NAT it is necessary to explicitly set the
|
32
|
+
# external IP using the {file:docs/bot_options.md#dccownip dcc.own_ip
|
33
|
+
# option}.
|
34
|
+
#
|
35
|
+
# @example Sending a file to a user
|
36
|
+
# match "send me something"
|
37
|
+
# def execute(m)
|
38
|
+
# m.user.dcc_send(open("/tmp/cookies"))
|
39
|
+
# end
|
40
|
+
class Send
|
41
|
+
# @param [Hash] opts
|
42
|
+
# @option opts [User] receiver
|
43
|
+
# @option opts [String] filename
|
44
|
+
# @option opts [File] io
|
45
|
+
# @option opts [String] own_ip
|
46
|
+
def initialize(opts = {})
|
47
|
+
@receiver, @filename, @io, @own_ip = opts.values_at(:receiver, :filename, :io, :own_ip)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Start the server
|
51
|
+
#
|
52
|
+
# @return [void]
|
53
|
+
def start_server
|
54
|
+
@socket = TCPServer.new(0)
|
55
|
+
@socket.listen(1)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Send the handshake to the user.
|
59
|
+
#
|
60
|
+
# @return [void]
|
61
|
+
def send_handshake
|
62
|
+
handshake = "\001DCC SEND %s %d %d %d\001" % [@filename, IPAddr.new(@own_ip).to_i, port, @io.size]
|
63
|
+
@receiver.send(handshake)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Listen for an incoming connection.
|
67
|
+
#
|
68
|
+
# This starts listening for an incoming connection to the server
|
69
|
+
# started by {#start_server}. After a client successfully
|
70
|
+
# connected, the server socket will be closed and the file
|
71
|
+
# transferred to the client.
|
72
|
+
#
|
73
|
+
# @raise [Timeout::Error] Raised if the receiver did not connect
|
74
|
+
# within 30 seconds
|
75
|
+
# @return [void]
|
76
|
+
# @note This method blocks.
|
77
|
+
def listen
|
78
|
+
fd = nil
|
79
|
+
Timeout.timeout(30) do
|
80
|
+
fd, _ = @socket.accept
|
81
|
+
end
|
82
|
+
send_data(fd)
|
83
|
+
fd.close
|
84
|
+
ensure
|
85
|
+
@socket.close
|
86
|
+
end
|
87
|
+
|
88
|
+
# Seek to `pos` in the data.
|
89
|
+
#
|
90
|
+
# @param [Integer] pos
|
91
|
+
# @return [void]
|
92
|
+
# @api private
|
93
|
+
def seek(pos)
|
94
|
+
@io.seek(pos)
|
95
|
+
end
|
96
|
+
|
97
|
+
# @return [Fixnum] The port used for the socket
|
98
|
+
def port
|
99
|
+
@port ||= @socket.addr[1]
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def send_data(fd)
|
105
|
+
@io.advise(:sequential)
|
106
|
+
|
107
|
+
while (chunk = @io.read(8096))
|
108
|
+
loop do
|
109
|
+
rs, ws = IO.select([fd], [fd])
|
110
|
+
if !rs.empty?
|
111
|
+
rs.first.recv(8096)
|
112
|
+
end
|
113
|
+
if !ws.empty?
|
114
|
+
ws.first.write(chunk)
|
115
|
+
break
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
data/lib/cinch/dcc.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "dcc/incoming"
|
4
|
+
require_relative "dcc/outgoing"
|
5
|
+
|
6
|
+
module Cinch
|
7
|
+
# Cinch supports the following DCC commands:
|
8
|
+
#
|
9
|
+
# - SEND (both {DCC::Incoming::Send incoming} and
|
10
|
+
# {DCC::Outgoing::Send outgoing})
|
11
|
+
# @since 2.0.0
|
12
|
+
module DCC
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cinch
|
4
|
+
# A collection of exceptions.
|
5
|
+
module Exceptions
|
6
|
+
# Generic error. Superclass for all Cinch-specific errors.
|
7
|
+
class Generic < ::StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
# Generic error when an argument is too long.
|
11
|
+
class ArgumentTooLong < Generic
|
12
|
+
end
|
13
|
+
|
14
|
+
# Error that is raised when a topic is too long to be set.
|
15
|
+
class TopicTooLong < ArgumentTooLong
|
16
|
+
end
|
17
|
+
|
18
|
+
# Error that is raised when a nick is too long to be used.
|
19
|
+
class NickTooLong < ArgumentTooLong
|
20
|
+
end
|
21
|
+
|
22
|
+
# Error that is raised when a kick reason is too long.
|
23
|
+
class KickReasonTooLong < ArgumentTooLong
|
24
|
+
end
|
25
|
+
|
26
|
+
# Raised whenever Cinch discovers a feature it doesn't support
|
27
|
+
# yet.
|
28
|
+
class UnsupportedFeature < Generic
|
29
|
+
end
|
30
|
+
|
31
|
+
# Raised when Cinch discovers a user or channel mode, which it
|
32
|
+
# doesn't support yet.
|
33
|
+
class UnsupportedMode < Generic
|
34
|
+
def initialize(mode)
|
35
|
+
super "Cinch does not support the mode '#{mode}' yet."
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Error stating that an invalid mode string was encountered.
|
40
|
+
class InvalidModeString < Generic
|
41
|
+
end
|
42
|
+
|
43
|
+
# Raised when a synced attribute hasn't been available for too
|
44
|
+
# long.
|
45
|
+
class SyncedAttributeNotAvailable < Generic
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cinch
|
4
|
+
# This module can be used for adding and removing colors and
|
5
|
+
# formatting to/from messages.
|
6
|
+
#
|
7
|
+
# The format codes used are those defined by mIRC, which are also
|
8
|
+
# the ones supported by most clients.
|
9
|
+
#
|
10
|
+
# For usage instructions and examples, see {.format}.
|
11
|
+
#
|
12
|
+
# List of valid colors
|
13
|
+
# =========================
|
14
|
+
# - aqua
|
15
|
+
# - black
|
16
|
+
# - blue
|
17
|
+
# - brown
|
18
|
+
# - green
|
19
|
+
# - grey
|
20
|
+
# - lime
|
21
|
+
# - orange
|
22
|
+
# - pink
|
23
|
+
# - purple
|
24
|
+
# - red
|
25
|
+
# - royal
|
26
|
+
# - silver
|
27
|
+
# - teal
|
28
|
+
# - white
|
29
|
+
# - yellow
|
30
|
+
#
|
31
|
+
# List of valid attributes
|
32
|
+
# ========================
|
33
|
+
# - bold
|
34
|
+
# - italic
|
35
|
+
# - reverse/reversed
|
36
|
+
# - underline/underlined
|
37
|
+
#
|
38
|
+
# Other
|
39
|
+
# =====
|
40
|
+
# - reset (Resets all formatting to the client's defaults)
|
41
|
+
#
|
42
|
+
# @since 2.0.0
|
43
|
+
module Formatting
|
44
|
+
# @private
|
45
|
+
COLORS = {
|
46
|
+
white: "00",
|
47
|
+
black: "01",
|
48
|
+
blue: "02",
|
49
|
+
green: "03",
|
50
|
+
red: "04",
|
51
|
+
brown: "05",
|
52
|
+
purple: "06",
|
53
|
+
orange: "07",
|
54
|
+
yellow: "08",
|
55
|
+
lime: "09",
|
56
|
+
teal: "10",
|
57
|
+
aqua: "11",
|
58
|
+
royal: "12",
|
59
|
+
pink: "13",
|
60
|
+
grey: "14",
|
61
|
+
silver: "15"
|
62
|
+
}
|
63
|
+
|
64
|
+
# @private
|
65
|
+
ATTRIBUTES = {
|
66
|
+
bold: 2.chr,
|
67
|
+
underlined: 31.chr,
|
68
|
+
underline: 31.chr,
|
69
|
+
reversed: 22.chr,
|
70
|
+
reverse: 22.chr,
|
71
|
+
italic: 29.chr,
|
72
|
+
reset: 15.chr
|
73
|
+
}
|
74
|
+
|
75
|
+
# @param [Array<Symbol>] settings The colors and attributes to apply.
|
76
|
+
# When supplying two colors, the first will be used for the
|
77
|
+
# foreground and the second for the background.
|
78
|
+
# @param [String] string The string to format.
|
79
|
+
# @return [String] the formatted string
|
80
|
+
# @since 2.0.0
|
81
|
+
# @raise [ArgumentError] When passing more than two colors as arguments.
|
82
|
+
# @see Helpers#Format Helpers#Format for easier access to this method.
|
83
|
+
#
|
84
|
+
# @example Nested formatting, combining text styles and colors
|
85
|
+
# reply = Format(:underline, "Hello %s! Is your favourite color %s?" % [Format(:bold, "stranger"), Format(:red, "red")])
|
86
|
+
def self.format(*settings, string)
|
87
|
+
string = string.dup
|
88
|
+
|
89
|
+
attributes = settings.select { |k| ATTRIBUTES.has_key?(k) }.map { |k| ATTRIBUTES[k] }
|
90
|
+
colors = settings.select { |k| COLORS.has_key?(k) }.map { |k| COLORS[k] }
|
91
|
+
if colors.size > 2
|
92
|
+
raise ArgumentError, "At most two colors (foreground and background) might be specified"
|
93
|
+
end
|
94
|
+
|
95
|
+
attribute_string = attributes.join
|
96
|
+
color_string = if colors.empty?
|
97
|
+
""
|
98
|
+
else
|
99
|
+
"\x03#{colors.join(",")}"
|
100
|
+
end
|
101
|
+
|
102
|
+
prepend = attribute_string + color_string
|
103
|
+
append = ATTRIBUTES[:reset]
|
104
|
+
|
105
|
+
# Attributes act as toggles, so e.g. underline+underline = no
|
106
|
+
# underline. We thus have to delete all duplicate attributes
|
107
|
+
# from nested strings.
|
108
|
+
string.delete!(attribute_string)
|
109
|
+
|
110
|
+
# Replace the reset code of nested strings to continue the
|
111
|
+
# formattings of the outer string.
|
112
|
+
string.gsub!(/#{ATTRIBUTES[:reset]}/, ATTRIBUTES[:reset] + prepend)
|
113
|
+
prepend + string + append
|
114
|
+
end
|
115
|
+
|
116
|
+
# Deletes all mIRC formatting codes from the string. This strips
|
117
|
+
# formatting for bold, underline and so on, as well as color
|
118
|
+
# codes. This does include removing the numeric arguments.
|
119
|
+
#
|
120
|
+
# @param [String] string The string to filter
|
121
|
+
# @return [String] The filtered string
|
122
|
+
# @since 2.2.0
|
123
|
+
def self.unformat(string)
|
124
|
+
string.gsub(/[\x02\x0f\x16\x1f\x12]|\x03(\d{1,2}(,\d{1,2})?)?/, "")
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cinch
|
4
|
+
# @since 2.0.0
|
5
|
+
class Handler
|
6
|
+
# @return [Bot]
|
7
|
+
attr_reader :bot
|
8
|
+
|
9
|
+
# @return [Symbol]
|
10
|
+
attr_reader :event
|
11
|
+
|
12
|
+
# @return [Pattern]
|
13
|
+
attr_reader :pattern
|
14
|
+
|
15
|
+
# @return [Array]
|
16
|
+
attr_reader :args
|
17
|
+
|
18
|
+
# @return [Proc]
|
19
|
+
attr_reader :block
|
20
|
+
|
21
|
+
# @return [Symbol]
|
22
|
+
attr_reader :group
|
23
|
+
|
24
|
+
# @return [Boolean]
|
25
|
+
attr_reader :strip_colors
|
26
|
+
|
27
|
+
# @return [ThreadGroup]
|
28
|
+
# @api private
|
29
|
+
attr_reader :thread_group
|
30
|
+
|
31
|
+
# @param [Bot] bot
|
32
|
+
# @param [Symbol] event
|
33
|
+
# @param [Pattern] pattern
|
34
|
+
# @param [Hash] options
|
35
|
+
# @option options [Symbol] :group (nil) Match group the h belongs
|
36
|
+
# to.
|
37
|
+
# @option options [Boolean] :execute_in_callback (false) Whether
|
38
|
+
# to execute the handler in an instance of {Callback}
|
39
|
+
# @option options [Boolean] :strip_colors (false) Strip colors
|
40
|
+
# from message before attemping match
|
41
|
+
# @option options [Array] :args ([]) Additional arguments to pass
|
42
|
+
# to the block
|
43
|
+
def initialize(bot, event, pattern, options = {}, &block)
|
44
|
+
options = {
|
45
|
+
group: nil,
|
46
|
+
execute_in_callback: false,
|
47
|
+
strip_colors: false,
|
48
|
+
args: []
|
49
|
+
}.merge(options)
|
50
|
+
@bot = bot
|
51
|
+
@event = event
|
52
|
+
@pattern = pattern
|
53
|
+
@group = options[:group]
|
54
|
+
@execute_in_callback = options[:execute_in_callback]
|
55
|
+
@strip_colors = options[:strip_colors]
|
56
|
+
@args = options[:args]
|
57
|
+
@block = block
|
58
|
+
|
59
|
+
@thread_group = ThreadGroup.new
|
60
|
+
end
|
61
|
+
|
62
|
+
# Unregisters the handler.
|
63
|
+
#
|
64
|
+
# @return [void]
|
65
|
+
def unregister
|
66
|
+
@bot.handlers.unregister(self)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Stops execution of the handler. This means stopping and killing
|
70
|
+
# all associated threads.
|
71
|
+
#
|
72
|
+
# @return [void]
|
73
|
+
def stop
|
74
|
+
@bot.loggers.debug "[Stopping handler] Stopping all threads of handler #{self}: #{@thread_group.list.size} threads..."
|
75
|
+
@thread_group.list.each do |thread|
|
76
|
+
Thread.new do
|
77
|
+
@bot.loggers.debug "[Ending thread] Waiting 10 seconds for #{thread} to finish..."
|
78
|
+
thread.join(10)
|
79
|
+
@bot.loggers.debug "[Killing thread] Killing #{thread}"
|
80
|
+
thread.kill
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Executes the handler.
|
86
|
+
#
|
87
|
+
# @param [Message] message Message that caused the invocation
|
88
|
+
# @param [Array] captures Capture groups of the pattern that are
|
89
|
+
# being passed as arguments
|
90
|
+
# @return [Thread]
|
91
|
+
def call(message, captures, arguments)
|
92
|
+
bargs = captures + arguments
|
93
|
+
|
94
|
+
thread = Thread.new {
|
95
|
+
@bot.loggers.debug "[New thread] For #{self}: #{Thread.current} -- #{@thread_group.list.size} in total."
|
96
|
+
|
97
|
+
begin
|
98
|
+
if @execute_in_callback
|
99
|
+
@bot.callback.instance_exec(message, *@args, *bargs, &@block)
|
100
|
+
else
|
101
|
+
@block.call(message, *@args, *bargs)
|
102
|
+
end
|
103
|
+
rescue => e
|
104
|
+
@bot.loggers.exception(e)
|
105
|
+
ensure
|
106
|
+
@bot.loggers.debug "[Thread done] For #{self}: #{Thread.current} -- #{@thread_group.list.size - 1} remaining."
|
107
|
+
end
|
108
|
+
}
|
109
|
+
|
110
|
+
@thread_group.add(thread)
|
111
|
+
thread
|
112
|
+
end
|
113
|
+
|
114
|
+
# @return [String]
|
115
|
+
def to_s
|
116
|
+
# TODO maybe add the number of running threads to the output?
|
117
|
+
"#<Cinch::Handler @event=#{@event.inspect} pattern=#{@pattern.inspect}>"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
require_relative "cached_list"
|
6
|
+
|
7
|
+
module Cinch
|
8
|
+
# @since 2.0.0
|
9
|
+
class HandlerList
|
10
|
+
include Enumerable
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@handlers = Hash.new { |h, k| h[k] = [] }
|
14
|
+
@mutex = Mutex.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def register(handler)
|
18
|
+
@mutex.synchronize do
|
19
|
+
handler.bot.loggers.debug "[on handler] Registering handler with pattern `#{handler.pattern.inspect}`, reacting on `#{handler.event}`"
|
20
|
+
@handlers[handler.event] << handler
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param [Handler, Array<Handler>] handlers The handlers to unregister
|
25
|
+
# @return [void]
|
26
|
+
# @see Handler#unregister
|
27
|
+
def unregister(*handlers)
|
28
|
+
@mutex.synchronize do
|
29
|
+
handlers.each do |handler|
|
30
|
+
@handlers[handler.event].delete(handler)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# @api private
|
36
|
+
# @return [Array<Handler>]
|
37
|
+
def find(type, msg = nil)
|
38
|
+
if (handlers = @handlers[type])
|
39
|
+
if msg.nil?
|
40
|
+
return handlers
|
41
|
+
end
|
42
|
+
|
43
|
+
handlers = handlers.select { |handler|
|
44
|
+
msg.match(handler.pattern.to_r(msg), type, handler.strip_colors)
|
45
|
+
}.group_by { |handler| handler.group }
|
46
|
+
|
47
|
+
handlers.values_at(*(handlers.keys - [nil])).map(&:first) + (handlers[nil] || [])
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# @param [Symbol] event The event type
|
52
|
+
# @param [Message, nil] msg The message which is responsible for
|
53
|
+
# and attached to the event, or nil.
|
54
|
+
# @param [Array] arguments A list of additional arguments to pass
|
55
|
+
# to event handlers
|
56
|
+
# @return [Array<Thread>]
|
57
|
+
def dispatch(event, msg = nil, *arguments)
|
58
|
+
threads = []
|
59
|
+
|
60
|
+
if (handlers = find(event, msg))
|
61
|
+
already_run = Set.new
|
62
|
+
handlers.each do |handler|
|
63
|
+
next if already_run.include?(handler.block)
|
64
|
+
already_run << handler.block
|
65
|
+
# calling Message#match multiple times is not a problem
|
66
|
+
# because we cache the result
|
67
|
+
captures = if msg
|
68
|
+
msg.match(handler.pattern.to_r(msg), event, handler.strip_colors).captures
|
69
|
+
else
|
70
|
+
[]
|
71
|
+
end
|
72
|
+
|
73
|
+
threads << handler.call(msg, captures, arguments)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
threads
|
78
|
+
end
|
79
|
+
|
80
|
+
# @yield [handler] Yields all registered handlers
|
81
|
+
# @yieldparam [Handler] handler
|
82
|
+
# @return [void]
|
83
|
+
def each(&block)
|
84
|
+
@handlers.values.flatten.each(&block)
|
85
|
+
end
|
86
|
+
|
87
|
+
# @api private
|
88
|
+
def stop_all
|
89
|
+
each { |h| h.stop }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|