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.
Files changed (54) hide show
  1. data/.gitignore +5 -0
  2. data/ChangeLog.old +1553 -0
  3. data/Gemfile +4 -0
  4. data/README.org +31 -0
  5. data/Rakefile +1 -0
  6. data/bin/nadoka +13 -0
  7. data/lib/rss_check.rb +206 -0
  8. data/lib/tagparts.rb +206 -0
  9. data/nadoka.gemspec +29 -0
  10. data/nadoka.rb +123 -0
  11. data/nadokarc +267 -0
  12. data/ndk/bot.rb +241 -0
  13. data/ndk/client.rb +288 -0
  14. data/ndk/config.rb +571 -0
  15. data/ndk/error.rb +61 -0
  16. data/ndk/logger.rb +311 -0
  17. data/ndk/server.rb +784 -0
  18. data/ndk/server_state.rb +324 -0
  19. data/ndk/version.rb +44 -0
  20. data/plugins/autoawaybot.nb +66 -0
  21. data/plugins/autodumpbot.nb +227 -0
  22. data/plugins/autoop.nb +56 -0
  23. data/plugins/backlogbot.nb +88 -0
  24. data/plugins/checkbot.nb +64 -0
  25. data/plugins/cronbot.nb +20 -0
  26. data/plugins/dictbot.nb +53 -0
  27. data/plugins/drbcl.rb +39 -0
  28. data/plugins/drbot.nb +93 -0
  29. data/plugins/evalbot.nb +49 -0
  30. data/plugins/gonzuibot.nb +41 -0
  31. data/plugins/googlebot.nb +345 -0
  32. data/plugins/identifynickserv.nb +43 -0
  33. data/plugins/mailcheckbot.nb +0 -0
  34. data/plugins/marldiabot.nb +99 -0
  35. data/plugins/messagebot.nb +96 -0
  36. data/plugins/modemanager.nb +150 -0
  37. data/plugins/opensearchbot.nb +156 -0
  38. data/plugins/opshop.nb +23 -0
  39. data/plugins/pastebot.nb +46 -0
  40. data/plugins/roulettebot.nb +33 -0
  41. data/plugins/rss_checkbot.nb +121 -0
  42. data/plugins/samplebot.nb +24 -0
  43. data/plugins/sendpingbot.nb +17 -0
  44. data/plugins/shellbot.nb +59 -0
  45. data/plugins/sixamobot.nb +77 -0
  46. data/plugins/tenkibot.nb +111 -0
  47. data/plugins/timestampbot.nb +62 -0
  48. data/plugins/titlebot.nb +226 -0
  49. data/plugins/translatebot.nb +301 -0
  50. data/plugins/twitterbot.nb +138 -0
  51. data/plugins/weba.nb +209 -0
  52. data/plugins/xibot.nb +113 -0
  53. data/rice/irc.rb +780 -0
  54. metadata +102 -0
@@ -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
+
@@ -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
+
@@ -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
+