nadoka 0.8.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.
- data/.gitignore +5 -0
- data/ChangeLog.old +1553 -0
- data/Gemfile +4 -0
- data/README.org +31 -0
- data/Rakefile +1 -0
- data/bin/nadoka +13 -0
- data/lib/rss_check.rb +206 -0
- data/lib/tagparts.rb +206 -0
- data/nadoka.gemspec +29 -0
- data/nadoka.rb +123 -0
- data/nadokarc +267 -0
- data/ndk/bot.rb +241 -0
- data/ndk/client.rb +288 -0
- data/ndk/config.rb +571 -0
- data/ndk/error.rb +61 -0
- data/ndk/logger.rb +311 -0
- data/ndk/server.rb +784 -0
- data/ndk/server_state.rb +324 -0
- data/ndk/version.rb +44 -0
- data/plugins/autoawaybot.nb +66 -0
- data/plugins/autodumpbot.nb +227 -0
- data/plugins/autoop.nb +56 -0
- data/plugins/backlogbot.nb +88 -0
- data/plugins/checkbot.nb +64 -0
- data/plugins/cronbot.nb +20 -0
- data/plugins/dictbot.nb +53 -0
- data/plugins/drbcl.rb +39 -0
- data/plugins/drbot.nb +93 -0
- data/plugins/evalbot.nb +49 -0
- data/plugins/gonzuibot.nb +41 -0
- data/plugins/googlebot.nb +345 -0
- data/plugins/identifynickserv.nb +43 -0
- data/plugins/mailcheckbot.nb +0 -0
- data/plugins/marldiabot.nb +99 -0
- data/plugins/messagebot.nb +96 -0
- data/plugins/modemanager.nb +150 -0
- data/plugins/opensearchbot.nb +156 -0
- data/plugins/opshop.nb +23 -0
- data/plugins/pastebot.nb +46 -0
- data/plugins/roulettebot.nb +33 -0
- data/plugins/rss_checkbot.nb +121 -0
- data/plugins/samplebot.nb +24 -0
- data/plugins/sendpingbot.nb +17 -0
- data/plugins/shellbot.nb +59 -0
- data/plugins/sixamobot.nb +77 -0
- data/plugins/tenkibot.nb +111 -0
- data/plugins/timestampbot.nb +62 -0
- data/plugins/titlebot.nb +226 -0
- data/plugins/translatebot.nb +301 -0
- data/plugins/twitterbot.nb +138 -0
- data/plugins/weba.nb +209 -0
- data/plugins/xibot.nb +113 -0
- data/rice/irc.rb +780 -0
- metadata +102 -0
data/ndk/error.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2004-2005 SASADA Koichi <ko1 at atdot.net>
|
3
|
+
#
|
4
|
+
# This program is free software with ABSOLUTELY NO WARRANTY.
|
5
|
+
# You can re-distribute and/or modify this program under
|
6
|
+
# the same terms of the Ruby's license.
|
7
|
+
#
|
8
|
+
#
|
9
|
+
# $Id$
|
10
|
+
# Create : K.S. 04/04/20 23:57:17
|
11
|
+
#
|
12
|
+
|
13
|
+
module Nadoka
|
14
|
+
|
15
|
+
class NDK_Error < Exception
|
16
|
+
end
|
17
|
+
|
18
|
+
class NDK_QuitClient < NDK_Error
|
19
|
+
end
|
20
|
+
|
21
|
+
class NDK_BotBreak < NDK_Error
|
22
|
+
end
|
23
|
+
|
24
|
+
class NDK_BotSendCancel < NDK_Error
|
25
|
+
end
|
26
|
+
|
27
|
+
class NDK_QuitProgram < NDK_Error
|
28
|
+
end
|
29
|
+
|
30
|
+
class NDK_RestartProgram < NDK_Error
|
31
|
+
end
|
32
|
+
|
33
|
+
class NDK_ReconnectToServer < NDK_Error
|
34
|
+
end
|
35
|
+
|
36
|
+
class NDK_InvalidMessage < NDK_Error
|
37
|
+
end
|
38
|
+
|
39
|
+
####
|
40
|
+
class NDK_FilterMessage_SendCancel < NDK_Error
|
41
|
+
end
|
42
|
+
|
43
|
+
class NDK_FilterMessage_Replace < NDK_Error
|
44
|
+
def initialize msg
|
45
|
+
@msg = msg
|
46
|
+
end
|
47
|
+
attr_reader :msg
|
48
|
+
end
|
49
|
+
|
50
|
+
class NDK_FilterMessage_OnlyBot < NDK_Error
|
51
|
+
end
|
52
|
+
|
53
|
+
class NDK_FilterMessage_OnlyLog < NDK_Error
|
54
|
+
end
|
55
|
+
|
56
|
+
class NDK_FilterMessage_BotAndLog < NDK_Error
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
|
data/ndk/logger.rb
ADDED
@@ -0,0 +1,311 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2004-2005 SASADA Koichi <ko1 at atdot.net>
|
3
|
+
#
|
4
|
+
# This program is free software with ABSOLUTELY NO WARRANTY.
|
5
|
+
# You can re-distribute and/or modify this program under
|
6
|
+
# the same terms of the Ruby's license.
|
7
|
+
#
|
8
|
+
#
|
9
|
+
# $Id$
|
10
|
+
# Create : K.S. 04/05/01 02:04:18
|
11
|
+
|
12
|
+
require 'kconv'
|
13
|
+
require 'fileutils'
|
14
|
+
require 'thread'
|
15
|
+
|
16
|
+
module Nadoka
|
17
|
+
|
18
|
+
class LogWriter
|
19
|
+
def initialize config, opts
|
20
|
+
@opts = opts
|
21
|
+
@config = config
|
22
|
+
|
23
|
+
@time_fmt = opts[:time_format]
|
24
|
+
@msg_fmts = opts[:message_format]
|
25
|
+
end
|
26
|
+
|
27
|
+
def write_log msg
|
28
|
+
raise "override me"
|
29
|
+
end
|
30
|
+
|
31
|
+
def log_format msgobj
|
32
|
+
@config.log_format @time_fmt, @msg_fmts, msgobj
|
33
|
+
end
|
34
|
+
|
35
|
+
def logging msgobj
|
36
|
+
msg = log_format(msgobj)
|
37
|
+
return if msg.empty?
|
38
|
+
write_log msg
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class LogUnWriter < LogWriter
|
43
|
+
def logging _
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class IOLogWriter < LogWriter
|
48
|
+
Lock = Mutex.new
|
49
|
+
def initialize config, opts
|
50
|
+
super
|
51
|
+
@io = opts[:io]
|
52
|
+
end
|
53
|
+
|
54
|
+
def write_log msg
|
55
|
+
Lock.synchronize{
|
56
|
+
@io.puts msg
|
57
|
+
}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class FileLogWriter < LogWriter
|
62
|
+
def initialize config, opts
|
63
|
+
super
|
64
|
+
@filename_fmt = opts[:file]
|
65
|
+
@channel_name_in_file_name = opts[:channel_name_in_file_name]
|
66
|
+
end
|
67
|
+
|
68
|
+
def logging msgobj
|
69
|
+
msg = log_format(msgobj)
|
70
|
+
return if msg.empty?
|
71
|
+
write_log_file make_logfilename(@filename_fmt, msgobj[:ch] || '', @channel_name_in_file_name), msg
|
72
|
+
end
|
73
|
+
|
74
|
+
def write_log_file basefile, msg
|
75
|
+
basedir = File.expand_path(@config.log_dir) + '/'
|
76
|
+
logfile = File.expand_path(basefile, basedir)
|
77
|
+
ldir = File.dirname(logfile) + '/'
|
78
|
+
|
79
|
+
if !FileTest.directory?(ldir)
|
80
|
+
raise "insecure directory: #{ldir} (pls check rc file.)" if /\A#{Regexp.quote(basedir)}/ !~ ldir
|
81
|
+
# make directory recursively
|
82
|
+
FileUtils.mkdir_p(ldir)
|
83
|
+
end
|
84
|
+
|
85
|
+
open(logfile, 'a'){|f|
|
86
|
+
f.flock(File::LOCK_EX)
|
87
|
+
f.puts msg
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
def make_logfilename tmpl, ch, cn
|
92
|
+
@config.make_logfilename tmpl, ch, cn
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class NDK_Logger
|
97
|
+
class MessageStore
|
98
|
+
def initialize limit
|
99
|
+
@limit = limit
|
100
|
+
@pool = []
|
101
|
+
end
|
102
|
+
|
103
|
+
attr_reader :pool
|
104
|
+
|
105
|
+
def limit=(lim)
|
106
|
+
@limit = lim
|
107
|
+
end
|
108
|
+
|
109
|
+
def truncate
|
110
|
+
while @pool.size > @limit
|
111
|
+
@pool.shift
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def push msgobj
|
116
|
+
truncate
|
117
|
+
@pool.push msgobj
|
118
|
+
end
|
119
|
+
|
120
|
+
def clear
|
121
|
+
@pool.clear
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class MessageStoreByTime < MessageStore
|
126
|
+
def truncate
|
127
|
+
lim = Time.now.to_i - @limit
|
128
|
+
while true
|
129
|
+
if @pool[0][:time].to_i < lim
|
130
|
+
@pool.shift
|
131
|
+
else
|
132
|
+
break
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class MessageStores
|
139
|
+
def initialize type, lim, config
|
140
|
+
@limit = lim
|
141
|
+
@class = type == :time ? MessageStoreByTime : MessageStore
|
142
|
+
@config = config
|
143
|
+
@pools = {}
|
144
|
+
end
|
145
|
+
|
146
|
+
attr_reader :pools
|
147
|
+
|
148
|
+
def push msgobj
|
149
|
+
ch = msgobj[:ccn]
|
150
|
+
|
151
|
+
unless pool = @pools[ch]
|
152
|
+
limit = (@config.channel_info[ch] && @config.channel_info[ch][:backloglines]) ||
|
153
|
+
@limit
|
154
|
+
@pools[ch] = pool = @class.new(limit)
|
155
|
+
end
|
156
|
+
pool.push msgobj
|
157
|
+
end
|
158
|
+
|
159
|
+
def each_channel_pool
|
160
|
+
@pools.each{|ch, store|
|
161
|
+
yield ch, store.pool
|
162
|
+
}
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
def initialize manager, config
|
168
|
+
@manager = manager
|
169
|
+
@config = config
|
170
|
+
@dlog = @config.debug_log
|
171
|
+
@message_stores = MessageStores.new(:size, @config.backlog_lines, @config)
|
172
|
+
end
|
173
|
+
|
174
|
+
attr_reader :message_stores
|
175
|
+
|
176
|
+
# debug message
|
177
|
+
def dlog msg
|
178
|
+
if @config.loglevel >= 3
|
179
|
+
msgobj = make_msgobj msg, 'DEBUG'
|
180
|
+
@config.debug_logwriter.logging msgobj
|
181
|
+
end
|
182
|
+
end
|
183
|
+
alias debug dlog
|
184
|
+
|
185
|
+
# system message
|
186
|
+
def slog msg, nostamp = false
|
187
|
+
msgobj = make_msgobj(msg, 'SYSTEM', nostamp)
|
188
|
+
if @config.loglevel >= 2
|
189
|
+
@config.system_logwriter.logging msgobj
|
190
|
+
@message_stores.push msgobj
|
191
|
+
end
|
192
|
+
|
193
|
+
str = @config.system_logwriter.log_format(msgobj)
|
194
|
+
@manager.send_to_clients Cmd.notice(@manager.state.nick, str) if @manager.state
|
195
|
+
dlog str
|
196
|
+
end
|
197
|
+
|
198
|
+
# channel message
|
199
|
+
def clog ch, msg, nostamp = false
|
200
|
+
clog_msgobj ch, make_msgobj(msg, 'SIMPLE', nostamp, ch)
|
201
|
+
end
|
202
|
+
|
203
|
+
# other irc log message
|
204
|
+
def olog msg
|
205
|
+
olog_msgobj make_msgobj(msg, 'OTHER')
|
206
|
+
end
|
207
|
+
|
208
|
+
#########################################
|
209
|
+
def make_msgobj msg, type = msg.command, nostamp = false, ch = nil
|
210
|
+
msgobj = {
|
211
|
+
:time => Time.now,
|
212
|
+
:type => type,
|
213
|
+
:orig => msg,
|
214
|
+
:nostamp => nostamp,
|
215
|
+
:ch => ch,
|
216
|
+
}
|
217
|
+
|
218
|
+
msgobj
|
219
|
+
end
|
220
|
+
|
221
|
+
def clog_msgobj ch, msgobj
|
222
|
+
if msgobj[:ccn] == :__talk__
|
223
|
+
logwriter = @config.talk_logwriter
|
224
|
+
else
|
225
|
+
logwriter = (@config.channel_info[ch] && @config.channel_info[ch][:logwriter]) ||
|
226
|
+
@config.default_logwriter
|
227
|
+
end
|
228
|
+
|
229
|
+
@message_stores.push msgobj
|
230
|
+
logwriter.logging msgobj
|
231
|
+
end
|
232
|
+
|
233
|
+
def olog_msgobj msgobj
|
234
|
+
if @config.loglevel >= 1
|
235
|
+
@config.system_logwriter.logging msgobj
|
236
|
+
@message_stores.push msgobj
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# logging
|
241
|
+
def logging msg
|
242
|
+
user = @manager.nick_of(msg)
|
243
|
+
rch = msg.params[0]
|
244
|
+
ch_ = ch = @config.canonical_channel_name(rch)
|
245
|
+
|
246
|
+
msgobj = make_msgobj(msg)
|
247
|
+
msgobj[:ch] = rch # should be raw
|
248
|
+
msgobj[:ccn] = ch
|
249
|
+
msgobj[:nick] = user
|
250
|
+
msgobj[:msg] = msg.params[1]
|
251
|
+
|
252
|
+
case msg.command
|
253
|
+
when 'PRIVMSG', 'NOTICE', 'TOPIC', 'JOIN', 'PART'
|
254
|
+
unless /\A[\&\#\+\!]/ =~ ch # talk?
|
255
|
+
msgobj[:sender] = user
|
256
|
+
msgobj[:receiver] = rch
|
257
|
+
msgobj[:ccn] = :__talk__
|
258
|
+
end
|
259
|
+
clog_msgobj ch, msgobj
|
260
|
+
|
261
|
+
when 'NICK', 'QUIT'
|
262
|
+
# ignore. see below.
|
263
|
+
|
264
|
+
when 'MODE'
|
265
|
+
msgobj[:msg] = msg.params[1..-1].join(', ')
|
266
|
+
|
267
|
+
if @manager.state.current_channels[ch]
|
268
|
+
clog_msgobj ch, msgobj
|
269
|
+
else
|
270
|
+
olog_msgobj msgobj
|
271
|
+
end
|
272
|
+
|
273
|
+
when 'KICK'
|
274
|
+
msgobj[:kicker] = msg.params[1]
|
275
|
+
msgobj[:msg] = msg.params[2]
|
276
|
+
clog_msgobj ch, msgobj
|
277
|
+
|
278
|
+
when /^\d+/
|
279
|
+
# reply
|
280
|
+
str = msg.command + ' ' + msg.params.join(' ')
|
281
|
+
olog str
|
282
|
+
|
283
|
+
else
|
284
|
+
# other command
|
285
|
+
olog msg.to_s
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def logging_nick ccn, rch, nick, newnick, msg
|
290
|
+
msgobj = make_msgobj(msg)
|
291
|
+
msgobj[:ch] = rch # should be raw
|
292
|
+
msgobj[:ccn] = ccn
|
293
|
+
msgobj[:nick] = nick
|
294
|
+
msgobj[:newnick] = newnick
|
295
|
+
clog_msgobj ccn, msgobj
|
296
|
+
end
|
297
|
+
|
298
|
+
def logging_quit ccn, rch, user, qmsg, msg
|
299
|
+
msgobj = make_msgobj(msg)
|
300
|
+
msgobj[:ch] = rch # should be raw
|
301
|
+
msgobj[:ccn] = ccn
|
302
|
+
msgobj[:nick] = user
|
303
|
+
msgobj[:msg] = qmsg
|
304
|
+
clog_msgobj ccn, msgobj
|
305
|
+
end
|
306
|
+
|
307
|
+
###
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
|
data/ndk/server.rb
ADDED
@@ -0,0 +1,784 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2004-2005 SASADA Koichi <ko1 at atdot.net>
|
3
|
+
#
|
4
|
+
# This program is free software with ABSOLUTELY NO WARRANTY.
|
5
|
+
# You can re-distribute and/or modify this program under
|
6
|
+
# the same terms of the Ruby's license.
|
7
|
+
#
|
8
|
+
#
|
9
|
+
# $Id$
|
10
|
+
# Create : K.S. 04/04/17 17:00:44
|
11
|
+
#
|
12
|
+
|
13
|
+
require 'rice/irc'
|
14
|
+
require 'ndk/error'
|
15
|
+
require 'ndk/config'
|
16
|
+
require 'ndk/server_state'
|
17
|
+
require 'ndk/client'
|
18
|
+
|
19
|
+
module Nadoka
|
20
|
+
Cmd = ::RICE::Command
|
21
|
+
Rpl = ::RICE::Reply
|
22
|
+
|
23
|
+
class NDK_Server
|
24
|
+
TimerIntervalSec = 60
|
25
|
+
MAX_PONG_FAIL = 5
|
26
|
+
|
27
|
+
def initialize rc
|
28
|
+
@rc = rc
|
29
|
+
@clients = []
|
30
|
+
@prev_timer = Time.now
|
31
|
+
|
32
|
+
@server_thread = nil
|
33
|
+
@clients_thread = nil
|
34
|
+
|
35
|
+
@state = nil
|
36
|
+
|
37
|
+
@state = NDK_State.new self
|
38
|
+
reload_config
|
39
|
+
|
40
|
+
@server = nil
|
41
|
+
@cserver = nil
|
42
|
+
|
43
|
+
@connected = false
|
44
|
+
@exitting = false
|
45
|
+
|
46
|
+
@pong_recieved = true
|
47
|
+
@pong_fail_count = 0
|
48
|
+
|
49
|
+
@isupport = {}
|
50
|
+
|
51
|
+
set_signal_trap
|
52
|
+
end
|
53
|
+
attr_reader :state, :connected, :rc
|
54
|
+
attr_reader :isupport
|
55
|
+
|
56
|
+
def client_count
|
57
|
+
@clients.size
|
58
|
+
end
|
59
|
+
|
60
|
+
def next_server_info
|
61
|
+
svinfo = @config.server_list.sort_by{rand}.shift
|
62
|
+
@config.server_list.push svinfo
|
63
|
+
[svinfo[:host], svinfo[:port], svinfo[:pass], svinfo[:ssl_params]]
|
64
|
+
end
|
65
|
+
|
66
|
+
def reload_config
|
67
|
+
@config.remove_previous_setting if defined?(@config)
|
68
|
+
@config = NDK_Config.new(self, @rc)
|
69
|
+
|
70
|
+
# reset logger
|
71
|
+
@logger = @config.logger
|
72
|
+
@state.logger = @logger
|
73
|
+
@state.config = @config
|
74
|
+
@clients.each{|c|
|
75
|
+
c.logger = @logger
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
def start_server_thread
|
80
|
+
@server_thread = Thread.new{
|
81
|
+
begin
|
82
|
+
@server = make_server()
|
83
|
+
@logger.slog "Server connection to #{@server.server}:#{@server.port}."
|
84
|
+
@pong_recieved = true
|
85
|
+
|
86
|
+
@server.start(1){|sv|
|
87
|
+
sv << Cmd.quit(@config.quit_message) if @config.quit_message
|
88
|
+
}
|
89
|
+
|
90
|
+
rescue RICE::Connection::Closed, SystemCallError, IOError
|
91
|
+
@connected = false
|
92
|
+
part_from_all_channels
|
93
|
+
@logger.slog "Connection closed by server. Trying to reconnect."
|
94
|
+
|
95
|
+
sleep @config.reconnect_delay
|
96
|
+
retry
|
97
|
+
|
98
|
+
rescue NDK_ReconnectToServer
|
99
|
+
@connected = false
|
100
|
+
part_from_all_channels
|
101
|
+
|
102
|
+
begin
|
103
|
+
@server.close if @server
|
104
|
+
rescue RICE::Connection::Closed, SystemCallError, IOError
|
105
|
+
end
|
106
|
+
|
107
|
+
@logger.slog "Reconnect request (no server response, or client request)."
|
108
|
+
|
109
|
+
sleep @config.reconnect_delay
|
110
|
+
retry
|
111
|
+
|
112
|
+
rescue Exception => e
|
113
|
+
ndk_error e
|
114
|
+
@clients_thread.kill if @clients_thread && @clients_thread.alive?
|
115
|
+
end
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
def make_server
|
120
|
+
host, port, @server_passwd, ssl_params = next_server_info
|
121
|
+
server = ::RICE::Connection.new(host, port, "\r\n", ssl_params)
|
122
|
+
server.regist{|rq, wq|
|
123
|
+
Thread.stop
|
124
|
+
@rq = rq
|
125
|
+
begin
|
126
|
+
@connected = false
|
127
|
+
server_main_proc
|
128
|
+
rescue Exception => e
|
129
|
+
ndk_error e
|
130
|
+
@server_thread.kill if @server_thread && @server_thread.alive?
|
131
|
+
@clients_thread.kill if @clients_thread && @clients_thread.alive?
|
132
|
+
ensure
|
133
|
+
@server.close
|
134
|
+
end
|
135
|
+
}
|
136
|
+
server
|
137
|
+
end
|
138
|
+
|
139
|
+
def server_main_proc
|
140
|
+
## login
|
141
|
+
|
142
|
+
# send passwd
|
143
|
+
if @server_passwd
|
144
|
+
send_to_server Cmd.pass(@server_passwd)
|
145
|
+
end
|
146
|
+
|
147
|
+
# send nick
|
148
|
+
if @config.away_nick && client_count == 0
|
149
|
+
@state.original_nick = @config.nick
|
150
|
+
@state.nick = @config.away_nick
|
151
|
+
else
|
152
|
+
@state.nick = @config.nick
|
153
|
+
end
|
154
|
+
send_to_server Cmd.nick(@state.nick)
|
155
|
+
|
156
|
+
# send user info
|
157
|
+
send_to_server Cmd.user(@config.user,
|
158
|
+
@config.hostname, @config.servername,
|
159
|
+
@config.realname)
|
160
|
+
|
161
|
+
# wait welcome message
|
162
|
+
while q = recv_from_server
|
163
|
+
case q.command
|
164
|
+
when '001'
|
165
|
+
break
|
166
|
+
when '433'
|
167
|
+
# Nickname is already in use.
|
168
|
+
nick = @state.nick_succ(q.params[1])
|
169
|
+
@state.nick = nick
|
170
|
+
send_to_server Cmd.nick(nick)
|
171
|
+
when 'NOTICE'
|
172
|
+
# ignore
|
173
|
+
when 'ERROR'
|
174
|
+
msg = "Server login fail!(#{q})"
|
175
|
+
@server_thread.raise NDK_ReconnectToServer
|
176
|
+
else
|
177
|
+
msg = "Server login fail!(#{q})"
|
178
|
+
@logger.slog msg
|
179
|
+
raise msg
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# change user mode
|
184
|
+
if @config.mode
|
185
|
+
send_to_server Cmd.mode(@state.nick, @config.mode)
|
186
|
+
end
|
187
|
+
|
188
|
+
|
189
|
+
# join to default channels
|
190
|
+
if @state.current_channels.size > 0
|
191
|
+
# if reconnect
|
192
|
+
@state.current_channels.each{|ch, chs|
|
193
|
+
join_to_channel ch
|
194
|
+
}
|
195
|
+
else
|
196
|
+
# default join process
|
197
|
+
@config.default_channels.each{|ch|
|
198
|
+
join_to_channel ch
|
199
|
+
}
|
200
|
+
end
|
201
|
+
|
202
|
+
@connected = true
|
203
|
+
@isupport = {}
|
204
|
+
|
205
|
+
##
|
206
|
+
if @clients.size == 0
|
207
|
+
enter_away
|
208
|
+
end
|
209
|
+
|
210
|
+
invoke_event :invoke_bot, :server_connected
|
211
|
+
|
212
|
+
# loop
|
213
|
+
while q = recv_from_server
|
214
|
+
|
215
|
+
case q.command
|
216
|
+
when 'PING'
|
217
|
+
send_to_server Cmd.pong(q.params[0])
|
218
|
+
next
|
219
|
+
when 'PRIVMSG'
|
220
|
+
if ctcp_message?(q.params[1])
|
221
|
+
ctcp_message(q)
|
222
|
+
end
|
223
|
+
when 'JOIN'
|
224
|
+
@state.on_join(nick_of(q), q.params[0])
|
225
|
+
when 'PART'
|
226
|
+
@state.on_part(nick_of(q), q.params[0])
|
227
|
+
when 'NICK'
|
228
|
+
@state.on_nick(nick_of(q), q.params[0], q)
|
229
|
+
when 'QUIT'
|
230
|
+
@state.on_quit(nick_of(q), q.params[0], q)
|
231
|
+
when 'TOPIC'
|
232
|
+
@state.on_topic(nick_of(q), q.params[0], q.params[1])
|
233
|
+
when 'MODE'
|
234
|
+
@state.on_mode(nick_of(q), q.params[0], q.params[1..-1])
|
235
|
+
when 'KICK'
|
236
|
+
@state.on_kick(nick_of(q), q.params[0], q.params[1], q.params[2])
|
237
|
+
|
238
|
+
when '353' # RPL_NAMREPLY
|
239
|
+
@state.on_353(q.params[2], q.params[3])
|
240
|
+
when '332' # RPL_TOPIC
|
241
|
+
@state.on_332(q.params[1], q.params[2])
|
242
|
+
|
243
|
+
when '403' # ERR_NOSUCHCHANNEL
|
244
|
+
@state.on_403(q.params[1])
|
245
|
+
|
246
|
+
when '433', '436', '437'
|
247
|
+
# ERR_NICKNAMEINUSE, ERR_NICKCOLLISION, ERR_UNAVAILRESOURCE
|
248
|
+
# change try nick
|
249
|
+
case q.params[1]
|
250
|
+
when /\A[\#&!+]/
|
251
|
+
# retry join after 1 minute
|
252
|
+
Thread.start(q.params[1]) do |ch|
|
253
|
+
sleep 60
|
254
|
+
join_to_channel ch
|
255
|
+
end
|
256
|
+
else
|
257
|
+
nick = @state.nick_succ(q.params[1])
|
258
|
+
send_to_server Cmd.nick(nick)
|
259
|
+
@logger.slog("Retry nick setting: #{nick}")
|
260
|
+
end
|
261
|
+
|
262
|
+
when '005' # RPL_ISUPPORT or RPL_BOUNCE
|
263
|
+
if /supported/i =~ q.params[-1]
|
264
|
+
q.params[1..-2].each do |param|
|
265
|
+
if /\A(-)?([A-Z0-9]+)(?:=(.*))?\z/ =~ param
|
266
|
+
negate, key, value = $~.captures
|
267
|
+
if negate
|
268
|
+
@isupport.delete(key)
|
269
|
+
else
|
270
|
+
@isupport[key] = value || true
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
@logger.dlog "isupport: #{@isupport.inspect}"
|
276
|
+
|
277
|
+
else
|
278
|
+
#
|
279
|
+
end
|
280
|
+
|
281
|
+
|
282
|
+
send_to_clients q
|
283
|
+
@logger.logging q
|
284
|
+
send_to_bot q
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
def join_to_channel ch
|
289
|
+
if @config.channel_info[ch] && @config.channel_info[ch][:key]
|
290
|
+
send_to_server Cmd.join(ch, @config.channel_info[ch][:key])
|
291
|
+
else
|
292
|
+
send_to_server Cmd.join(ch)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def enter_away
|
297
|
+
return if @exitting || !@connected
|
298
|
+
|
299
|
+
send_to_server Cmd.away(@config.away_message) if @config.away_message
|
300
|
+
|
301
|
+
# change nick
|
302
|
+
if @state.nick != @config.away_nick && @config.away_nick
|
303
|
+
@state.original_nick = @state.nick
|
304
|
+
send_to_server Cmd.nick(@config.away_nick)
|
305
|
+
end
|
306
|
+
|
307
|
+
# part channel
|
308
|
+
@config.login_channels.each{|ch|
|
309
|
+
if @config.channel_info[ch] && @state.channels.include?(ch)
|
310
|
+
if @config.channel_info[ch][:part_message]
|
311
|
+
send_to_server Cmd.part(ch, @config.channel_info[ch][:part_message])
|
312
|
+
else
|
313
|
+
send_to_server Cmd.part(ch)
|
314
|
+
end
|
315
|
+
end
|
316
|
+
}
|
317
|
+
end
|
318
|
+
|
319
|
+
def leave_away
|
320
|
+
return if @exitting || !@connected
|
321
|
+
|
322
|
+
send_to_server Cmd.away()
|
323
|
+
|
324
|
+
if @config.away_nick && @state.original_nick
|
325
|
+
sleep 2 # wait for server response
|
326
|
+
send_to_server Cmd.nick(@state.original_nick)
|
327
|
+
@state.original_nick = nil
|
328
|
+
sleep 1 # wait for server response
|
329
|
+
end
|
330
|
+
|
331
|
+
@config.login_channels.each{|ch|
|
332
|
+
send_to_server Cmd.join(ch)
|
333
|
+
}
|
334
|
+
end
|
335
|
+
|
336
|
+
def start_clients_thread
|
337
|
+
return unless @config.client_server_port
|
338
|
+
@clients_thread = Thread.new{
|
339
|
+
begin
|
340
|
+
@cserver = TCPServer.new(@config.client_server_host,
|
341
|
+
@config.client_server_port)
|
342
|
+
@logger.slog "Open Client Server Port: #{@cserver.addr.join(' ')}"
|
343
|
+
|
344
|
+
while true
|
345
|
+
# wait for client connections
|
346
|
+
Thread.start(@cserver.accept){|cc|
|
347
|
+
client = nil
|
348
|
+
begin
|
349
|
+
if !@config.acl_object || @config.acl_object.allow_socket?(cc)
|
350
|
+
client = NDK_Client.new(@config, cc, self)
|
351
|
+
@clients << client
|
352
|
+
client.start
|
353
|
+
else
|
354
|
+
@logger.slog "ACL denied: #{cc.peeraddr.join(' ')}"
|
355
|
+
end
|
356
|
+
rescue Exception => e
|
357
|
+
ndk_error e
|
358
|
+
ensure
|
359
|
+
@clients.delete client
|
360
|
+
invoke_event :enter_away, client_count
|
361
|
+
cc.close unless cc.closed?
|
362
|
+
end
|
363
|
+
}
|
364
|
+
end
|
365
|
+
rescue Exception => e
|
366
|
+
ndk_error e
|
367
|
+
ensure
|
368
|
+
@clients.each{|cl|
|
369
|
+
cl.kill
|
370
|
+
}
|
371
|
+
if @cserver
|
372
|
+
@logger.slog "Close Client Server Port: #{@cserver.addr.join(' ')}"
|
373
|
+
@cserver.close unless @cserver.closed?
|
374
|
+
end
|
375
|
+
@server_thread.kill if @server_thread.alive?
|
376
|
+
end
|
377
|
+
}
|
378
|
+
end
|
379
|
+
|
380
|
+
def start
|
381
|
+
start_server_thread
|
382
|
+
start_clients_thread
|
383
|
+
timer_thread = Thread.new{
|
384
|
+
begin
|
385
|
+
@pong_recieved = true
|
386
|
+
@pong_fail_count = 0
|
387
|
+
while true
|
388
|
+
slp = Time.now.to_i % TimerIntervalSec
|
389
|
+
slp = TimerIntervalSec if slp < (TimerIntervalSec / 2)
|
390
|
+
sleep slp
|
391
|
+
send_to_bot :timer, Time.now
|
392
|
+
|
393
|
+
if @connected
|
394
|
+
if @pong_recieved
|
395
|
+
@pong_fail_count = 0
|
396
|
+
else
|
397
|
+
# fail
|
398
|
+
@pong_fail_count += 1
|
399
|
+
@logger.slog "PONG MISS: #{@pong_fail_count}"
|
400
|
+
|
401
|
+
if @pong_fail_count > MAX_PONG_FAIL
|
402
|
+
@pong_fail_count = 0
|
403
|
+
invoke_event :reconnect_to_server
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
@pong_recieved = false
|
408
|
+
@server << Cmd.ping(@server.server)
|
409
|
+
else
|
410
|
+
@pong_recieved = true
|
411
|
+
@pong_fail_count = 0
|
412
|
+
end
|
413
|
+
|
414
|
+
end
|
415
|
+
|
416
|
+
rescue Exception => e
|
417
|
+
ndk_error e
|
418
|
+
end
|
419
|
+
}
|
420
|
+
|
421
|
+
begin
|
422
|
+
@server_thread.join
|
423
|
+
rescue Interrupt
|
424
|
+
@exitting = true
|
425
|
+
ensure
|
426
|
+
@server_thread.kill if @server_thread && @server_thread.alive?
|
427
|
+
@clients_thread.kill if @clients_thread && @clients_thread.alive?
|
428
|
+
timer_thread.kill if timer_thread && timer_thread.alive?
|
429
|
+
|
430
|
+
@server.close if @server
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
def send_to_server msg
|
435
|
+
str = msg.to_s
|
436
|
+
if /[\r\n]/ =~ str.chomp
|
437
|
+
@logger.dlog "![>S] #{str}"
|
438
|
+
raise NDK_InvalidMessage, "Message must not include [\\r\\n]: #{str.inspect}"
|
439
|
+
else
|
440
|
+
@logger.dlog "[>S] #{str}"
|
441
|
+
@server << msg
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
def recv_from_server
|
446
|
+
while q = @rq.pop
|
447
|
+
|
448
|
+
# Event
|
449
|
+
if q.kind_of? Array
|
450
|
+
exec_event q
|
451
|
+
next
|
452
|
+
end
|
453
|
+
|
454
|
+
# Server -> Nadoka message
|
455
|
+
if !@config.primitive_filters.nil? && !@config.primitive_filters[q.command].nil? && !@config.primitive_filters[q.command].empty?
|
456
|
+
next unless filter_message(@config.primitive_filters[q.command], q)
|
457
|
+
end
|
458
|
+
|
459
|
+
case q.command
|
460
|
+
when 'PING'
|
461
|
+
@server << Cmd.pong(q.params[0])
|
462
|
+
when 'PONG'
|
463
|
+
@pong_recieved = true
|
464
|
+
when 'NOTICE'
|
465
|
+
@logger.dlog "[<S] #{q}"
|
466
|
+
if msg = filter_message(@config.notice_filter, q)
|
467
|
+
return q
|
468
|
+
end
|
469
|
+
when 'PRIVMSG'
|
470
|
+
@logger.dlog "[<S] #{q}"
|
471
|
+
if msg = filter_message(@config.privmsg_filter, q)
|
472
|
+
return q
|
473
|
+
end
|
474
|
+
else
|
475
|
+
@logger.dlog "[<S] #{q}"
|
476
|
+
return q
|
477
|
+
end
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
def filter_message filter, msg
|
482
|
+
return msg if filter.nil? || filter.empty?
|
483
|
+
|
484
|
+
begin
|
485
|
+
if filter.respond_to? :each
|
486
|
+
filter.each{|fil|
|
487
|
+
fil.call msg.dup
|
488
|
+
}
|
489
|
+
else
|
490
|
+
filter.call msg.dup
|
491
|
+
end
|
492
|
+
rescue NDK_FilterMessage_SendCancel
|
493
|
+
@logger.dlog "[NDK] Message Canceled"
|
494
|
+
return false
|
495
|
+
rescue NDK_FilterMessage_Replace => e
|
496
|
+
@logger.dlog "[NDK] Message Replaced: #{e}"
|
497
|
+
return e.msg
|
498
|
+
rescue NDK_FilterMessage_OnlyBot
|
499
|
+
@logger.dlog "[NDK] Message only bot"
|
500
|
+
send_to_bot msg
|
501
|
+
return false
|
502
|
+
rescue NDK_FilterMessage_OnlyLog
|
503
|
+
@logger.dlog "[NDK] Message only log"
|
504
|
+
@logger.logging msg
|
505
|
+
return false
|
506
|
+
rescue NDK_FilterMessage_BotAndLog
|
507
|
+
@logger.dlog "[NDK] Message log and bot"
|
508
|
+
send_to_bot msg
|
509
|
+
@logger.logging msg
|
510
|
+
return false
|
511
|
+
end
|
512
|
+
msg
|
513
|
+
end
|
514
|
+
|
515
|
+
def invoke_event ev, *arg
|
516
|
+
arg.unshift ev
|
517
|
+
@rq && (@rq << arg)
|
518
|
+
end
|
519
|
+
|
520
|
+
def exec_event q
|
521
|
+
# special event
|
522
|
+
case q[0]
|
523
|
+
when :reload_config
|
524
|
+
# q[1] must be client object
|
525
|
+
begin
|
526
|
+
reload_config
|
527
|
+
@logger.slog "configuration is reloaded"
|
528
|
+
rescue Exception => e
|
529
|
+
@logger.slog "error is occure while reloading configuration"
|
530
|
+
ndk_error e
|
531
|
+
end
|
532
|
+
|
533
|
+
when :quit_program
|
534
|
+
@exitting = true
|
535
|
+
Thread.main.raise NDK_QuitProgram
|
536
|
+
|
537
|
+
when :restart_program
|
538
|
+
@exitting = true
|
539
|
+
Thread.main.raise NDK_RestartProgram
|
540
|
+
|
541
|
+
when :reconnect_to_server
|
542
|
+
@connected = false
|
543
|
+
@server_thread.raise NDK_ReconnectToServer
|
544
|
+
|
545
|
+
when :invoke_bot
|
546
|
+
# q[1], q[2] are message and argument
|
547
|
+
send_to_bot q[1], *q[2..-1]
|
548
|
+
|
549
|
+
when :enter_away
|
550
|
+
if q[1] == 0
|
551
|
+
enter_away
|
552
|
+
end
|
553
|
+
|
554
|
+
when :leave_away
|
555
|
+
if q[1] == 1
|
556
|
+
leave_away
|
557
|
+
end
|
558
|
+
end
|
559
|
+
end
|
560
|
+
|
561
|
+
def set_signal_trap
|
562
|
+
list = Signal.list
|
563
|
+
Signal.trap(:INT){
|
564
|
+
# invoke_event :quit_program
|
565
|
+
Thread.main.raise NDK_QuitProgram
|
566
|
+
} if list['INT']
|
567
|
+
Signal.trap(:TERM){
|
568
|
+
# invoke_event :quit_program
|
569
|
+
Thread.main.raise NDK_QuitProgram
|
570
|
+
} if list.any?{|e| e == 'TERM'}
|
571
|
+
|
572
|
+
Signal.trap(:HUP){
|
573
|
+
# reload config
|
574
|
+
invoke_event :reload_config
|
575
|
+
} if list['HUP']
|
576
|
+
trap(:USR1){
|
577
|
+
# SIGUSR1
|
578
|
+
invoke_event :invoke_bot, :sigusr1
|
579
|
+
} if list['USR1']
|
580
|
+
trap(:USR2){
|
581
|
+
# SIGUSR2
|
582
|
+
invoke_event :invoke_bot, :sigusr2
|
583
|
+
} if list['USR2']
|
584
|
+
end
|
585
|
+
|
586
|
+
def about_me? msg
|
587
|
+
qnick = Regexp.quote(@state.nick || '')
|
588
|
+
if msg.prefix =~ /^#{qnick}!/
|
589
|
+
true
|
590
|
+
else
|
591
|
+
false
|
592
|
+
end
|
593
|
+
end
|
594
|
+
|
595
|
+
def own_nick_change? msg
|
596
|
+
if msg.command == 'NICK' && msg.params[0] == @state.nick
|
597
|
+
nick_of(msg)
|
598
|
+
else
|
599
|
+
false
|
600
|
+
end
|
601
|
+
end
|
602
|
+
|
603
|
+
def part_from_all_channels
|
604
|
+
@state.channels.each{|ch, cs|
|
605
|
+
cmd = Cmd.part(ch)
|
606
|
+
cmd.prefix = @state.nick #m
|
607
|
+
send_to_clients cmd
|
608
|
+
}
|
609
|
+
@state.clear_channels_member
|
610
|
+
end
|
611
|
+
|
612
|
+
# server -> clients
|
613
|
+
def send_to_clients msg
|
614
|
+
if msg.command == 'PRIVMSG' && !(msg = filter_message(@config.privmsg_filter_light, msg))
|
615
|
+
return
|
616
|
+
end
|
617
|
+
|
618
|
+
if(old_nick = own_nick_change?(msg))
|
619
|
+
@clients.each{|cl|
|
620
|
+
cl.add_prefix2(msg, old_nick)
|
621
|
+
cl << msg
|
622
|
+
}
|
623
|
+
elsif about_me? msg
|
624
|
+
@clients.each{|cl|
|
625
|
+
cl.add_prefix(msg)
|
626
|
+
cl << msg
|
627
|
+
}
|
628
|
+
else
|
629
|
+
@clients.each{|cl|
|
630
|
+
cl << msg
|
631
|
+
}
|
632
|
+
end
|
633
|
+
end
|
634
|
+
|
635
|
+
def ping_to_clients
|
636
|
+
@clients.each{|cl|
|
637
|
+
cl << Cmd.ping(cl.remote_host)
|
638
|
+
}
|
639
|
+
end
|
640
|
+
|
641
|
+
# clientA -> other clients
|
642
|
+
# bot -> clients
|
643
|
+
def send_to_clients_otherwise msg, elt
|
644
|
+
@clients.each{|cl|
|
645
|
+
if cl != elt
|
646
|
+
cl.add_prefix(msg) unless msg.prefix
|
647
|
+
cl << msg
|
648
|
+
end
|
649
|
+
}
|
650
|
+
invoke_event :invoke_bot, msg if elt
|
651
|
+
@logger.logging msg
|
652
|
+
end
|
653
|
+
|
654
|
+
def ctcp_message? arg
|
655
|
+
arg[0] == ?\x1
|
656
|
+
end
|
657
|
+
|
658
|
+
def ctcp_message msg
|
659
|
+
if /\001(.+)\001/ =~ msg.params[1]
|
660
|
+
ctcp_cmd = $1
|
661
|
+
case ctcp_cmd
|
662
|
+
when 'VERSION'
|
663
|
+
send_to_server Cmd.notice(nick_of(msg), "\001VERSION #{Nadoka.version}\001")
|
664
|
+
when 'TIME'
|
665
|
+
send_to_server Cmd.notice(nick_of(msg), "\001TIME #{Time.now}\001")
|
666
|
+
else
|
667
|
+
|
668
|
+
end
|
669
|
+
end
|
670
|
+
end
|
671
|
+
|
672
|
+
def nick_of msg
|
673
|
+
if /^([^!]+)\!?/ =~ msg.prefix.to_s
|
674
|
+
$1
|
675
|
+
else
|
676
|
+
@state.nick
|
677
|
+
end
|
678
|
+
end
|
679
|
+
|
680
|
+
class PrefixObject
|
681
|
+
def initialize prefix
|
682
|
+
parse_prefix prefix
|
683
|
+
@prefix = prefix
|
684
|
+
end
|
685
|
+
attr_reader :nick, :user, :host, :prefix
|
686
|
+
|
687
|
+
def parse_prefix prefix
|
688
|
+
if /^(.+?)\!(.+?)@(.+)/ =~ prefix.to_s
|
689
|
+
# command
|
690
|
+
@nick, @user, @host = $1, $2, $3
|
691
|
+
else
|
692
|
+
# server reply
|
693
|
+
@nick, @user, @host = nil, nil, prefix
|
694
|
+
end
|
695
|
+
end
|
696
|
+
|
697
|
+
def to_s
|
698
|
+
@prefix
|
699
|
+
end
|
700
|
+
end
|
701
|
+
|
702
|
+
def make_prefix_object msg
|
703
|
+
prefix = msg.prefix
|
704
|
+
if prefix
|
705
|
+
PrefixObject.new(prefix)
|
706
|
+
else
|
707
|
+
if /^d+$/ =~ msg.command
|
708
|
+
PrefixObject.new(@config.nadoka_server_name)
|
709
|
+
else
|
710
|
+
PrefixObject.new("#{@state.nick}!#{@config.user}@#{@config.nadoka_server_name}")
|
711
|
+
end
|
712
|
+
end
|
713
|
+
end
|
714
|
+
|
715
|
+
# dispatch to bots
|
716
|
+
def send_to_bot msg, *arg
|
717
|
+
|
718
|
+
selector = 'on_' +
|
719
|
+
if msg.respond_to? :command
|
720
|
+
if /^\d+$/ =~ msg.command
|
721
|
+
# reply
|
722
|
+
prefix = make_prefix_object msg
|
723
|
+
RICE::Reply::Replies_num_to_name[msg.command]
|
724
|
+
else
|
725
|
+
# command
|
726
|
+
prefix = make_prefix_object msg
|
727
|
+
msg.command.downcase
|
728
|
+
end
|
729
|
+
else
|
730
|
+
prefix = nil
|
731
|
+
msg.to_s
|
732
|
+
end
|
733
|
+
|
734
|
+
@config.bots.each{|bot|
|
735
|
+
begin
|
736
|
+
if bot.respond_to? selector
|
737
|
+
unless prefix
|
738
|
+
bot.__send__(selector, *arg)
|
739
|
+
else
|
740
|
+
bot.__send__(selector, prefix, *msg.params)
|
741
|
+
end
|
742
|
+
end
|
743
|
+
|
744
|
+
if prefix && bot.respond_to?(:on_every_message)
|
745
|
+
bot.__send__(:on_every_message, prefix, msg.command, *msg.params)
|
746
|
+
end
|
747
|
+
|
748
|
+
rescue NDK_BotBreak
|
749
|
+
break
|
750
|
+
|
751
|
+
rescue NDK_BotSendCancel
|
752
|
+
return false
|
753
|
+
|
754
|
+
rescue Exception
|
755
|
+
ndk_error $!
|
756
|
+
end
|
757
|
+
}
|
758
|
+
true
|
759
|
+
end
|
760
|
+
|
761
|
+
def ndk_status
|
762
|
+
[ '== Nadoka Running Status ==',
|
763
|
+
'- nadoka version: ' + Nadoka.version,
|
764
|
+
'- connecting to ' + "#{@server.server}:#{@server.port}",
|
765
|
+
'- clients status:',
|
766
|
+
@clients.map{|e| '-- ' + e.state},
|
767
|
+
'- Bots status:',
|
768
|
+
@config.bots.map{|bot| '-- ' + bot.bot_state},
|
769
|
+
'== End of Status =='
|
770
|
+
].flatten
|
771
|
+
end
|
772
|
+
|
773
|
+
def ndk_error err
|
774
|
+
@logger.slog "Exception #{err.class} - #{err}"
|
775
|
+
@logger.slog "-- backtrace --"
|
776
|
+
err.backtrace.each{|line|
|
777
|
+
@logger.slog "| " + line
|
778
|
+
}
|
779
|
+
end
|
780
|
+
|
781
|
+
end
|
782
|
+
end
|
783
|
+
|
784
|
+
|