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
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 [Integer] 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 [Integer] 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 [Integer] 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,147 @@
|
|
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
|
+
#
|
47
|
+
# @attr_reader filename
|
48
|
+
class Send
|
49
|
+
# @private
|
50
|
+
PRIVATE_NETS = [IPAddr.new("fc00::/7"),
|
51
|
+
IPAddr.new("10.0.0.0/8"),
|
52
|
+
IPAddr.new("172.16.0.0/12"),
|
53
|
+
IPAddr.new("192.168.0.0/16")]
|
54
|
+
|
55
|
+
# @private
|
56
|
+
LOCAL_NETS = [IPAddr.new("127.0.0.0/8"),
|
57
|
+
IPAddr.new("::1/128")]
|
58
|
+
|
59
|
+
# @return [User]
|
60
|
+
attr_reader :user
|
61
|
+
|
62
|
+
# @return [Integer]
|
63
|
+
attr_reader :size
|
64
|
+
|
65
|
+
# @return [String]
|
66
|
+
attr_reader :ip
|
67
|
+
|
68
|
+
# @return [Fixnum]
|
69
|
+
attr_reader :port
|
70
|
+
|
71
|
+
# @param [Hash] opts
|
72
|
+
# @option opts [User] user
|
73
|
+
# @option opts [String] filename
|
74
|
+
# @option opts [Integer] size
|
75
|
+
# @option opts [String] ip
|
76
|
+
# @option opts [Fixnum] port
|
77
|
+
# @api private
|
78
|
+
def initialize(opts)
|
79
|
+
@user, @filename, @size, @ip, @port = opts.values_at(:user, :filename, :size, :ip, :port)
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [String] The basename of the file name, with
|
83
|
+
# (back)slashes removed.
|
84
|
+
def filename
|
85
|
+
File.basename(File.expand_path(@filename)).delete("/\\")
|
86
|
+
end
|
87
|
+
|
88
|
+
# This method is used for accepting a DCC SEND offer. It
|
89
|
+
# expects an object to save the result to (usually an instance
|
90
|
+
# of IO or String).
|
91
|
+
#
|
92
|
+
# @param [#<<] io The object to write the data to.
|
93
|
+
# @return [Boolean] True if the transfer finished
|
94
|
+
# successfully, false otherwise.
|
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
|
+
|
108
|
+
while buf = socket.readpartial(8192)
|
109
|
+
total += buf.bytesize
|
110
|
+
|
111
|
+
begin
|
112
|
+
socket.write_nonblock [total].pack("N")
|
113
|
+
rescue Errno::EWOULDBLOCK, Errno::AGAIN
|
114
|
+
# Nobody cares about ACKs, really. And if the sender
|
115
|
+
# couldn't receive it at this point, they probably don't
|
116
|
+
# care, either.
|
117
|
+
end
|
118
|
+
io << buf
|
119
|
+
|
120
|
+
# Break here in case the sender doesn't close the
|
121
|
+
# connection on the final ACK.
|
122
|
+
break if total == @size
|
123
|
+
end
|
124
|
+
|
125
|
+
socket.close
|
126
|
+
return true
|
127
|
+
rescue EOFError
|
128
|
+
return false
|
129
|
+
end
|
130
|
+
|
131
|
+
# @return [Boolean] True if the DCC originates from a private ip
|
132
|
+
# @see #from_localhost?
|
133
|
+
def from_private_ip?
|
134
|
+
ip = IPAddr.new(@ip)
|
135
|
+
PRIVATE_NETS.any? {|n| n.include?(ip)}
|
136
|
+
end
|
137
|
+
|
138
|
+
# @return [Boolean] True if the DCC originates from localhost
|
139
|
+
# @see #from_private_ip?
|
140
|
+
def from_localhost?
|
141
|
+
ip = IPAddr.new(@ip)
|
142
|
+
LOCAL_NETS.any? {|n| n.include?(ip)}
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require "cinch/dcc/outgoing/send"
|
@@ -0,0 +1,122 @@
|
|
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:docs/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
|
+
end
|
81
|
+
send_data(fd)
|
82
|
+
fd.close
|
83
|
+
ensure
|
84
|
+
@socket.close
|
85
|
+
end
|
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
|
+
def send_data(fd)
|
104
|
+
@io.advise(:sequential)
|
105
|
+
|
106
|
+
while chunk = @io.read(8096)
|
107
|
+
while true
|
108
|
+
rs, ws = IO.select([fd], [fd])
|
109
|
+
if !rs.empty?
|
110
|
+
rs.first.recv(8096)
|
111
|
+
end
|
112
|
+
if !ws.empty?
|
113
|
+
ws.first.write(chunk)
|
114
|
+
break
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Cinch
|
2
|
+
# A collection of exceptions.
|
3
|
+
module Exceptions
|
4
|
+
# Generic error. Superclass for all Cinch-specific errors.
|
5
|
+
class Generic < ::StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
# Generic error when an argument is too long.
|
9
|
+
class ArgumentTooLong < Generic
|
10
|
+
end
|
11
|
+
|
12
|
+
# Error that is raised when a topic is too long to be set.
|
13
|
+
class TopicTooLong < ArgumentTooLong
|
14
|
+
end
|
15
|
+
|
16
|
+
# Error that is raised when a nick is too long to be used.
|
17
|
+
class NickTooLong < ArgumentTooLong
|
18
|
+
end
|
19
|
+
|
20
|
+
# Error that is raised when a kick reason is too long.
|
21
|
+
class KickReasonTooLong < ArgumentTooLong
|
22
|
+
end
|
23
|
+
|
24
|
+
# Raised whenever Cinch discovers a feature it doesn't support
|
25
|
+
# yet.
|
26
|
+
class UnsupportedFeature < Generic
|
27
|
+
end
|
28
|
+
|
29
|
+
# Raised when Cinch discovers a user or channel mode, which it
|
30
|
+
# doesn't support yet.
|
31
|
+
class UnsupportedMode < Generic
|
32
|
+
def initialize(mode)
|
33
|
+
super "Cinch does not support the mode '#{mode}' yet."
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Error stating that an invalid mode string was encountered.
|
38
|
+
class InvalidModeString < Generic
|
39
|
+
end
|
40
|
+
|
41
|
+
# Raised when a synced attribute hasn't been available for too
|
42
|
+
# long.
|
43
|
+
class SyncedAttributeNotAvailable < Generic
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module Cinch
|
2
|
+
# This module can be used for adding and removing colors and
|
3
|
+
# formatting to/from messages.
|
4
|
+
#
|
5
|
+
# The format codes used are those defined by mIRC, which are also
|
6
|
+
# the ones supported by most clients.
|
7
|
+
#
|
8
|
+
# For usage instructions and examples, see {.format}.
|
9
|
+
#
|
10
|
+
# List of valid colors
|
11
|
+
# =========================
|
12
|
+
# - aqua
|
13
|
+
# - black
|
14
|
+
# - blue
|
15
|
+
# - brown
|
16
|
+
# - green
|
17
|
+
# - grey
|
18
|
+
# - lime
|
19
|
+
# - orange
|
20
|
+
# - pink
|
21
|
+
# - purple
|
22
|
+
# - red
|
23
|
+
# - royal
|
24
|
+
# - silver
|
25
|
+
# - teal
|
26
|
+
# - white
|
27
|
+
# - yellow
|
28
|
+
#
|
29
|
+
# List of valid attributes
|
30
|
+
# ========================
|
31
|
+
# - bold
|
32
|
+
# - italic
|
33
|
+
# - reverse/reversed
|
34
|
+
# - underline/underlined
|
35
|
+
#
|
36
|
+
# Other
|
37
|
+
# =====
|
38
|
+
# - reset (Resets all formatting to the client's defaults)
|
39
|
+
#
|
40
|
+
# @since 2.0.0
|
41
|
+
module Formatting
|
42
|
+
# @private
|
43
|
+
Colors = {
|
44
|
+
:white => "00",
|
45
|
+
:black => "01",
|
46
|
+
:blue => "02",
|
47
|
+
:green => "03",
|
48
|
+
:red => "04",
|
49
|
+
:brown => "05",
|
50
|
+
:purple => "06",
|
51
|
+
:orange => "07",
|
52
|
+
:yellow => "08",
|
53
|
+
:lime => "09",
|
54
|
+
:teal => "10",
|
55
|
+
:aqua => "11",
|
56
|
+
:royal => "12",
|
57
|
+
:pink => "13",
|
58
|
+
:grey => "14",
|
59
|
+
:silver => "15",
|
60
|
+
}
|
61
|
+
|
62
|
+
# @private
|
63
|
+
Attributes = {
|
64
|
+
:bold => 2.chr,
|
65
|
+
:underlined => 31.chr,
|
66
|
+
:underline => 31.chr,
|
67
|
+
:reversed => 22.chr,
|
68
|
+
:reverse => 22.chr,
|
69
|
+
:italic => 29.chr,
|
70
|
+
:reset => 15.chr,
|
71
|
+
}
|
72
|
+
|
73
|
+
# @param [Array<Symbol>] settings The colors and attributes to apply.
|
74
|
+
# When supplying two colors, the first will be used for the
|
75
|
+
# foreground and the second for the background.
|
76
|
+
# @param [String] string The string to format.
|
77
|
+
# @return [String] the formatted string
|
78
|
+
# @since 2.0.0
|
79
|
+
# @raise [ArgumentError] When passing more than two colors as arguments.
|
80
|
+
# @see Helpers#Format Helpers#Format for easier access to this method.
|
81
|
+
#
|
82
|
+
# @example Nested formatting, combining text styles and colors
|
83
|
+
# reply = Format(:underline, "Hello %s! Is your favourite color %s?" % [Format(:bold, "stranger"), Format(:red, "red")])
|
84
|
+
def self.format(*settings, string)
|
85
|
+
string = string.dup
|
86
|
+
|
87
|
+
attributes = settings.select {|k| Attributes.has_key?(k)}.map {|k| Attributes[k]}
|
88
|
+
colors = settings.select {|k| Colors.has_key?(k)}.map {|k| Colors[k]}
|
89
|
+
if colors.size > 2
|
90
|
+
raise ArgumentError, "At most two colors (foreground and background) might be specified"
|
91
|
+
end
|
92
|
+
|
93
|
+
attribute_string = attributes.join
|
94
|
+
color_string = if colors.empty?
|
95
|
+
""
|
96
|
+
else
|
97
|
+
"\x03#{colors.join(",")}"
|
98
|
+
end
|
99
|
+
|
100
|
+
prepend = attribute_string + color_string
|
101
|
+
append = Attributes[:reset]
|
102
|
+
|
103
|
+
# attributes act as toggles, so e.g. underline+underline = no
|
104
|
+
# underline. We thus have to delete all duplicate attributes
|
105
|
+
# from nested strings.
|
106
|
+
string.delete!(attribute_string)
|
107
|
+
|
108
|
+
# Replace the reset code of nested strings to continue the
|
109
|
+
# formattings of the outer string.
|
110
|
+
string.gsub!(/#{Attributes[:reset]}/, Attributes[:reset] + prepend)
|
111
|
+
return prepend + string + append
|
112
|
+
end
|
113
|
+
|
114
|
+
# Deletes all mIRC formatting codes from the string. This strips
|
115
|
+
# formatting for bold, underline and so on, as well as color
|
116
|
+
# codes. This does include removing the numeric arguments.
|
117
|
+
#
|
118
|
+
# @param [String] string The string to filter
|
119
|
+
# @return [String] The filtered string
|
120
|
+
# @since 2.2.0
|
121
|
+
def self.unformat(string)
|
122
|
+
string.gsub(/[\x02\x0f\x16\x1f\x12]|\x03(\d{1,2}(,\d{1,2})?)?/, '')
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|