nakiircbot 0.2.0 → 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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/lib/nakiircbot.rb +242 -88
  3. data/nakiircbot.gemspec +1 -1
  4. metadata +8 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 92edbf539aabbbac1e6b60cd752b5cb8051eac5cac7fb1a7f66d62fc3d150f89
4
- data.tar.gz: 2d8b504a2f9fa52e2648aaa14bac414b1d21c62072367b4f21de8fdbfbb53754
3
+ metadata.gz: 0c070c5f505213c2ca011f3a87911603f2b327c2787406c5ffe2b9e8fdc60495
4
+ data.tar.gz: 145078acb492b0c2afb570be54145eeaca0fa749978b1dcbce59470ac7f84a74
5
5
  SHA512:
6
- metadata.gz: 8601bfe95fa4066b256489cf129e7371f5f879bae8959a6ae494fd1c8cf014edc03df16df9449f434053893873ee52b7d92fffc8f8c018e6ac3c5227757ea348
7
- data.tar.gz: c902841c4e88f71122ba2d0d5b32e204e337cf9d227d35fbaf2771ba450932e9ec2558ecd2f73443b8af3e7d81d6b0de9cf29272789f25872fd28a98853bd286
6
+ metadata.gz: 4277a56a3a649aca36bf058cb89bbf6a2ac17de4c9388ae22d341d2de234239d4252d7335c422f41bab6b5fc4e954491900660d4b851eb22040f50de604deed9
7
+ data.tar.gz: c62d1a88df3c5b58aa3bfce9f0f0b52a7804ec85ca32c3e9e82ebd5dd0ff8fa61412a23553213872a497df505d4c8f666736b5f4fe2332f9193a74c6e8e79852
data/lib/nakiircbot.rb CHANGED
@@ -1,66 +1,104 @@
1
1
  module NakiIRCBot
2
- class << self
3
- attr_accessor :queue
4
- end
5
- self.queue = []
2
+ require "base64"
3
+
4
+ Reconnect = ::Class.new ::RuntimeError
5
+ Queue = ::Queue.new
6
6
  def self.start server, port, bot_name, master_name, welcome001, *channels, identity: nil, password: nil, masterword: nil, processors: [], tags: false
7
7
  # @@channels.replace channels.dup
8
8
 
9
9
  abort "matching bot_name and master_name may cause infinite recursion" if bot_name == master_name
10
- require "base64"
11
10
  require "fileutils"
12
- FileUtils.mkdir_p "logs"
11
+ ::FileUtils.mkdir_p "logs"
13
12
  require "logger"
14
- original_formatter = Logger::Formatter.new
15
- logger = Logger.new "logs/txt", "daily",
13
+ # TODO: check how I've implemented the logger in trovobot
14
+ original_formatter = ::Logger::Formatter.new
15
+ logger = ::Logger.new "logs/txt", "daily",
16
16
  progname: bot_name, datetime_format: "%y%m%d %H%M%S",
17
17
  formatter: lambda{ |severity, datetime, progname, msg|
18
18
  puts "#{datetime.strftime "%H%M%S"} #{severity.to_s[0]} #{progname} #{msg.scrub.inspect[1..-2]}"
19
- original_formatter.call severity, datetime, progname, Base64.strict_encode64(msg)
19
+ original_formatter.call severity, datetime, progname, ::Base64.strict_encode64(msg)
20
20
  # TODO: maybe encode the whole string for a case of invalid progname?
21
21
  }
22
- logger.level = ENV["LOGLEVEL_#{name}"].to_sym if ENV.include? "LOGLEVEL_#{name}"
22
+ logger.level = ::Logger::WARN
23
+ logger.level = ::ENV["LOGLEVEL_#{name}"].to_sym if ::ENV.include? "LOGLEVEL_#{name}"
23
24
  puts "#{name} logger.level = #{logger.level}"
24
25
 
25
- # https://en.wikipedia.org/wiki/List_of_Internet_Relay_Chat_commands
26
- loop do
27
- logger.info "reconnect"
28
- require "socket"
29
- socket = TCPSocket.new server, port
30
-
31
- # https://stackoverflow.com/a/49476047/322020
32
- socket_send = lambda do |str|
33
- logger.info "> #{str}"
34
- socket.send str + "\n", 0
26
+ # https://stackoverflow.com/a/49476047/322020
27
+
28
+ require "socket"
29
+ socket = ::Module.new do
30
+ @logger = logger
31
+ @server = server
32
+ @port = port
33
+
34
+ @socket = nil
35
+ @mutex = ::Mutex.new
36
+ def self.socket
37
+ reconnect = lambda do
38
+ @logger.warn "socket: reconnecting"
39
+ begin
40
+ @socket = ::TCPSocket.new @server, @port
41
+ rescue ::SocketError, ::Errno::ENETDOWN, ::Errno::ETIMEDOUT #, Errno::ENETUNREACH
42
+ @logger.warn "socket: exception: #{$!}, waiting 5 sec"
43
+ sleep 5
44
+ retry
45
+ end
46
+ raise Reconnect
47
+ end
48
+ @mutex.synchronize do
49
+ reconnect.call if @socket.nil?
50
+ begin
51
+ yield @socket
52
+ rescue ::SocketError #, Errno::ENETDOWN, Errno::ENETUNREACH
53
+ @logger.warn "socket: exception: #{$!}, waiting 5 sec"
54
+ sleep 5
55
+ reconnect.call
56
+ end
57
+ end
35
58
  end
36
- # socket.send "PASS #{password.strip}\n", 0 if twitch
37
- socket_send.call "CAP REQ :sasl" if password
38
- socket_send.call "NICK #{bot_name}"
39
- socket_send.call "USER #{bot_name} #{bot_name} #{bot_name} #{bot_name}" #unless twitch
59
+ private_class_method :socket
60
+ def self.write str
61
+ socket{ |_| _.send str + "\n", 0 }
62
+ end
63
+ def self.log str
64
+ @logger.warn "> #{str}"
65
+ write str
66
+ end
67
+ @buffer = ""
68
+ def self.read
69
+ until i = @buffer.index(?\n)
70
+ @buffer.concat( socket do |s|
71
+ return unless select [s], nil, nil, 1
72
+ s.read(s.nread).tap{ |_| raise SocketError, "empty read" if _.empty? }
73
+ end )
74
+ end
75
+ @buffer.slice!(0..i).chomp
76
+ end
77
+ end
78
+ prev_privmsg_time = ::Time.now
79
+ queue_thread = ::Thread.new do
80
+ ::Thread.current.abort_on_exception = true
81
+ loop do
82
+ sleep [prev_privmsg_time + 5 - Time.now, 0].max
83
+ addr, msg = Queue.pop
84
+ fail "I should not PRIVMSG myself" if bot_name == addr = addr.codepoints.pack("U*").tr("\x00\x0A\x0D", "")
85
+ privmsg = "PRIVMSG #{addr} :#{msg.to_s.codepoints.pack("U*").chomp[/^(\x01*)(.*)/m,2].gsub("\x00", "[NUL]").gsub("\x0A", "[LF]").gsub("\x0D", "[CR]")}"
86
+ privmsg[-4..-1] = "..." until privmsg.bytesize <= 475
87
+ prev_privmsg_time = ::Time.now
88
+ socket.log privmsg
89
+ end
90
+ end
40
91
 
41
- self.queue = []
42
- prev_socket_time = prev_privmsg_time = Time.now
92
+ # https://en.wikipedia.org/wiki/List_of_Internet_Relay_Chat_commands
93
+ loop do
94
+ Queue.clear
95
+ prev_socket_time = prev_privmsg_time = ::Time.now
43
96
  loop do
44
- begin
45
- addr, msg = self.queue.shift
46
- next unless addr && msg # TODO: how is it possible to have only one of them?
47
- addr = addr.codepoints.pack("U*").tr("\x00\x0A\x0D", "")
48
- fail "I should not PRIVMSG myself" if addr == bot_name
49
- msg = msg.to_s.codepoints.pack("U*").chomp[/^(\x01*)(.*)/m,2].gsub("\x00", "[NUL]").gsub("\x0A", "[LF]").gsub("\x0D", "[CR]")
50
- privmsg = "PRIVMSG #{addr} :#{msg}"
51
- privmsg[-4..-1] = "..." until privmsg.bytesize <= 475
52
- prev_socket_time = prev_privmsg_time = Time.now
53
- socket_send.call privmsg
54
- break
55
- end until self.queue.empty? if prev_privmsg_time + 5 < Time.now || server == "localhost"
56
-
57
- unless _ = Kernel::select([socket], nil, nil, 1)
58
- break if Time.now - prev_socket_time > 300
59
- next
97
+ unless socket_str = socket.read
98
+ socket.instance_variable_set :@socket, nil if ::Time.now - prev_socket_time > 300
99
+ next ::Thread.pass
60
100
  end
61
- prev_socket_time = Time.now
62
- socket_str = _[0][0].gets chomp: true
63
- break unless socket_str
101
+ prev_socket_time = ::Time.now
64
102
  str = socket_str.force_encoding("utf-8").scrub
65
103
  if /\A:\S+ 372 /.match? str # MOTD
66
104
  logger.debug "< #{str}"
@@ -72,75 +110,191 @@ module NakiIRCBot
72
110
  break if /\AERROR :Closing Link: /.match? str
73
111
 
74
112
  # if str[/^:\S+ 433 * #{Regexp.escape bot_name} :Nickname is already in use\.$/]
75
- # socket_send.call "NICK #{bot_name + "_"}"
113
+ # socket_log.call "NICK #{bot_name + "_"}"
76
114
  # next
77
115
  # end
78
116
 
79
117
  # next socket.send("JOIN #{$2}"+"\n"),0 if str[/^:(.+?)!\S+ KICK (\S+) #{Regexp.escape bot_name} /i]
80
118
  case str
81
- when /\A:[a-z.]+ 001 #{Regexp.escape bot_name} :Welcome to the #{Regexp.escape welcome001} #{Regexp.escape bot_name}\z/
119
+ when /\A:[a-z.]+ 001 #{::Regexp.escape bot_name} :Welcome to the #{::Regexp.escape welcome001} #{::Regexp.escape bot_name}\z/
82
120
  # we join only when we are sure we are on the correct server
83
121
  # TODO: maybe abort if the server is wrong?
84
- next socket_send.call "JOIN #{channels.join ","}"
122
+ socket.log "JOIN #{channels.join ","}"
123
+ next
85
124
 
86
- when /\A:tmi.twitch.tv 001 #{Regexp.escape bot_name} :Welcome, GLHF!\z/
87
- socket_send.call "JOIN #{channels.join ","}"
88
- socket_send.call "CAP REQ :twitch.tv/membership twitch.tv/tags twitch.tv/commands"
125
+ when /\A:tmi.twitch.tv 001 #{::Regexp.escape bot_name} :Welcome, GLHF!\z/
126
+ socket.log "JOIN #{channels.join ","}"
127
+ socket.log "CAP REQ :twitch.tv/membership twitch.tv/tags twitch.tv/commands"
128
+ tags = true
89
129
  next
90
- when /\A:NickServ!NickServ@services\. NOTICE #{Regexp.escape bot_name} :This nickname is registered. Please choose a different nickname, or identify via \x02\/msg NickServ identify <password>\x02\.\z/,
91
- /\A:NickServ!NickServ@services\.libera\.chat NOTICE #{Regexp.escape bot_name} :This nickname is registered. Please choose a different nickname, or identify via \x02\/msg NickServ IDENTIFY #{Regexp.escape bot_name} <password>\x02\z/
130
+ when /\A:NickServ!NickServ@services\. NOTICE #{::Regexp.escape bot_name} :This nickname is registered. Please choose a different nickname, or identify via \x02\/msg NickServ identify <password>\x02\.\z/,
131
+ /\A:NickServ!NickServ@services\.libera\.chat NOTICE #{::Regexp.escape bot_name} :This nickname is registered. Please choose a different nickname, or identify via \x02\/msg NickServ IDENTIFY #{Regexp.escape bot_name} <password>\x02\z/
92
132
  abort "no password" unless password
93
- logger.info "password"
94
- next socket.send "PRIVMSG NickServ :identify #{bot_name} #{password.strip}\n", 0
133
+ logger.warn "password"
134
+ next socket.write "PRIVMSG NickServ :identify #{bot_name} #{password.strip}"
95
135
  # when /\A:[a-z]+\.libera\.chat CAP \* LS :/
96
- # next socket_send "CAP REQ :sasl" if $'.split.include? "sasl"
136
+ # next socket_log "CAP REQ :sasl" if $'.split.include? "sasl"
97
137
  when /\A:[a-z]+\.libera\.chat CAP \* ACK :sasl\z/
98
- next socket_send.call "AUTHENTICATE PLAIN"
138
+ next socket.log "AUTHENTICATE PLAIN"
99
139
  when /\AAUTHENTICATE \+\z/
100
- logger.info "password"
101
- next socket.send "AUTHENTICATE #{Base64.strict_encode64 "\0#{identity || bot_name}\0#{password}"}\n", 0
140
+ logger.warn "password"
141
+ next socket.write "AUTHENTICATE #{::Base64.strict_encode64 "\0#{identity || bot_name}\0#{password}"}"
102
142
  when /\A:[a-z]+\.libera\.chat 903 #{bot_name} :SASL authentication successful\z/
103
- next socket_send.call "CAP END"
143
+ next socket.log "CAP END"
104
144
 
105
145
  when /\APING :/
106
- next socket.send "PONG :#{$'}\n", 0 # Quakenet uses timestamp, Freenode and Twitch use server name
107
- when /\A:([^!]+)!\S+ PRIVMSG #{Regexp.escape bot_name} :\x01VERSION\x01\z/
108
- next socket_send.call "NOTICE #{$1} :\x01VERSION name 0.0.0\x01"
146
+ next socket.write "PONG :#{$'}" # Quakenet uses timestamp, Freenode and Twitch use server name
147
+ when /\A:([^!]+)!\S+ PRIVMSG #{::Regexp.escape bot_name} :\x01VERSION\x01\z/
148
+ next socket.log "NOTICE #{$1} :\x01VERSION name 0.0.0\x01"
109
149
  # when /^:([^!]+)!\S+ PRIVMSG #{Regexp.escape bot_name} :\001PING (\d+)\001$/
110
- # socket_send.call "NOTICE",$1,"\001PING #{rand 10000000000}\001"
150
+ # socket_log.call "NOTICE",$1,"\001PING #{rand 10000000000}\001"
111
151
  # when /^:([^!]+)!\S+ PRIVMSG #{Regexp.escape bot_name} :\001TIME\001$/
112
- # socket_send.call "NOTICE",$1,"\001TIME 6:06:06, 6 Jun 06\001"
113
- when /\A#{'\S+ ' if tags}:(?<who>[^!]+)!\S+ PRIVMSG (?<where>\S+) :(?<what>.+)/
114
- next( if processors.empty?
115
- self.queue.push [master_name, "nothing to reload"]
116
- else
117
- processors.each do |processor|
118
- self.queue.push [master_name, "reloading #{processor}"]
119
- load File.absolute_path processor
120
- end
121
- end ) if $~.named_captures == {"who"=>master_name, "where"=>bot_name, "what"=>"#{masterword.strip} reload"}
152
+ # socket_log.call "NOTICE",$1,"\001TIME 6:06:06, 6 Jun 06\001"
122
153
  end
123
154
 
124
155
  begin
125
- yield str, ->(where, what){ self.queue.push [where, what] }
126
- rescue => e
127
- puts e.full_message
128
- self.queue.push [master_name, "yield error: #{e}"]
156
+ yield str,
157
+ ->(where, what){ Queue.push [where, what] },
158
+ ->(new_password){ password.replace new_password; socket.instance_variable_set :@socket, nil },
159
+ */\A#{'\S+ ' if tags}:(?<who>[^!]+)!\S+ PRIVMSG (?<where>\S+) :(?<what>.+)/.match(str).to_a.drop(1).tap{ |who, where, what|
160
+ logger.warn "#{where} <#{who}> #{what}" if what
161
+ }
162
+ rescue
163
+ puts $!.full_message
164
+ Queue.push ["##{bot_name}", "error: #{$!}, #{$!.backtrace.first}"]
129
165
  end
130
166
 
131
- rescue => e
132
- puts e.full_message
133
- case e
134
- when Errno::ECONNRESET, Errno::ECONNABORTED, Errno::ETIMEDOUT, Errno::EPIPE
135
- sleep 5
136
- break
137
- else
138
- self.queue.push [master_name, "unhandled error: #{e}"]
139
- sleep 5
140
- end
167
+ rescue Reconnect
168
+ # https://ircv3.net/specs/extensions/sasl-3.1.html
169
+ socket.log "CAP REQ :sasl" if password
170
+ socket.write "PASS #{password.strip}" # https://dev.twitch.tv/docs/irc/authenticate-bot/
171
+ socket.log "NICK #{bot_name}"
172
+ socket.log "USER #{bot_name} #{bot_name} #{bot_name} #{bot_name}"
173
+
141
174
  end
142
175
 
143
176
  end
144
177
 
145
178
  end
179
+
180
+ module Common
181
+ def self.ping add_to_queue, what
182
+ return add_to_queue.call what[1..-1].tr "iI", "oO" if "\\ping" == what.downcase
183
+ return add_to_queue.call what[1..-1].tr "иИ", "оO" if "\\пинг" == what.downcase
184
+ end
185
+ end
186
+
187
+ def self.parse_log path, bot_name
188
+ require "time"
189
+ get_tags = lambda do |str|
190
+ str[1..-1].split(?;).map do |pair|
191
+ (a, b) = pair.split ?=
192
+ fail if a.empty?
193
+ [a, b]
194
+ end.to_h
195
+ end
196
+ File.new(path).each(chomp: true).drop(1).map do |line|
197
+ case line
198
+ when /\AD, /
199
+ when /\A[IW], \[(\S+) #\d+\] (?:INFO|WARN) -- #{bot_name}: (.+)\z/
200
+ _ = Base64.decode64($2).force_encoding "utf-8"
201
+ [
202
+ DateTime.parse($1).to_time,
203
+ *case _
204
+ when /\A> /,
205
+ "< :tmi.twitch.tv 002 #{bot_name} :Your host is tmi.twitch.tv",
206
+ "< :tmi.twitch.tv 003 #{bot_name} :This server is rather new",
207
+ "< :tmi.twitch.tv 004 #{bot_name} :-",
208
+ "< :tmi.twitch.tv 375 #{bot_name} :-",
209
+ "< :tmi.twitch.tv 376 #{bot_name} :>",
210
+ /\A< :#{bot_name}!#{bot_name}@#{bot_name}\.tmi\.twitch\.tv JOIN #[a-z\d_]+\z/,
211
+ /\A< :#{bot_name}\.tmi\.twitch\.tv 353 #{bot_name} /,
212
+ /\A< :#{bot_name}\.tmi\.twitch\.tv 366 #{bot_name} /,
213
+ "< :tmi.twitch.tv CAP * ACK :twitch.tv/membership twitch.tv/tags twitch.tv/commands",
214
+ "< :tmi.twitch.tv CAP * NAK :sasl",
215
+ "< :tmi.twitch.tv NOTICE * :Improperly formatted auth",
216
+ "< :tmi.twitch.tv RECONNECT"
217
+ when /\A< (\S+) :tmi\.twitch\.tv USERSTATE ##{bot_name}\z/ # wtf?
218
+ when /\Aexception: /
219
+ when "reconnect",
220
+ "socket: reconnecting",
221
+ /\Asocket: exception: /,
222
+ "< :tmi.twitch.tv 001 #{bot_name} :Welcome, GLHF!"
223
+ [nil, "RECONNECT"]
224
+ when /\A< :([^\s!]+)!\1@\1\.tmi\.twitch\.tv (JOIN|PART) #([a-z\d_]+)\z/
225
+ [$3, $2, $1]
226
+ when /\A#([a-z\d_]+) <(\S+)> (.+)\z/
227
+ [$1, "PRIVMSG", $2, $3]
228
+ when /\A< (\S+) :tmi\.twitch\.tv CLEARMSG #([a-z\d_]+) :((?:\S.*)?\S)\z/
229
+ [$2, "CLEARMSG", get_tags[$1].fetch("login"), $3]
230
+ when /\A< (\S+) :tmi\.twitch\.tv CLEARCHAT #([a-z\d_]+) :([^\s!]+)\z/
231
+ [$2, "CLEARCHAT", $3, get_tags[$1].fetch("target-user-id")]
232
+ when /\A< @emote-only=0;room-id=\d+ :tmi\.twitch\.tv ROOMSTATE #([a-z\d_]+)\z/
233
+ [$1, "ROOMSTATE EMOTEONLY 0"]
234
+ when /\A< @emote-only=1;room-id=\d+ :tmi\.twitch\.tv ROOMSTATE #([a-z\d_]+)\z/
235
+ [$1, "ROOMSTATE EMOTEONLY 1"]
236
+ when /\A< @msg-id=emote_only_off :tmi\.twitch\.tv NOTICE #([a-z\d_]+) :This room is no longer in emote-only mode\.\z/
237
+ [$1, "EMOTE_ONLY_OFF"]
238
+ when /\A< @msg-id=emote_only_on :tmi\.twitch\.tv NOTICE #([a-z\d_]+) :This room is now in emote-only mode\.\z/
239
+ [$1, "EMOTE_ONLY_ON"]
240
+ when /\A< @followers-only=-1;room-id=\d+ :tmi\.twitch\.tv ROOMSTATE #([a-z\d_]+)\z/
241
+ [$1, "ROOMSTATE FOLLOWERSONLY 0"]
242
+ when /\A< @msg-id=followers_off :tmi\.twitch\.tv NOTICE #([a-z\d_]+) :This room is no longer in followers-only mode\.\z/
243
+ [$1, "FOLLOWERS_ONLY_OFF"]
244
+ when /\A< :tmi\.twitch\.tv HOSTTARGET #([a-z\d_]+) :(\S+) (\d+)\z/
245
+ next if "-" == $2 # wtf?
246
+ fail unless $2 == $2.downcase
247
+ [$1, "HOST", $2, $3.to_i]
248
+ when /\A< @msg-id=host_target_went_offline :tmi\.twitch\.tv NOTICE #([a-z\d_]+) :(\S+) has gone offline\. Exiting host mode\.\z/
249
+ fail unless $2 == $2.downcase
250
+ [$1, "HOST_TARGET_WENT_OFFLINE", $2]
251
+ when /\A< @msg-id=host_on :tmi\.twitch\.tv NOTICE #([a-z\d_]+) :Now hosting (\S+)\.\z/
252
+ [$1, "NOTICE HOST", $2]
253
+ when /\A< (\S+) :tmi\.twitch\.tv USERNOTICE #([a-z\d_]+)(?: :((?:\S.*)?\S))?\z/
254
+ tags = get_tags[$1]
255
+ fail unless tags.fetch("display-name").downcase == tags.fetch("login")
256
+ [
257
+ $2,
258
+ tags["msg-id"].upcase,
259
+ *case tags.fetch "msg-id"
260
+ when "raid"
261
+ fail if $3
262
+ [tags.fetch("display-name"), tags.fetch("msg-param-viewerCount").to_i.tap{ |_| fail unless _ > 0 }]
263
+ when "resub"
264
+ [tags.fetch("display-name"), *$3]
265
+ when "sub"
266
+ fail if $3
267
+ [tags.fetch("display-name")]
268
+ when "submysterygift"
269
+ # fail unless tags["msg-param-mass-gift-count"] == "1"
270
+ # fail unless tags["msg-param-sender-count"] == "1"
271
+ fail if $3
272
+ [tags.fetch("display-name"), tags.fetch("msg-param-mass-gift-count")]
273
+ when "subgift"
274
+ fail unless "1" == tags.fetch("msg-param-gift-months")
275
+ # fail unless tags["msg-param-months"] == "1"
276
+ fail if $3
277
+ [tags.fetch("display-name")]
278
+ when "bitsbadgetier"
279
+ fail unless $3
280
+ [tags.fetch("display-name")]
281
+ when "primepaidupgrade"
282
+ fail if $3
283
+ [tags.fetch("display-name")]
284
+ when "viewermilestone"
285
+ fail if $3
286
+ [tags.fetch("display-name")]
287
+ else
288
+ fail "unknown USERNOTICE: #{[tags["msg-id"], _, $3].inspect}"
289
+ end
290
+ ]
291
+ else
292
+ fail "bad log line: #{_.inspect}"
293
+ end
294
+ ]
295
+ else
296
+ fail line.inspect
297
+ end
298
+ end.compact.tap{ |_| fail unless 1 == _.map(&:first).map(&:day).uniq.size }
299
+ end
146
300
  end
data/nakiircbot.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "nakiircbot"
3
- spec.version = "0.2.0"
3
+ spec.version = "1.1.0"
4
4
  spec.summary = "IRC bot framework"
5
5
 
6
6
  spec.author = "Victor Maslov aka Nakilon"
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nakiircbot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Victor Maslov aka Nakilon
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-26 00:00:00.000000000 Z
11
+ date: 2023-07-18 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description:
13
+ description:
14
14
  email: nakilon@gmail.com
15
15
  executables: []
16
16
  extensions: []
@@ -19,12 +19,12 @@ files:
19
19
  - LICENSE
20
20
  - lib/nakiircbot.rb
21
21
  - nakiircbot.gemspec
22
- homepage:
22
+ homepage:
23
23
  licenses:
24
24
  - MIT
25
25
  metadata:
26
26
  source_code_uri: https://github.com/nakilon/nakiircbot
27
- post_install_message:
27
+ post_install_message:
28
28
  rdoc_options: []
29
29
  require_paths:
30
30
  - lib
@@ -39,8 +39,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  requirements: []
42
- rubygems_version: 3.2.22
43
- signing_key:
42
+ rubygems_version: 3.4.13
43
+ signing_key:
44
44
  specification_version: 4
45
45
  summary: IRC bot framework
46
46
  test_files: []