grinch 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|