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