Changeset 6a6dd47 for perl/modules/jabber.pl
- Timestamp:
- Nov 6, 2006, 1:59:10 AM (18 years ago)
- Branches:
- master, barnowl_perlaim, debian, release-1.10, release-1.4, release-1.5, release-1.6, release-1.7, release-1.8, release-1.9
- Children:
- 960395d
- Parents:
- b6a253c
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
perl/modules/jabber.pl
rb6a253c r6a6dd47 2 2 use Authen::SASL qw(Perl); 3 3 use Net::Jabber; 4 use Net::DNS; 5 use Getopt::Long; 6 4 7 ################################################################################ 5 8 # owl perl jabber support … … 17 20 ################################################################################ 18 21 19 our $client; 20 our $jid; 21 our $roster; 22 our $connections; 23 our %vars; 22 24 23 25 sub onStart … … 40 42 sub onMainLoop 41 43 { 42 return if ( $client == undef);44 return if (!connected()); 43 45 44 my $status = $client->Process(0); 45 if ($status == 0 # No data received 46 foreach my $jid (keys %$connections) 47 { 48 my $client = \$connections->{$jid}->{client}; 49 50 my $status = $$client->Process(0); 51 if ($status == 0 # No data received 46 52 || $status == 1) # Data received 47 { 48 } 49 else #Error 50 { 51 queue_admin_msg("Jabber disconnected."); 52 $roster = undef; 53 $client = undef; 54 return; 55 } 56 57 if ($::shutdown) 58 { 59 $roster = undef; 60 $client->Disconnect(); 61 $client = undef; 62 return; 53 { 54 } 55 else #Error 56 { 57 do_logout($jid); 58 return; 59 } 60 61 if ($::shutdown) 62 { 63 do_logout($jid); 64 return; 65 } 63 66 } 64 67 } … … 66 69 sub blist_listBuddy 67 70 { 71 my $roster = shift; 68 72 my $buddy = shift; 69 73 my $blistStr .= " "; 70 my %jq = $ roster->query($buddy);71 my $res = $ roster->resource($buddy);74 my %jq = $$roster->query($buddy); 75 my $res = $$roster->resource($buddy); 72 76 73 77 $blistStr .= $jq{name} ? $jq{name} : $buddy->GetJID(); … … 75 79 if ($res) 76 80 { 77 my %rq = $ roster->resourceQuery($buddy, $res);81 my %rq = $$roster->resourceQuery($buddy, $res); 78 82 $blistStr .= " [".($rq{show} ? $rq{show} : 'online')."]"; 79 83 $blistStr .= " ".$rq{status} if $rq{status}; … … 90 94 sub onGetBuddyList 91 95 { 92 return "" if ($client == undef); 93 my $blist = "\n".boldify("Jabber Roster for ".$jid->GetJID('base'))."\n"; 94 95 foreach my $group ($roster->groups()) 96 { 97 $blist .= " Group: $group\n"; 98 foreach my $buddy ($roster->jids('group',$group)) 99 { 100 $blist .= blist_listBuddy($buddy); 101 } 102 } 103 104 my @unsorted = $roster->jids('nogroup'); 105 if (@unsorted) 106 { 107 $blist .= " [unsorted]\n"; 108 foreach my $buddy (@unsorted) 109 { 110 $blist .= blist_listBuddy($buddy); 111 } 112 } 113 114 $blist .= "\n"; 96 my $blist = ""; 97 foreach my $jid (keys %{$connections}) 98 { 99 my $roster = \$connections->{$jid}->{roster}; 100 if ($$roster) 101 { 102 $blist .= "\n".boldify("Jabber Roster for $jid\n"); 103 104 foreach my $group ($$roster->groups()) 105 { 106 $blist .= " Group: $group\n"; 107 foreach my $buddy ($$roster->jids('group',$group)) 108 { 109 $blist .= blist_listBuddy($roster, $buddy); 110 } 111 } 112 113 my @unsorted = $$roster->jids('nogroup'); 114 if (@unsorted) 115 { 116 $blist .= " [unsorted]\n"; 117 foreach my $buddy (@unsorted) 118 { 119 $blist .= blist_listBuddy($roster, $buddy); 120 } 121 } 122 } 123 } 124 return $blist; 115 125 } 116 126 … … 160 170 sub cmd_login 161 171 { 162 if ($client != undef) 163 { 164 queue_admin_msg("Already logged in."); 172 my $cmd = shift; 173 my $jid = new Net::XMPP::JID; 174 $jid->SetJID(shift); 175 176 my $uid = $jid->GetUserID(); 177 my $componentname = $jid->GetServer(); 178 my $resource = $jid->GetResource() || 'owl'; 179 $jid->SetResource($resource); 180 my $jidStr = $jid->GetJID('full'); 181 182 if (!$uid || !$componentname) 183 { 184 owl::error("usage: $cmd {jid}"); 165 185 return; 166 186 } 167 187 168 %muc_roster = (); 169 $client = Net::Jabber::Client->new(); 170 $roster = $client->Roster(); 188 if ($connections->{$jidStr}) 189 { 190 owl::error("Already logged in as $jidStr."); 191 return; 192 } 193 194 my ($server, $port) = getServerFromJID($jid); 195 196 $connections->{$jidStr}->{client} = Net::Jabber::Client->new(); 197 my $client = \$connections->{$jidStr}->{client}; 198 $connections->{$jidStr}->{roster} = $connections->{$jidStr}->{client}->Roster(); 171 199 172 200 #XXX Todo: Add more callbacks. 173 201 # MUC presence handlers 174 $client->SetMessageCallBacks(chat => sub { owl_jabber::process_incoming_chat_message(@_) }, 175 error => sub { owl_jabber::process_incoming_error_message(@_) }, 176 groupchat => sub { owl_jabber::process_incoming_groupchat_message(@_) }, 177 headline => sub { owl_jabber::process_incoming_headline_message(@_) }, 178 normal => sub { owl_jabber::process_incoming_normal_message(@_) }); 179 180 #XXX Todo: Parameterize the arguments to Connect() 181 my $status = $client->Connect(hostname => 'jabber.mit.edu', 182 tls => 1, 183 port => 5222, 184 componentname => 'mit.edu'); 185 202 $$client->SetMessageCallBacks(chat => sub { owl_jabber::process_incoming_chat_message(@_) }, 203 error => sub { owl_jabber::process_incoming_error_message(@_) }, 204 groupchat => sub { owl_jabber::process_incoming_groupchat_message(@_) }, 205 headline => sub { owl_jabber::process_incoming_headline_message(@_) }, 206 normal => sub { owl_jabber::process_incoming_normal_message(@_) }); 207 208 $vars{jlogin_connhash} = {hostname => $server, 209 tls => 1, 210 port => $port, 211 componentname => $componentname}; 212 213 my $status = $$client->Connect(%{$vars{jlogin_connhash}}); 214 186 215 if (!$status) 187 216 { 217 delete $connections->{$jidStr}; 218 delete $vars{jlogin_connhash}; 188 219 owl::error("We failed to connect"); 189 $client = undef; 190 return; 191 } 192 193 194 my @result = $client->AuthSend(username => $ENV{USER}, resource => 'owl', password => ''); 195 if($result[0] ne 'ok') { 220 return ""; 221 } 222 223 224 $vars{jlogin_authhash} = {username => $uid, resource => $resource, password => ''}; 225 my @result = $$client->AuthSend(%{$vars{jlogin_authhash}}); 226 if($result[0] ne 'ok') 227 { 228 if ($result[1] = "401") 229 { 230 $vars{jlogin_jid} = $jidStr; 231 delete $connections->{$jidStr}; 232 owl::start_password("Password for $jidStr: ", \&do_login_with_pw); 233 return ""; 234 } 196 235 owl::error("Error in connect: " . join(" ", $result[1..$#result])); 197 $roster = undef; 198 $client->Disconnect(); 199 $client = undef; 200 return; 201 } 202 203 $jid = new Net::Jabber::JID; 204 $jid->SetJID(userid => $ENV{USER}, 205 server => ($client->{SERVER}->{componentname} || 206 $client->{SERVER}->{hostname}), 207 resource => 'owl'); 208 209 $roster->fetch(); 210 $client->PresenceSend(priority => 1); 211 queue_admin_msg("Connected to jabber as ".$jid->GetJID('full')); 212 236 do_logout($jidStr); 237 delete $vars{jlogin_connhash}; 238 delete $vars{jlogin_authhash}; 239 return ""; 240 } 241 $connections->{$jidStr}->{roster}->fetch(); 242 $$client->PresenceSend(priority => 1); 243 queue_admin_msg("Connected to jabber as $jidStr"); 244 delete $vars{jlogin_connhash}; 245 delete $vars{jlogin_authhash}; 213 246 return ""; 214 247 } 215 248 249 sub do_login_with_pw 250 { 251 $vars{jlogin_authhash}->{password} = shift; 252 my $jidStr = delete $vars{jlogin_jid}; 253 if (!$jidStr) 254 { 255 owl::error("Got password but have no jid!"); 256 } 257 258 $connections->{$jidStr}->{client} = Net::Jabber::Client->new(); 259 my $client = \$connections->{$jidStr}->{client}; 260 $connections->{$jidStr}->{roster} = $connections->{$jidStr}->{client}->Roster(); 261 262 $$client->SetMessageCallBacks(chat => sub { owl_jabber::process_incoming_chat_message(@_) }, 263 error => sub { owl_jabber::process_incoming_error_message(@_) }, 264 groupchat => sub { owl_jabber::process_incoming_groupchat_message(@_) }, 265 headline => sub { owl_jabber::process_incoming_headline_message(@_) }, 266 normal => sub { owl_jabber::process_incoming_normal_message(@_) }); 267 268 my $status = $$client->Connect(%{$vars{jlogin_connhash}}); 269 if (!$status) 270 { 271 delete $connections->{$jidStr}; 272 delete $vars{jlogin_connhash}; 273 delete $vars{jlogin_authhash}; 274 owl::error("We failed to connect"); 275 return ""; 276 } 277 278 my @result = $$client->AuthSend(%{$vars{jlogin_authhash}}); 279 280 if($result[0] ne 'ok') 281 { 282 owl::error("Error in connect: " . join(" ", $result[1..$#result])); 283 do_logout($jidStr); 284 delete $vars{jlogin_connhash}; 285 delete $vars{jlogin_authhash}; 286 return ""; 287 } 288 289 $connections->{$jidStr}->{roster}->fetch(); 290 $$client->PresenceSend(priority => 1); 291 queue_admin_msg("Connected to jabber as $jidStr"); 292 delete $vars{jlogin_connhash}; 293 delete $vars{jlogin_authhash}; 294 return ""; 295 } 296 297 sub do_logout 298 { 299 my $jid = shift; 300 $connections->{$jid}->{client}->Disconnect(); 301 delete $connections->{$jid}; 302 queue_admin_msg("Jabber disconnected ($jid)."); 303 } 304 216 305 sub cmd_logout 217 306 { 218 if ($client) 219 { 220 $roster = undef; 221 $client->Disconnect(); 222 $client = undef; 223 queue_admin_msg("Jabber disconnected."); 307 # Logged into multiple accounts 308 if (connected() > 1) 309 { 310 # Logged into multiple accounts, no accout specified. 311 if (!$_[1]) 312 { 313 my $errStr = "You are logged into multiple accounts. Please specify an account to log out of.\n"; 314 foreach my $jid (keys %$connections) 315 { 316 $errStr .= "\t$jid\n"; 317 } 318 queue_admin_msg($errStr); 319 } 320 # Logged into multiple accounts, account specified. 321 else 322 { 323 if ($_[1] = '-a') #All accounts. 324 { 325 foreach my $jid (keys %$connections) 326 { 327 do_logout($jid); 328 } 329 } 330 else #One account. 331 { 332 my $jid = resolveJID($_[1]); 333 do_logout($jid) if ($jid ne ''); 334 } 335 } 336 } 337 else # Only one account logged in. 338 { 339 340 do_logout((keys %$connections)[0]); 224 341 } 225 342 return ""; … … 228 345 sub cmd_jlist 229 346 { 230 if (! $client)347 if (!(scalar keys %$connections)) 231 348 { 232 349 owl::error("You are not logged in to Jabber."); … … 236 353 } 237 354 238 our $jwrite_to;239 our $jwrite_thread;240 our $jwrite_subject;241 our $jwrite_type;242 355 sub cmd_jwrite 243 356 { 244 if (! $client)357 if (!connected()) 245 358 { 246 359 owl::error("You are not logged in to Jabber."); … … 248 361 } 249 362 250 $jwrite_to = ""; 251 $jwrite_thread = ""; 252 $jwrite_subject = ""; 253 $jwrite_type = "chat"; 363 my $jwrite_to = ""; 364 my $jwrite_from = ""; 365 my $jwrite_thread = ""; 366 my $jwrite_subject = ""; 367 my $jwrite_type = "chat"; 368 254 369 my @args = @_; 255 my $argsLen = @args; 256 257 JW_ARG: for (my $i = 1; $i < $argsLen; $i++) 258 { 259 $args[$i] =~ /^-t$/ && ($jwrite_thread = $args[++$i] and next JW_ARG); 260 $args[$i] =~ /^-s$/ && ($jwrite_subject = $args[++$i] and next JW_ARG); 261 $args[$i] =~ /^-g$/ && ($jwrite_type = "groupchat" and next JW_ARG); 262 263 if ($jwrite_to ne '') 264 { 265 # Too many To's 266 $jwrite_to = ''; 267 last; 268 } 269 if ($jwrite_to) 270 { 271 $jwrite_to == ''; 272 last; 273 } 274 $jwrite_to = $args[$i]; 275 } 276 277 if(!$jwrite_to) 278 { 279 owl::error("Usage: jwrite JID [-t thread] [-s 'subject']"); 280 return; 281 } 282 370 shift; 371 local @::ARGV = @_; 372 my $gc; 373 GetOptions('thread=s' => \$jwrite_thread, 374 'subject=s' => \$jwrite_subject, 375 'account=s' => \$jwrite_from, 376 'groupchat' => \$gc); 377 $jwrite_type = 'groupchat' if $gc; 378 379 if (scalar @::ARGV != 1) 380 { 381 owl::error("Usage: jwrite JID [-g] [-t thread] [-s 'subject'] [-a account]"); 382 return; 383 } 384 else 385 { 386 $jwrite_to = @::ARGV[0]; 387 } 388 389 if (!$jwrite_from) 390 { 391 if (connected() == 1) 392 { 393 $jwrite_from = (keys %$connections)[0]; 394 } 395 else 396 { 397 owl::error("Please specify an account with -a {jid}"); 398 return; 399 } 400 } 401 else 402 { 403 $jwrite_from = resolveJID($jwrite_from); 404 return unless $jwrite_from; 405 } 406 407 $vars{jwrite} = {to => $jwrite_to, 408 from => $jwrite_from, 409 subject => $jwrite_subject, 410 thread => $jwrite_thread, 411 type => $jwrite_type}; 283 412 284 413 owl::message("Type your message below. End with a dot on a line by itself. ^C will quit."); … … 286 415 } 287 416 417 #XXX Todo: Split off sub-commands into their own subroutines. 418 # It'll make them more managable. 288 419 sub cmd_jmuc 289 420 { 290 if (! $client)421 if (!connected()) 291 422 { 292 423 owl::error("You are not logged in to Jabber."); … … 294 425 } 295 426 296 if (!$_[1]) 427 my $ocmd = shift; 428 my $cmd = shift; 429 if (!$cmd) 297 430 { 298 431 #XXX TODO: Write general usage for jmuc command. … … 300 433 } 301 434 302 my $cmd = $_[1];303 304 435 if ($cmd eq 'join') 305 436 { 306 if (!$_[2]) 307 { 308 owl::error('Usage: jmuc join {muc} [password]'); 437 local @::ARGV = @_; 438 my $password; 439 my $jid; 440 GetOptions('password=s' => \$password, 441 'account=s' => \$jid); 442 443 my $muc; 444 if (scalar @::ARGV != 1) 445 { 446 owl::error('Usage: jmuc join {muc} [-p password] [-a account]'); 309 447 return; 310 448 } 311 my $muc = $_[2]; 449 else 450 { 451 $muc = @::ARGV[0]; 452 } 453 454 if (!$jid) 455 { 456 if (connected() == 1) 457 { 458 $jid = (keys %$connections)[0]; 459 } 460 else 461 { 462 owl::error("Please specify an account with -a {jid}"); 463 return; 464 } 465 } 466 else 467 { 468 $jid = resolveJID($jid); 469 return unless $jid; 470 } 471 312 472 my $x = new XML::Stream::Node('x'); 313 473 $x->put_attrib(xmlns => 'http://jabber.org/protocol/muc'); 314 474 $x->add_child('history')->put_attrib(maxchars => '0'); 315 475 316 if ($ _[3]) #password317 { 318 $x->add_child('password')->add_cdata($ _[3]);476 if ($jmuc_password) 477 { 478 $x->add_child('password')->add_cdata($jmuc_password); 319 479 } 320 480 … … 322 482 $presence->SetPresence(to => $muc); 323 483 $presence->AddX($x); 324 $c lient->Send($presence);484 $connections->{$jid}->{client}->Send($presence); 325 485 } 326 486 elsif ($cmd eq 'part') 327 487 { 328 488 my $muc; 329 if (!$_[2]) 489 my $jid; 490 if (!$_[0]) 330 491 { 331 492 my $m = owl::getcurmsg(); 332 493 if ($m->is_jabber && $m->{jtype} eq 'groupchat') 333 494 { 334 $muc = $m->{muc}; 495 $muc = $m->{room}; 496 $jid = $m->{to}; 335 497 } 336 498 else 337 499 { 338 owl::error('Usage: "jmuc part [muc]"');500 owl::error('Usage: jmuc part {muc} [-a account]'); 339 501 return; 340 502 } … … 342 504 else 343 505 { 344 $muc = $_[2]; 345 } 346 $client->PresenceSend(to => $muc, type => 'unavailable'); 506 local @::ARGV = @_; 507 GetOptions('account=s' => \$jid); 508 if (scalar @::ARGV != 1) 509 { 510 owl::error('Usage: jmuc part {muc} [-a account]'); 511 return; 512 } 513 else 514 { 515 $muc = @::ARGV[0]; 516 } 517 if (!$jid) 518 { 519 if (connected() == 1) 520 { 521 $jid = (keys %$connections)[0]; 522 } 523 else 524 { 525 owl::error("Please specify an account with -a {jid}"); 526 return; 527 } 528 } 529 else 530 { 531 $jid = resolveJID($jid); 532 return unless $jid; 533 } 534 } 535 $connections->{$jid}->{client}->PresenceSend(to => $muc, type => 'unavailable'); 536 queue_admin_msg("$jid has left $muc."); 347 537 } 348 538 elsif ($cmd eq 'invite') 349 539 { 350 540 my $jid; 541 my $invite_jid; 351 542 my $muc; 352 543 353 owl::error('Usage: jmuc invite {jid} [muc]') if (!$_[2]); 544 owl::error('Usage: jmuc invite {jid} [muc] [-a account]') if (!$_[0]); 545 $invite_jid = $_[0]; 354 546 355 if (!@_[ 3])547 if (!@_[1]) 356 548 { 357 549 my $m = owl::getcurmsg(); 358 550 if ($m->is_jabber && $m->{jtype} eq 'groupchat') 359 551 { 360 $muc = $m->{muc}; 552 $muc = $m->{room}; 553 $jid = $m->{to}; 361 554 } 362 555 else 363 556 { 364 owl::error('Usage: jmuc invite {jid} [muc] ');557 owl::error('Usage: jmuc invite {jid} [muc] [-a account]'); 365 558 return; 366 559 } … … 368 561 else 369 562 { 370 $muc = $_[3]; 563 local @::ARGV = @_; 564 GetOptions('account=s' => \$jid); 565 if (scalar @::ARGV != 2) 566 { 567 owl::error('Usage: jmuc invite {jid} [muc] [-a account]'); 568 return; 569 } 570 else 571 { 572 ($muc, $invite_jid) = @::ARGV; 573 } 574 if (!$jid) 575 { 576 if (connected() == 1) 577 { 578 $jid = (keys %$connections)[0]; 579 } 580 else 581 { 582 owl::error("Please specify an account with -a {jid}"); 583 return; 584 } 585 } 586 else 587 { 588 $jid = resolveJID($jid); 589 return unless $jid; 590 } 371 591 } 372 592 373 593 my $x = new XML::Stream::Node('x'); 374 594 $x->put_attrib(xmlns => 'http://jabber.org/protocol/muc#user'); 375 $x->add_child('invite')->put_attrib(to => $ _[2]);595 $x->add_child('invite')->put_attrib(to => $invite_jid); 376 596 377 597 my $message = new Net::Jabber::Message; 378 598 $message->SetTo($muc); 379 599 $message->AddX($x); 380 381 $client->Send($message);600 $connections->{$jid}->{client}->Send($message); 601 queue_admin_msg("$jid has invited $invite_jid to $muc."); 382 602 } 383 603 else … … 396 616 my $j = new Net::XMPP::Message; 397 617 $body =~ s/\n\z//; 398 $j->SetMessage(to => $ jwrite_to,399 from => $ jid->GetJID('full'),400 type => $ jwrite_type,618 $j->SetMessage(to => $vars{jwrite}{to}, 619 from => $vars{jwrite}{from}, 620 type => $vars{jwrite}{type}, 401 621 body => $body 402 622 ); 403 $j->SetThread($ jwrite_thread) if ($jwrite_thread);404 $j->SetSubject($ jwrite_subject) if ($jwrite_subject);405 623 $j->SetThread($vars{jwrite}{thread}) if ($vars{jwrite}{thread}); 624 $j->SetSubject($vars{jwrite}{subject}) if ($vars{jwrite}{subject}); 625 406 626 my $m = j2o($j, 'out'); 407 if ($ jwrite_typene 'groupchat')627 if ($vars{jwrite}{type} ne 'groupchat') 408 628 { 409 629 #XXX TODO: Check for displayoutgoing. 410 630 owl::queue_message($m); 411 631 } 412 $client->Send($j); 632 $connections->{$vars{jwrite}{from}}->{client}->Send($j); 633 delete $vars{jwrite}; 413 634 } 414 635 … … 499 720 { 500 721 $props{replycmd} = "jwrite ".(($dir eq 'in') ? $props{from} : $props{to}); 722 $props{replycmd} .= " -a ".(($dir eq 'out') ? $props{from} : $props{to}); 501 723 $props{isprivate} = 1; 502 724 } … … 506 728 my $room = $props{room} = $from->GetJID('base'); 507 729 $props{replycmd} = "jwrite -g $room"; 730 $props{replycmd} .= " -a ".(($dir eq 'out') ? $props{from} : $props{to}); 508 731 509 $props{sender} = $nick ;732 $props{sender} = $nick || $room; 510 733 $props{recipient} = $room; 511 734 … … 562 785 } 563 786 787 sub getServerFromJID 788 { 789 my $jid = shift; 790 my $res = new Net::DNS::Resolver; 791 my $packet = $res->search('_xmpp-client._tcp.'.$jid->GetServer(), 'srv'); 792 793 if ($packet) # Got srv record. 794 { 795 my @answer = $packet->answer; 796 return $answer[0]{target}, 797 $answer[0]{port}; 798 } 799 800 return $jid->GetServer(), 5222; 801 } 802 803 sub connected 804 { 805 return scalar keys %$connections; 806 } 807 808 sub resolveJID 809 { 810 my $givenJidStr = shift; 811 my $givenJid = new Net::XMPP::JID; 812 $givenJid->SetJID($givenJidStr); 813 814 # Account fully specified. 815 if ($givenJid->GetResource()) 816 { 817 # Specified account exists 818 if (defined $connections->{$givenJidStr}) 819 { 820 return $givenJidStr; 821 } 822 else #Specified account doesn't exist 823 { 824 owl::error("Invalid account: $givenJidStr"); 825 } 826 } 827 # Disambiguate. 828 else 829 { 830 my $matchingJid = ""; 831 my $errStr = "Ambiguous account reference. Please specify a resource.\n"; 832 my $ambiguous = 0; 833 834 foreach my $jid (keys %$connections) 835 { 836 my $cJid = new Net::XMPP::JID; 837 $cJid->SetJID($jid); 838 if ($givenJidStr eq $cJid->GetJID('base')) 839 { 840 $ambiguous = 1 if ($matchingJid ne ""); 841 $matchingJid = $jid; 842 $errStr .= "\t$jid\n"; 843 } 844 } 845 # Need further disambiguation. 846 if ($ambiguous) 847 { 848 queue_admin_msg($errStr); 849 } 850 # Not one of ours. 851 elsif ($matchingJid eq "") 852 { 853 owl::error("Invalid account: $givenJidStr"); 854 } 855 # Log out this one. 856 else 857 { 858 return $matchingJid; 859 } 860 } 861 return ""; 862 }
Note: See TracChangeset
for help on using the changeset viewer.