grinch 1.0.1 → 1.1.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 +4 -4
- data/README.md +4 -2
- data/lib/cinch.rb +7 -5
- data/lib/cinch/ban.rb +6 -2
- data/lib/cinch/bot.rb +21 -31
- data/lib/cinch/cached_list.rb +2 -0
- data/lib/cinch/callback.rb +2 -0
- data/lib/cinch/channel.rb +43 -44
- data/lib/cinch/channel_list.rb +2 -0
- data/lib/cinch/configuration.rb +8 -6
- data/lib/cinch/configuration/bot.rb +35 -33
- data/lib/cinch/configuration/dcc.rb +4 -2
- data/lib/cinch/configuration/plugins.rb +8 -6
- data/lib/cinch/configuration/sasl.rb +6 -4
- data/lib/cinch/configuration/ssl.rb +7 -5
- data/lib/cinch/configuration/timeouts.rb +4 -2
- data/lib/cinch/constants.rb +3 -1
- data/lib/cinch/dcc.rb +2 -0
- data/lib/cinch/dcc/dccable_object.rb +6 -8
- data/lib/cinch/dcc/incoming.rb +2 -0
- data/lib/cinch/dcc/incoming/send.rb +10 -8
- data/lib/cinch/dcc/outgoing.rb +2 -0
- data/lib/cinch/dcc/outgoing/send.rb +13 -14
- data/lib/cinch/exceptions.rb +2 -0
- data/lib/cinch/formatting.rb +32 -30
- data/lib/cinch/handler.rb +15 -13
- data/lib/cinch/handler_list.rb +13 -13
- data/lib/cinch/helpers.rb +16 -16
- data/lib/cinch/irc.rb +118 -142
- data/lib/cinch/isupport.rb +22 -20
- data/lib/cinch/log_filter.rb +3 -2
- data/lib/cinch/logger.rb +7 -2
- data/lib/cinch/logger/formatted_logger.rb +17 -13
- data/lib/cinch/logger/zcbot_logger.rb +4 -0
- data/lib/cinch/logger_list.rb +15 -14
- data/lib/cinch/mask.rb +11 -9
- data/lib/cinch/message.rb +6 -6
- data/lib/cinch/message_queue.rb +5 -4
- data/lib/cinch/mode_parser.rb +7 -5
- data/lib/cinch/network.rb +2 -0
- data/lib/cinch/open_ended_queue.rb +5 -5
- data/lib/cinch/pattern.rb +11 -8
- data/lib/cinch/plugin.rb +43 -49
- data/lib/cinch/plugin_list.rb +4 -4
- data/lib/cinch/rubyext/float.rb +3 -3
- data/lib/cinch/rubyext/module.rb +2 -0
- data/lib/cinch/rubyext/string.rb +8 -6
- data/lib/cinch/sasl.rb +2 -0
- data/lib/cinch/sasl/dh_blowfish.rb +5 -3
- data/lib/cinch/sasl/diffie_hellman.rb +6 -3
- data/lib/cinch/sasl/mechanism.rb +2 -0
- data/lib/cinch/sasl/plain.rb +2 -0
- data/lib/cinch/syncable.rb +10 -9
- data/lib/cinch/target.rb +15 -17
- data/lib/cinch/timer.rb +9 -7
- data/lib/cinch/user.rb +52 -48
- data/lib/cinch/user_list.rb +8 -11
- data/lib/cinch/utilities/deprecation.rb +5 -5
- data/lib/cinch/utilities/encoding.rb +9 -9
- data/lib/cinch/utilities/kernel.rb +4 -1
- data/lib/cinch/version.rb +3 -1
- metadata +4 -4
data/lib/cinch/isupport.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Cinch
|
2
4
|
# This class exposes parsed ISUPPORT information of the IRC network.
|
3
5
|
class ISupport < Hash
|
4
6
|
@@mappings = {
|
5
|
-
%w[PREFIX] => lambda {|v|
|
7
|
+
%w[PREFIX] => lambda { |v|
|
6
8
|
modes, prefixes = v.match(/^\((.+)\)(.+)$/)[1..2]
|
7
9
|
h = {}
|
8
10
|
modes.split("").each_with_index do |c, i|
|
@@ -11,18 +13,18 @@ module Cinch
|
|
11
13
|
h
|
12
14
|
},
|
13
15
|
|
14
|
-
%w[CHANTYPES] =>
|
15
|
-
%w[CHANMODES] => lambda {|v|
|
16
|
+
%w[CHANTYPES] => ->(v) { v.split("") },
|
17
|
+
%w[CHANMODES] => lambda { |v|
|
16
18
|
h = {}
|
17
|
-
h["A"], h["B"], h["C"], h["D"] = v.split(",").map {|l| l.split("")}
|
19
|
+
h["A"], h["B"], h["C"], h["D"] = v.split(",").map { |l| l.split("") }
|
18
20
|
h
|
19
21
|
},
|
20
22
|
|
21
23
|
%w[MODES MAXCHANNELS NICKLEN MAXBANS TOPICLEN
|
22
|
-
|
23
|
-
|
24
|
+
KICKLEN CHANNELLEN CHIDLEN SILENCE AWAYLEN
|
25
|
+
MAXTARGETS WATCH MONITOR] => ->(v) { v.to_i },
|
24
26
|
|
25
|
-
%w[CHANLIMIT MAXLIST IDCHAN] => lambda {|v|
|
27
|
+
%w[CHANLIMIT MAXLIST IDCHAN] => lambda { |v|
|
26
28
|
h = {}
|
27
29
|
v.split(",").each do |pair|
|
28
30
|
args, num = pair.split(":")
|
@@ -33,7 +35,7 @@ module Cinch
|
|
33
35
|
h
|
34
36
|
},
|
35
37
|
|
36
|
-
%w[TARGMAX] => lambda {|v|
|
38
|
+
%w[TARGMAX] => lambda { |v|
|
37
39
|
h = {}
|
38
40
|
v.split(",").each do |pair|
|
39
41
|
name, value = pair.split(":")
|
@@ -42,11 +44,11 @@ module Cinch
|
|
42
44
|
h
|
43
45
|
},
|
44
46
|
|
45
|
-
%w[NETWORK] =>
|
46
|
-
%w[STATUSMSG] =>
|
47
|
-
%w[CASEMAPPING] =>
|
48
|
-
%w[ELIST] =>
|
49
|
-
# TODO STD
|
47
|
+
%w[NETWORK] => ->(v) { v },
|
48
|
+
%w[STATUSMSG] => ->(v) { v.split("") },
|
49
|
+
%w[CASEMAPPING] => ->(v) { v.to_sym },
|
50
|
+
%w[ELIST] => ->(v) { v.split("") },
|
51
|
+
# TODO: STD
|
50
52
|
}
|
51
53
|
|
52
54
|
def initialize(*args)
|
@@ -56,13 +58,13 @@ module Cinch
|
|
56
58
|
# allowing the use of strictness=:strict for servers that don't
|
57
59
|
# support ISUPPORT (hopefully none, anyway)
|
58
60
|
|
59
|
-
self["PREFIX"] = {"o" => "@", "v" => "+"}
|
61
|
+
self["PREFIX"] = { "o" => "@", "v" => "+" }
|
60
62
|
self["CHANTYPES"] = ["#"]
|
61
63
|
self["CHANMODES"] = {
|
62
|
-
"A"
|
63
|
-
"B"
|
64
|
-
"C"
|
65
|
-
"D"
|
64
|
+
"A" => ["b"],
|
65
|
+
"B" => ["k"],
|
66
|
+
"C" => ["l"],
|
67
|
+
"D" => %w[i m n p s t r],
|
66
68
|
}
|
67
69
|
self["MODES"] = 1
|
68
70
|
self["NICKLEN"] = Float::INFINITY
|
@@ -74,7 +76,7 @@ module Cinch
|
|
74
76
|
self["AWAYLEN"] = Float::INFINITY
|
75
77
|
self["MAXTARGETS"] = 1
|
76
78
|
self["MAXCHANNELS"] = Float::INFINITY # deprecated
|
77
|
-
self["CHANLIMIT"] = {"#" => Float::INFINITY}
|
79
|
+
self["CHANLIMIT"] = { "#" => Float::INFINITY }
|
78
80
|
self["STATUSMSG"] = []
|
79
81
|
self["CASEMAPPING"] = :rfc1459
|
80
82
|
self["ELIST"] = []
|
@@ -87,7 +89,7 @@ module Cinch
|
|
87
89
|
options.each do |option|
|
88
90
|
name, value = option.split("=")
|
89
91
|
if value
|
90
|
-
proc = @@mappings.find {|key, _| key.include?(name)}
|
92
|
+
proc = @@mappings.find { |key, _| key.include?(name) }
|
91
93
|
self[name] = (proc && proc[1].call(value)) || value
|
92
94
|
else
|
93
95
|
self[name] = true
|
data/lib/cinch/log_filter.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Cinch
|
2
4
|
# LogFilter describes an interface for filtering log messages before
|
3
5
|
# they're printed.
|
@@ -15,7 +17,6 @@ module Cinch
|
|
15
17
|
# :error, :fatal] event The kind of message
|
16
18
|
# @return [String, nil] The modified message, as it should be
|
17
19
|
# logged, or nil if the message shouldn't be logged at all
|
18
|
-
def filter(message, event)
|
19
|
-
end
|
20
|
+
def filter(message, event); end
|
20
21
|
end
|
21
22
|
end
|
data/lib/cinch/logger.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Cinch
|
2
4
|
# This is the base logger class from which all loggers have to
|
3
5
|
# inherit.
|
@@ -5,7 +7,7 @@ module Cinch
|
|
5
7
|
# @version 2.0.0
|
6
8
|
class Logger
|
7
9
|
# @private
|
8
|
-
LevelOrder = [
|
10
|
+
LevelOrder = %i[debug log info warn error fatal].freeze
|
9
11
|
|
10
12
|
# @return [Array<:debug, :log, :info, :warn, :error, :fatal>]
|
11
13
|
# The minimum level of events to log
|
@@ -109,13 +111,15 @@ module Cinch
|
|
109
111
|
# @version 2.0.0
|
110
112
|
def log(messages, event = :debug, level = event)
|
111
113
|
return unless will_log?(level)
|
114
|
+
|
112
115
|
@mutex.synchronize do
|
113
116
|
Array(messages).each do |message|
|
114
117
|
message = format_general(message)
|
115
118
|
message = format_message(message, event)
|
116
119
|
|
117
120
|
next if message.nil?
|
118
|
-
|
121
|
+
|
122
|
+
@output.puts message.encode("locale", invalid: :replace, undef: :replace)
|
119
123
|
end
|
120
124
|
end
|
121
125
|
end
|
@@ -129,6 +133,7 @@ module Cinch
|
|
129
133
|
end
|
130
134
|
|
131
135
|
private
|
136
|
+
|
132
137
|
def format_message(message, level)
|
133
138
|
__send__ "format_#{level}", message
|
134
139
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "cinch/logger"
|
2
4
|
|
3
5
|
module Cinch
|
@@ -6,24 +8,25 @@ module Cinch
|
|
6
8
|
class FormattedLogger < Logger
|
7
9
|
# @private
|
8
10
|
Colors = {
|
9
|
-
:
|
10
|
-
:
|
11
|
-
:
|
12
|
-
:
|
13
|
-
:
|
14
|
-
:
|
15
|
-
:
|
16
|
-
:
|
17
|
-
}
|
11
|
+
reset: "\e[0m",
|
12
|
+
bold: "\e[1m",
|
13
|
+
red: "\e[31m",
|
14
|
+
green: "\e[32m",
|
15
|
+
yellow: "\e[33m",
|
16
|
+
blue: "\e[34m",
|
17
|
+
black: "\e[30m",
|
18
|
+
bg_white: "\e[47m",
|
19
|
+
}.freeze
|
18
20
|
|
19
21
|
# (see Logger#exception)
|
20
22
|
def exception(e)
|
21
23
|
lines = ["#{e.backtrace.first}: #{e.message} (#{e.class})"]
|
22
|
-
lines.concat e.backtrace[1..-1].map {|s| "\t" + s}
|
24
|
+
lines.concat e.backtrace[1..-1].map { |s| "\t" + s }
|
23
25
|
log(lines, :exception, :error)
|
24
26
|
end
|
25
27
|
|
26
28
|
private
|
29
|
+
|
27
30
|
def timestamp
|
28
31
|
Time.now.strftime("[%Y/%m/%d %H:%M:%S.%L]")
|
29
32
|
end
|
@@ -34,6 +37,7 @@ module Cinch
|
|
34
37
|
# @return [String] colorized string
|
35
38
|
def colorize(text, *codes)
|
36
39
|
return text unless @output.tty?
|
40
|
+
|
37
41
|
codes = Colors.values_at(*codes).join
|
38
42
|
text = text.gsub(/#{Regexp.escape(Colors[:reset])}/, Colors[:reset] + codes)
|
39
43
|
codes + text + Colors[:reset]
|
@@ -71,9 +75,9 @@ module Cinch
|
|
71
75
|
end
|
72
76
|
|
73
77
|
"%s %s %s %s" % [timestamp,
|
74
|
-
|
75
|
-
|
76
|
-
|
78
|
+
prefix,
|
79
|
+
pre_parts.join(" "),
|
80
|
+
msg ? colorize(":#{msg}", :yellow) : ""]
|
77
81
|
end
|
78
82
|
|
79
83
|
def format_outgoing(message)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "cinch/logger"
|
2
4
|
module Cinch
|
3
5
|
class Logger
|
@@ -10,10 +12,12 @@ module Cinch
|
|
10
12
|
# (see Logger#log)
|
11
13
|
def log(messages, event, level = event)
|
12
14
|
return if event != :incoming
|
15
|
+
|
13
16
|
super
|
14
17
|
end
|
15
18
|
|
16
19
|
private
|
20
|
+
|
17
21
|
def format_incoming(message)
|
18
22
|
Time.now.strftime("%m/%d/%Y %H:%M:%S ") + message
|
19
23
|
end
|
data/lib/cinch/logger_list.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Cinch
|
2
4
|
# This class allows Cinch to use multiple loggers at once. A common
|
3
5
|
# use-case would be to log formatted messages to STDERR and a
|
@@ -22,62 +24,61 @@ module Cinch
|
|
22
24
|
|
23
25
|
# (see Logger#level=)
|
24
26
|
def level=(level)
|
25
|
-
each {|l| l.level = level}
|
27
|
+
each { |l| l.level = level }
|
26
28
|
end
|
27
29
|
|
28
30
|
# (see Logger#log)
|
29
31
|
def log(messages, event = :debug, level = event)
|
30
|
-
messages = Array(messages).map {|m| filter(m, event)}.compact
|
31
|
-
each {|l| l.log(messages, event, level)}
|
32
|
+
messages = Array(messages).map { |m| filter(m, event) }.compact
|
33
|
+
each { |l| l.log(messages, event, level) }
|
32
34
|
end
|
33
35
|
|
34
36
|
# (see Logger#debug)
|
35
37
|
def debug(message)
|
36
|
-
(m = filter(message, :debug)) && each {|l| l.debug(m)}
|
38
|
+
(m = filter(message, :debug)) && each { |l| l.debug(m) }
|
37
39
|
end
|
38
40
|
|
39
41
|
# (see Logger#error)
|
40
42
|
def error(message)
|
41
|
-
(m = filter(message, :error)) && each {|l| l.error(m)}
|
43
|
+
(m = filter(message, :error)) && each { |l| l.error(m) }
|
42
44
|
end
|
43
45
|
|
44
46
|
# (see Logger#error)
|
45
47
|
def fatal(message)
|
46
|
-
(m = filter(message, :fatal)) && each {|l| l.fatal(m)}
|
48
|
+
(m = filter(message, :fatal)) && each { |l| l.fatal(m) }
|
47
49
|
end
|
48
50
|
|
49
51
|
# (see Logger#info)
|
50
52
|
def info(message)
|
51
|
-
(m = filter(message, :info)) && each {|l| l.info(m)}
|
53
|
+
(m = filter(message, :info)) && each { |l| l.info(m) }
|
52
54
|
end
|
53
55
|
|
54
56
|
# (see Logger#warn)
|
55
57
|
def warn(message)
|
56
|
-
(m = filter(message, :warn)) && each {|l| l.warn(m)}
|
58
|
+
(m = filter(message, :warn)) && each { |l| l.warn(m) }
|
57
59
|
end
|
58
60
|
|
59
61
|
# (see Logger#incoming)
|
60
62
|
def incoming(message)
|
61
|
-
(m = filter(message, :incoming)) && each {|l| l.incoming(m)}
|
63
|
+
(m = filter(message, :incoming)) && each { |l| l.incoming(m) }
|
62
64
|
end
|
63
65
|
|
64
66
|
# (see Logger#outgoing)
|
65
67
|
def outgoing(message)
|
66
|
-
(m = filter(message, :outgoing)) && each {|l| l.outgoing(m)}
|
68
|
+
(m = filter(message, :outgoing)) && each { |l| l.outgoing(m) }
|
67
69
|
end
|
68
70
|
|
69
71
|
# (see Logger#exception)
|
70
72
|
def exception(e)
|
71
|
-
each {|l| l.exception(e)}
|
73
|
+
each { |l| l.exception(e) }
|
72
74
|
end
|
73
75
|
|
74
76
|
private
|
77
|
+
|
75
78
|
def filter(m, ev)
|
76
79
|
@filters.each do |f|
|
77
80
|
m = f.filter(m, ev)
|
78
|
-
if m.nil?
|
79
|
-
break
|
80
|
-
end
|
81
|
+
break if m.nil?
|
81
82
|
end
|
82
83
|
m
|
83
84
|
end
|
data/lib/cinch/mask.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Cinch
|
2
4
|
# This class represents masks, which are primarily used for bans.
|
3
5
|
class Mask
|
@@ -39,11 +41,11 @@ module Cinch
|
|
39
41
|
# @return [Boolean]
|
40
42
|
# @version 1.1.2
|
41
43
|
def match(target)
|
42
|
-
|
44
|
+
self.class.from(target).mask =~ @regexp
|
43
45
|
|
44
|
-
# TODO support CIDR (freenode)
|
46
|
+
# TODO: support CIDR (freenode)
|
45
47
|
end
|
46
|
-
|
48
|
+
alias =~ match
|
47
49
|
|
48
50
|
# @return [String]
|
49
51
|
def to_s
|
@@ -57,13 +59,13 @@ module Cinch
|
|
57
59
|
def self.from(target)
|
58
60
|
return target if target.is_a?(self)
|
59
61
|
|
60
|
-
if target.respond_to?(:mask)
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
62
|
+
mask = if target.respond_to?(:mask)
|
63
|
+
target.mask
|
64
|
+
else
|
65
|
+
Mask.new(target.to_s)
|
66
|
+
end
|
65
67
|
|
66
|
-
|
68
|
+
mask
|
67
69
|
end
|
68
70
|
end
|
69
71
|
end
|
data/lib/cinch/message.rb
CHANGED
@@ -108,11 +108,11 @@ module Cinch
|
|
108
108
|
@params = parse_params(raw_params)
|
109
109
|
@tags = parse_tags(tags)
|
110
110
|
|
111
|
-
@user
|
111
|
+
@user = parse_user
|
112
112
|
@channel, @statusmsg_mode = parse_channel
|
113
|
-
@target
|
114
|
-
@server
|
115
|
-
@error
|
113
|
+
@target = @channel || @user
|
114
|
+
@server = parse_server
|
115
|
+
@error = parse_error
|
116
116
|
@message = parse_message
|
117
117
|
|
118
118
|
@ctcp_message = parse_ctcp_message
|
@@ -333,9 +333,9 @@ module Cinch
|
|
333
333
|
statusmsg = @bot.irc.isupport["STATUSMSG"]
|
334
334
|
if statusmsg.include?(s[0]) && chantypes.include?(s[1])
|
335
335
|
status = @bot.irc.isupport["PREFIX"].invert[s[0]]
|
336
|
-
|
336
|
+
[s[1..-1], status]
|
337
337
|
elsif chantypes.include?(s[0])
|
338
|
-
|
338
|
+
[s, nil]
|
339
339
|
end
|
340
340
|
end
|
341
341
|
|
data/lib/cinch/message_queue.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require "cinch/open_ended_queue"
|
3
4
|
|
4
5
|
module Cinch
|
@@ -11,7 +12,7 @@ module Cinch
|
|
11
12
|
@socket = socket
|
12
13
|
@bot = bot
|
13
14
|
|
14
|
-
@queues = {:
|
15
|
+
@queues = { generic: OpenEndedQueue.new }
|
15
16
|
@queues_to_process = Queue.new
|
16
17
|
@queued_queues = Set.new
|
17
18
|
|
@@ -61,6 +62,7 @@ module Cinch
|
|
61
62
|
end
|
62
63
|
|
63
64
|
private
|
65
|
+
|
64
66
|
def wait
|
65
67
|
if @log.size > 1
|
66
68
|
mps = @bot.config.messages_per_second || @bot.irc.network.default_messages_per_second
|
@@ -74,7 +76,7 @@ module Cinch
|
|
74
76
|
if effective_size <= 0
|
75
77
|
@log.clear
|
76
78
|
elsif effective_size >= max_queue_size
|
77
|
-
sleep 1.0/mps
|
79
|
+
sleep 1.0 / mps
|
78
80
|
end
|
79
81
|
end
|
80
82
|
end
|
@@ -102,6 +104,5 @@ module Cinch
|
|
102
104
|
@bot.loggers.error "Could not send message (connectivity problems): #{message}"
|
103
105
|
end
|
104
106
|
end
|
105
|
-
|
106
107
|
end # class MessageQueue
|
107
108
|
end
|
data/lib/cinch/mode_parser.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "cinch/exceptions"
|
2
4
|
|
3
5
|
module Cinch
|
@@ -16,7 +18,7 @@ module Cinch
|
|
16
18
|
# A mapping describing which modes require parameters
|
17
19
|
# @return [(Array<(Symbol<:add, :remove>, String<char>, String<param>), foo)]
|
18
20
|
def self.parse_modes(modes, params, param_modes = {})
|
19
|
-
if modes.
|
21
|
+
if modes.empty?
|
20
22
|
return nil, ErrEmptyString
|
21
23
|
# raise Exceptions::InvalidModeString, 'Empty mode string'
|
22
24
|
end
|
@@ -47,8 +49,8 @@ module Cinch
|
|
47
49
|
count = 0
|
48
50
|
else
|
49
51
|
param = nil
|
50
|
-
if param_modes.
|
51
|
-
if params.
|
52
|
+
if param_modes.key?(direction) && param_modes[direction].include?(ch)
|
53
|
+
if !params.empty?
|
52
54
|
param = params.shift
|
53
55
|
else
|
54
56
|
return changes, NotEnoughParametersError.new(ch)
|
@@ -60,7 +62,7 @@ module Cinch
|
|
60
62
|
end
|
61
63
|
end
|
62
64
|
|
63
|
-
|
65
|
+
unless params.empty?
|
64
66
|
return changes, TooManyParametersError.new(modes, params)
|
65
67
|
# raise Exceptions::InvalidModeString, 'Too many parameters: %s %s' % [modes, params]
|
66
68
|
end
|
@@ -70,7 +72,7 @@ module Cinch
|
|
70
72
|
# raise Exceptions::InvalidModeString, 'Empty mode sequence: %s' % modes
|
71
73
|
end
|
72
74
|
|
73
|
-
|
75
|
+
[changes, nil]
|
74
76
|
end
|
75
77
|
end
|
76
78
|
end
|