1 package Thrasher
::Protocol
::Purple
;
7 Thrasher::Protocol::Purple - test protocol for Thrasher Bird
11 This package is the base class for protocols using libpurple.
12 Each protocol may have slightly different details w.r.t. how they work
13 with Thrasher, which is handled by specializing them further,
14 such as with Thrasher::Protocol::Purple::AIM. As you find such
15 differences, please be sure to add hooks in here, not hack on
20 use base
'Thrasher::Protocol';
22 use Thrasher
::Component
qw(strip_resource);
23 use Thrasher
::Log
qw(log logger debug);
25 use Thrasher
::Session
;
26 use Thrasher
::XMPPStreamIn
;
27 use Glib
qw(G_PRIORITY_DEFAULT);
33 use Thrasher
::Protocol
::Purple
::Vars
qw(:all);
37 use Thrasher
::Plugin
::Vcard
;
38 use Thrasher
::Plugin
::EntityCapabilities
;
42 # Initialize the wrapper
43 THPPW
::thrasher_wrapper_init
44 (Thrasher
::error_wrap
("timeout_add", \
&_timeout_add
),
45 Thrasher
::error_wrap
("input_add", \
&_input_add
),
46 Thrasher
::error_wrap
("source_remove", \
&_source_remove
),
47 Thrasher
::error_wrap
("incoming_msg", \
&_incoming_msg
),
48 Thrasher
::error_wrap
("presence_in", \
&_presence_in
),
49 Thrasher
::error_wrap
("subscription_add", \
&_subscription_add
),
50 Thrasher
::error_wrap
("legacy_user_adding_user", \
&_legacy_user_adding_user
),
51 Thrasher
::error_wrap
("connection_error", \
&_connection_error
),
52 Thrasher
::error_wrap
("connection", \
&_connection
),
53 Thrasher
::error_wrap
("incoming_chatstate", \
&_incoming_chatstate
),
58 THPPW
::thrasher_purple_debug
($DEBUG);
60 # Initialize the remainder
61 THPPW
::thrasher_init
();
63 # Globalize component object so we can receive messages
64 our $global_component;
67 return 'Thrasher::EventLoop::Glib';
70 # This internal routine allows us to group the initial
71 # subscribe additions and throw them back.
75 if (! $session || !$session->{'jid'}) {
76 debug
("_initial_roster: No session. Must be post-logoff?");
80 debug
("_initial_roster($session->{jid}) called\n");
84 foreach my $username (keys %{$session->{initial_roster
}}) {
85 $username = $session->{protocol
}->process_remote_username($username);
86 $roster->{$username} = Thrasher
::Roster
::subscribed
();
87 # We should also handle presence info here (?)
90 $session->{protocol
}->set_current_legacy_roster($session, $roster);
92 delete $session->{initial_roster_timeout_active
};
93 $session->{initial_roster
} = [];
95 # Thrasher can now be sure the protocol is completely online and
96 # ready to e.g. send IMs queued while login was in progress.
97 if ($session->{'protocol_state'} eq 'logging in') {
98 $session->{'protocol'}->set_session_state($session, 'online');
101 # We don't want the timeout to loop, so destroy with 0
105 # This appears to only be called for things on our initial roster.
106 # FIXME: If that's true, change this.
107 sub _subscription_add
{
108 my $orig_jid = shift;
109 my $orig_sender = shift;
112 my $jid = Encode
::decode
("UTF-8", $orig_jid);
113 my $sender = Encode
::decode
("UTF-8", $orig_sender);
115 debug
("_subscription_add($orig_jid, $orig_sender) called\n");
117 my $session = $global_component->session_for($jid);
119 # Set a timeout if we have no previous jid information
120 if (not defined $session->{initial_roster
}) {
122 # Set a flag so we don't push subscription additions
124 $session->{initial_roster_timeout_active
} = 1;
126 # Heuristically manage our initial roster as we cannot
127 # tell when the libpurple protocols are done giving us the
128 # subscribe user list.
129 Glib
::Timeout
->add(5000,
135 # Verify we're actually within a timeout
136 if ($session->{initial_roster_timeout_active
}) {
137 # This is a bit ugly, but it allows us to bind sender/status
138 # info to JIDs for timeouts
139 $session->{initial_roster
}{$sender} = $status;
141 # We aren't in a timeout, we need to send new subscribe info up
145 # Subscription-in information has the presence information
146 # loaded onto it too, at least for AIM
148 _presence_in
($orig_jid, $orig_sender, undef, undef, $status);
151 debug
("_subscription_add done\n");
156 sub _legacy_user_adding_user
{
157 my $jid_target = shift;
158 my $legacy_username_adding = shift;
160 $jid_target = Encode
::decode
("UTF-8", $jid_target);
161 $legacy_username_adding = Encode
::decode
("UTF-8", $legacy_username_adding);
163 log("$legacy_username_adding requesting add for $jid_target");
165 my $session = $global_component->session_for($jid_target);
168 $session->{protocol
}->adding_contact($legacy_username_adding,
171 log("Got request to add user $jid_target, but $jid_target is "
176 sub process_message
{ return $_[1]; }
177 sub process_remote_username
{ return $_[1]; }
179 # Callback of presence in
188 $jid = Encode
::decode
("UTF-8", $jid);
189 $sender = Encode
::decode
("UTF-8", $sender);
190 $alias = Encode
::decode
("UTF-8", $alias);
191 $group = Encode
::decode
("UTF-8", $group);
192 $status = Encode
::decode
("UTF-8", $status);
193 $message = Encode
::decode
("UTF-8", $message);
195 debug
("_presence_in($jid, $sender, $status) called\n");
197 my $session = $global_component->session_for($jid);
198 my $self = $session->{protocol
};
199 if (!defined($self)) {
200 debug
("No session defined for $jid, must be post-logoff?");
204 my $clean_sender = $self->process_remote_username($sender);
206 # Nothing is done with protocol?
207 if ($status eq $purple_presence{offline
}) {
208 $self->legacy_presence_update
209 ($session, $clean_sender, 'unavailable',
212 elsif ($status eq $purple_presence{'available'}) {
213 $self->legacy_presence_update
214 ($session, $clean_sender, '',
217 elsif ($status eq $purple_presence{'away'}) {
218 $self->legacy_presence_update
219 ($session, $clean_sender, '',
222 elsif ($status eq $purple_presence{'xaway'}) {
223 $self->legacy_presence_update
224 ($session, $clean_sender, '',
228 log("Unknown presence status of $status was sent by "
229 ."$clean_sender to $jid.");
233 debug
("_presence_in done\n");
237 sub _incoming_chatstate
{
238 my ($orig_jid, $orig_sender, $state) = @_;
239 debug
("_incoming_chatstate($orig_jid, $orig_sender, $state) called\n");
242 # loosely <http://xmpp.org/extensions/xep-0085.html>
243 if ($state == $THPPW::PURPLE_TYPING
) {
244 $state_tag = 'composing';
246 elsif ($state == $THPPW::PURPLE_TYPED
) {
247 $state_tag = 'paused';
249 elsif ($state == $THPPW::PURPLE_NOT_TYPING
) {
250 $state_tag = 'inactive';
256 my $jid = Encode
::decode
('UTF-8', $orig_jid);
257 my $sender = Encode
::decode
('UTF-8', $orig_sender);
259 my $session = $global_component->session_for($jid);
261 debug
("No session?!!\n");
264 if (! $session->{'protocol'}) {
265 debug
("No session protocol?!!\n");
268 $session->{'protocol'}->incoming_chatstate($session, $sender, $state_tag);
274 my $jid = Encode
::decode
("UTF-8", $orig_jid);
275 debug
("_connection($jid) called\n");
277 my $session = $global_component->session_for($jid);
279 # Component::logout and thrasher.c:thrasher_logout() will
280 # happily destroy the session and thrasher_connection while
281 # libpurple is waiting asynchronously for connection events.
282 # Once the connection completes and libpurple starts firing
283 # callbacks, weird errors arise because the session is gone
284 # and Thrasher has lost track of what is connected.
286 # Maybe we should reject the logout and defer it to
287 # _connection{,_error}, relying on one of them always being
289 log("_connection($jid): No session? Assuming already logged out.");
290 # Ensure the thrasher_connection gets gone.
291 Glib
::Timeout
->add(1,
293 # Log off just after logon finishes, not during.
295 # Turns out purple_connection_set_state()
296 # (which called the connected ui_op) crashes
297 # if prpl_info is yanked out from under it.
298 THPPW
::thrasher_action_logout
($orig_jid);
305 my $protocol = $session->{'protocol'};
307 log("_connection($jid): No protocol?!!");
311 $session->{'purple_connection_created'} = 1;
312 my $continuation = delete($session->{'connection_cb'});
314 $continuation->($session);
317 log("_connection($jid): No connection_cb?!!");
321 # But libpurple prpl might not be ready to send IMs queued during
322 # login. Wait until _initial_roster() for online protocol_state.
324 # If after no _subscription_add()/_initial_roster() happens
325 # (perhaps the account has no current legacy roster at all?)
326 # ensure session is eventually set online anyway.
329 if (! $session->{'initial_roster_timeout_active'}
330 && ! $session->{'initial_roster'}
331 && $session->{'protocol_state'} eq 'logging in') {
332 debug
("Never called _initial_roster($session->{jid})?\n");
333 $protocol->set_session_state($session, 'online');
335 return 0; # No repeat
344 sub _connection_error
{
346 my $error_code = shift;
349 $jid = Encode
::decode
("UTF-8", $jid);
350 $message = Encode
::decode
("UTF-8", $message);
352 debug
("_connection_error($jid)\n");
354 my $session = $global_component->session_for($jid);
356 log("No session?!! Error was $error_code/'$message'.");
359 my $protocol = $session->{protocol
};
361 my $attempt_reconnect = 0;
364 if ($session->{status
} =~ /disconnecting/) {
365 log("Got error code $error_code, but ignoring it since "
366 ."we're in the middle of disconnecting.");
370 # Some of these cases are poorly tested since it's either
371 # hard or borderline impossible for them to occur.
372 # We also have to think about whether to attempt reconnection
374 switch
($error_code) {
375 case
($ERROR_NETWORK_ERROR) {
376 $protocol->network_error($jid);
377 $error = "Network error, attempting reconnection";
378 $attempt_reconnect = 1;
380 case
($ERROR_INVALID_USERNAME) {
381 $protocol->invalid_username($jid);
382 $error = "Remote server reports invalid username; please reregister";
384 case
($ERROR_AUTHENTICATION_FAILED) {
385 $protocol->wrong_authentication($jid);
386 $error = "Username or password invalid; please register with correct information";
388 case
($ERROR_AUTHENTICATION_IMPOSSIBLE) {
389 $protocol->_handle_error
390 ($jid, 'Thrasher Bird can not negotiate an '
391 .'authentication technique with the remote '
392 .'service', 'service_unavailable');
393 # This is a bad one, we don't know what to do.
394 $error = "Authentication impossible";
396 case
($ERROR_NO_SSL_SUPPORT) {
397 $protocol->_handle_error
398 ($jid, 'libpurple was compiled without SSL '
399 .'support, but SSL is required by the '
400 .'remote service.', 'service_unavailable');
401 $error = "Thrasher Bird is unable to connect";
403 case
($ERROR_ENCRYPTION_ERROR) {
404 $protocol->_handle_error
405 ($jid, 'There was an error negotiating SSL with '
406 .'the remote service, or the remote service '
407 .'does not support encryption but an account '
408 .'option was set to require it.',
409 'service_unavailable');
410 $error = "Thrasher Bird is unable to connect";
412 case
($ERROR_NAME_IN_USE) {
413 $protocol->name_in_use($jid);
414 $error = "The remote service reports your username is in use";
416 case
($ERROR_INVALID_SETTINGS) {
417 $protocol->invalid_username($jid);
418 $error = "Remote server reports invalid username; please reregister";
420 case
($ERROR_OTHER_ERROR) {
421 my $error_message = "Unknown connection error.";
423 $error_message .= ' The legacy service reported: '
426 $protocol->_handle_error
427 ($jid, $error_message, 'internal_server_error');
430 log("Got connection error: $error_code for $jid");
434 # This needs to be kept in sync with libpurple's
435 # connection.c -> purple_connection_is_fatal, which
436 # tracks whether libpurple is going to automatically
437 # log out our connection in purple_connection_error_reason.
438 my $purple_will_kill = !($error_code == $ERROR_NETWORK_ERROR ||
439 $error_code == $ERROR_ENCRYPTION_ERROR);
440 $session->{purple_will_kill
} = $purple_will_kill;
441 $session->{purple_will_kill
} ||= $protocol->purple_forces_kill;
443 $protocol->{component
}->logout($session, undef,
446 # Probe the user's presence to trigger a re-connect attempt
447 # if they are still online. They may have gone offline in the
448 # meantime, in which case we don't want to reconnect.
449 if ($attempt_reconnect) {
450 my $full_jid = $session->{full_jid
};
452 $protocol->{component
}->send_presence_xml($full_jid,
457 log("Going to attempt reconnect in 15 seconds for $session->{full_jid}");
459 _timeout_add
(15000, $callback, undef, "Reconnect $session->{full_jid}");
461 # If you want C-end handling, we need to throw some returns above
465 # Callback for incoming messages
472 $jid = Encode
::decode
("UTF-8", $jid);
473 $sender = Encode
::decode
("UTF-8", $sender);
474 $message = Encode
::decode
("UTF-8", $message);
476 debug
("_incoming_msg from $sender for $jid\n");
478 my $session = $global_component->session_for($jid);
479 my $protocol = $session->{protocol
};
481 my $clean_sender = $session->{protocol
}->process_remote_username($sender);
483 # Type is currently hard coded...
484 $protocol->sending_message($clean_sender, $session->{legacy_login
},
487 debug
("_incoming_msg done\n");
489 # Thrasher::Protocol::sending_message currently has no returned value
496 my $registration_info = shift;
498 debug
("###registration($jid) called");
500 # As a special case, if the registration info's username is
501 # "fail", we return an error given by $registration_info->{password}.
502 if ($registration_info->{username
} eq 'fail') {
503 return 0, $registration_info->{password
};
505 return $self->SUPER::registration
($jid, $registration_info);
509 # This really should be overridden
510 sub name
{ 'Purple' }
512 sub identifier
{ 'aim' }
514 # This method identifies which protocol we're using in Pidgin.
518 die "prpl not set up for " . ref($self);
521 sub create_login_session
{
523 my $continuation = shift;
524 my $registration_info = shift;
525 my $full_jid = shift;
526 my $component = shift;
527 my $jid = strip_resource
($full_jid);
529 debug
("###create_login_session($full_jid)");
531 # FIXME: Check for existing logins.
532 my $session = new Thrasher
::Session
($full_jid,
535 $registration_info->{username
});
536 $global_component = $component;
537 $self->set_session_state($session, 'logging in');
538 $component->set_session_for($jid, $session);
540 for my $key (keys %$registration_info) {
541 $registration_info->{$key} =
542 Encode
::encode
("UTF-8", $registration_info->{$key});
545 if (!$self->valid_id($registration_info->{username
}) ||
546 !$self->valid_password($registration_info->{password
})) {
547 $self->wrong_authentication($full_jid);
548 $continuation->('not_acceptable');
549 $component->logout($session);
553 # 0 => no thrasher_action_login error.
554 my $login_error = THPPW
::thrasher_action_login
({
555 jid
=> Encode
::encode
("UTF-8", $jid),
556 proto
=> $self->prpl,
559 if ($login_error == 2) {
560 # PurpleAccount already exists. But if component called here,
561 # the session must already be gone. Thus, must have logged out
562 # during the async libpurple connection attempt and now trying
565 # Reject for now. Eventually _connection or _connection_error
566 # will come back and login attempts will be possible again.
568 # Must not be confused with the bad credentials case lest
569 # Component put the failure in authentication_errors and lock
570 # logins until the registration changes.
572 # Could have this session "take over" the PurpleAccount, but
573 # what if credentials differ?
574 $continuation->('conflict');
575 $component->logout($session);
578 elsif ($login_error != 0) {
579 # Rejected before we're even trying to connect pretty
580 # much means syntactically invalid credentials
581 $continuation->('not_acceptable');
582 $component->logout($session);
586 $session->{'connection_cb'} = $continuation;
593 debug
("###initial_login called");
595 $session->{logged_in
} = 1;
602 # FIXME: Can occur if the first action after aim.transport comes
603 # online is to unregister.
604 if ($global_component) {
605 my $session = $global_component->session_for($jid);
607 $self->{component
}->logout($session);
611 log("What? No \$global_component in remove?!?");
614 # A user who attempted to unregister while the transport was
615 # offline won't log in when it comes back up (and thus doesn't
616 # need to log out) but might still be registered with the backend.
617 $self->{backend
}->remove($jid);
623 my $target_name = shift;
624 my $continuation = shift;
626 debug
("###subscribe($session->{jid}, $target_name) called");
628 $session->{subscribed
}->{$target_name} = 1;
630 THPPW
::thrasher_action_buddy_add
(Encode
::encode
("UTF-8",
632 Encode
::encode
("UTF-8", $target_name));
640 my $target_name = shift;
641 my $continuation = shift;
643 debug
("###unsubscribe($session->{jid}, $target_name) called");
645 if (!(delete $session->{subscribed
}->{$target_name})) {
646 print STDERR
"Warning, removing nonexistant contact\n";
649 THPPW
::thrasher_action_buddy_remove
(Encode
::encode
("UTF-8",
651 Encode
::encode
("UTF-8", $target_name));
658 my ($session, $continuation) = @_;
660 debug
("###logout($session->{jid}) called");
662 if ($session->{purple_connection_created
}
663 && !$session->{purple_will_kill
}) {
664 THPPW
::thrasher_action_logout
(Encode
::encode
("UTF-8", $session->{jid
}));
666 elsif (! $session->{purple_connection_created
}) {
667 debug
('No purple connection created to log out.');
670 $continuation->($session);
671 return $self->SUPER::logout
(@_);
674 sub debug_logged_in
{
675 my $component = $global_component;
677 debug
("No component?!!\n");
681 my $protocol = $component->{'protocol'};
683 debug
("No protocol?!!\n");
687 print STDERR
'prpl = ' . $protocol->prpl() . "\n";
689 if ($protocol->{'username_to_session'}) {
690 print STDERR
"protocol->username_to_session:\n";
691 while (my ($legacy_name, $session)
692 = each(%{$protocol->{'username_to_session'}})) {
693 print STDERR
"\t$legacy_name => $session\n";
697 debug
("No username_to_session?!!\n");
700 if ($component->{'sessions'}) {
701 print STDERR
"component->sessions:\n";
702 while (my ($jid, $session) = each(%{$component->{'sessions'}})) {
703 print STDERR
"\t$jid => $session\n";
707 debug
("No component sessions?!!\n");
710 THPPW
::thrasher_action_debug_logged_in
();
716 my ($session, $to, $body_text, $type, $error_sub) = @_;
718 debug
("###send_message called");
719 if ($session->{'protocol_state'} eq 'logging in') {
720 debug
("###send_message deferred; $session->{jid} still logging in.\n");
721 $session->on_connection_complete(sub {
722 $self->send_message(@orig_args);
727 $body_text = $self->process_message($body_text);
729 debug
("###Message From: ".$session->{jid
}.", To: $to, body: $body_text\n");
731 my $result = THPPW
::thrasher_action_outgoing_msg
732 (Encode
::encode
("UTF-8", $session->{jid
}),
733 Encode
::encode
("UTF-8", $to),
734 Encode
::encode
("UTF-8", $body_text));
735 debug
("Message send result: $result\n");
738 sub outgoing_chatstate
{
739 my ($self, $session, $to, $chatstate) = @_;
740 debug
("###outgoing_chatstate($session->{jid}, $to, $chatstate)\n");
742 our $chatstate_to_purple ||= {
743 'composing' => $THPPW::PURPLE_TYPING
,
744 'paused' => $THPPW::PURPLE_TYPED
,
745 'inactive' => $THPPW::PURPLE_NOT_TYPING
,
746 'active' => $THPPW::PURPLE_NOT_TYPING
,
748 my $purple_typing_state = $chatstate_to_purple->{$chatstate};
749 if (! defined($purple_typing_state)) {
750 debug
("Untranslated chatstate: '$chatstate'\n");
754 THPPW
::thrasher_action_outgoing_chatstate
($session->{'jid'},
756 $purple_typing_state);
763 my $component = shift;
764 my $legacy_username = shift;
766 debug
("###subscribed called: $legacy_username permitted for $session->{jid}");
768 THPPW
::thrasher_action_buddy_authorize
769 (Encode
::encode
("UTF-8", $session->{jid
}),
770 Encode
::encode
("UTF-8", $legacy_username));
772 $self->SUPER::subscribed
($session, $component, $legacy_username);
778 my $component = shift;
779 my $legacy_username = shift;
781 debug
("###unsubscribed($session->{jid}, $legacy_username) called");
783 if (!defined($legacy_username)) {
784 confess
"Unsubscribing an undef user; shouldn't be called.";
787 THPPW
::thrasher_action_buddy_deauthorize
788 (Encode
::encode
("UTF-8", $session->{jid
}),
789 Encode
::encode
("UTF-8", $legacy_username));
793 my ($self, $id) = @_;
795 THPPW
::thrasher_action_ft_ui_ready
($id);
796 return 1; # repeat this notification.
803 return "Gateway prompt";
809 return $self->{gateway_desc
};
812 sub user_presence_update
{
815 my $type = shift || '';
816 my $show = shift || '';
817 my $status = shift || '';
819 debug
("user_presence_update called\n");
823 # State table for type/show to purple_status
824 if ($show eq 'away') {
828 $purple_status = $purple_presence{'xaway'};
832 $purple_status = $purple_presence{'away'};
836 logger
("Unknown type/show of [$type/$show]");
839 elsif ($show eq 'chat' || $show eq '') {
842 # This seems like it might have more states
843 $purple_status = $purple_presence{'available'};
845 elsif ($type eq 'unavailable') {
847 $purple_status = $purple_presence{'offline'};
850 logger
("Unknown type/show of [$type/$show]");
854 logger
("Unknown type/show of [$type/$show] (show is completely unrecognized)");
857 THPPW
::thrasher_action_presence
858 (Encode
::encode
("UTF-8", $session->{jid
}),
859 $purple_status, # integer does not need encoding
860 Encode
::encode
("UTF-8", $status));
862 #debug("User presence update: type: $type, show: $show, purple: $purple_status, status: $status");
865 # Don't do anything with this right now.
866 sub user_targeted_presence_update
{
870 my $type = shift || '';
871 my $show = shift || '';
872 my $status = shift || '';
873 my $target_user = shift || '';
875 #log("User presence update to $target_user: type: $type, show: $show, status: $status");
878 # Subrefs for which to satiate the libpurple monster
880 my $interval = shift;
884 debug
("perl::timeout_add called\n", 3);
886 debug
("\tinterval = $interval\n", 3) if $interval;
887 debug
("\tcode = $code\n", 3) if $code;
888 debug
("\ttrigger = $trigger\n", 3) if $trigger;
890 my $ret = Glib
::Timeout
->add($interval,
894 debug
("Glib::Timeout->add returned [$ret]\n", 3);
900 debug
("perl::timeout_remove called with $_[0]\n", 3);
902 return Glib
::Source
->remove($_[0]);
912 debug
("_input_add\n", 3);
916 debug
("\t$i = $_\n");
921 debug
("\tfd = $fd\n", 3) if $fd;
922 debug
("\tcond = $cond\n", 3) if $cond;
923 debug
("\tcode = $code\n", 3) if $code;
924 debug
("\ttrigger = $trigger\n", 3) if $trigger;
926 $cond = ($cond == 1) ?
'G_IO_IN' : 'G_IO_OUT';
928 $cond = [$cond, 'G_IO_ERR', 'G_IO_HUP', 'G_IO_NVAL'];
930 my $ret = Glib
::IO
->add_watch($fd,
936 debug
("Glib::IO->add_watch returned [$ret]\n", 3);
938 debug
("_input_add done\n", 3);
943 # Returns if the given ID is a valid id for the service. This avoids
944 # some problems that services have when you jam illegal logins in.
945 # For instance, log in to Yahoo with a Japanese username, and it
946 # just hangs on the connection, rather than doing anything.
947 # Note that this is more about not sending in logins that confuse
948 # the remote services so badly we get no errors, NOT about precisely
949 # labelling which fields are possible. If the remote service correctly
950 # determines the password is invalid, then everything's fine.
956 my ($self, $password) = @_;
958 # If the prpl requires a password, _purple_connection_new() will
959 # fail when password is NULL or zero-length without returning an
960 # error thrasher_login() can detect. Worse, the check in
961 # purple_account_connect() is slightly different so it wouldn't
962 # even be detectable through purple_account_request_password() and
965 # Registering with an empty password therefore begins an
966 # apparently successful async login that never completes or
967 # errors. The user also can't re-register or log out because
968 # they're already "logging in". :(
970 # If the prpl has OPT_PROTO_PASSWORD_OPTIONAL or OPT_PROTO_NO_PASSWORD
971 # the corresponding subclass should override this.
975 sub purple_forces_kill
{ return 0; }