File: Rejoin.pm

package info (click to toggle)
tiarra 20100212-4
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 2,732 kB
  • ctags: 1,712
  • sloc: perl: 32,032; lisp: 193; sh: 109; makefile: 10
file content (374 lines) | stat: -rw-r--r-- 11,635 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
# -----------------------------------------------------------------------------
# $Id: Rejoin.pm 36718 2010-02-11 17:21:29Z topia $
# -----------------------------------------------------------------------------
# このモジュールは動作時に掲示板のdo-not-touch-mode-of-channelsを使います。
# -----------------------------------------------------------------------------
package Channel::Rejoin;
use strict;
use warnings;
use base qw(Module);
use BulletinBoard;
use Multicast;
use RunLoop;
use NumericReply;

sub new {
    my $class = shift;
    my $this = $class->SUPER::new(@_);
    $this->{sessions} = {}; # チャンネルフルネーム => セッション情報
    # セッション情報 : HASH
    # ch_fullname => チャンネルフルネーム
    # ch_shortname => チャンネルショートネーム
    # ch => ChannelInfo
    # server => IrcIO::Server
    # got_mode => 既にMODEを取得しているかどうか。
    # got_blist => 既に+bリストを(略
    # got_elist => +e(略
    # got_Ilist => +I(略
    # got_oper => 既にPART->JOINしているかどうか。
    # cmd_buf => ARRAY<Tiarra::IRC::Message>
    # num_got_errors => このチャンネルのエラーをみた回数
    $this;
}

sub message_arrived {
    my ($this,$msg,$sender) = @_;
    if ($sender->isa('IrcIO::Server')) {
	# PART,KICK,QUIT,KILLが、それぞれ一人になる要因。
	my $cmd = $msg->command;
	if ($cmd eq 'PART') {
	    foreach my $ch_fullname (split /,/,$msg->param(0)) {
		$this->check_and_rejoin_channel(
		    scalar Multicast::detatch($ch_fullname),
		    $sender);
	    }
	}
	elsif ($cmd eq 'KICK') {
	    # RFC2812によると、複数のチャンネルを持つKICKメッセージが
	    # クライアントに届く事は無い。
	    $this->check_and_rejoin_channel(
		scalar Multicast::detatch($msg->param(0)),
		$sender);
	}
	elsif ($cmd eq 'QUIT' || $cmd eq 'KILL') {
	    # 註釈affected-channelsに影響のあったチャンネルのリストが入っているはず。
	    foreach (@{$msg->remark('affected-channels')}) {
		$this->check_and_rejoin_channel($_,$sender);
	    }
	}

	$this->session_work($msg,$sender);
    }
    $msg;
}

sub check_and_rejoin_channel {
    my ($this,$ch_name,$server) = @_;
    if ($this->check_channel($ch_name,$server)) {
	$this->rejoin($ch_name,$server);
    }
}

sub check_channel {
    my ($this,$ch_name,$server) = @_;
    if ($ch_name =~ m/^\+/) {
	# +チャンネルに@は付かない。
	return;
    }
    my $ch = $server->channel($ch_name);
    if (!defined $ch) {
	# 自分が入っていない
	return;
    }
    if ($ch->switches('a')) {
	# +aチャンネルでは一人になったかどうかの判定が面倒である上に、
	# @を復活させる意味も無ければ復活させない方が望ましい。
	return;
    }
    if ($ch->names(undef,undef,'size') > 1) {
	# 二人以上いる。
	return;
    }
    my $myself = $ch->names($server->current_nick);
    if (defined $myself && $myself->has_o) {
	# 自分が@を持っている。
	return;
    }
    if ($ch->remark('chanserv-controlled')) {
	# ChanServ 管理チャンネルであれば、無駄な努力はしない。
	return;
    }
    return 1;
}

sub rejoin {
    my ($this,$ch_name,$server) = @_;
    my $ch_fullname = Multicast::attach($ch_name,$server->network_name);
    if (defined $this->{sessions}->{$ch_fullname}) {
	# 動作中のセッションがあるのでキャンセルする。
	return;
    }
    RunLoop->shared->notify_msg(
	"Channel::Rejoin is going to rejoin to ${ch_fullname}.");

    ###############
    #   処理の流れ
    ### phase 1 ###
    # セッション作成。
    # 掲示板に「このチャンネルのモードを変更するな」と書き込む。
    # TOPICを覚える。
    # 備考switches-are-knownが偽ならMODE #channel実行。
    # 必要ならMODE #channel +b,MODE #channel +e,MODE #channel +Iを実行。
    ### phase 2 ###
    # 324(modeリプライ),368(+bリスト終わり),
    # 349(+eリスト終わり),347(+Iリスト終わり)をそれぞれ必要なら待つ。
    ### phase 3 ###
    # PART #channel実行。
    # JOIN #channel実行。
    # 自分のJOINを待つ。
    # 少しずつ命令バッファに溜まったコマンドを実行していく。Timer使用。
    #   命令バッファにはMODEやTOPICが入っている。
    # 掲示板から消す。
    # セッションを破棄。
    ###############

    # チャンネル取得
    my $ch = $server->channel($ch_name);

    # セッション登録
    my $session = $this->{sessions}->{$ch_fullname} = {
	ch_fullname => $ch_fullname,
	ch_shortname => $ch_name,
	ch => $ch,
	server => $server,
	cmd_buf => [],
	num_got_errors => 0,
    };
    
    # do-not-touch-mode-of-channelsを取得
    my $untouchables = BulletinBoard->shared->do_not_touch_mode_of_channels;
    if (!defined $untouchables) {
	$untouchables = {};
	BulletinBoard->shared->set('do-not-touch-mode-of-channels',$untouchables);
    }
    # このチャンネルをフルネームで登録
    $untouchables->{$ch_fullname} = 1;
    
    # TOPICを覚える。
    if ($ch->topic ne '') {
	push @{$session->{cmd_buf}},$this->construct_irc_message(
	    Command => 'TOPIC',
	    Params => [$ch_name,$ch->topic]);
    }
    
    # 必要ならMODE #channel実行。
    #if ($ch->remarks('switches-are-known')) {
    #	$session->{got_mode} = 1;
    #	push @{$session->{cmd_buf}},$this->construct_irc_message(
    #	    Command => 'MODE',
    #}
    # やっぱりやめ。面倒。防衛BOTとして使いたかったらこんなモジュール使わないこと。
    #else {
    	$server->send_message(
    	    $this->construct_irc_message(
		Command => 'MODE',
		Param => $ch_name));
    #}
    
    # 必要なら+e,+b,+I実行。
    if ($this->config->save_lists) {
	foreach (qw/+e +b +I/) {
	    $server->send_message(
		$this->construct_irc_message(
		    Command => 'MODE',
		    Params => [$ch_name,$_]));
	}
	$session->{got_elist} =
	    $session->{got_blist} =
	    $session->{got_Ilist} = 0;
    }
    else {
	$session->{got_elist} =
	    $session->{got_blist} =
	    $session->{got_Ilist} = 1;
    }

    # 待たなければならないものはあるか?
    if ($this->{got_mode} && $this->{got_elist} &&
	$this->{got_blist} && $this->{got_Ilist}) {
	# もう何も無い。
	$this->part_and_join($session);
    }
}

sub part_and_join {
    my ($this,$session) = @_;
    $session->{got_oper} = 1;
    if (!$this->check_channel($session->{ch_shortname}, $session->{server})) {
	# 情報を取得している間に状況が変化した
	RunLoop->shared->notify_msg(
	    "Channel::Rejoin is cancelled to rejoin to $session->{ch_fullname}.");
	# part/join をやめたので発行すべきコマンドはない。
	$session->{cmd_buf} = [];
	# フラグ類のクリーンアップを行う
	$this->revive($session);
	return;
    }
    foreach (qw/PART JOIN/) {
	$session->{server}->send_message(
	    $this->construct_irc_message(
		Command => $_,
		Param => $session->{ch_shortname}));
    }
}

sub session_work {
    my ($this,$msg,$server) = @_;
    my $session;
    # ウォッチの対象になるのはJOIN,324,368,349,347,482。
    # リストはコマンドを発行していれば IrcIO::Server が
    # 保持しておいてくれる。

    my $got_reply = sub {
	my $type = shift;
	my ($flagname,$listname) = do {
	    if ($type eq 'b') {
		('got_blist','banlist');
	    }
	    elsif ($type eq 'e') {
		('got_elist','exceptionlist');
	    }
	    elsif ($type eq 'I') {
		('got_Ilist','invitelist');
	    }
	};
	
	$session = $this->{sessions}->{$msg->param(1)};
	if (defined $session) {
	    $session->{$flagname} = 1;
	    
	    my $list = $session->{ch}->$listname();
	    my $list_size = @$list;
	    # 3つずつまとめる。
	    for (my $i = 0; $i < $list_size; $i+=3) {
		my @masks = ($list->[$i]);
		push @masks,$list->[$i+1] if $i+1 < $list_size;
		push @masks,$list->[$i+2] if $i+2 < $list_size;
		
		push @{$session->{cmd_buf}},$this->construct_irc_message(
		    Command => 'MODE',
		    Params => [$session->{ch_shortname},
			       '+'.($type x scalar(@masks)),
			       @masks]);
	    }
	}
    };

    if ($msg->command eq RPL_CHANNELMODEIS) {
	# MODEリプライ
	$session = $this->{sessions}->{$msg->param(1)};
	if (defined $session) {
	    $session->{got_mode} = 1;
	    my $ch = $session->{ch};
	    
	    my ($params, @params) = $ch->mode_string;
	    if (length($params) > 1) {
		# 設定すべきモードがある。
		push @{$session->{cmd_buf}},$this->construct_irc_message(
		    Command => 'MODE',
		    Params => [$session->{ch_shortname},
			       $params,
			       @params]);
	    }
	}
    }
    elsif ($msg->command eq RPL_ENDOFBANLIST) {
	# +bリスト終わり
	$got_reply->('b');
    }
    elsif ($msg->command eq RPL_ENDOFEXCEPTLIST) {
	# +eリスト終わり
	$got_reply->('e');
    }
    elsif ($msg->command eq RPL_ENDOFINVITELIST) {
	# +Iリスト終わり
	$got_reply->('I');
    }
    elsif ($msg->command eq 'JOIN') {
	$session = $this->{sessions}->{$msg->param(0)};
	if (defined $session && defined $msg->nick &&
	    $msg->nick eq RunLoop->shared->current_nick) {
	    # 入り直した。
	    $session->{got_oper} = 1; # 既にセットされている筈だが念のため
	    $this->revive($session);
	}
    }
    elsif ($msg->command eq ERR_CHANOPRIVSNEEDED) {
	$session = $this->{sessions}->{$msg->param(1)};
	if (defined $session) {
	    $session->{num_got_errors}++;
	}
    }

    # $sessionが空でなければ、必要な情報が全て揃った可能性がある。
    if (defined $session && !$session->{got_oper} &&
	$session->{got_mode} && ($session->{got_blist} +
	$session->{got_elist} + $session->{got_Ilist} +
	$session->{num_got_errors}) >= 3) {
	$this->part_and_join($session);
    }
}

sub revive {
    my ($this,$session) = @_;
    Timer->new(
	Name => 'Channel::Rejoin cmd queue',
	Module => $this,
	Interval => 1,
	Repeat => 1,
	Code => sub {
	    my $timer = shift;
	    my $cmd_buf = $session->{cmd_buf};
	    if (@$cmd_buf > 0) {
		# 一度に二つずつ送り出す。
		my $msg_per_trigger = 2;
		for (my $i = 0; $i < @$cmd_buf && $i < $msg_per_trigger; $i++) {
		    $session->{server}->send_message($cmd_buf->[$i]);
		}
		splice @$cmd_buf,0,$msg_per_trigger;
	    }
	    if (@$cmd_buf == 0) {
		# cmd_bufが空だったら終了。
		# ただし、10秒以内に再び単独になっても無視する
		# untouchablesから消去
		my $untouchables = BulletinBoard->shared->do_not_touch_mode_of_channels;
		delete $untouchables->{$session->{ch_fullname}};
		Timer->new(
		    Name => 'Channel::Rejoin delay cleanup',
		    Module => $this,
		    After => 10,
		    Code => sub {
			# session消去
			delete $this->{sessions}->{$session->{ch_fullname}};
		    })->install;
		# タイマーをアンインストール
		$timer->uninstall;
	    }
	})->install;
}

1;

=pod
info: チャンネルオペレータ権限を無くしたとき、一人ならjoinし直す。
default: off
section: important

# +チャンネルや+aされているチャンネル以外でチャンネルオペレータ権限を持たずに
# 一人きりになった時、そのチャンネルの@を復活させるために自動的にjoinし直すモジュール。
# トピック、モード、banリスト等のあらゆるチャンネル属性をも保存します。

# +b,+I,+eリストの復旧を行なうかどうか。
# あまりに長いリストを取得するとMax Send-Q Exceedで落とされるかも知れません。
save-lists: 1
=cut