ssc.bot 0.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 +7 -0
- data/CHANGELOG.md +40 -0
- data/Gemfile +25 -0
- data/LICENSE.txt +165 -0
- data/README.md +115 -0
- data/Rakefile +52 -0
- data/lib/ssc.bot.rb +44 -0
- data/lib/ssc.bot/chat_log.rb +283 -0
- data/lib/ssc.bot/chat_log/message.rb +86 -0
- data/lib/ssc.bot/chat_log/message_parsable.rb +62 -0
- data/lib/ssc.bot/chat_log/message_parser.rb +562 -0
- data/lib/ssc.bot/chat_log/messages.rb +193 -0
- data/lib/ssc.bot/chat_log_file.rb +52 -0
- data/lib/ssc.bot/error.rb +61 -0
- data/lib/ssc.bot/ssc_file.rb +132 -0
- data/lib/ssc.bot/user/jrobot_message_sender.rb +194 -0
- data/lib/ssc.bot/user/message_sender.rb +378 -0
- data/lib/ssc.bot/util.rb +90 -0
- data/lib/ssc.bot/version.rb +26 -0
- data/ssc.bot.gemspec +68 -0
- data/test/test_helper.rb +28 -0
- metadata +179 -0
@@ -0,0 +1,283 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: UTF-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
#--
|
6
|
+
# This file is part of SSC.Bot.
|
7
|
+
# Copyright (c) 2020 Jonathan Bradley Whited (@esotericpig)
|
8
|
+
#
|
9
|
+
# SSC.Bot is free software: you can redistribute it and/or modify
|
10
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
11
|
+
# the Free Software Foundation, either version 3 of the License, or
|
12
|
+
# (at your option) any later version.
|
13
|
+
#
|
14
|
+
# SSC.Bot is distributed in the hope that it will be useful,
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
17
|
+
# GNU Lesser General Public License for more details.
|
18
|
+
#
|
19
|
+
# You should have received a copy of the GNU Lesser General Public License
|
20
|
+
# along with SSC.Bot. If not, see <https://www.gnu.org/licenses/>.
|
21
|
+
#++
|
22
|
+
|
23
|
+
|
24
|
+
require 'attr_bool'
|
25
|
+
require 'set'
|
26
|
+
|
27
|
+
require 'ssc.bot/ssc_file'
|
28
|
+
|
29
|
+
require 'ssc.bot/chat_log/message'
|
30
|
+
require 'ssc.bot/chat_log/message_parsable'
|
31
|
+
require 'ssc.bot/chat_log/message_parser'
|
32
|
+
require 'ssc.bot/chat_log/messages'
|
33
|
+
|
34
|
+
|
35
|
+
module SSCBot
|
36
|
+
###
|
37
|
+
# @author Jonathan Bradley Whited (@esotericpig)
|
38
|
+
# @since 0.1.0
|
39
|
+
###
|
40
|
+
class ChatLog
|
41
|
+
include MessageParsable
|
42
|
+
|
43
|
+
attr_reader? :alive
|
44
|
+
attr_accessor :file_mode
|
45
|
+
attr_reader :file_opt
|
46
|
+
attr_accessor :filename
|
47
|
+
attr_accessor :idle_secs
|
48
|
+
attr_reader :observers
|
49
|
+
attr_reader :thread
|
50
|
+
|
51
|
+
def initialize(filename,file_mode: 'rt',file_opt: {},idle_secs: 0.250,**parser_kargs)
|
52
|
+
super()
|
53
|
+
|
54
|
+
@alive = false
|
55
|
+
@file_mode = file_mode
|
56
|
+
@file_opt = file_opt
|
57
|
+
@filename = filename
|
58
|
+
@idle_secs = idle_secs
|
59
|
+
@observers = {}
|
60
|
+
@parser = MessageParser.new(**parser_kargs)
|
61
|
+
@semaphore = Mutex.new()
|
62
|
+
@thread = nil
|
63
|
+
end
|
64
|
+
|
65
|
+
def add_observer(observer=nil,*funcs,type: :any,&block)
|
66
|
+
if observer.nil?() && block.nil?()
|
67
|
+
raise ArgumentError,'no observer'
|
68
|
+
end
|
69
|
+
|
70
|
+
check_type(type)
|
71
|
+
|
72
|
+
type_observers = fetch_observers(type: type)
|
73
|
+
|
74
|
+
if !observer.nil?()
|
75
|
+
funcs << :call if funcs.empty?()
|
76
|
+
|
77
|
+
type_observers << Observer.new(observer,*funcs)
|
78
|
+
end
|
79
|
+
|
80
|
+
if !block.nil?()
|
81
|
+
type_observers << Observer.new(block,:call)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def add_observers(*observers,type: :any,func: :call,&block)
|
86
|
+
if observers.empty?() && block.nil?()
|
87
|
+
raise ArgumentError,'no observer'
|
88
|
+
end
|
89
|
+
|
90
|
+
check_type(type)
|
91
|
+
|
92
|
+
type_observers = fetch_observers(type: type)
|
93
|
+
|
94
|
+
observers.each() do |observer|
|
95
|
+
type_observers << Observer.new(observer,func)
|
96
|
+
end
|
97
|
+
|
98
|
+
if !block.nil?()
|
99
|
+
type_observers << Observer.new(block,:call)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def check_type(type,nil_ok: false)
|
104
|
+
if type.nil?()
|
105
|
+
if !nil_ok
|
106
|
+
raise ArgumentError,"invalid type{#{type.inspect()}}"
|
107
|
+
end
|
108
|
+
else
|
109
|
+
if type != :any && !Message.valid_type?(type)
|
110
|
+
raise ArgumentError,"invalid type{#{type.inspect()}}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def clear_content()
|
116
|
+
SSCFile.clear(@filename)
|
117
|
+
end
|
118
|
+
|
119
|
+
def count_observers(type: nil)
|
120
|
+
check_type(type,nil_ok: true)
|
121
|
+
|
122
|
+
count = 0
|
123
|
+
|
124
|
+
if type.nil?()
|
125
|
+
@observers.each_value() do |type_observers|
|
126
|
+
count += type_observers.length
|
127
|
+
end
|
128
|
+
else
|
129
|
+
type_observers = @observers[type]
|
130
|
+
|
131
|
+
if !type_observers.nil?()
|
132
|
+
count += type_observers.length
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
return count
|
137
|
+
end
|
138
|
+
|
139
|
+
def delete_observer(observer,type: nil)
|
140
|
+
delete_observers(observer,type: type)
|
141
|
+
end
|
142
|
+
|
143
|
+
def delete_observers(*observers,type: nil)
|
144
|
+
check_type(type,nil_ok: true)
|
145
|
+
|
146
|
+
if observers.empty?()
|
147
|
+
if type.nil?()
|
148
|
+
@observers.clear()
|
149
|
+
else
|
150
|
+
type_observers = @observers[type]
|
151
|
+
|
152
|
+
if !type_observers.nil?()
|
153
|
+
type_observers.clear()
|
154
|
+
end
|
155
|
+
end
|
156
|
+
else
|
157
|
+
observers = observers.to_set()
|
158
|
+
|
159
|
+
if type.nil?()
|
160
|
+
@observers.each_value() do |type_observers|
|
161
|
+
type_observers.delete_if() do |observer|
|
162
|
+
observers.include?(observer.object)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
else
|
166
|
+
type_observers = @observers[type]
|
167
|
+
|
168
|
+
if !type_observers.nil?()
|
169
|
+
type_observers.delete_if() do |observer|
|
170
|
+
observers.include?(observer.object)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def fetch_observers(type: :any)
|
178
|
+
check_type(type)
|
179
|
+
|
180
|
+
type_observers = @observers[type]
|
181
|
+
|
182
|
+
if type_observers.nil?()
|
183
|
+
type_observers = []
|
184
|
+
@observers[type] = type_observers
|
185
|
+
end
|
186
|
+
|
187
|
+
return type_observers
|
188
|
+
end
|
189
|
+
|
190
|
+
def notify_observers(message)
|
191
|
+
any_observers = @observers[:any]
|
192
|
+
type_observers = @observers[message.type]
|
193
|
+
|
194
|
+
if !any_observers.nil?()
|
195
|
+
any_observers.each() do |observer|
|
196
|
+
observer.notify(self,message)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
if !type_observers.nil?()
|
201
|
+
type_observers.each() do |observer|
|
202
|
+
observer.notify(self,message)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def run(seek_to_end: true)
|
208
|
+
@semaphore.synchronize() do
|
209
|
+
return if @alive # Already running
|
210
|
+
end
|
211
|
+
|
212
|
+
stop() # Justin Case
|
213
|
+
|
214
|
+
@semaphore.synchronize() do
|
215
|
+
@alive = true
|
216
|
+
|
217
|
+
soft_touch() # Create the file if it doesn't exist
|
218
|
+
|
219
|
+
@thread = Thread.new() do
|
220
|
+
SSCFile.open(@filename,@file_mode,**@file_opt) do |fin|
|
221
|
+
fin.seek_to_end() if seek_to_end
|
222
|
+
|
223
|
+
while @alive
|
224
|
+
while !(line = fin.get_line()).nil?()
|
225
|
+
message = @parser.parse(line)
|
226
|
+
|
227
|
+
notify_observers(message)
|
228
|
+
end
|
229
|
+
|
230
|
+
sleep(@idle_secs)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def soft_touch()
|
238
|
+
SSCFile.soft_touch(@filename)
|
239
|
+
end
|
240
|
+
|
241
|
+
def stop(wait_secs=5)
|
242
|
+
@semaphore.synchronize() do
|
243
|
+
@alive = false
|
244
|
+
|
245
|
+
if !@thread.nil?()
|
246
|
+
if @thread.alive?()
|
247
|
+
# First, try to kill it gracefully (waiting X secs).
|
248
|
+
@thread.join(@idle_secs + wait_secs)
|
249
|
+
|
250
|
+
# Die!
|
251
|
+
@thread.kill() if @thread.alive?()
|
252
|
+
end
|
253
|
+
|
254
|
+
@thread = nil
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
###
|
260
|
+
# @author Jonathan Bradley Whited (@esotericpig)
|
261
|
+
# @since 0.1.0
|
262
|
+
###
|
263
|
+
class Observer
|
264
|
+
attr_reader :funcs
|
265
|
+
attr_reader :object
|
266
|
+
|
267
|
+
def initialize(object,*funcs)
|
268
|
+
super()
|
269
|
+
|
270
|
+
raise ArgumentError,'empty funcs' if funcs.empty?()
|
271
|
+
|
272
|
+
@funcs = funcs
|
273
|
+
@object = object
|
274
|
+
end
|
275
|
+
|
276
|
+
def notify(chatlog,message)
|
277
|
+
@funcs.each() do |func|
|
278
|
+
@object.__send__(func,chatlog,message)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: UTF-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
#--
|
6
|
+
# This file is part of SSC.Bot.
|
7
|
+
# Copyright (c) 2020 Jonathan Bradley Whited (@esotericpig)
|
8
|
+
#
|
9
|
+
# SSC.Bot is free software: you can redistribute it and/or modify
|
10
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
11
|
+
# the Free Software Foundation, either version 3 of the License, or
|
12
|
+
# (at your option) any later version.
|
13
|
+
#
|
14
|
+
# SSC.Bot is distributed in the hope that it will be useful,
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
17
|
+
# GNU Lesser General Public License for more details.
|
18
|
+
#
|
19
|
+
# You should have received a copy of the GNU Lesser General Public License
|
20
|
+
# along with SSC.Bot. If not, see <https://www.gnu.org/licenses/>.
|
21
|
+
#++
|
22
|
+
|
23
|
+
|
24
|
+
require 'set'
|
25
|
+
|
26
|
+
|
27
|
+
module SSCBot
|
28
|
+
class ChatLog
|
29
|
+
###
|
30
|
+
# The base class of all parsed messages from a chat log file.
|
31
|
+
#
|
32
|
+
# @author Jonathan Bradley Whited (@esotericpig)
|
33
|
+
# @since 0.1.0
|
34
|
+
###
|
35
|
+
class Message
|
36
|
+
# Valid types of messages.
|
37
|
+
#
|
38
|
+
# You can add your own custom type(s) that you parse manually:
|
39
|
+
# SSCBot::ChatLog::Message::TYPES.add(:custom)
|
40
|
+
TYPES = Set[
|
41
|
+
# In order of F1 Help box.
|
42
|
+
*%i{
|
43
|
+
pub team private remote freq chat
|
44
|
+
?lines ?namelen ?ignore ?nopubchat ?obscene ?away ?log ?logbuffer
|
45
|
+
?kill kill ?enter enter ?leave leave ?message ?messages ?chat
|
46
|
+
?status ?scorereset ?team ?spec ?target ?time ?flags ?score ?crown
|
47
|
+
?best ?buy
|
48
|
+
?owner ?password ?usage ?userid ?find ?ping ?packetloss ?lag ?music
|
49
|
+
?sound ?alarm ?sheep ?getnews
|
50
|
+
?squadowner ?squad ?squadlist ?loadmacro ?savemacro
|
51
|
+
unknown
|
52
|
+
},
|
53
|
+
]
|
54
|
+
|
55
|
+
# @param type [Symbol] the type to check if valid
|
56
|
+
# @return [Boolean] +true+ if +type+ is one of {TYPES}, else +false+
|
57
|
+
def self.valid_type?(type)
|
58
|
+
return TYPES.include?(type)
|
59
|
+
end
|
60
|
+
|
61
|
+
attr_reader :line # @return [String] the raw (unparsed) line from the file
|
62
|
+
attr_reader :type # @return [Symbol] what type of message this is; one of {TYPES}
|
63
|
+
|
64
|
+
# @param line [String] the raw (unparsed) line from the file
|
65
|
+
# @param type [Symbol] what type of message this is; must be one of {TYPES}
|
66
|
+
def initialize(line,type:)
|
67
|
+
type = type.to_sym()
|
68
|
+
|
69
|
+
raise ArgumentError,"invalid line{#{line.inspect()}}" if line.nil?()
|
70
|
+
raise ArgumentError,"invalid type{#{type.inspect()}}" if !self.class.valid_type?(type)
|
71
|
+
|
72
|
+
@line = line
|
73
|
+
@type = type
|
74
|
+
end
|
75
|
+
|
76
|
+
# A convenience method for comparing anything that responds to
|
77
|
+
# +:to_sym():+, like +String+.
|
78
|
+
#
|
79
|
+
# @param type [String,Symbol] the type to convert & compare against
|
80
|
+
# @return [Boolean] +true+ if this message is of type +type+, else +false+
|
81
|
+
def type?(type)
|
82
|
+
return @type == type.to_sym()
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: UTF-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
#--
|
6
|
+
# This file is part of SSC.Bot.
|
7
|
+
# Copyright (c) 2020 Jonathan Bradley Whited (@esotericpig)
|
8
|
+
#
|
9
|
+
# SSC.Bot is free software: you can redistribute it and/or modify
|
10
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
11
|
+
# the Free Software Foundation, either version 3 of the License, or
|
12
|
+
# (at your option) any later version.
|
13
|
+
#
|
14
|
+
# SSC.Bot is distributed in the hope that it will be useful,
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
17
|
+
# GNU Lesser General Public License for more details.
|
18
|
+
#
|
19
|
+
# You should have received a copy of the GNU Lesser General Public License
|
20
|
+
# along with SSC.Bot. If not, see <https://www.gnu.org/licenses/>.
|
21
|
+
#++
|
22
|
+
|
23
|
+
|
24
|
+
require 'forwardable'
|
25
|
+
|
26
|
+
require 'ssc.bot/chat_log/message_parser'
|
27
|
+
|
28
|
+
|
29
|
+
module SSCBot
|
30
|
+
class ChatLog
|
31
|
+
###
|
32
|
+
# @author Jonathan Bradley Whited (@esotericpig)
|
33
|
+
# @since 0.1.0
|
34
|
+
###
|
35
|
+
module MessageParsable
|
36
|
+
extend Forwardable
|
37
|
+
|
38
|
+
MAX_NAMELEN = MessageParser::MAX_NAMELEN
|
39
|
+
|
40
|
+
def_delegators(:@parser,
|
41
|
+
:autoset_namelen?,:autoset_namelen=,
|
42
|
+
:check_history_count,:check_history_count=,
|
43
|
+
:commands,
|
44
|
+
:messages,
|
45
|
+
:namelen,:namelen=,
|
46
|
+
:regex_cache,
|
47
|
+
:store_history?,:store_history=,
|
48
|
+
:strict?,:strict=,
|
49
|
+
|
50
|
+
:parse,
|
51
|
+
|
52
|
+
:clear_history,
|
53
|
+
:reset_namelen,
|
54
|
+
:store_command,
|
55
|
+
|
56
|
+
:command?,
|
57
|
+
)
|
58
|
+
|
59
|
+
attr_reader :parser
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,562 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: UTF-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
#--
|
6
|
+
# This file is part of SSC.Bot.
|
7
|
+
# Copyright (c) 2020 Jonathan Bradley Whited (@esotericpig)
|
8
|
+
#
|
9
|
+
# SSC.Bot is free software: you can redistribute it and/or modify
|
10
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
11
|
+
# the Free Software Foundation, either version 3 of the License, or
|
12
|
+
# (at your option) any later version.
|
13
|
+
#
|
14
|
+
# SSC.Bot is distributed in the hope that it will be useful,
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
17
|
+
# GNU Lesser General Public License for more details.
|
18
|
+
#
|
19
|
+
# You should have received a copy of the GNU Lesser General Public License
|
20
|
+
# along with SSC.Bot. If not, see <https://www.gnu.org/licenses/>.
|
21
|
+
#++
|
22
|
+
|
23
|
+
|
24
|
+
require 'attr_bool'
|
25
|
+
require 'set'
|
26
|
+
|
27
|
+
require 'ssc.bot/error'
|
28
|
+
require 'ssc.bot/util'
|
29
|
+
|
30
|
+
require 'ssc.bot/chat_log/message'
|
31
|
+
require 'ssc.bot/chat_log/messages'
|
32
|
+
|
33
|
+
|
34
|
+
module SSCBot
|
35
|
+
class ChatLog
|
36
|
+
###
|
37
|
+
# @author Jonathan Bradley Whited (@esotericpig)
|
38
|
+
# @since 0.1.0
|
39
|
+
###
|
40
|
+
class MessageParser
|
41
|
+
MAX_NAMELEN = 24
|
42
|
+
|
43
|
+
attr_accessor? :autoset_namelen
|
44
|
+
attr_accessor :check_history_count
|
45
|
+
attr_reader :commands
|
46
|
+
attr_reader :messages
|
47
|
+
attr_accessor :namelen
|
48
|
+
attr_reader :regex_cache
|
49
|
+
attr_accessor? :store_history
|
50
|
+
attr_accessor? :strict
|
51
|
+
|
52
|
+
def initialize(autoset_namelen: true,check_history_count: 5,namelen: nil,store_history: true,strict: true)
|
53
|
+
super()
|
54
|
+
|
55
|
+
@autoset_namelen = autoset_namelen
|
56
|
+
@check_history_count = check_history_count
|
57
|
+
@commands = {}
|
58
|
+
@messages = []
|
59
|
+
@namelen = namelen
|
60
|
+
@regex_cache = {}
|
61
|
+
@store_history = store_history
|
62
|
+
@strict = strict
|
63
|
+
end
|
64
|
+
|
65
|
+
# The Ruby interpreter should cache the args' default values,
|
66
|
+
# so no reason to manually cache them unless a variable is involved inside.
|
67
|
+
#
|
68
|
+
# @example Default Format
|
69
|
+
# 'X Name> Message'
|
70
|
+
def match_player(line,type_name:,name_prefix: '',name_suffix: '> ',type_prefix: %r{..},use_namelen: true)
|
71
|
+
cached_regex = @regex_cache[type_name]
|
72
|
+
|
73
|
+
if cached_regex.nil?()
|
74
|
+
cached_regex = {}
|
75
|
+
@regex_cache[type_name] = cached_regex
|
76
|
+
end
|
77
|
+
|
78
|
+
if use_namelen && !@namelen.nil?()
|
79
|
+
regex = cached_regex[@namelen]
|
80
|
+
|
81
|
+
if regex.nil?()
|
82
|
+
name_prefix = Util.quote_str_or_regex(name_prefix)
|
83
|
+
name_suffix = Util.quote_str_or_regex(name_suffix)
|
84
|
+
type_prefix = Util.quote_str_or_regex(type_prefix)
|
85
|
+
|
86
|
+
# Be careful to not use spaces ' ', but to use '\\ ' (or '\s') instead
|
87
|
+
# because of the '/x' option.
|
88
|
+
regex = /
|
89
|
+
\A#{type_prefix}
|
90
|
+
#{name_prefix}(?<name>.{#{@namelen}})#{name_suffix}
|
91
|
+
(?<message>.*)\z
|
92
|
+
/x
|
93
|
+
|
94
|
+
cached_regex[@namelen] = regex
|
95
|
+
end
|
96
|
+
else
|
97
|
+
regex = cached_regex[:no_namelen]
|
98
|
+
|
99
|
+
if regex.nil?()
|
100
|
+
name_prefix = Util.quote_str_or_regex(name_prefix)
|
101
|
+
name_suffix = Util.quote_str_or_regex(name_suffix)
|
102
|
+
type_prefix = Util.quote_str_or_regex(type_prefix)
|
103
|
+
|
104
|
+
# Be careful to not use spaces ' ', but to use '\\ ' (or '\s') instead
|
105
|
+
# because of the '/x' option.
|
106
|
+
regex = /
|
107
|
+
\A#{type_prefix}
|
108
|
+
#{name_prefix}(?<name>.*?\S)#{name_suffix}
|
109
|
+
(?<message>.*)\z
|
110
|
+
/x
|
111
|
+
|
112
|
+
cached_regex[:no_namelen] = regex
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
return regex.match(line)
|
117
|
+
end
|
118
|
+
|
119
|
+
def parse(line)
|
120
|
+
if line.nil?()
|
121
|
+
if @strict
|
122
|
+
raise ArgumentError,"invalid line{#{line.inspect()}}"
|
123
|
+
else
|
124
|
+
line = ''
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
message = nil
|
129
|
+
|
130
|
+
if !line.empty?()
|
131
|
+
case line[0]
|
132
|
+
when 'C'
|
133
|
+
message = parse_chat(line)
|
134
|
+
when 'E'
|
135
|
+
message = parse_freq(line)
|
136
|
+
when 'P'
|
137
|
+
if (match = match_remote?(line))
|
138
|
+
message = parse_remote(line,match: match)
|
139
|
+
else
|
140
|
+
message = parse_private(line)
|
141
|
+
end
|
142
|
+
when 'T'
|
143
|
+
message = parse_team(line)
|
144
|
+
else
|
145
|
+
# Check this one first to prevent abuse from pubbers.
|
146
|
+
if (match = match_pub?(line))
|
147
|
+
message = parse_pub(line,match: match)
|
148
|
+
else
|
149
|
+
if (match = match_kill?(line))
|
150
|
+
message = parse_kill(line,match: match)
|
151
|
+
elsif (match = match_q_log?(line))
|
152
|
+
message = parse_q_log(line,match: match)
|
153
|
+
elsif (match = match_q_namelen?(line))
|
154
|
+
message = parse_q_namelen(line,match: match)
|
155
|
+
else
|
156
|
+
# These are last because too flexible.
|
157
|
+
if (match = match_q_find?(line))
|
158
|
+
message = parse_q_find(line,match: match)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
if message.nil?()
|
166
|
+
message = Message.new(line,type: :unknown)
|
167
|
+
end
|
168
|
+
|
169
|
+
if @store_history
|
170
|
+
@messages << message
|
171
|
+
end
|
172
|
+
|
173
|
+
return message
|
174
|
+
end
|
175
|
+
|
176
|
+
# @example Format
|
177
|
+
# # NOT affected by namelen.
|
178
|
+
# 'C 1:Name> Message'
|
179
|
+
def parse_chat(line)
|
180
|
+
match = match_player(line,type_name: :chat,name_prefix: %r{(?<channel>\d+)\:},use_namelen: false)
|
181
|
+
player = parse_player(line,type_name: :chat,match: match)
|
182
|
+
|
183
|
+
return nil if player.nil?()
|
184
|
+
|
185
|
+
channel = match[:channel].to_i()
|
186
|
+
|
187
|
+
return ChatMessage.new(line,channel: channel,name: player.name,message: player.message)
|
188
|
+
end
|
189
|
+
|
190
|
+
# @example Format
|
191
|
+
# 'E Name> Message'
|
192
|
+
def parse_freq(line)
|
193
|
+
player = parse_player(line,type_name: :freq)
|
194
|
+
|
195
|
+
return nil if player.nil?()
|
196
|
+
|
197
|
+
return FreqMessage.new(line,name: player.name,message: player.message)
|
198
|
+
end
|
199
|
+
|
200
|
+
# @example Format
|
201
|
+
# ' Killed.Name(100) killed by: Killer.Name'
|
202
|
+
def parse_kill(line,match:)
|
203
|
+
if match.nil?()
|
204
|
+
if @strict
|
205
|
+
raise ParseError,"invalid kill message{#{line}}"
|
206
|
+
else
|
207
|
+
return nil
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
killed = match[:killed]
|
212
|
+
bounty = match[:bounty].to_i()
|
213
|
+
killer = match[:killer]
|
214
|
+
|
215
|
+
return KillMessage.new(line,killed: killed,bounty: bounty,killer: killer)
|
216
|
+
end
|
217
|
+
|
218
|
+
# @example Default Format
|
219
|
+
# 'X Name> Message'
|
220
|
+
def parse_player(line,type_name:,match: :default)
|
221
|
+
if match.nil?()
|
222
|
+
if @strict
|
223
|
+
raise ParseError,"invalid #{type_name} message{#{line}}"
|
224
|
+
else
|
225
|
+
return nil
|
226
|
+
end
|
227
|
+
elsif match == :default
|
228
|
+
# Use type_name of :player (not passed in param) for regex_cache.
|
229
|
+
match = match_player(line,type_name: :player)
|
230
|
+
end
|
231
|
+
|
232
|
+
name = Util.u_lstrip(match[:name])
|
233
|
+
message = match[:message]
|
234
|
+
|
235
|
+
if name.empty?() || name.length > MAX_NAMELEN
|
236
|
+
if @strict
|
237
|
+
raise ParseError,"invalid player name for #{type_name} message{#{line}}"
|
238
|
+
else
|
239
|
+
return nil
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
return PlayerMessage.new(line,type: :unknown,name: name,message: message)
|
244
|
+
end
|
245
|
+
|
246
|
+
# @example Format
|
247
|
+
# 'P Name> Message'
|
248
|
+
def parse_private(line)
|
249
|
+
player = parse_player(line,type_name: :private)
|
250
|
+
|
251
|
+
return nil if player.nil?()
|
252
|
+
|
253
|
+
return PrivateMessage.new(line,name: player.name,message: player.message)
|
254
|
+
end
|
255
|
+
|
256
|
+
# @example Format
|
257
|
+
# ' Name> Message'
|
258
|
+
def parse_pub(line,match:)
|
259
|
+
player = parse_player(line,type_name: :pub,match: match)
|
260
|
+
|
261
|
+
return nil if player.nil?()
|
262
|
+
|
263
|
+
cmd = Util.u_strip(player.message).downcase()
|
264
|
+
|
265
|
+
if cmd.start_with?('?find')
|
266
|
+
store_command(:pub,%s{?find})
|
267
|
+
end
|
268
|
+
|
269
|
+
return PubMessage.new(line,name: player.name,message: player.message)
|
270
|
+
end
|
271
|
+
|
272
|
+
# @example Format
|
273
|
+
# ' Not online, last seen more than 10 days ago'
|
274
|
+
# ' Not online, last seen 9 days ago'
|
275
|
+
# ' Not online, last seen 18 hours ago'
|
276
|
+
# ' Not online, last seen 0 hours ago'
|
277
|
+
# ' Name - Public 0'
|
278
|
+
# ' TWCore - (Private arena)'
|
279
|
+
# ' Name is in SSCJ Devastation'
|
280
|
+
# ' Name is in SSCC Metal Gear CTF'
|
281
|
+
def parse_q_find(line,match:)
|
282
|
+
if match.nil?()
|
283
|
+
if @strict
|
284
|
+
raise ParseError,"invalid ?find message{#{line}}"
|
285
|
+
else
|
286
|
+
return nil
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
caps = match.named_captures
|
291
|
+
q_find = nil
|
292
|
+
|
293
|
+
if (days = caps['days'])
|
294
|
+
more = caps.key?('more')
|
295
|
+
days = days.to_i()
|
296
|
+
|
297
|
+
q_find = QFindMessage.new(line,find_type: :days,more: more,days: days)
|
298
|
+
elsif (hours = caps['hours'])
|
299
|
+
hours = hours.to_i()
|
300
|
+
|
301
|
+
q_find = QFindMessage.new(line,find_type: :hours,hours: hours)
|
302
|
+
elsif (player = caps['player'])
|
303
|
+
if (arena = caps['arena'])
|
304
|
+
private = (arena == '(Private arena)')
|
305
|
+
|
306
|
+
q_find = QFindMessage.new(line,find_type: :arena,player: player,arena: arena,private: private)
|
307
|
+
elsif (zone = caps['zone'])
|
308
|
+
q_find = QFindMessage.new(line,find_type: :zone,player: player,zone: zone)
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
if q_find.nil?() && @strict
|
313
|
+
raise ParseError,"invalid ?find message{#{line}}"
|
314
|
+
end
|
315
|
+
|
316
|
+
return q_find
|
317
|
+
end
|
318
|
+
|
319
|
+
# @example Format
|
320
|
+
# ' Log file open: session.log'
|
321
|
+
# ' Log file closed'
|
322
|
+
def parse_q_log(line,match:)
|
323
|
+
if match.nil?()
|
324
|
+
if @strict
|
325
|
+
raise ParseError,"invalid ?log message{#{line}}"
|
326
|
+
else
|
327
|
+
return nil
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
filename = match.named_captures['filename']
|
332
|
+
log_type = filename.nil?() ? :close : :open
|
333
|
+
|
334
|
+
return QLogMessage.new(line,log_type: log_type,filename: filename)
|
335
|
+
end
|
336
|
+
|
337
|
+
# @example Format
|
338
|
+
# ' Message Name Length: 24'
|
339
|
+
def parse_q_namelen(line,match:)
|
340
|
+
if match.nil?()
|
341
|
+
if @strict
|
342
|
+
raise ParseError,"invalid ?namelen message{#{line}}"
|
343
|
+
else
|
344
|
+
return nil
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
namelen = match[:namelen].to_i()
|
349
|
+
|
350
|
+
if namelen < 1
|
351
|
+
if @strict
|
352
|
+
raise ParseError,"invalid namelen for ?namelen message{#{line}}"
|
353
|
+
else
|
354
|
+
return nil
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
if @autoset_namelen
|
359
|
+
@namelen = namelen
|
360
|
+
end
|
361
|
+
|
362
|
+
return QNamelenMessage.new(line,namelen: namelen)
|
363
|
+
end
|
364
|
+
|
365
|
+
# @example Format
|
366
|
+
# # NOT affected by namelen.
|
367
|
+
# 'P :Self.Name:Message'
|
368
|
+
# 'P (Name)>Message'
|
369
|
+
def parse_remote(line,match:)
|
370
|
+
player = parse_player(line,type_name: 'remote private',match: match)
|
371
|
+
|
372
|
+
return nil if player.nil?()
|
373
|
+
|
374
|
+
own = (line[2] == ':')
|
375
|
+
squad = (player.name[0] == '#')
|
376
|
+
|
377
|
+
return RemoteMessage.new(line,
|
378
|
+
own: own,squad: squad,
|
379
|
+
name: player.name,message: player.message,
|
380
|
+
)
|
381
|
+
end
|
382
|
+
|
383
|
+
# @example Format
|
384
|
+
# 'T Name> Message'
|
385
|
+
def parse_team(line)
|
386
|
+
player = parse_player(line,type_name: :team)
|
387
|
+
|
388
|
+
return nil if player.nil?()
|
389
|
+
|
390
|
+
return TeamMessage.new(line,name: player.name,message: player.message)
|
391
|
+
end
|
392
|
+
|
393
|
+
def clear_history()
|
394
|
+
@messages.clear()
|
395
|
+
end
|
396
|
+
|
397
|
+
def reset_namelen()
|
398
|
+
@namelen = nil
|
399
|
+
end
|
400
|
+
|
401
|
+
def store_command(type,name)
|
402
|
+
type_hash = @commands[type]
|
403
|
+
|
404
|
+
if type_hash.nil?()
|
405
|
+
type_hash = {}
|
406
|
+
@commands[type] = type_hash
|
407
|
+
end
|
408
|
+
|
409
|
+
type_hash[name] = @messages.length # Index of when command was found/stored
|
410
|
+
end
|
411
|
+
|
412
|
+
def command?(type,name,delete: true)
|
413
|
+
return true if @check_history_count < 1
|
414
|
+
|
415
|
+
type_hash = @commands[type]
|
416
|
+
|
417
|
+
if !type_hash.nil?()
|
418
|
+
index = type_hash[name]
|
419
|
+
|
420
|
+
if !index.nil?() && (@messages.length - index) <= @check_history_count
|
421
|
+
type_hash.delete(name) if delete
|
422
|
+
|
423
|
+
return true
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
return false
|
428
|
+
end
|
429
|
+
|
430
|
+
# @example Format
|
431
|
+
# ' Killed.Name(100) killed by: Killer.Name'
|
432
|
+
def match_kill?(line)
|
433
|
+
return false if line.length < 19 # ' N(0) killed by: N'
|
434
|
+
|
435
|
+
return /\A (?<killed>.*?\S)\((?<bounty>\d+)\) killed by: (?<killer>.*?\S)\z/.match(line)
|
436
|
+
end
|
437
|
+
|
438
|
+
# @example Format
|
439
|
+
# ' Name> Message'
|
440
|
+
def match_pub?(line)
|
441
|
+
return false if line.length < 5 # ' N> '
|
442
|
+
|
443
|
+
match = match_player(line,type_name: :pub,type_prefix: ' ')
|
444
|
+
|
445
|
+
if !match.nil?()
|
446
|
+
name = Util.u_lstrip(match[:name])
|
447
|
+
|
448
|
+
if name.empty?() || name.length > MAX_NAMELEN
|
449
|
+
return false
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
return match
|
454
|
+
end
|
455
|
+
|
456
|
+
# @example Format
|
457
|
+
# ' Not online, last seen more than 10 days ago'
|
458
|
+
# ' Not online, last seen 9 days ago'
|
459
|
+
# ' Not online, last seen 18 hours ago'
|
460
|
+
# ' Not online, last seen 0 hours ago'
|
461
|
+
# ' Name - Public 0'
|
462
|
+
# ' TWCore - (Private arena)'
|
463
|
+
# ' Name is in SSCJ Devastation'
|
464
|
+
# ' Name is in SSCC Metal Gear CTF'
|
465
|
+
def match_q_find?(line)
|
466
|
+
return false if line.length < 7 # ' N - A'
|
467
|
+
return false unless command?(:pub,%s{?find})
|
468
|
+
|
469
|
+
if line.start_with?(' Not online, last seen ')
|
470
|
+
match = line.match(/(?<more>more) than (?<days>\d+) days ago\z/)
|
471
|
+
|
472
|
+
if match.nil?()
|
473
|
+
match = line.match(/(?<days>\d+) days? ago\z/)
|
474
|
+
end
|
475
|
+
|
476
|
+
if match.nil?()
|
477
|
+
match = line.match(/(?<hours>\d+) hours? ago\z/)
|
478
|
+
end
|
479
|
+
|
480
|
+
return match
|
481
|
+
else
|
482
|
+
match = line.match(/\A (?<player>.+) is in (?<zone>.+)\z/)
|
483
|
+
|
484
|
+
if match.nil?()
|
485
|
+
match = line.match(/\A (?<player>.+) - (?<arena>.+)\z/)
|
486
|
+
end
|
487
|
+
|
488
|
+
if match
|
489
|
+
caps = match.named_captures
|
490
|
+
|
491
|
+
player = caps['player']
|
492
|
+
|
493
|
+
if player.length > MAX_NAMELEN
|
494
|
+
return false
|
495
|
+
end
|
496
|
+
|
497
|
+
if caps.key?('arena')
|
498
|
+
area = caps['arena']
|
499
|
+
elsif caps.key?('zone')
|
500
|
+
area = caps['zone']
|
501
|
+
else
|
502
|
+
return false
|
503
|
+
end
|
504
|
+
|
505
|
+
[player[0],player[-1],area[0],area[-1]].each() do |c|
|
506
|
+
if c =~ /[[:space:]]/
|
507
|
+
return false
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
return match
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
return false
|
516
|
+
end
|
517
|
+
|
518
|
+
# @example Format
|
519
|
+
# ' Log file open: session.log'
|
520
|
+
# ' Log file closed'
|
521
|
+
def match_q_log?(line)
|
522
|
+
return false if line.length < 17
|
523
|
+
|
524
|
+
match = /\A Log file open: (?<filename>.+)\z/.match(line)
|
525
|
+
|
526
|
+
if match.nil?()
|
527
|
+
match = /\A Log file closed\z/.match(line)
|
528
|
+
end
|
529
|
+
|
530
|
+
return match
|
531
|
+
end
|
532
|
+
|
533
|
+
# @example Format
|
534
|
+
# ' Message Name Length: 24'
|
535
|
+
def match_q_namelen?(line)
|
536
|
+
return false if line.length < 24 # '...: 0'
|
537
|
+
return false if line[21] != ':'
|
538
|
+
|
539
|
+
return /\A Message Name Length: (?<namelen>\d+)\z/.match(line)
|
540
|
+
end
|
541
|
+
|
542
|
+
# @example Format
|
543
|
+
# # NOT affected by namelen.
|
544
|
+
# 'P :Self.Name:Message'
|
545
|
+
# 'P (Name)>Message'
|
546
|
+
def match_remote?(line)
|
547
|
+
return false if line.length < 5 # 'P :N:'
|
548
|
+
|
549
|
+
case line[2]
|
550
|
+
when ':'
|
551
|
+
return match_player(line,type_name: %s{remote.out},
|
552
|
+
name_prefix: ':',name_suffix: ':',use_namelen: false)
|
553
|
+
when '('
|
554
|
+
return match_player(line,type_name: %s{remote.in},
|
555
|
+
name_prefix: '(',name_suffix: ')>',use_namelen: false)
|
556
|
+
end
|
557
|
+
|
558
|
+
return false
|
559
|
+
end
|
560
|
+
end
|
561
|
+
end
|
562
|
+
end
|