nakiircbot 0.2.0 → 1.0.0

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