1# ----------------------------------------------------------------------------- 2# $Id: Rejoin.pm 36718 2010-02-11 17:21:29Z topia $ 3# ----------------------------------------------------------------------------- 4# このモジュールは動作時に掲示板のdo-not-touch-mode-of-channelsを使います。 5# ----------------------------------------------------------------------------- 6package Channel::Rejoin; 7use strict; 8use warnings; 9use base qw(Module); 10use BulletinBoard; 11use Multicast; 12use RunLoop; 13use NumericReply; 14 15sub new { 16 my $class = shift; 17 my $this = $class->SUPER::new(@_); 18 $this->{sessions} = {}; # チャンネルフルネーム => セッション情報 19 # セッション情報 : HASH 20 # ch_fullname => チャンネルフルネーム 21 # ch_shortname => チャンネルショートネーム 22 # ch => ChannelInfo 23 # server => IrcIO::Server 24 # got_mode => 既にMODEを取得しているかどうか。 25 # got_blist => 既に+bリストを(略 26 # got_elist => +e(略 27 # got_Ilist => +I(略 28 # got_oper => 既にPART->JOINしているかどうか。 29 # cmd_buf => ARRAY<Tiarra::IRC::Message> 30 # num_got_errors => このチャンネルのエラーをみた回数 31 $this; 32} 33 34sub message_arrived { 35 my ($this,$msg,$sender) = @_; 36 if ($sender->isa('IrcIO::Server')) { 37 # PART,KICK,QUIT,KILLが、それぞれ一人になる要因。 38 my $cmd = $msg->command; 39 if ($cmd eq 'PART') { 40 foreach my $ch_fullname (split /,/,$msg->param(0)) { 41 $this->check_and_rejoin_channel( 42 scalar Multicast::detatch($ch_fullname), 43 $sender); 44 } 45 } 46 elsif ($cmd eq 'KICK') { 47 # RFC2812によると、複数のチャンネルを持つKICKメッセージが 48 # クライアントに届く事は無い。 49 $this->check_and_rejoin_channel( 50 scalar Multicast::detatch($msg->param(0)), 51 $sender); 52 } 53 elsif ($cmd eq 'QUIT' || $cmd eq 'KILL') { 54 # 註釈affected-channelsに影響のあったチャンネルのリストが入っているはず。 55 foreach (@{$msg->remark('affected-channels')}) { 56 $this->check_and_rejoin_channel($_,$sender); 57 } 58 } 59 60 $this->session_work($msg,$sender); 61 } 62 $msg; 63} 64 65sub check_and_rejoin_channel { 66 my ($this,$ch_name,$server) = @_; 67 if ($this->check_channel($ch_name,$server)) { 68 $this->rejoin($ch_name,$server); 69 } 70} 71 72sub check_channel { 73 my ($this,$ch_name,$server) = @_; 74 if ($ch_name =~ m/^\+/) { 75 # +チャンネルに@は付かない。 76 return; 77 } 78 my $ch = $server->channel($ch_name); 79 if (!defined $ch) { 80 # 自分が入っていない 81 return; 82 } 83 if ($ch->switches('a')) { 84 # +aチャンネルでは一人になったかどうかの判定が面倒である上に、 85 # @を復活させる意味も無ければ復活させない方が望ましい。 86 return; 87 } 88 if ($ch->names(undef,undef,'size') > 1) { 89 # 二人以上いる。 90 return; 91 } 92 my $myself = $ch->names($server->current_nick); 93 if (defined $myself && $myself->has_o) { 94 # 自分が@を持っている。 95 return; 96 } 97 if ($ch->remark('chanserv-controlled')) { 98 # ChanServ 管理チャンネルであれば、無駄な努力はしない。 99 return; 100 } 101 return 1; 102} 103 104sub rejoin { 105 my ($this,$ch_name,$server) = @_; 106 my $ch_fullname = Multicast::attach($ch_name,$server->network_name); 107 if (defined $this->{sessions}->{$ch_fullname}) { 108 # 動作中のセッションがあるのでキャンセルする。 109 return; 110 } 111 RunLoop->shared->notify_msg( 112 "Channel::Rejoin is going to rejoin to ${ch_fullname}."); 113 114 ############### 115 # 処理の流れ 116 ### phase 1 ### 117 # セッション作成。 118 # 掲示板に「このチャンネルのモードを変更するな」と書き込む。 119 # TOPICを覚える。 120 # 備考switches-are-knownが偽ならMODE #channel実行。 121 # 必要ならMODE #channel +b,MODE #channel +e,MODE #channel +Iを実行。 122 ### phase 2 ### 123 # 324(modeリプライ),368(+bリスト終わり), 124 # 349(+eリスト終わり),347(+Iリスト終わり)をそれぞれ必要なら待つ。 125 ### phase 3 ### 126 # PART #channel実行。 127 # JOIN #channel実行。 128 # 自分のJOINを待つ。 129 # 少しずつ命令バッファに溜まったコマンドを実行していく。Timer使用。 130 # 命令バッファにはMODEやTOPICが入っている。 131 # 掲示板から消す。 132 # セッションを破棄。 133 ############### 134 135 # チャンネル取得 136 my $ch = $server->channel($ch_name); 137 138 # セッション登録 139 my $session = $this->{sessions}->{$ch_fullname} = { 140 ch_fullname => $ch_fullname, 141 ch_shortname => $ch_name, 142 ch => $ch, 143 server => $server, 144 cmd_buf => [], 145 num_got_errors => 0, 146 }; 147 148 # do-not-touch-mode-of-channelsを取得 149 my $untouchables = BulletinBoard->shared->do_not_touch_mode_of_channels; 150 if (!defined $untouchables) { 151 $untouchables = {}; 152 BulletinBoard->shared->set('do-not-touch-mode-of-channels',$untouchables); 153 } 154 # このチャンネルをフルネームで登録 155 $untouchables->{$ch_fullname} = 1; 156 157 # TOPICを覚える。 158 if ($ch->topic ne '') { 159 push @{$session->{cmd_buf}},$this->construct_irc_message( 160 Command => 'TOPIC', 161 Params => [$ch_name,$ch->topic]); 162 } 163 164 # 必要ならMODE #channel実行。 165 #if ($ch->remarks('switches-are-known')) { 166 # $session->{got_mode} = 1; 167 # push @{$session->{cmd_buf}},$this->construct_irc_message( 168 # Command => 'MODE', 169 #} 170 # やっぱりやめ。面倒。防衛BOTとして使いたかったらこんなモジュール使わないこと。 171 #else { 172 $server->send_message( 173 $this->construct_irc_message( 174 Command => 'MODE', 175 Param => $ch_name)); 176 #} 177 178 # 必要なら+e,+b,+I実行。 179 if ($this->config->save_lists) { 180 foreach (qw/+e +b +I/) { 181 $server->send_message( 182 $this->construct_irc_message( 183 Command => 'MODE', 184 Params => [$ch_name,$_])); 185 } 186 $session->{got_elist} = 187 $session->{got_blist} = 188 $session->{got_Ilist} = 0; 189 } 190 else { 191 $session->{got_elist} = 192 $session->{got_blist} = 193 $session->{got_Ilist} = 1; 194 } 195 196 # 待たなければならないものはあるか? 197 if ($this->{got_mode} && $this->{got_elist} && 198 $this->{got_blist} && $this->{got_Ilist}) { 199 # もう何も無い。 200 $this->part_and_join($session); 201 } 202} 203 204sub part_and_join { 205 my ($this,$session) = @_; 206 $session->{got_oper} = 1; 207 if (!$this->check_channel($session->{ch_shortname}, $session->{server})) { 208 # 情報を取得している間に状況が変化した 209 RunLoop->shared->notify_msg( 210 "Channel::Rejoin is cancelled to rejoin to $session->{ch_fullname}."); 211 # part/join をやめたので発行すべきコマンドはない。 212 $session->{cmd_buf} = []; 213 # フラグ類のクリーンアップを行う 214 $this->revive($session); 215 return; 216 } 217 foreach (qw/PART JOIN/) { 218 $session->{server}->send_message( 219 $this->construct_irc_message( 220 Command => $_, 221 Param => $session->{ch_shortname})); 222 } 223} 224 225sub session_work { 226 my ($this,$msg,$server) = @_; 227 my $session; 228 # ウォッチの対象になるのはJOIN,324,368,349,347,482。 229 # リストはコマンドを発行していれば IrcIO::Server が 230 # 保持しておいてくれる。 231 232 my $got_reply = sub { 233 my $type = shift; 234 my ($flagname,$listname) = do { 235 if ($type eq 'b') { 236 ('got_blist','banlist'); 237 } 238 elsif ($type eq 'e') { 239 ('got_elist','exceptionlist'); 240 } 241 elsif ($type eq 'I') { 242 ('got_Ilist','invitelist'); 243 } 244 }; 245 246 $session = $this->{sessions}->{$msg->param(1)}; 247 if (defined $session) { 248 $session->{$flagname} = 1; 249 250 my $list = $session->{ch}->$listname(); 251 my $list_size = @$list; 252 # 3つずつまとめる。 253 for (my $i = 0; $i < $list_size; $i+=3) { 254 my @masks = ($list->[$i]); 255 push @masks,$list->[$i+1] if $i+1 < $list_size; 256 push @masks,$list->[$i+2] if $i+2 < $list_size; 257 258 push @{$session->{cmd_buf}},$this->construct_irc_message( 259 Command => 'MODE', 260 Params => [$session->{ch_shortname}, 261 '+'.($type x scalar(@masks)), 262 @masks]); 263 } 264 } 265 }; 266 267 if ($msg->command eq RPL_CHANNELMODEIS) { 268 # MODEリプライ 269 $session = $this->{sessions}->{$msg->param(1)}; 270 if (defined $session) { 271 $session->{got_mode} = 1; 272 my $ch = $session->{ch}; 273 274 my ($params, @params) = $ch->mode_string; 275 if (length($params) > 1) { 276 # 設定すべきモードがある。 277 push @{$session->{cmd_buf}},$this->construct_irc_message( 278 Command => 'MODE', 279 Params => [$session->{ch_shortname}, 280 $params, 281 @params]); 282 } 283 } 284 } 285 elsif ($msg->command eq RPL_ENDOFBANLIST) { 286 # +bリスト終わり 287 $got_reply->('b'); 288 } 289 elsif ($msg->command eq RPL_ENDOFEXCEPTLIST) { 290 # +eリスト終わり 291 $got_reply->('e'); 292 } 293 elsif ($msg->command eq RPL_ENDOFINVITELIST) { 294 # +Iリスト終わり 295 $got_reply->('I'); 296 } 297 elsif ($msg->command eq 'JOIN') { 298 $session = $this->{sessions}->{$msg->param(0)}; 299 if (defined $session && defined $msg->nick && 300 $msg->nick eq RunLoop->shared->current_nick) { 301 # 入り直した。 302 $session->{got_oper} = 1; # 既にセットされている筈だが念のため 303 $this->revive($session); 304 } 305 } 306 elsif ($msg->command eq ERR_CHANOPRIVSNEEDED) { 307 $session = $this->{sessions}->{$msg->param(1)}; 308 if (defined $session) { 309 $session->{num_got_errors}++; 310 } 311 } 312 313 # $sessionが空でなければ、必要な情報が全て揃った可能性がある。 314 if (defined $session && !$session->{got_oper} && 315 $session->{got_mode} && ($session->{got_blist} + 316 $session->{got_elist} + $session->{got_Ilist} + 317 $session->{num_got_errors}) >= 3) { 318 $this->part_and_join($session); 319 } 320} 321 322sub revive { 323 my ($this,$session) = @_; 324 Timer->new( 325 Name => 'Channel::Rejoin cmd queue', 326 Module => $this, 327 Interval => 1, 328 Repeat => 1, 329 Code => sub { 330 my $timer = shift; 331 my $cmd_buf = $session->{cmd_buf}; 332 if (@$cmd_buf > 0) { 333 # 一度に二つずつ送り出す。 334 my $msg_per_trigger = 2; 335 for (my $i = 0; $i < @$cmd_buf && $i < $msg_per_trigger; $i++) { 336 $session->{server}->send_message($cmd_buf->[$i]); 337 } 338 splice @$cmd_buf,0,$msg_per_trigger; 339 } 340 if (@$cmd_buf == 0) { 341 # cmd_bufが空だったら終了。 342 # ただし、10秒以内に再び単独になっても無視する 343 # untouchablesから消去 344 my $untouchables = BulletinBoard->shared->do_not_touch_mode_of_channels; 345 delete $untouchables->{$session->{ch_fullname}}; 346 Timer->new( 347 Name => 'Channel::Rejoin delay cleanup', 348 Module => $this, 349 After => 10, 350 Code => sub { 351 # session消去 352 delete $this->{sessions}->{$session->{ch_fullname}}; 353 })->install; 354 # タイマーをアンインストール 355 $timer->uninstall; 356 } 357 })->install; 358} 359 3601; 361 362=pod 363info: チャンネルオペレータ権限を無くしたとき、一人ならjoinし直す。 364default: off 365section: important 366 367# +チャンネルや+aされているチャンネル以外でチャンネルオペレータ権限を持たずに 368# 一人きりになった時、そのチャンネルの@を復活させるために自動的にjoinし直すモジュール。 369# トピック、モード、banリスト等のあらゆるチャンネル属性をも保存します。 370 371# +b,+I,+eリストの復旧を行なうかどうか。 372# あまりに長いリストを取得するとMax Send-Q Exceedで落とされるかも知れません。 373save-lists: 1 374=cut 375