1# ----------------------------------------------------------------------------- 2# $Id: Server.pm 36720 2010-02-11 18:13:54Z topia $ 3# ----------------------------------------------------------------------------- 4# IrcIO::ServerはIRCサーバーに接続し、IRCメッセージをやり取りするクラスです。 5# このクラスはサーバーからメッセージを受け取ってチャンネル情報や現在のnickなどを保持しますが、 6# 受け取ったメッセージをモジュールに通したり各クライアントに転送したりはしません。 7# それはRunLoopの役目です。 8# ----------------------------------------------------------------------------- 9package IrcIO::Server; 10use strict; 11use warnings; 12use base qw(IrcIO); 13use Carp; 14use ChannelInfo; 15use PersonalInfo; 16use PersonInChannel; 17use UNIVERSAL; 18use Multicast; 19use NumericReply; 20use Tiarra::Utils; 21use Tiarra::Socket::Connect; 22use Tiarra::Resolver; 23utils->define_attr_getter(0, 24 qw(network_name current_nick logged_in), 25 qw(server_hostname isupport config), 26 [qw(host server_host)]); 27utils->define_attr_accessor(0, qw(state finalizing)); 28utils->define_attr_enum_accessor('state', 'eq', 29 qw(connecting finalizing terminating), 30 qw(terminated finalized connected), 31 qw(reconnecting)); 32 33 34sub out_encoding { shift->config_local_or_general('out-encoding', 'server-') } 35sub in_encoding { shift->config_local_or_general('in-encoding', 'server-') } 36 37sub new { 38 my ($class,$runloop,$network_name) = @_; 39 my $this = $class->SUPER::new( 40 $runloop, 41 name => "network/$network_name"); 42 $this->{network_name} = $network_name; 43 $this->{current_nick} = ''; # 現在使用中のnick。ログインしていなければ空。 44 $this->{server_hostname} = ''; # サーバが主張している hostname。こちらもログインしてなければ空。 45 46 $this->{logged_in} = 0; # このサーバーへのログインに成功しているかどうか。 47 $this->{new_connection} = 1; 48 49 $this->{receiving_namreply} = {}; # RPL_NAMREPLYを受け取ると<チャンネル名,1>になり、RPL_ENDOFNAMESを受け取るとそのチャンネルの要素が消える。 50 $this->{receiving_banlist} = {}; # 同上。RPL_BANLIST 51 $this->{receiving_exceptlist} = {}; # 同上。RPL_EXCEPTLIST 52 $this->{receiving_invitelist} = {}; # 同上、RPL_INVITELIST 53 54 $this->{channels} = {}; # 小文字チャンネル名 => ChannelInfo 55 $this->{people} = {}; # nick => PersonalInfo 56 $this->{isupport} = {}; # isupport 57 58 $this->{connecting} = undef; 59 $this->{finalizing} = undef; 60 $this->state('initializing'); 61 62 $this->reconnect; 63} 64 65sub connecting { defined shift->{connecting}; } 66 67sub _connect_interrupted { 68 my $this = shift; 69 $this->state_terminating || $this->state_finalizing || 70 $this->state_terminated || $this->state_finalized; 71} 72 73sub _gen_msg { 74 my ($this, $msg) = @_; 75 76 $this->name.': '.$msg; 77} 78 79sub die { 80 my ($this, $msg) = @_; 81 CORE::die($this->_gen_msg($msg)); 82} 83 84sub warn { 85 my ($this, $msg) = @_; 86 CORE::warn($this->_gen_msg($msg)); 87} 88 89sub printmsg { 90 my ($this, $msg) = @_; 91 92# if (defined $this->{last_msg} && 93# $this->{last_msg}->[0] eq $msg && 94# $this->{last_msg}->[1] <= (time - 10)) { 95# # repeated 96# return 97# } 98# $this->{last_msg} = [$msg, time]; 99 ::printmsg($this->_gen_msg($msg)); 100} 101 102sub nick_p { 103 my ($this, $nick) = @_; 104 105 Multicast::nick_p($nick, $this->isupport->{NICKLEN}); 106} 107 108sub channel_p { 109 my ($this, $name) = @_; 110 111 Multicast::channel_p($name, $this->isupport->{CHANTYPES}); 112} 113 114sub channels { 115 # {小文字チャンネル名 => ChannelInfo}のハッシュリファを返す。 116 # @options(省略可能): 117 # 'even-if-kicked-out': 既に自分が蹴り出されてゐるチャンネルも返す。この動作は高速である。 118 my ($this, @options) = @_; 119 if (defined $options[0] && $options[0] eq 'even-if-kicked-out') { 120 $this->{channels}; 121 } 122 else { 123 # kicked-outフラグが立つてゐないチャンネルのみ返す。 124 my %result; 125 while (my ($name, $ch) = each %{$this->{channels}}) { 126 if (!$ch->remark('kicked-out')) { 127 $result{$name} = $ch; 128 } 129 } 130 \%result; 131 } 132} 133 134sub channels_list { 135 # @options(省略可能): 136 # 'even-if-kicked-out': 既に自分が蹴り出されてゐるチャンネルも返す。この動作は高速である。 137 my ($this, @options) = @_; 138 if (defined $options[0] && $options[0] eq 'even-if-kicked-out') { 139 values %{$this->{channels}}; 140 } 141 else { 142 # kicked-outフラグが立つてゐないチャンネルのみ返す。 143 grep { 144 !$_->remarks('kicked-out'); 145 } values %{$this->{channels}}; 146 } 147} 148 149sub person_list { 150 values %{shift->{people}}; 151} 152 153sub fullname { 154 $_[0]->{current_nick}.'!'.$_[0]->{user_shortname}.'@'.$_[0]->{server_host}; 155} 156 157sub config_local_or_general { 158 my ($this, $base, $general_prefix, $local_prefix, $default) = @_; 159 160 foreach ([$this->config, $local_prefix], 161 [$this->_conf_general, $general_prefix]) { 162 my ($conf, $prefix) = @$_; 163 $prefix = '' unless defined $prefix; 164 my $value = $conf->get("$prefix$base"); 165 if (defined $value) { 166 return $value; 167 } 168 } 169 return $default; 170} 171 172sub reload_config { 173 my $this = shift; 174 my $conf = $this->{config} = $this->_conf->get($this->{network_name}); 175 my @servers = $conf->server('all'); 176 if (!@servers) { 177 @servers = ([$conf->host, $conf->port]); 178 } else { 179 @servers = map {[split(/\s+/, $_)]} @servers; 180 } 181 $this->{server_hosts} = [ 182 map { +{ 183 host => shift(@$_), 184 port => $_, 185 } } @servers, 186 ]; 187 $this->{server_password} = $conf->password; 188 $this->{initial_nick} = $this->config_local_or_general('nick'); # ログイン時に設定するnick。 189 $this->{user_shortname} = $this->config_local_or_general('user'); 190 $this->{user_realname} = $this->config_local_or_general('name'); 191 #$this->{prefer_socket_types} = [qw(ipv6 ipv4)]; 192} 193 194sub destination { 195 my $this = shift; 196 Tiarra::Socket->repr_destination( 197 host => $this->{server_host}, 198 addr => $this->{server_addr}, 199 port => $this->{server_port}, 200 type => $this->{proto}); 201} 202 203sub person_if_exists { 204 my ($this, $nick) = @_; 205 $this->{people}{$nick}; 206} 207 208sub person { 209 # nick以外は全て省略可能。 210 # 未知のnickが指定された場合は新規に追加する。 211 my ($this,$nick,$username,$userhost,$realname,$server) = @_; 212 return if !defined $nick; 213 214 my $info = $this->{people}->{$nick}; 215 if (!defined($info)) { 216 $info = $this->{people}->{$nick} = 217 new PersonalInfo(Nick => $nick, 218 UserName => $username, 219 UserHost => $userhost, 220 RealName => $realname, 221 Server => $server); 222 } 223 else { 224 $info->username($username); 225 $info->userhost($userhost); 226 $info->realname($realname); 227 $info->server($server); 228 } 229 $info; 230} 231 232sub channel { 233 my $this = $_[0]; 234 my $channel_name = Multicast::lc($_[1]); 235 $this->{channels}->{$channel_name}; 236} 237 238sub _queue_retry { 239 my $this = shift; 240 241 $this->state_reconnecting(1); 242 243 $this->_cleanup if defined $this->{timer}; 244 $this->{connecting} = undef; 245 $this->{timer} = Timer->new( 246 Name => $this->_gen_msg('retry timer'), 247 After => 15, 248 Code => sub { 249 $this->{timer} = undef; 250 return if $this->finalizing; 251 $this->reconnect; 252 })->install; 253} 254 255sub reconnect { 256 my $this = shift; 257 $this->reload_config; 258 $this->connect; 259} 260 261# connect --(resolve)--> _connect_try_next --> 262 263sub connect { 264 my $this = shift; 265 #return if $this->connected; 266 croak 'connected!' if $this->connected; 267 croak 'connecting!' if $this->connecting; 268 $this->finalizing(undef); 269 270 # 初期化すべきフィールドを初期化 271 $this->{server_host} = undef; 272 $this->{server_port} = undef; 273 $this->{nick_retry} = 0; 274 $this->{logged_in} = undef; 275 $this->state_connecting(1); 276 277 $this->{server_queue} = $this->{server_hosts}; 278 279 $this->_connect_try_next; 280 $this; 281} 282 283 284sub _connect_try_next { 285 my $this = shift; 286 287 return if $this->finalizing; 288 my $trying = 289 $this->{connecting} = shift @{$this->{server_queue}}; 290 if (defined $trying) { 291 292 $this->{connector} = Tiarra::Socket::Connect->new( 293 host => $this->{connecting}->{host}, 294 port => $this->{connecting}->{port}, 295 callback => sub { 296 my ($subject, $socket, $obj, $errno) = @_; 297 298 if ($subject eq 'sock') { 299 $this->attach($socket); 300 } elsif ($subject eq 'error') { 301 $this->_connect_error($obj); 302 } elsif ($subject eq 'warn') { 303 $this->_connect_warn($obj); 304 } elsif ($subject eq 'progress') { 305 $this->_connect_warn($obj); 306 } 307 }, 308 hooks => { 309 before_connect => sub { 310 $this->_hook_before_connect(@_); 311 }, 312 }, 313 ); 314 315 } else { 316 $this->printmsg("Couldn't connect to any host"); 317 $this->_queue_retry; 318 return; 319 } 320} 321 322sub _hook_before_connect { 323 my ($this, $stage, $connecting) = @_; 324 325 my $type = $connecting->{type}; 326 my $bind_addr; 327 if ($type eq 'ipv4') { 328 # 下は過去互換性の為に残す。 329 $bind_addr = $this->config_local_or_general('ipv4-bind-addr') || 330 $this->config_local_or_general('bind-addr'); 331 } elsif ($type eq 'ipv6') { 332 $bind_addr = $this->config_local_or_general('ipv6-bind-addr'); 333 } 334 if (defined $bind_addr) { 335 $connecting->{bind_addr} = $bind_addr; 336 } 337} 338 339sub attach { 340 my ($this, $connector) = @_; 341 342 $this->SUPER::attach($connector->sock); 343 $this->{connecting} = undef; 344 $this->{server_host} = $connector->host; 345 $this->{server_addr} = $connector->current_addr; 346 $this->{server_port} = $connector->current_port; 347 $this->{proto} = $connector->current_type; 348 $this->state_connected(1); 349 350 $this->_send_connection_messages; 351 352 $this->{connector} = undef; 353 $this->printmsg("Opened connection to ". $this->destination ."."); 354 $this->install; 355 $this; 356} 357 358sub _connect_error { 359 my ($this, $msg) = @_; 360 361 $this->printmsg("$msg"); 362 $this->_connect_try_next; 363} 364 365sub _connect_warn { 366 my ($this, $msg) = @_; 367 368 $this->printmsg("$msg"); 369} 370 371sub _send_connection_messages { 372 my $this = shift; 373 # (PASS) -> NICK -> USERの順に送信し、中に入る。 374 # NICKが成功したかどうかは接続後のreceiveメソッドが判断する。 375 my $server_password = $this->{server_password}; 376 if (defined $server_password && $server_password ne '') { 377 $this->send_message($this->construct_irc_message( 378 Command => 'PASS', 379 Param => $this->{server_password})); 380 } 381 if (!defined $this->{current_nick} || $this->{current_nick} eq '') { 382 $this->{current_nick} = $this->{initial_nick}; 383 } 384 $this->send_message($this->construct_irc_message( 385 Command => 'NICK', 386 Param => $this->{current_nick})); 387 388 # +iなどの文字列からユーザーモード値を算出する。 389 my $usermode = 0; 390 if (my $usermode_str = $this->_conf_general->user_mode) { 391 if ($usermode_str =~ /^\+/) { 392 foreach my $c (split //,substr($usermode_str,1)) { 393 if ($c eq 'w') { 394 $usermode |= (1 << 2); 395 } 396 elsif ($c eq 'i') { 397 $usermode |= (1 << 3); 398 } 399 } 400 } 401 } 402 $this->send_message($this->construct_irc_message( 403 Command => 'USER', 404 Params => [$this->{user_shortname}, 405 $usermode, 406 '*', 407 $this->{user_realname}])); 408} 409 410sub terminate { 411 my ($this, $msg) = @_; 412 413 $this->_interrupt($msg, 'terminating'); 414} 415 416sub finalize { 417 my ($this, $msg) = @_; 418 419 $this->_interrupt($msg, 'finalizing'); 420 $this->finalizing(1); 421} 422 423sub _interrupt { 424 my ($this, $msg, $state) = @_; 425 426 if ($this->logged_in) { 427 $this->state($state); 428 $this->quit($msg); 429 } elsif ($this->state_connecting || $this->state_reconnecting) { 430 $this->state($state); 431 $this->_cleanup; 432 } else { 433 if (!$this->state_connected) { 434 $this->warn('_interrupt/unexpected state: '.$this->state) 435 if &::debug_mode; 436 } 437 $this->state($state); 438 $this->disconnect; 439 } 440} 441 442sub disconnect { 443 my ($this, $genre, $errno, @params) = @_; 444 445 $this->_cleanup; 446 $this->SUPER::disconnect($genre, $errno, @params); 447 if (defined $errno) { 448 $this->printmsg($this->sock_errno_to_msg( 449 $errno, 450 "Disconnected from ".$this->destination.": $genre error")); 451 } else { 452 $this->printmsg("Disconnected from ".$this->destination."."); 453 } 454 if ($this->state_reconnecting || $this->state_connected) { 455 $this->state_reconnecting(1); 456 $this->reload_config; 457 $this->_queue_retry; 458 } 459 $this->{logged_in} = undef; 460} 461 462sub _cleanup { 463 my ($this, $mode) = @_; 464 465 $this->{connecting} = undef; 466 if (defined $this->{connector}) { 467 $this->{connector}->interrupt; 468 $this->{connector} = undef; 469 } 470 if (defined $this->{timer}) { 471 $this->{timer}->uninstall; 472 $this->{timer} = undef; 473 } 474 if (defined $this->{rejoin_timer}) { 475 $this->{rejoin_timer}->uninstall; 476 $this->{rejoin_timer} = undef; 477 } 478 if ($this->state_terminating) { 479 $this->state_terminated(1); 480 } elsif ($this->state_finalizing) { 481 $this->state_finalized(1); 482 } 483 # remove pong drop counter 484 $this->remark('pong-drop-counter', undef, 'delete'); 485} 486 487sub quit { 488 my ($this, $msg) = @_; 489 return $this->send_message( 490 $this->construct_irc_message( 491 Command => 'QUIT', 492 Param => $msg)); 493} 494 495sub send_message { 496 my ($this,$msg) = @_; 497 498 if (!defined $msg) { 499 croak "IrcIO::Server->send_message, Arg[1] was undef.\n"; 500 } 501 elsif (!ref($msg)) { 502 croak "IrcIO::Server->send_message, Arg[1] was not ref.\n"; 503 } 504 elsif (!UNIVERSAL::isa($msg, $this->irc_message_class)) { 505 croak "IrcIO::Server->send_message, Arg[1] was bad ref: ".ref($msg)."\n"; 506 } 507 508 # 各モジュールへ通知 509 #$this->_runloop->notify_modules('notification_of_message_io',$msg,$this,'out'); 510 511 $this->SUPER::send_message( 512 $msg, 513 $this->out_encoding); 514} 515 516sub read { 517 my $this = shift; 518 $this->SUPER::read($this->in_encoding); 519 520 # 接続が切れたら、各モジュールとRunLoopへ通知 521 if (!$this->connected) { 522 $this->_runloop->notify_modules('disconnected_from_server',$this); 523 $this->_runloop->disconnected_server($this); 524 } 525} 526 527sub pop_queue { 528 my ($this) = shift; 529 my $msg = $this->SUPER::pop_queue; 530 531 # このメソッドはログインしていなければログインするが、 532 # パスワードが違うなどで何度やり直してもログイン出来る見込みが無ければ 533 # 接続を切ってからdieします。 534 if (defined $msg) { 535 # ログイン作業中か? 536 if ($this->logged_in) { 537 # ログイン作業中でない。 538 return $this->_receive_after_logged_in($msg); 539 } 540 else { 541 return $this->_receive_while_logging_in($msg); 542 } 543 } 544 return $msg; 545} 546 547sub _receive_while_logging_in { 548 my ($this,$first_msg) = @_; 549 550 # まだログイン作業中であるのなら、ログインに成功したかどうかを 551 # 最初に受け取った行が001(成功)か433(nick重複)かそれ以外かで判断する。 552 my $reply = $first_msg->command; 553 if ($reply eq RPL_WELCOME) { 554 # 成功した。 555 $this->{current_nick} = $first_msg->param(0); 556 $this->{server_hostname} = $first_msg->prefix; 557 if (!$this->_runloop->multi_server_mode_p && 558 $this->_runloop->current_nick ne $this->{current_nick}) { 559 $this->_runloop->broadcast_to_clients( 560 $this->construct_irc_message( 561 Command => 'NICK', 562 Param => $first_msg->param(0), 563 Remarks => {'fill-prefix-when-sending-to-client' => 1 564 })); 565 566 $this->_runloop->set_current_nick($first_msg->param(0)); 567 } 568 $this->{logged_in} = 1; 569 $this->person($this->{current_nick}, 570 $this->{user_shortname}, 571 $this->{user_realname}); 572 573 574 $this->printmsg("Logged-in successfuly into ".$this->destination."."); 575 576 # 各モジュールにサーバー追加の通知を行なう。 577 $this->_runloop->notify_modules('connected_to_server',$this,$this->{new_connection}); 578 # 再接続だった場合の処理 579 if (!$this->{new_connection}) { 580 $this->_runloop->reconnected_server($this); 581 } 582 $this->{new_connection} = undef; 583 return; 584 } 585 elsif ($reply eq ERR_NICKNAMEINUSE) { 586 # nick重複。 587 $this->_set_to_next_nick($first_msg->param(1)); 588 return; # 何も返さない→クライアントにはこの結果を知らせない。 589 } 590 elsif ($reply eq ERR_UNAVAILRESOURCE) { 591 # nick/channel is temporarily unavailable(この場合は nick) 592 $this->_set_to_next_nick($first_msg->param(1)); 593 return; # 何も返さない→クライアントにはこの結果を知らせない。 594 } 595 elsif ($reply eq RPL_HELLO) { 596 # RPL_HELLO (irc2.11.x) 597 $this->printmsg("Server replied 020(RPL_HELLO). Please wait."); 598 return; # 何も返さない→クライアントにはこの結果を知らせない。 599 } 600 elsif (grep { $_ eq $reply } qw(NOTICE PRIVMSG)) { 601 # NOTICE / PRIVMSG 602 return; # 何もしない 603 } 604 elsif ($reply eq 'PING') { 605 $this->send_message( 606 $this->construct_irc_message( 607 Command => 'PONG', 608 Param => $first_msg->param(0))); 609 } 610 else { 611 # それ以外。手の打ちようがないのでconnectionごと切断してしまう。 612 # 但し、エラーニューメリックリプライでもERRORでもなければ無視する。 613 if ($reply =~ /^[0-3]\d+/) { 614 $this->printmsg("Server replied $reply, ignored.\n".$first_msg->serialize."\n"); 615 return; 616 } elsif ($reply eq 'ERROR' or $reply !~ m/^\d+/) { 617 $this->disconnect; 618 $this->die("Server replied $reply.\n".$first_msg->serialize."\n"); 619 } 620 else { 621 $this->printmsg("Server replied $reply, ignored.\n".$first_msg->serialize."\n"); 622 return; 623 } 624 } 625 return $first_msg; 626} 627 628sub _receive_after_logged_in { 629 my ($this,$msg) = @_; 630 631 $this->person($msg->nick,$msg->name,$msg->host); # nameとhostを覚えておく。 632 633 if (defined $msg->nick && 634 $msg->nick ne $this->current_nick) { 635 $msg->remark('message-send-by-other', 1); 636 } 637 638 if ($msg->command eq 'NICK') { 639 # nickを変えたのが自分なら、それをクライアントには伝えない。 640 my $current_nick = $this->{current_nick}; 641 if ($msg->nick eq $current_nick) { 642 $this->{current_nick} = $msg->param(0); 643 644 if ($this->_runloop->multi_server_mode_p) { 645 # ここで消してしまうとプラグインにすらNICKが行かなくなる。 646 # 消す代わりに"do-not-send-to-clients => 1"という註釈を付ける。 647 $msg->remark('do-not-send-to-clients',1); 648 649 # ローカルnickと違っていれば、その旨を通知する。 650 # 但し、networks/always-notify-new-nickが設定されていれば常に通知する。 651 my $local_nick = $this->_runloop->current_nick; 652 if ($this->_conf_networks->always_notify_new_nick || 653 $this->{current_nick} ne $local_nick) { 654 655 my $old_nick = $msg->nick; 656 $this->_runloop->broadcast_to_clients( 657 $this->construct_irc_message( 658 Prefix => $this->_runloop->sysmsg_prefix(qw(priv nick::system)), 659 Command => 'NOTICE', 660 Params => [$local_nick, 661 "*** Your global nick in ". 662 $this->{network_name}." changed ". 663 "$old_nick -> ". 664 $this->{current_nick}."."])); 665 } 666 } else { 667 $this->_runloop->set_current_nick($msg->param(0)); 668 } 669 } 670 $this->_NICK($msg); 671 } 672 elsif ($msg->command eq ERR_NICKNAMEINUSE) { 673 # nickが既に使用中 674 return $this->_handle_fix_nick($msg); 675 } 676 elsif ($msg->command eq ERR_UNAVAILRESOURCE) { 677 # nick/channel temporary unavaliable 678 if (Multicast::nick_p($msg->param(1))) { 679 return $this->_handle_fix_nick($msg); 680 } 681 } 682 elsif ($msg->command eq 'JOIN') { 683 $this->_JOIN($msg); 684 } 685 elsif ($msg->command eq 'KICK') { 686 $this->_KICK($msg); 687 } 688 elsif ($msg->command eq 'MODE') { 689 $this->_MODE($msg); 690 } 691 elsif ($msg->command eq 'NJOIN') { 692 $this->_NJOIN($msg); 693 } 694 elsif ($msg->command eq 'PART') { 695 $this->_PART($msg); 696 } 697 elsif ($msg->command eq 'QUIT' || $msg->command eq 'KILL') { 698 # QUITとKILLは同じように扱う。 699 $this->_QUIT($msg); 700 } 701 elsif ($msg->command eq 'TOPIC') { 702 $this->_TOPIC($msg); 703 } 704 else { 705 my $name = NumericReply::fetch_name($msg->command); 706 if (defined $name) { 707 foreach ( 708 map("RPL_$_", 709 qw(CHANNELMODEIS NOTOPIC TOPIC TOPICWHOTIME 710 CREATIONTIME WHOREPLY NAMREPLY ENDOFNAMES 711 WHOISUSER WHOISSERVER AWAY ENDOFWHOIS 712 ISUPPORT YOURID), 713 map({("${_}LIST", "ENDOF${_}LIST");} 714 qw(INVITE EXCEPT BAN)), 715 )) { 716 if ($name eq $_) { 717 no strict 'refs'; 718 my $funcname = "_$_"; 719 &$funcname($this, $msg); # $this->$funcname($msg) 720 last; 721 } 722 } 723 } 724 } 725 return $msg; 726} 727 728sub _KICK { 729 my ($this,$msg) = @_; 730 my @ch_names = split(/,/,$msg->param(0)); 731 my @nicks = split(/,/,$msg->param(1)); 732 my $kick = sub { 733 my ($ch,$nick_to_kick) = @_; 734 if ($nick_to_kick eq $this->{current_nick}) { 735 # KICKされたのが自分だった 736 $ch->remarks('kicked-out','1'); 737 } 738 else { 739 $ch->names($nick_to_kick,undef,'delete'); 740 } 741 }; 742 if (@ch_names == @nicks) { 743 # チャンネル名とnickが1対1で対応 744 map { 745 my ($ch_name,$nick) = ($ch_names[$_],$nicks[$_]); 746 my $ch = $this->channel($ch_name); 747 if (defined $ch) { 748 #$ch->names($nick,undef,'delete'); 749 $kick->($ch,$nick); 750 } 751 } 0 .. $#ch_names; 752 } 753 elsif (@ch_names == 1) { 754 # 一つのチャンネルから1人以上をkick 755 my $ch = $this->channel($ch_names[0]); 756 if (defined $ch) { 757 map { 758 #$ch->names($_,undef,'delete'); 759 $kick->($ch,$_); 760 } @nicks; 761 } 762 } 763} 764 765sub _MODE { 766 my ($this,$msg) = @_; 767 if ($msg->param(0) eq $this->{current_nick}) { 768 # MODEの対象が自分なのでここでは無視。 769 return; 770 } 771 772 my $ch = $this->channel($msg->param(0)); 773 if (defined $ch) { 774 my $n_params = @{$msg->params}; 775 776 my $plus = 0; # 現在評価中のモードが+なのか-なのか。 777 my $mode_char_pos = 1; # 現在評価中のmode characterの位置。 778 my $mode_param_offset = 0; # $mode_char_posから幾つの追加パラメタを拾ったか。 779 780 my $fetch_param = sub { 781 $mode_param_offset++; 782 return $msg->param($mode_char_pos + $mode_param_offset); 783 }; 784 785 for (;$mode_char_pos < $n_params;$mode_char_pos += $mode_param_offset + 1) { 786 $mode_param_offset = 0; # これは毎回リセットする。 787 foreach my $c (split //,$msg->param($mode_char_pos)) { 788 my $add_or_delete = ($plus ? 'add' : 'delete'); 789 my $undef_or_delete = ($plus ? undef : 'delete'); 790 if ($c eq '+') { 791 $plus = 1; 792 } 793 elsif ($c eq '-') { 794 $plus = 0; 795 } 796 elsif (index('aimnpqrst',$c) != -1) { 797 $ch->switches($c,1,$undef_or_delete); 798 } 799 elsif ($c eq 'b') { 800 $ch->banlist($add_or_delete,&$fetch_param); 801 } 802 elsif ($c eq 'e') { 803 $ch->exceptionlist($add_or_delete,&$fetch_param); 804 } 805 elsif ($c eq 'I') { 806 $ch->invitelist($add_or_delete,&$fetch_param); 807 } 808 elsif ($c eq 'k') { 809 $ch->parameters('k',&$fetch_param,$undef_or_delete); 810 } 811 elsif ($c eq 'l') { 812 $ch->parameters('l',($plus ? &$fetch_param : undef),$undef_or_delete); 813 } 814 elsif ($c eq 'o' || $c eq 'O') { 815 # oとOは同一視 816 eval { 817 $ch->names(&$fetch_param)->has_o($plus); 818 }; 819 } 820 elsif ($c eq 'v') { 821 eval { 822 $ch->names(&$fetch_param)->has_v($plus); 823 }; 824 } 825 } 826 } 827 } 828} 829 830sub _JOIN { 831 my ($this,$msg) = @_; 832 833 map { 834 m/^([^\x07]+)(?:\x07(.*))?/; 835 my ($ch_name,$mode) = ($1,(defined $2 ? $2 : '')); 836 837 my $ch = $this->channel($ch_name); 838 if (defined $ch) { 839 # 知っているチャンネル。もしkickedフラグが立っていたらクリア。 840 $ch->remarks('kicked-out',undef,'delete'); 841 } 842 else { 843 # 知らないチャンネル。 844 $ch = ChannelInfo->new($ch_name,$this->{network_name}); 845 $this->{channels}{Multicast::lc($ch_name)} = $ch; 846 } 847 $ch->names($msg->nick, 848 new PersonInChannel( 849 $this->person($msg->nick,$msg->name,$msg->host), 850 index($mode,"o") != -1 || index($mode,"O") != -1, # oもOも今は同一視 851 index($mode,"v") != -1)); 852 } split(/,/,$msg->param(0)); 853} 854 855sub _NJOIN { 856 my ($this,$msg) = @_; 857 my $ch_name = $msg->param(0); 858 my $ch = $this->channel($ch_name); 859 unless (defined $ch) { 860 # 知らないチャンネル。 861 $ch = ChannelInfo->new($ch_name,$this->{network_name}); 862 $this->{channels}{Multicast::lc($ch_name)} = $ch; 863 } 864 map { 865 m/^([@+]*)(.+)$/; 866 my ($mode,$nick) = ($1,$2); 867 868 $ch->names($nick, 869 new PersonInChannel( 870 $this->person($nick), 871 index($mode,"@") != -1, # 今は@と@@を同一視。 872 index($mode,"+") != -1)); 873 } split(/,/,$msg->param(1)); 874} 875 876sub _PART { 877 my ($this,$msg) = @_; 878 map { 879 my $ch_name = $_; 880 my $ch = $this->channel($ch_name); 881 if (defined $ch) { 882 if ($msg->nick eq $this->{current_nick}) { 883 # PARTしたのが自分だった 884 delete $this->{channels}->{Multicast::lc($ch_name)}; 885 } 886 else { 887 $ch->names($msg->nick,undef,'delete'); 888 } 889 } 890 } split(/,/,$msg->param(0)); 891 892 # 全チャンネルを走査し、このnickを持つ人物が一人も居なくなつてゐたらpeopleからも消す。 893 my $alive; 894 foreach my $ch (values %{$this->{channels}}) { 895 if (defined $ch->names($msg->nick)) { 896 $alive = 1; 897 } 898 } 899 if (!$alive) { 900 delete $this->{people}{$msg->nick}; 901 } 902} 903 904sub _NICK { 905 my ($this,$msg) = @_; 906 # PersonalInfoとChannelInfoがnickを持っているので書き換える。 907 my ($old,$new) = ($msg->nick,$msg->param(0)); 908 909 if (!defined $this->{people}->{$old}) { 910 return; 911 } 912 913 $this->{people}->{$old}->nick($new); 914 $this->{people}->{$new} = $this->{people}->{$old}; 915 delete $this->{people}->{$old}; 916 917 my @channels = grep { 918 defined $_->names($old); 919 } values %{$this->{channels}}; 920 921 # このNICKが影響を及ぼした全チャンネル名のリストを 922 # "affected-channels"として註釈を付ける。 923 my @affected = map { 924 my $ch = $_; 925 $ch->names($new,$ch->names($old)); 926 $ch->names($old,undef,'delete'); 927 $ch->name; 928 } @channels; 929 $msg->remark('affected-channels',\@affected); 930} 931 932sub _QUIT { 933 my ($this,$msg) = @_; 934 # people及びchannelsから削除する。 935 delete $this->{people}->{$msg->nick}; 936 937 my @channels = grep { 938 defined $_->names($msg->nick); 939 } values %{$this->{channels}}; 940 941 # このNICKが影響を及ぼした全チャンネル名のリストを 942 # "affected-channels"として註釈を付ける。 943 my @affected = map { 944 my $ch = $_; 945 $ch->names($msg->nick,undef,'delete'); 946 $ch->name; 947 } @channels; 948 $msg->remark('affected-channels',\@affected); 949} 950 951sub _TOPIC { 952 my ($this,$msg) = @_; 953 my $ch = $this->channel($msg->param(0)); 954 if (defined $ch) { 955 # 古いトピックを"old-topic"として註釈を付ける。 956 $msg->remark('old-topic', $ch->topic); 957 $ch->topic($msg->param(1)); 958 959 # topic_who と topic_time を指定する 960 $ch->topic_who($msg->prefix); 961 $ch->topic_time(time); 962 } 963} 964 965sub _RPL_NAMREPLY { 966 my ($this,$msg) = @_; 967 my $ch = $this->channel($msg->param(2)); 968 return unless defined $ch; 969 970 my $receiving_namreply = $this->{receiving_namreply}->{$msg->param(2)}; 971 unless (defined $receiving_namreply && 972 $receiving_namreply == 1) { 973 # NAMESを初期化 974 $ch->names(undef,undef,'clear'); 975 # NAMREPLY受信中フラグを立てる 976 $this->{receiving_namreply}->{$msg->param(2)} = 1; 977 } 978 979 if (defined $ch) { 980 # @なら+s,*なら+p、=ならそのどちらでもない事が確定している。 981 my $ch_property = $msg->param(1); 982 if ($ch_property eq '@') { 983 $ch->switches('s',1); 984 $ch->switches('p',undef,'delete'); 985 } 986 elsif ($ch_property eq '*') { 987 $ch->switches('s',undef,'delete'); 988 $ch->switches('p',1); 989 } 990 else { 991 $ch->switches('s',undef,'delete'); 992 $ch->switches('p',undef,'delete'); 993 } 994 995 my @people = map { 996 m/^([@\+]{0,2})(.+)$/; 997 my ($mode,$nick) = ($1,$2); 998 999 $ch->names($nick, 1000 new PersonInChannel( 1001 $this->person($nick), 1002 index($mode,"@") != -1, 1003 index($mode,"+") != -1)); 1004 } split(/ /,$msg->param(3)); 1005 } 1006} 1007 1008sub _RPL_ENDOFNAMES { 1009 my ($this,$msg) = @_; 1010 delete $this->{receiving_namreply}->{$msg->param(1)}; 1011} 1012 1013sub _RPL_WHOISUSER { 1014 my ($this,$msg) = @_; 1015 my $p = $this->{people}->{$msg->param(1)}; 1016 if (defined $p) { 1017 $p->username($msg->param(2)); 1018 $p->userhost($msg->param(3)); 1019 $p->realname($msg->param(5)); 1020 $this->_START_WHOIS_REPLY($p); 1021 } 1022} 1023 1024sub _START_WHOIS_REPLY { 1025 my ($this,$p) = @_; 1026 $p->remark('wait-rpl_away', 1); 1027} 1028 1029sub _RPL_ENDOFWHOIS { 1030 my ($this,$msg) = @_; 1031 my $p = $this->{people}->{$msg->param(1)}; 1032 if (defined $p) { 1033 if ($p->remark('wait-rpl_away')) { 1034 $p->remark('wait-rpl_away', 0); 1035 $p->away(''); 1036 } 1037 } 1038} 1039 1040sub _RPL_AWAY { 1041 my ($this,$msg) = @_; 1042 my $p = $this->{people}->{$msg->param(1)}; 1043 if (defined $p) { 1044 $p->remark('wait-rpl_away', 0); 1045 $p->away($msg->param(2)); 1046 } 1047} 1048 1049sub _RPL_WHOISSERVER { 1050 my ($this,$msg) = @_; 1051 my $p = $this->{people}->{$msg->param(1)}; 1052 if (defined $p) { 1053 $p->server($msg->param(2)); 1054 } 1055} 1056 1057sub _RPL_NOTOPIC { 1058 my ($this,$msg) = @_; 1059 my $ch = $this->channel($msg->param(1)); 1060 if (defined $ch) { 1061 $ch->topic(''); 1062 } 1063} 1064 1065sub _RPL_TOPIC { 1066 my ($this,$msg) = @_; 1067 my $ch = $this->channel($msg->param(1)); 1068 if (defined $ch) { 1069 $ch->topic($msg->param(2)); 1070 } 1071} 1072 1073sub _RPL_TOPICWHOTIME { 1074 my ($this,$msg) = @_; 1075 my $ch = $this->channel($msg->param(1)); 1076 if (defined $ch) { 1077 $ch->topic_who($msg->param(2)); 1078 $ch->topic_time($msg->param(3)); 1079 } 1080} 1081 1082sub _RPL_CREATIONTIME { 1083 my ($this,$msg) = @_; 1084 my $ch = $this->channel($msg->param(1)); 1085 if (defined $ch) { 1086 $ch->remark('creation-time', $msg->param(2)); 1087 } 1088} 1089 1090sub _RPL_INVITELIST { 1091 my ($this,$msg) = @_; 1092 my $ch = $this->channel($msg->param(1)); 1093 1094 my $receiving_invitelist = $this->{receiving_invitelist}->{$msg->param(1)}; 1095 if (defined $receiving_invitelist && 1096 $receiving_invitelist == 1) { 1097 # +Iリストを初期化 1098 $ch->invitelist(undef,undef,'clear'); 1099 # INVITELIST受信中フラグを立てる 1100 $this->{receiving_invitelist}->{$msg->param(1)} = 1; 1101 } 1102 1103 if (defined $ch) { 1104 # 重複防止のため、一旦deleteしてからadd。 1105 $ch->invitelist('delete',$msg->param(2)); 1106 $ch->invitelist('add',$msg->param(2)); 1107 } 1108} 1109 1110sub _RPL_ENDOFINVITELIST { 1111 my ($this,$msg) = @_; 1112 delete $this->{receiving_invitelist}->{$msg->param(1)}; 1113} 1114 1115sub _RPL_EXCEPTLIST { 1116 my ($this,$msg) = @_; 1117 my $ch = $this->channel($msg->param(1)); 1118 1119 my $receiving_exceptlist = $this->{receiving_exceptlist}->{$msg->param(1)}; 1120 if (defined $receiving_exceptlist && 1121 $receiving_exceptlist == 1) { 1122 # +eリストを初期化 1123 $ch->exceptionlist(undef,undef,'clear'); 1124 # EXCEPTLIST受信中フラグを立てる 1125 $this->{receiving_exceptlist}->{$msg->param(1)} = 1; 1126 } 1127 1128 if (defined $ch) { 1129 # 重複防止のため、一旦deleteしてからadd。 1130 $ch->exceptionlist('delete',$msg->param(2)); 1131 $ch->exceptionlist('add',$msg->param(2)); 1132 } 1133} 1134 1135sub _RPL_ENDOFEXCEPTLIST { 1136 my ($this,$msg) = @_; 1137 delete $this->{receiving_exceptlist}->{$msg->param(1)}; 1138} 1139 1140sub _RPL_BANLIST { 1141 my ($this,$msg) = @_; 1142 my $ch = $this->channel($msg->param(1)); 1143 1144 my $receiving_banlist = $this->{receiving_banlist}->{$msg->param(1)}; 1145 if (defined $receiving_banlist && 1146 $receiving_banlist == 1) { 1147 # +bリストを初期化 1148 $ch->banlist(undef,undef,'clear'); 1149 # BANLIST受信中フラグを立てる 1150 $this->{receiving_banlist}->{$msg->param(1)} = 1; 1151 } 1152 1153 if (defined $ch) { 1154 # 重複防止のため、一旦deleteしてからadd。 1155 $ch->banlist('delete',$msg->param(2)); 1156 $ch->banlist('add',$msg->param(2)); 1157 } 1158} 1159 1160sub _RPL_ENDOFBANLIST { 1161 my ($this,$msg) = @_; 1162 delete $this->{receiving_banlist}->{$msg->param(1)}; 1163} 1164 1165sub _RPL_WHOREPLY { 1166 my ($this,$msg) = @_; 1167 my $p = $this->{people}->{$msg->param(5)}; 1168 if (defined $p) { 1169 $p->username($msg->param(2)); 1170 $p->userhost($msg->param(3)); 1171 $p->server($msg->param(4)); 1172 $p->realname((split / /,$msg->param(7),2)[1]); 1173 if ($msg->param(6) =~ /^G/) { 1174 $p->away('Gone.'); 1175 } else { 1176 $p->away(''); 1177 } 1178 my $hops = $this->remark('server-hops') || {}; 1179 $hops->{$p->server} = (split / /,$msg->param(7),2)[0]; 1180 $this->remark('server-hops', $hops); 1181 } 1182 1183 #use Data::Dumper; 1184 #open(LOG,"> log.txt"); 1185 #print LOG "------- people --------\n"; 1186 #print LOG Dumper($this->{people}),"\n"; 1187 #print LOG "------- channels --------\n"; 1188 #print LOG Dumper($this->{channels}),"\n"; 1189 #close(LOG); 1190} 1191 1192sub _RPL_CHANNELMODEIS { 1193 my ($this,$msg) = @_; 1194 # 既知のチャンネルなら、そのチャンネルに 1195 # switches-are-known => 1という備考を付ける。 1196 my $ch = $this->channel($msg->param(1)); 1197 if (defined $ch) { 1198 $ch->remarks('switches-are-known',1); 1199 1200 # switches と parameters は必ず得られると仮定して、クリア処理を行う 1201 $ch->switches(undef, undef, 'clear'); 1202 $ch->parameters(undef, undef, 'clear'); 1203 } 1204 1205 # 鯖がMODEを実行したことにして、_MODEに処理を代行させる。 1206 my @args = @{$msg->params}; 1207 @args = @args[1 .. $#args]; 1208 1209 $this->_MODE( 1210 $this->construct_irc_message( 1211 Prefix => $msg->prefix, 1212 Command => 'MODE', 1213 Params => \@args)); 1214} 1215 1216sub _RPL_ISUPPORT { 1217 # 歴史的な理由で、 RPL_ISUPPORT(005) は 1218 # RPL_BOUNCE(005) として使われていることがある。 1219 my ($this,$msg) = @_; 1220 if ($msg->n_params >= 2 && # nick + [params] + 'are supported by this server' 1221 $msg->param($msg->n_params - 1) =~ /supported/i) { 1222 foreach my $param ((@{$msg->params})[1...($msg->n_params - 2)]) { 1223 my ($negate, $key, $value) = $param =~ /^(-)?([[:alnum:]]+)(?:=(.+))?$/; 1224 if (!defined $negate) { 1225 # empty value 1226 $value = '' unless defined $value; 1227 $this->{isupport}->{$key} = $value; 1228 } elsif (!defined $value) { 1229 # negate a previously specified parameter 1230 delete $this->{isupport}->{$key}; 1231 } else { 1232 # inconsistency param 1233 carp("inconsistency RPL_ISUPPORT param: $param"); 1234 } 1235 } 1236 } 1237} 1238 1239sub _RPL_YOURID { 1240 my ($this,$msg) = @_; 1241 1242 $this->remark('uid', $msg->param(1)); 1243} 1244 1245sub _handle_fix_nick { 1246 my ($this, $msg) = @_; 1247 # 接続時以外のnick重複を処理します。 1248 my $mode = $this->config_local_or_general('nick-fix-mode'); 1249 1250 if ($mode == 0) { 1251 # 常に Tiarra が処理します。 1252 1253 $this->_set_to_next_nick($msg->param(1)); 1254 $msg->remark('do-not-send-to-clients',1); 1255 ## 破棄せず do-not-send-to-clients をつける。 1256 return $msg; 1257 } elsif ($mode == 1) { 1258 # クライアントにそのまま投げます。 1259 # 複数のクライアントが nick 重複を処理する場合は非常に危険です。 1260 # (設定不足の IRC クライアントが複数つながっている場合も含みます) 1261 return $msg; 1262 } elsif ($mode == 2) { 1263 # 対応するエラーメッセージ付きの NOTICE に変換して、 1264 # クライアントに投げます。 1265 1266 my $new_msg = $this->construct_irc_message( 1267 Prefix => $this->_runloop->sysmsg_prefix(qw(priv nick::system)), 1268 Command => 'NOTICE', 1269 Params => [$this->_runloop->current_nick, 1270 ''], 1271 ); 1272 if ($msg->command eq ERR_NICKNAMEINUSE) { 1273 $new_msg->param(1, 'Nickname is already in use: ' . 1274 $msg->param(1)); 1275 } elsif ($msg->command eq ERR_UNAVAILRESOURCE) { 1276 $new_msg->param(1, 'Nick/channel is temporarily unavailable: ' . 1277 $msg->param(1)); 1278 } else { 1279 return $msg; 1280 } 1281 return $new_msg; 1282 } 1283} 1284 1285sub _set_to_next_nick { 1286 my ($this,$failed_nick) = @_; 1287 # failed_nickの次のnickを試します。nick重複でログインに失敗した時に使います。 1288 my $next_nick = modify_nick($failed_nick, $this->isupport->{NICKLEN}); 1289 1290 my $msg_for_user = "Nick $failed_nick was already in use in the ".$this->network_name.". Trying ".$next_nick."..."; 1291 $this->send_message( 1292 $this->construct_irc_message( 1293 Command => 'NICK', 1294 Param => $next_nick)); 1295 $this->_runloop->broadcast_to_clients( 1296 $this->construct_irc_message( 1297 Prefix => $this->_runloop->sysmsg_prefix(qw(priv nick::system)), 1298 Command => 'NOTICE', 1299 Params => [$this->_runloop->current_nick,$msg_for_user])); 1300 $this->printmsg($msg_for_user); 1301} 1302 1303sub modify_nick { 1304 my $nick = shift; 1305 my $nicklen = shift || 9; 1306 1307 if ($nick =~ /^(.*\D)?(\d+)$/) { 1308 # 最後の数文字が数字だったら、それをインクリメント 1309 my $base = $1; 1310 my $next_num = $2 + 1; 1311 if (($next_num - 1) eq $next_num) { 1312 # 桁あふれしているので数字部分を全部消す。 1313 $nick = $base; 1314 } elsif (length($base . $next_num) <= $nicklen) { 1315 # $nicklen 文字以内に収まるのでこれで試す。 1316 $nick = $base . $next_num; 1317 } 1318 else { 1319 # 収まらないので $nicklen 文字に縮める。 1320 $nick = substr($base,0,$nicklen - length($next_num)) . $next_num; 1321 } 1322 } 1323 elsif ($nick =~ /_$/ && length($nick) >= $nicklen) { 1324 # 最後の文字が_で、それ以上_を付けられない場合、それを0に。 1325 $nick =~ s/_$/0/; 1326 } 1327 else { 1328 # 最後に_を付ける。 1329 if (length($nick) >= $nicklen) { 1330 $nick =~ s/.$/_/; 1331 } 1332 else { 1333 $nick .= '_'; 1334 } 1335 } 1336 return $nick; 1337} 1338 1339sub rejoin_all_channels { 1340 my ($this) = @_; 1341 # 記憶している全てのチャンネルにJOINする。 1342 # そもそもJOINしていないチャンネルは通常IrcIO::Serverは記憶していないが、 1343 # サーバーから切断された時だけは例外である。 1344 my @ch_with_key; # パスワードを持ったチャンネルの配列。要素は["チャンネル名","パスワード"] 1345 my @ch_without_key; # パスワードを持たないチャンネルの配列。要素は"チャンネル名" 1346 1347 foreach my $ch ($this->channels_list) { 1348 my $key = $ch->parameters('k'); 1349 if (defined $key && $key ne '') { 1350 push(@ch_with_key, [$ch->name, $key]); 1351 } else { 1352 push(@ch_without_key, $ch->name); 1353 } 1354 } 1355 1356 my $interval = $this->_runloop->_conf_general->join_interval; 1357 my $channels = $this->_runloop->_conf_general->join_channels_per_command; 1358 1359 $this->{rejoin_timer} = Timer->new( 1360 Name => $this->_gen_msg('rejoin timer'), 1361 Interval => $interval, 1362 Repeat => 1, 1363 Code => sub { 1364 my $timer = shift; 1365 my $remind = $channels; 1366 if ($remind && @ch_with_key) { 1367 my (@targets) = splice(@ch_with_key, 0, $remind); 1368 $this->send_message( 1369 $this->construct_irc_message( 1370 Command => 'JOIN', 1371 Params => [join(',', map { $_->[0] } @targets), 1372 join(',', map { $_->[1] } @targets)])); 1373 $remind -= scalar(@targets); 1374 } 1375 if ($remind && @ch_without_key) { 1376 my (@targets) = splice(@ch_without_key, 0, $remind); 1377 $this->send_message( 1378 $this->construct_irc_message( 1379 Command => 'JOIN', 1380 Params => [join(',', @targets)])); 1381 $remind -= scalar(@targets); 1382 } 1383 if (!@ch_with_key && !@ch_without_key) { 1384 ## nothing found 1385 $this->{rejoin_timer} = undef; 1386 $timer->uninstall; 1387 } 1388 })->install; 1389} 1390 13911; 1392