Net::REPL::Client: Allow read/print portions to be overridden separately.
[thrasher.git] / perl / lib / Thrasher / Protocol.pm
blobc64f4d469def9670140c072ba878b91cf24bb6fe
1 package Thrasher::Protocol;
2 use strict;
3 use warnings;
5 use Thrasher::Log qw(:all);
6 use Thrasher::Constants qw(:all);
7 use Thrasher::Callbacks qw(:all);
8 use Thrasher::Roster qw(roster_diff);
10 =head1 NAME
12 Thrasher::Protocol - base class/interface for protocol differences.
14 =head1 DESCRIPTION
16 Thrasher::Protocol abstracts out the various protocol differences
17 into a simple interface to implement them. Because there are minor
18 differences, especially in registration and such, it's slightly
19 more complicated than
21 Note that, theoretically, Thrasher can turn anything that conforms
22 to the API protocol defined in this module into a transport; it
23 doesn't actually have to back to libpurple! Twitter transport,
24 anybody?
26 If you're adding a libpurple protocol, you probably want to subclass
27 from Thrasher::Protocol::LibPurple which will do a lot of the
28 libpurple work for you.
30 Protocol differences include, but are not limited to:
32 =over 4
34 =item *
36 Name of the protocol.
38 =item *
40 Registration instructions.
42 =item *
44 Registration result; this object will recieve notification
45 when a registration is completed.
47 =item *
49 How to actually perform the various work parts of the protocol.
51 =item *
53 Methods to reflect actions from the protocol back out to the
54 component. The Thrasher::Protocol superclass will implement the
55 communication with the ::Component, and that may be sufficient for
56 your protocol, but the ::Protocol gets a chance at the call just
57 in case you need something.
59 =back
61 Some of the methods below have a useful default implementation that
62 will work for the vast majority of implementations, but some don't.
63 Some methods can be entirely overridden, some must call the superclass.
65 =head1 METHODS
67 Only the top-level configuration file will call:
69 =over 4
71 =item *
73 C<new>($configuration, $backend): The configuration argument hash is
74 what is passed in by component.pl.
76 The default implementation copies these into keys of $self.
78 =back
80 =head2 Component Interface
82 These are things that the ::Component will call the ::Protocol with.
84 =over 4
86 =cut
88 sub new {
89 my $class = shift;
90 my $self = {};
91 bless $self, $class;
93 $self->{configuration} = shift;
94 $self->{backend} = shift;
96 return $self;
99 =pod
101 =item *
103 C<registration_xml>(): Should return the instructions for
104 registration, in the XML format favored by Thrasher::XMPPStreamOut.
105 Be sure to put the tags in the correct $NS_REGISTER namespace.
107 If the user is already registered according to the backend,
108 a <registered/> tag should be emitted, in accordance with the
109 standard section 4.2.1 #2, as well as returning the relevant
110 values for the registration.
112 This will automatically be used as the children of a <query> tag,
113 so you should just return the children (in an array ref).
115 A default implementation is provided, based on the C<registered>
116 method of the backend, as long the protocol only needs a
117 standard username and password.
119 =cut
121 sub registration_xml {
122 my $self = shift;
123 my $jid = shift;
125 my $registration = $self->{backend}->registered($jid);
127 return [[[$NS_REGISTER, 'instructions'], {},
128 ['Please provide your username and password for '
129 .$self->name]],
130 ($registration ? ([[$NS_REGISTER, 'registered'], {}, []])
131 : ()),
132 [[$NS_REGISTER, 'username'], {},
133 [$registration->{username} ? ($registration->{username}) : ()]],
134 [[$NS_REGISTER, 'password'], {},
135 [$registration->{password} ? ($registration->{password}) : ()]]];
138 =pod
140 =item *
142 C<registration_items>: Should return a list of elements that are
143 required to login. This will be verified against the user's
144 registration at login time to verify that they have all required
145 elements.
147 The default implementation returns qw(username password).
149 If the user only has some pieces, the user is completely
150 unregistered.
152 =cut
154 # This addresses a bug where users were somehow able to have only a
155 # password in the DB. I'm not sure how this is possible, but since
156 # it's a segfault for libpurple for this to happen, it has to be
157 # stopped, both by fixing the real problem and by making sure that
158 # even if it happens it still doesn't crash us.
160 sub registration_items {
161 return qw(username password);
164 =pod
166 =item *
168 C<registration_defaults>: Should return a hashref of values indicating
169 the defaults for registration, if given. If the user fails to pass
170 back a value, but there is a default, the default will be used.
172 A value must be given for each item or the registration will fail.
173 username and password should not be given defaults (unless you have
174 a really good reason of some kind), but since other clients sometimes
175 choke on anything other than having a username and password, we
176 need to be able to provide defaults for such clients.
178 The default implementation returns {}, no surprise.
180 =cut
182 # The temptation to write
183 # sub registartion defaults { { } }
184 # was strong...
185 sub registration_defaults {
186 return {};
189 =pod
191 =item *
193 C<name>(): Should return the name of the protocol, suitable for
194 concatenation with the string " Gateway" for describing the gateway.
196 The default implementation returns $self->{name}, but that won't be
197 useful if you don't defined a ->{name}.
199 =cut
201 sub name {
202 return $_[0]->{name};
205 =pod
207 =item *
209 C<identifier>(): Should return a lowercase-letter name of the protocol
210 suitable for use in the domain of the component. i.e., the MSN
211 transport might return "Microsoft Messenger" for the call to C<name>,
212 but should return "msn" for this method.
214 The default implemenation returns $self->{identifier}, but that won't
215 be useful if you didn't define a ->{identifier}.
217 =cut
219 sub identifier {
220 return $_[0]->{identifier};
223 =pod
225 =item *
227 C<event_loop>(): Should return a Perl module conforming to the
228 interface set out in Thrasher::EventLoop that can be used to set
229 up the event loop for Thrasher Bird, including both whatever
230 loop is needed for the protocol loop and for talking to the XMPP
231 server.
233 =cut
235 sub event_loop {
236 return 'Thrasher::EventLoop';
239 =pod
241 =item *
243 C<gateway_desc>($lang): Should return the thing you want to prompt the
244 user with, in order to ask them for their user name. See the examples,
245 or see XEP-0100 section 6.3. This corresponds to the
246 <desc> tag, hence the klunky name.
248 $xml_lang will be the language requested by the user, as seen in their
249 xml language attribute in the query. It will already have been
250 normalized to 'en' if not specified.
252 The default implementation returns undef, which means "don't give
253 a description", which the standard permits.
255 =cut
257 sub gateway_desc {
258 my $self = shift;
259 return undef;
262 =pod
264 =item *
266 C<gateway_prompt>($lang): Should return the simple name of the
267 thing you are asking for when adding a contact. See XEP-0100
268 section 6.3. This corresponds to the <prompt> tag, hence the method
269 name.
271 You must implement this.
273 =cut
275 sub gateway_prompt {
276 my $self = shift;
277 die "Method gateway_prompt not implemented in " . ref($self);
280 =pod
282 =item *
284 C<registration>($from_jid, $registration_info): $registration_info is
285 a hash ref containing whatever the user sent back.
287 This should return a list with the following elements:
289 =over 4
291 =item *
293 A boolean indicating if the registration was successful.
295 =item *
297 An identifier which is a key in the C<Thrasher::Component::IQ_ERRORS>
298 which represents the error to be sent back to the user.
300 =back
302 The second is not needed if the registration was successful.
304 Note that XEP-0100 specifies that the error that should result
305 if the user's username and password failed to verify is
306 'not_acceptable' (note Component.pm uses an underscore).
308 The default implementation passes this on to the backend's
309 C<register> method, after processing the registration defaults.
311 =cut
313 sub registration {
314 my $self = shift;
315 my $jid = shift;
316 my $registration_info = shift;
318 log("Registration defaults: " . Dumper($self->registration_defaults));
320 my $registration_defaults = $self->registration_defaults;
321 for my $key (keys %$registration_defaults) {
322 $registration_info->{$key} ||= $registration_defaults->{$key};
325 use Data::Dumper;
326 log("Registering with: " . Dumper($registration_info));
328 $self->{backend}->register($jid, $registration_info);
331 =pod
333 =item *
335 C<remove>($from_jid): The given JID (with no resource) is
336 unregistering from the transport. The protocol likely doesn't care,
337 but it might. The backend certainly does. The default implementation
338 passes this along to the backend.
340 You should probably extend this to disconnect the user from the
341 legacy protocol.
343 =cut
345 sub remove {
346 my $self = shift;
347 my $jid = shift;
349 $self->{backend}->remove($jid);
352 # FIXME - rewrite this for clarity.
354 =pod
356 =item *
358 C<login>($continuation, $registration_info, $full_jid, $component): This
359 should generate either a Thrasher::Connection object corresponding to
360 the given login information (possibly an existing object if we somehow
361 are logging in without having logged off), OR an string corresponding
362 to an entry in C<%Thrasher::Component::IQ_ERRORS> indicating
363 an error.
365 $full_jid is the JID of the user logging in, with resource.
367 $registration_info will be a hash corresponding to the one requested
368 by C<registration_xml>.
370 $component is a reference to the component generating the login
371 request. This should be used to reflect the initial presence
372 state of the legacy subscribed users back out along the connection.
374 Note the use of "generate" rather than "return"; the generated
375 Thrasher::Connection or error string should be passed into the
376 continuation. This is because logging on could be a lengthy
377 operation and we of course can't pause for that. Each continuation is
378 thus a user-specific closure--wires will get crossed if the wrong
379 continuation is called for an account.
381 The Thrasher::Protocol object is also expected to remember the connection.
383 The protocol specifies that you should try to translate the error
384 you receive into the set of XMPP errors you can send.
386 This method's default implementation is to forward these parameters
387 along to the create_login_session, which will either create a
388 session and return it, or return a string corresponding to
389 an error. This method will then store that session with:
391 $self->{logged_in}->{$registration_info->{username}} = $session;
393 which is necessary for routing info later.
395 Note this is accomplished by adding on to the continuation, so
396 create_login_session is still documented correctly; it, too, must
397 call the continuation.
399 Generally, you won't need to override this method; what you want
400 to override is C<create_login_session>.
402 =cut
404 sub login {
405 my $self = shift;
406 my $continuation = shift;
407 my $registration_info = shift;
409 my $new_continuation = sub {
410 my $session_or_error = shift;
412 if (ref($session_or_error)) {
413 $self->{username_to_session}->{$registration_info->{username}} =
414 $session_or_error;
417 $continuation->($session_or_error);
420 $self->create_login_session
421 ($new_continuation, $registration_info, @_);
424 =pod
426 =item *
428 C<logout>($session, $continuation): This should log off the user
429 from the legacy protocol. $session will be a Thrasher::Session
430 object, which you should have loaded up with any necessary information
431 in your login method call.
433 Once the logoff has been completed, call the $continuation with
434 no arguments.
436 C<logout> must not fail, and should run $continuation in some reasonable
437 period of time.
439 Be sure to consider the state of the current connection.
441 The default implementation does not run $continuation and is thus
442 never sufficient. It exists only to manage some internal bookkeeping
443 for other methods' default implementations.
445 =cut
447 sub logout {
448 my $self = shift;
449 my $session = shift;
450 my $continuation = shift;
452 my $legacy_login = $session->{'legacy_login'};
453 delete($self->{'username_to_session'}->{$legacy_login});
456 =pod
458 =item *
460 C<subscribe>($session, $legacy_name, $continuation): This should
461 subscribe the user to the given $legacy_name on the legacy service.
462 $legacy_name has been converted to be the user name expected by
463 the legacy service already, you don't need to "unescape" it.
465 The continuation should be called with either a true value,
466 indicating the subscription was successful, or a false value,
467 indicating it failed for any reason. (The standard assumes it
468 was because the remote user rejected it, so things like "the
469 user doesn't exist" will be simply failures.)
471 This has no default implementation.
473 =cut
475 sub subscribe {
476 my $self = shift;
477 my $legacy_name = shift;
478 my $continuation = shift;
480 die "subscribe method not implemented in " . ref($self);
483 =pod
485 =item *
487 C<unsubscribe>($session, $legacy_name, $continuation): This
488 should unsubscribe the user to the given $legacy_name on the
489 legacy service. The continuation should be called when
490 this is complete, with no arguments. (XMPP basically assumes
491 that unsubscriptions always complete.)
493 =cut
495 sub unsubscribe {
496 my $self = shift;
498 die "unsubscribe method not implemented in " . ref($self);
501 =pod
503 =item *
505 C<send_message>($session, $to, $body_text, $type, $error_sub):
506 Send the given message to the given user of the given (XMPP message)
507 type. That is, the XMPP user has sent a message to
508 somebody@transport.type, and this method needs to implement the
509 sending of that message along the transport.
511 If the message sending fails, call the $error_sub with the appropriate
512 error name. The standard specifies (converted to Thrasher's
513 constants):
515 =over 4
517 =item *
519 C<item_not_found>: Legacy User address is not valid.
521 =item *
523 C<registration_required>: Jabber User is not registered with Gateway.
525 C<service_unavailable>: Legacy User is offline and Legacy Service (or
526 Gateway) does not provide offline message storage.
528 C<remote_server_timeout>: Legacy Service cannot be reached.
530 =back
532 C<$to> will be the legacy name, not the JID. If you need them, you can
533 either convert them back out or get the sender JID from the session.
535 This has no default implementation.
537 =cut
539 sub send_message {
540 my $self = shift;
541 die "send_message method not implemented in " . ref($self);
544 =pod
546 =item *
548 C<outgoing_chatstate>($session, $to, $chatstate):
549 Called when a chatstates stanza is received from the XMPP user
550 directed at the legacy username C<$to>. C<$to> is as for send_message.
552 The default implementation is to ignore chatstates.
554 =cut
556 sub outgoing_chatstate {
557 my ($self, $session, $to, $chatstate) = @_;
558 # ignore!
561 =pod
563 =item *
565 C<subscribed>($session, $component, $legacy_username): The user has accepted
566 the subscription request by the given legacy username; handle it
567 as you should. ::Session actually takes care of the presence tags.
569 The default implementation will propogate presence information stored
570 in $session->{presence_waiting_for_subscribe} as appropriate. You will
571 need to override this method, but the override should call this back.
573 =cut
576 sub subscribed {
577 my $self = shift;
578 my $session = shift;
579 my $component = shift;
580 my $legacy_username = shift;
582 if (my $presence_info =
583 delete $session->{presence_waiting_for_subscribe}->{$legacy_username}) {
584 log("Using stored presence information for $legacy_username");
585 $component->send_presence($session->{jid}, $legacy_username,
586 @$presence_info);
590 =pod
592 =item *
594 C<unsubscribed>($session, $component, $legacy_username): The user has rejected
595 a subscription request or removed a subscription. Handle it as
596 you should for your protocol.
598 This has no default implementation.
600 =cut
602 sub unsubscribed {
603 my $self = shift;
604 die "unsubscribed method not implemented in " . ref($self);
607 =pod
609 =item *
611 C<ft_local_ready>($ft_id):
612 Called when the file transfer proxy to the user is ready for I/O to
613 the remote protocol.
615 Must return true of the protocol should continue to receive this
616 notification when the proxy is ready. If the protocol will somehow
617 manage this itself, return false.
619 =over 4
621 =item *
623 C<ft_id>: Protocol's file transfer identifier or object.
625 =back
627 =cut
629 sub ft_local_ready {
630 my ($self, $id) = @_;
631 die 'ft_local_ready method not implemented in ' . ref($self);
634 =pod
636 =back
638 =head2 Protocol Interface
640 These are methods that you will call in your protocol handler to
641 inform the component about events. Default implementations are
642 provided in Thrasher::Protocol, and are likely to be sufficient
643 for your needs.
645 Note how all these method names have gerund names.
647 =over 4
649 =item *
651 C<adding_contact>($legacy_username_from, $jid_to): A legacy user
652 is adding a user on this transport to their roster. The component will
653 handle the XML going out to the user when you call
654 C<$component->add_contact($jid_to, $legacy_username_from)>.
656 The default implementation of this does that and is probably
657 sufficient.
659 =cut
661 sub adding_contact {
662 my $self = shift;
663 my $legacy_username_from = shift;
664 my $subscription_to = shift;
666 my $subscription_from = $self->{backend}->legacy_name_to_jid
667 ($subscription_to, $legacy_username_from,
668 $self->{component}->{component_name}, 'en');
670 log("Adding contact: $legacy_username_from -> $subscription_to");
672 my $session = $self->{component}->session_for($subscription_to);
673 if (!defined($session)) {
674 log("Getting a contact addition request for a username that "
675 ."doesn't seem to be logged in: $legacy_username_from is "
676 ."asking to subscribe to $subscription_to with the "
677 .$self->name . " protocol.");
678 return;
681 my $component = $session->{component};
682 my $jid_to = $session->{jid};
683 # Implements section 5.1.1 #2
684 $component->add_contact($jid_to, $legacy_username_from);
686 $self->{backend}->set_roster_user_state
687 ($session->{jid}, $subscription_from,
688 $self->{backend}->want_subscribe);
691 =pod
693 =item *
695 C<deleting_contact>($subscription_from, $subscription_to): A
696 legacy user is removing the XMPP user from their subscription
697 list.
699 The default implementation of this is probably sufficient.
701 =cut
703 sub deleting_contact {
704 my $self = shift;
705 my $subscription_from = shift;
706 my $subscription_to = shift;
708 log("Deleting contact: $subscription_from -> $subscription_to");
710 my $session = $self->{username_to_session}->{$subscription_to};
711 if (!defined($session)) {
712 log("Getting a contact removal request for a username that "
713 ."doesn't seem to be logged in: $subscription_from is "
714 ."asking to unsubscribe from $subscription_to with the "
715 .$self->name . " protocol.");
716 return;
719 my $component = $session->{component};
720 my $jid_to = $session->{jid};
721 $component->delete_contact($jid_to, $subscription_from);
723 $self->{backend}->set_roster_user_state
724 ($session->{jid}, $subscription_from,
725 $self->{backend}->unsubscribed);
728 =pod
730 =item *
732 C<sending_message>($legacy_from, $legacy_to, $message, $is_xhtml_ish):
733 A legacy user has sent a message. $is_xhtml_ish is propagated to the
734 ::Component::send_message method.
736 The default implementation of this is probably sufficient.
738 =cut
740 sub sending_message {
741 my $self = shift;
742 my $legacy_from = shift;
743 my $legacy_to = shift;
744 my $message = shift;
745 my $is_xhtml_ish = shift;
747 my $session = $self->{username_to_session}->{$legacy_to};
748 my $component = $session->{component};
749 my $jid_to = $session->{jid};
751 my $jid_from = $self->{backend}->legacy_name_to_jid
752 ($jid_to, $legacy_from, $component->{component_name},
753 'en'); # FIXME: Should know lang in component
755 $component->send_message($jid_from, $jid_to, $message, {
756 is_xhtml_ish => $is_xhtml_ish,
757 children => [ [[ $NS_CHATSTATES, 'active' ], {}, []] ],
761 =pod
763 =item *
765 C<initial_login>($session): This is a chance to be called to be
766 notified about the initial successful login for a given session,
767 an opportunity to do something with the session.
769 The default behavior of this function is to set the current
770 session state to "online".
772 =cut
774 sub initial_login {
775 my $self = shift;
776 my $session = shift;
778 $self->set_session_state($session, 'online');
781 =pod
783 =item *
785 C<incoming_chatstate>($session, $sender, $state_tag): Send a message
786 from the legacy $sender with only the given chatstate $state_tag for
787 the $session.
789 =cut
791 sub incoming_chatstate {
792 my ($self, $session, $sender, $state_tag) = @_;
794 my $clean_sender = $self->process_remote_username($sender);
795 my $component = $session->{'component'};
796 my $jid_from = $self->{'backend'}->legacy_name_to_jid(
797 $session->{'jid'},
798 $clean_sender,
799 $component->{'component_name'},
800 # FIXME: Should know lang in component
801 'en',
804 $component->xml_out([
805 [$NS_COMPONENT, 'message'], {
806 from => $jid_from,
807 to => $session->{'full_jid'},
808 type => 'chat',
810 [ [[ $NS_CHATSTATES, $state_tag ], {}, []] ]
814 =pod
816 =item *
818 C<set_session_state>($session, $state): The session's state is one
819 of several strings indicating what the state of the session is.
821 The first state is 'disconnected'. The user is disconnected, and
822 quite frankly we shouldn't even have a session unless they're
823 trying to connect right now.
825 The second state is 'logging in'; the user has indicated they
826 wish to log in and they are in the process of doing so. In this
827 state, everything that would normally go out the connection needs
828 to be stored away with the connection is being made, and if the
829 connection fails, actions may need to be taken.
831 The third state is 'online', which means the user is online and
832 all actions can be taken immediately.
834 In particular, calling C<set_session_state($session, 'online')>
835 will cause all the deferred processing to take place, which is
836 the primary purpose of this method. Going from "logging in"
837 to "login failed" will cause all deferred error processing to
838 occur, then the state will be switched to "disconnected".
840 =cut
842 sub set_session_state {
843 my $self = shift;
844 my $session = shift;
845 my $state = shift;
847 my $current_state = $session->{protocol_state};
848 $session->{protocol_state} = $state;
850 if ($state eq 'online' && $current_state eq 'logging in') {
851 succeeded('legacy_login_' . $session->{internal_id});
854 if ($state eq 'login failed' && $current_state eq 'logging in') {
855 failed('legacy_login_' . $session->{internal_id});
859 =pod
861 =item *
863 C<user_presence_update($session, $type, $show, $status)>: One of our
864 XMPP users has sent us a presence update, and we need to reflect
865 that back out to the transport.
867 For whatever action you need to take, you really ought to use
868 C<do_when_logged_in>, so you wait until the user is fully
869 online before trying to send the update.
871 This is the "general" presence update.
873 =cut
875 sub user_presence_update {
876 my $self = shift;
877 die "user_presence_update not implemented in " . ref($self);
880 =pod
882 =item *
884 C<user_targeted_presence_update($session, $type, $show, $status,
885 $target_user)>: The XMPP user has sent a targetted presence update,
886 at the $target_user (which will already be converted to the
887 legacy user name).
889 Note that targeted presence updates I<generally> accompany
890 general presence updates (as processed in C<user_presence_update>),
891 so the naive implementation that sets the presence on the legacy
892 user is probably not desirable; you should do something more
893 intelligent, though I'm still not sure exactly what.
895 =cut
897 sub user_targeted_presence_update {
898 my $self = shift;
899 die "user_targeted_presence_update not implemented in "
900 . ref($self);
903 =pod
905 =item *
907 C<legacy_presence_update($session, $legacy_name, $type, $show,
908 $status)>: A legacy user has sent a presence update of some
909 kind. Translate it into the $type, $show, and $status of XMPP,
910 and call this method. Implemented in Thrasher::Protocol and
911 probably doesn't need to be overridden.
913 =cut
915 sub legacy_presence_update {
916 my $self = shift;
917 my $session = shift;
918 my $legacy_name = shift;
919 my $type = shift;
920 my $show = shift;
921 my $status = shift;
923 # If the XMPP user has still not accepted this presence,
924 # store it away in the session
925 if ((my $state = $self->{backend}->get_roster_user_state
926 ($session->{jid}, $legacy_name)) !=
927 $self->{backend}->subscribed) {
928 log("Storing presence information for $legacy_name");
929 $session->{presence_waiting_for_subscribe}->{$legacy_name} =
930 [$type, $show, $status];
931 return;
932 } else {
933 log("Not storing presence info for $legacy_name because state is $state");
936 $session->{component}->send_presence
937 ($session->{jid}, $legacy_name, $type, $show, $status);
940 =pod
942 =item *
944 C<disconnecting>($session): Call this when the protocol is
945 disconnecting from the remote service, regardless of the reason.
946 Thrasher will work out whether the user asked for it or not,
947 and take appropriate action.
949 =cut
951 sub disconnecting {
952 my $self = shift;
953 my $session = shift;
955 my $state = $session->{protocol_state};
957 # If the user is already at "disconnected", then the user
958 # requested this and life is good.
959 if ($state eq 'disconnected') {
960 return;
962 if ($state eq 'logging in') {
963 # FIXME: Login evidently failed, shouldn't we find out
964 # more explicitly?
966 if ($state eq 'online') {
967 # Note we can only get here if we successfully logged in,
968 # so we shouldn't see the error case of using all
969 # connections because the user gave the wrong password.
970 log("Connection for " . $session->{full_jid} .
971 "unexpectedly dropped, scheduling re-connection.");
973 $session->{component}->login($session->{full_jid});
977 =pod
979 =back
981 =head2 Protocol Services
983 These are methods that multiple protocols will likely need to use,
984 so we centralize them here. However, you are not required to use them.
986 =over
988 =item *
990 C<set_current_legacy_roster>($session, $current_roster): The backend is
991 required to maintain a copy of the legacy roster, as reflected
992 in the user's XMPP roster. (Which can go out of sync if they fiddle
993 with it while not connected to the gateway, or if the gateway isn't
994 connected, but we can't do anything about in a standards-complaint
995 way, so far as I know.) If you call this with the current roster,
996 with the roster working as defined in L<Thrasher::Roster> (a hash
997 with the legacy user names as keys and values corresponding to
998 their current subscription state), this will compare it to the
999 roster as stored in the backend, and issue the necesary <presence>
1000 tags to bring the user's XMPP roster up-to-date, and also handle
1001 the logic necessary to initiate some changes that need to
1002 go longer term (if the user is subscribed remotely and unsubscribed
1003 locally, the move to want_subscribe, not subscribed).
1005 =cut
1007 my $subscribed = Thrasher::Roster::subscribed;
1008 my $unsubscribed = Thrasher::Roster::unsubscribed;
1009 my $want_subscribe = Thrasher::Roster::want_subscribe;
1011 # The actions to take for a given presence transition
1012 my %presence_table =
1014 "$subscribed,$unsubscribed" =>
1015 [['unsubscribed', 'unsubscribe'], $unsubscribed],
1016 # This is a bit crazy, but it's also an unlikely transition
1017 "$subscribed,$want_subscribe" =>
1018 [['unsubscribe', 'unsubscribed', 'subscribe'], $want_subscribe],
1019 "$unsubscribed,$subscribed" =>
1020 [['subscribe', 'subscribed'], $want_subscribe],
1021 "$unsubscribed,$want_subscribe" =>
1022 [['subscribe'], $want_subscribe],
1023 "$want_subscribe,$subscribed" =>
1024 [['subscribe', 'subscribed'], $want_subscribe],
1025 "$want_subscribe,$unsubscribed" =>
1026 [['unsubscribe', 'unsubscribed'], $unsubscribed]
1029 sub set_current_legacy_roster {
1030 my $self = shift;
1031 my $session = shift;
1032 my $current_legacy_roster = shift;
1034 my $jid = $session->{jid};
1036 my $current_roster =
1037 $self->{backend}->get_roster($jid);
1039 my $roster_diffs = roster_diff($current_roster,
1040 $current_legacy_roster);
1042 my $component = $session->{component};
1044 for my $legacy_username (sort keys %$roster_diffs) {
1045 my $key = join ",", @{$roster_diffs->{$legacy_username}};
1047 # FIXME: We need to centralize the process of unsubscribing
1048 # and make that a callback, so that avatar handling can
1049 # catch unsubscriptions and remove dead avatars.
1051 my ($presence_to_send, $new_state) =
1052 @{$presence_table{$key}};
1054 for my $presence (@$presence_to_send) {
1055 $component->send_presence($jid, $legacy_username,
1056 $presence);
1059 # Set the actual current value as processed through the
1060 # logic
1061 $current_legacy_roster->{$legacy_username} = $new_state;
1064 $self->{backend}->set_roster($jid, $current_legacy_roster);
1067 =pod
1069 =back
1071 =head2 PRE-CANNED ERRORS
1073 Some errors in the protocol that need to be reflected back to the user
1074 are fairly common across protocols. These methods can be called
1075 to provide certain canned error messages back out to the user.
1076 If you're I<really> lucky, they'll even be localized!
1078 These will also handle logging out the user if appropriate
1079 (specifically, the component showing as "offline") and setting
1080 an appropriate detailed presence for the component.
1082 =over 4
1084 =item *
1086 C<wrong_authentication>($jid): Will send a message back of the form "The
1087 username and password you have tried to use is being reported as
1088 invalid by the remote service."
1090 =cut
1092 # These will eventually be localized as well
1093 sub _handle_error {
1094 my $self = shift;
1095 my $jid = shift;
1096 my $error = shift;
1097 my $error_type = shift;
1099 log("Handling error: $error");
1101 $self->{component}->send_error_message($jid, $error, $error_type);
1104 sub wrong_authentication {
1105 my $self = shift;
1106 my $jid = shift;
1108 $self->_handle_error($jid, 'The username and password you have '
1109 .'tried to use is being reported as '
1110 .'invalid by the remote service.',
1111 'forbidden');
1114 =pod
1116 =item *
1118 C<name_in_use>($jid): "The name you are using with this service is
1119 logged in at another location."
1121 Note that if the protocol in question already automatically sends
1122 out a message like this, you should not send another. (AIM for
1123 instance will automatically send this out.)
1125 =cut
1127 sub name_in_use {
1128 my $self = shift;
1129 my $jid = shift;
1131 $self->_handle_error($jid, 'The name you are using with this '
1132 .'service is logged in at another location.',
1133 'bad_request');
1136 =pod
1138 =item *
1140 C<invalid_username>($jid): "The username you are trying to
1141 use with this service is an invalid username, according to
1142 the service."
1144 Use this when the username is syntactically invalid. Incorrect
1145 authentication credentials for an otherwise syntactically-valid
1146 username is C<wrong_authentication>.
1148 =cut
1150 sub invalid_username {
1151 my $self = shift;
1152 my $jid = shift;
1154 $self->_handle_error
1155 ($jid, "The username you are trying to use with "
1156 ."this service is an invalid username, according "
1157 ."to this service.", 'not_acceptable');
1160 =pod
1162 =item *
1164 C<network_error>($jid): "There was a network error while
1165 attempting to connect to the remote service."
1167 =cut
1169 sub network_error {
1170 my $self = shift;
1171 my $jid = shift;
1173 $self->_handle_error($jid, 'There was a network error while '
1174 .'attempting to connect to the remote '
1175 .'service.', 'service_unavailable');
1178 =pod
1180 =item *
1182 C<process_remote_username>($username): This gives the backend an
1183 opportunity to munge the username if needed. This gives you the
1184 opportunity to make a protocol-specific hack for things like
1185 case sensitivity or other protocol-specific things. AIM, for instance,
1186 will very freely send "Prof Gilzot", "profgilzot", and "ProfGilzot",
1187 all within the same session, but in general Thrasher will (properly,
1188 IMHO) consider that three separate names unless you normalize
1189 them with this function.
1191 The default, of course, is to do nothing to the string.
1193 =cut
1195 sub process_remote_username {
1196 my $self = shift;
1197 my $username = shift;
1199 return $username;
1202 =pod
1204 =item *
1206 C<fake_up_a_legacy_name>($user_jid, $jid, $legacy_guess): Gives the
1207 protocol a chance to change the backend's $legacy_guess for $jid's
1208 legacy ID. The default is to accept the guess the backend has already
1209 made; a protocol may override this and return a different string to
1210 improve upon that guess.
1212 The default is to do return the backend's guess unmodified.
1214 =cut
1216 sub fake_up_a_legacy_name {
1217 my ($self, $user_jid, $jid, $legacy_guess) = @_;
1219 return $legacy_guess;
1222 =pod
1224 =back
1226 =cut