Push public IM aliases as XMPP roster names and vCard full names.
[thrasher.git] / perl / lib / Thrasher / Protocol.pm
blob61954487249a7291187e766116175202d707dee7
1 package Thrasher::Protocol;
2 use strict;
3 use warnings;
5 use Data::Dumper;
6 use Thrasher::Log qw(:all);
7 use Thrasher::Constants qw(:all);
8 use Thrasher::Callbacks qw(:all);
9 use Thrasher::Roster qw(roster_diff);
11 =head1 NAME
13 Thrasher::Protocol - base class/interface for protocol differences.
15 =head1 DESCRIPTION
17 Thrasher::Protocol abstracts out the various protocol differences
18 into a simple interface to implement them. Because there are minor
19 differences, especially in registration and such, it's slightly
20 more complicated than
22 Note that, theoretically, Thrasher can turn anything that conforms
23 to the API protocol defined in this module into a transport; it
24 doesn't actually have to back to libpurple! Twitter transport,
25 anybody?
27 If you're adding a libpurple protocol, you probably want to subclass
28 from Thrasher::Protocol::LibPurple which will do a lot of the
29 libpurple work for you.
31 Protocol differences include, but are not limited to:
33 =over 4
35 =item *
37 Name of the protocol.
39 =item *
41 Registration instructions.
43 =item *
45 Registration result; this object will recieve notification
46 when a registration is completed.
48 =item *
50 How to actually perform the various work parts of the protocol.
52 =item *
54 Methods to reflect actions from the protocol back out to the
55 component. The Thrasher::Protocol superclass will implement the
56 communication with the ::Component, and that may be sufficient for
57 your protocol, but the ::Protocol gets a chance at the call just
58 in case you need something.
60 =back
62 Some of the methods below have a useful default implementation that
63 will work for the vast majority of implementations, but some don't.
64 Some methods can be entirely overridden, some must call the superclass.
66 =head1 METHODS
68 Only the top-level configuration file will call:
70 =over 4
72 =item *
74 C<new>($configuration, $backend): The configuration argument hash is
75 what is passed in by component.pl.
77 The default implementation copies these into keys of $self.
79 =back
81 =head2 Component Interface
83 These are things that the ::Component will call the ::Protocol with.
85 =over 4
87 =cut
89 sub new {
90 my $class = shift;
91 my $self = {};
92 bless $self, $class;
94 $self->{configuration} = shift;
95 $self->{backend} = shift;
97 return $self;
100 =pod
102 =item *
104 C<registration_xml>(): Should return the instructions for
105 registration, in the XML format favored by Thrasher::XMPPStreamOut.
106 Be sure to put the tags in the correct $NS_REGISTER namespace.
108 If the user is already registered according to the backend,
109 a <registered/> tag should be emitted, in accordance with the
110 standard section 4.2.1 #2, as well as returning the relevant
111 values for the registration.
113 This will automatically be used as the children of a <query> tag,
114 so you should just return the children (in an array ref).
116 A default implementation is provided, based on the C<registered>
117 method of the backend, as long the protocol only needs a
118 standard username and password.
120 =cut
122 sub registration_xml {
123 my $self = shift;
124 my $jid = shift;
126 my $registration = $self->{backend}->registered($jid);
128 return [[[$NS_REGISTER, 'instructions'], {},
129 ['Please provide your username and password for '
130 .$self->name]],
131 ($registration ? ([[$NS_REGISTER, 'registered'], {}, []])
132 : ()),
133 [[$NS_REGISTER, 'username'], {},
134 [$registration->{username} ? ($registration->{username}) : ()]],
135 [[$NS_REGISTER, 'password'], {},
136 [$registration->{password} ? ($registration->{password}) : ()]]];
139 =pod
141 =item *
143 C<registration_items>: Should return a list of elements that are
144 required to login. This will be verified against the user's
145 registration at login time to verify that they have all required
146 elements.
148 The default implementation returns qw(username password).
150 If the user only has some pieces, the user is completely
151 unregistered.
153 =cut
155 # This addresses a bug where users were somehow able to have only a
156 # password in the DB. I'm not sure how this is possible, but since
157 # it's a segfault for libpurple for this to happen, it has to be
158 # stopped, both by fixing the real problem and by making sure that
159 # even if it happens it still doesn't crash us.
161 sub registration_items {
162 return qw(username password);
165 =pod
167 =item *
169 C<registration_defaults>: Should return a hashref of values indicating
170 the defaults for registration, if given. If the user fails to pass
171 back a value, but there is a default, the default will be used.
173 A value must be given for each item or the registration will fail.
174 username and password should not be given defaults (unless you have
175 a really good reason of some kind), but since other clients sometimes
176 choke on anything other than having a username and password, we
177 need to be able to provide defaults for such clients.
179 The default implementation returns {}, no surprise.
181 =cut
183 # The temptation to write
184 # sub registartion defaults { { } }
185 # was strong...
186 sub registration_defaults {
187 return {};
190 =pod
192 =item *
194 C<name>(): Should return the name of the protocol, suitable for
195 concatenation with the string " Gateway" for describing the gateway.
197 The default implementation returns $self->{name}, but that won't be
198 useful if you don't defined a ->{name}.
200 =cut
202 sub name {
203 return $_[0]->{name};
206 =pod
208 =item *
210 C<identifier>(): Should return a lowercase-letter name of the protocol
211 suitable for use in the domain of the component. i.e., the MSN
212 transport might return "Microsoft Messenger" for the call to C<name>,
213 but should return "msn" for this method.
215 The default implemenation returns $self->{identifier}, but that won't
216 be useful if you didn't define a ->{identifier}.
218 =cut
220 sub identifier {
221 return $_[0]->{identifier};
224 =pod
226 =item *
228 C<event_loop>(): Should return a Perl module conforming to the
229 interface set out in Thrasher::EventLoop that can be used to set
230 up the event loop for Thrasher Bird, including both whatever
231 loop is needed for the protocol loop and for talking to the XMPP
232 server.
234 =cut
236 sub event_loop {
237 return 'Thrasher::EventLoop';
240 =pod
242 =item *
244 C<gateway_desc>($lang): Should return the thing you want to prompt the
245 user with, in order to ask them for their user name. See the examples,
246 or see XEP-0100 section 6.3. This corresponds to the
247 <desc> tag, hence the klunky name.
249 $xml_lang will be the language requested by the user, as seen in their
250 xml language attribute in the query. It will already have been
251 normalized to 'en' if not specified.
253 The default implementation returns undef, which means "don't give
254 a description", which the standard permits.
256 =cut
258 sub gateway_desc {
259 my $self = shift;
260 return undef;
263 =pod
265 =item *
267 C<gateway_prompt>($lang): Should return the simple name of the
268 thing you are asking for when adding a contact. See XEP-0100
269 section 6.3. This corresponds to the <prompt> tag, hence the method
270 name.
272 You must implement this.
274 =cut
276 sub gateway_prompt {
277 my $self = shift;
278 die "Method gateway_prompt not implemented in " . ref($self);
281 =pod
283 =item *
285 C<registration>($from_jid, $registration_info): $registration_info is
286 a hash ref containing whatever the user sent back.
288 This should return a list with the following elements:
290 =over 4
292 =item *
294 A boolean indicating if the registration was successful.
296 =item *
298 An identifier which is a key in the C<Thrasher::Component::IQ_ERRORS>
299 which represents the error to be sent back to the user.
301 =back
303 The second is not needed if the registration was successful.
305 Note that XEP-0100 specifies that the error that should result
306 if the user's username and password failed to verify is
307 'not_acceptable' (note Component.pm uses an underscore).
309 The default implementation passes this on to the backend's
310 C<register> method, after processing the registration defaults.
312 =cut
314 sub registration {
315 my $self = shift;
316 my $jid = shift;
317 my $registration_info = shift;
319 log("Registration defaults: " . Dumper($self->registration_defaults));
321 my $registration_defaults = $self->registration_defaults;
322 for my $key (keys %$registration_defaults) {
323 $registration_info->{$key} ||= $registration_defaults->{$key};
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, @args) = @_;
412 if (ref($session_or_error)) {
413 $self->{username_to_session}->{$registration_info->{username}} =
414 $session_or_error;
417 $continuation->($session_or_error, @args);
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 # TODO: displaynames are allowed to persist while the protocol object does.
454 my $legacy_login = $session->{'legacy_login'};
455 delete($self->{'username_to_session'}->{$legacy_login});
458 =pod
460 =item *
462 C<subscribe>($session, $legacy_name, $continuation): This should
463 subscribe the user to the given $legacy_name on the legacy service.
464 $legacy_name has been converted to be the user name expected by
465 the legacy service already, you don't need to "unescape" it.
467 The continuation should be called with either a true value,
468 indicating the subscription was successful, or a false value,
469 indicating it failed for any reason. (The standard assumes it
470 was because the remote user rejected it, so things like "the
471 user doesn't exist" will be simply failures.)
473 This has no default implementation.
475 =cut
477 sub subscribe {
478 my $self = shift;
479 my $legacy_name = shift;
480 my $continuation = shift;
482 die "subscribe method not implemented in " . ref($self);
485 =pod
487 =item *
489 C<unsubscribe>($session, $legacy_name, $continuation): This
490 should unsubscribe the user to the given $legacy_name on the
491 legacy service. The continuation should be called when
492 this is complete, with no arguments. (XMPP basically assumes
493 that unsubscriptions always complete.)
495 =cut
497 sub unsubscribe {
498 my $self = shift;
500 die "unsubscribe method not implemented in " . ref($self);
503 =pod
505 =item *
507 C<send_message>($session, $to, $body_text, $type, $error_sub):
508 Send the given message to the given user of the given (XMPP message)
509 type. That is, the XMPP user has sent a message to
510 somebody@transport.type, and this method needs to implement the
511 sending of that message along the transport.
513 If the message sending fails, call the $error_sub with the appropriate
514 error name. The standard specifies (converted to Thrasher's
515 constants):
517 =over 4
519 =item *
521 C<item_not_found>: Legacy User address is not valid.
523 =item *
525 C<registration_required>: Jabber User is not registered with Gateway.
527 C<service_unavailable>: Legacy User is offline and Legacy Service (or
528 Gateway) does not provide offline message storage.
530 C<remote_server_timeout>: Legacy Service cannot be reached.
532 =back
534 C<$to> will be the legacy name, not the JID. If you need them, you can
535 either convert them back out or get the sender JID from the session.
537 This has no default implementation.
539 =cut
541 sub send_message {
542 my $self = shift;
543 die "send_message method not implemented in " . ref($self);
546 =pod
548 =item *
550 C<outgoing_chatstate>($session, $to, $chatstate):
551 Called when a chatstates stanza is received from the XMPP user
552 directed at the legacy username C<$to>. C<$to> is as for send_message.
554 The default implementation is to ignore chatstates.
556 =cut
558 sub outgoing_chatstate {
559 my ($self, $session, $to, $chatstate) = @_;
560 # ignore!
563 =pod
565 =item *
567 C<subscribed>($session, $component, $legacy_username): The user has accepted
568 the subscription request by the given legacy username; handle it
569 as you should. ::Session actually takes care of the presence tags.
571 The default implementation will propogate presence information stored
572 in $session->{presence_waiting_for_subscribe} as appropriate. You will
573 need to override this method, but the override should call this back.
575 =cut
578 sub subscribed {
579 my $self = shift;
580 my $session = shift;
581 my $component = shift;
582 my $legacy_username = shift;
584 if (my $presence_info =
585 delete $session->{presence_waiting_for_subscribe}->{$legacy_username}) {
586 log("Using stored presence information for $legacy_username");
587 $component->send_presence($session->{jid}, $legacy_username,
588 @$presence_info);
589 $session->resend_displayname($legacy_username);
593 =pod
595 =item *
597 C<unsubscribed>($session, $component, $legacy_username): The user has rejected
598 a subscription request or removed a subscription. Handle it as
599 you should for your protocol.
601 This has no default implementation.
603 =cut
605 sub unsubscribed {
606 my $self = shift;
607 die "unsubscribed method not implemented in " . ref($self);
610 =pod
612 =item *
614 C<ft_local_ready>($ft_id):
615 Called when the file transfer proxy to the user is ready for I/O to
616 the remote protocol.
618 Must return true of the protocol should continue to receive this
619 notification when the proxy is ready. If the protocol will somehow
620 manage this itself, return false.
622 =over 4
624 =item *
626 C<ft_id>: Protocol's file transfer identifier or object.
628 =back
630 =cut
632 sub ft_local_ready {
633 my ($self, $id) = @_;
634 die 'ft_local_ready method not implemented in ' . ref($self);
637 =pod
639 =item *
641 C<get_displayname>($jid, $legacy_username):
642 Returns the displayname of the legacy user as it should be presented to $jid.
644 The default implementation should be sufficient.
646 =cut
648 sub get_displayname {
649 my ($self, $jid, $legacy_username) = @_;
651 return $self->{'displayname'}->{$jid}->{$legacy_username};
654 =pod
656 =back
658 =head2 Protocol Interface
660 These are methods that you will call in your protocol handler to
661 inform the component about events. Default implementations are
662 provided in Thrasher::Protocol, and are likely to be sufficient
663 for your needs.
665 Note how all these method names have gerund names.
667 =over 4
669 =item *
671 C<adding_contact>($legacy_username_from, $jid_to): A legacy user
672 is adding a user on this transport to their roster. The component will
673 handle the XML going out to the user when you call
674 C<$component->add_contact($jid_to, $legacy_username_from)>.
676 The default implementation of this does that and is probably
677 sufficient.
679 =cut
681 sub adding_contact {
682 my $self = shift;
683 my $legacy_username_from = shift;
684 my $subscription_to = shift;
686 my $subscription_from = $self->{backend}->legacy_name_to_jid
687 ($subscription_to, $legacy_username_from,
688 $self->{component}->{component_name}, 'en');
690 log("Adding contact: $legacy_username_from -> $subscription_to");
692 my $session = $self->{component}->session_for($subscription_to);
693 if (!defined($session)) {
694 log("Getting a contact addition request for a username that "
695 ."doesn't seem to be logged in: $legacy_username_from is "
696 ."asking to subscribe to $subscription_to with the "
697 .$self->name . " protocol.");
698 return;
701 my $component = $session->{component};
702 my $jid_to = $session->{jid};
703 # Implements section 5.1.1 #2
704 $component->add_contact($jid_to, $legacy_username_from);
706 my $state = $self->{backend}->get_roster_user_state($session->{jid},
707 $legacy_username_from);
708 debug("roster state was $state");
709 if ($state != $self->{backend}->subscribed()) {
710 $self->{backend}->set_roster_user_state(
711 $session->{jid},
712 $legacy_username_from,
713 $self->{backend}->want_subscribe
718 =pod
720 =item *
722 C<deleting_contact>($subscription_from, $subscription_to): A
723 legacy user is removing the XMPP user from their subscription
724 list.
726 The default implementation of this is probably sufficient.
728 =cut
730 sub deleting_contact {
731 my $self = shift;
732 my $subscription_from = shift;
733 my $subscription_to = shift;
735 log("Deleting contact: $subscription_from -> $subscription_to");
737 my $session = $self->{username_to_session}->{$subscription_to};
738 if (!defined($session)) {
739 log("Getting a contact removal request for a username that "
740 ."doesn't seem to be logged in: $subscription_from is "
741 ."asking to unsubscribe from $subscription_to with the "
742 .$self->name . " protocol.");
743 return;
746 my $component = $session->{component};
747 my $jid_to = $session->{jid};
748 $component->delete_contact($jid_to, $subscription_from);
750 $self->{backend}->set_roster_user_state
751 ($session->{jid}, $subscription_from,
752 $self->{backend}->unsubscribed);
755 =pod
757 =item *
759 C<sending_message>($legacy_from, $legacy_to, $message, $is_xhtml_ish):
760 A legacy user has sent a message. $is_xhtml_ish is propagated to the
761 ::Component::send_message method.
763 The default implementation of this is probably sufficient.
765 =cut
767 sub sending_message {
768 my $self = shift;
769 my $legacy_from = shift;
770 my $legacy_to = shift;
771 my $message = shift;
772 my $is_xhtml_ish = shift;
774 my $session = $self->{username_to_session}->{$legacy_to};
775 my $component = $session->{component};
776 my $jid_to = $session->{jid};
778 my $jid_from = $self->{backend}->legacy_name_to_jid
779 ($jid_to, $legacy_from, $component->{component_name},
780 'en'); # FIXME: Should know lang in component
782 $component->send_message($jid_from, $jid_to, $message, {
783 is_xhtml_ish => $is_xhtml_ish,
784 children => [ [[ $NS_CHATSTATES, 'active' ], {}, []] ],
788 =pod
790 =item *
792 C<initial_login>($session): This is a chance to be called to be
793 notified about the initial successful login for a given session,
794 an opportunity to do something with the session.
796 The default behavior of this function is to set the current
797 session state to "online".
799 =cut
801 sub initial_login {
802 my $self = shift;
803 my $session = shift;
805 $self->set_session_state($session, 'online');
808 =pod
810 =item *
812 C<incoming_chatstate>($session, $sender, $state_tag): Send a message
813 from the legacy $sender with only the given chatstate $state_tag for
814 the $session.
816 =cut
818 sub incoming_chatstate {
819 my ($self, $session, $sender, $state_tag) = @_;
821 my $clean_sender = $self->process_remote_username($sender);
822 my $component = $session->{'component'};
823 my $jid_from = $self->{'backend'}->legacy_name_to_jid(
824 $session->{'jid'},
825 $clean_sender,
826 $component->{'component_name'},
827 # FIXME: Should know lang in component
828 'en',
831 $component->xml_out([
832 [$NS_COMPONENT, 'message'], {
833 from => $jid_from,
834 to => $session->{'full_jid'},
835 type => 'chat',
837 [ [[ $NS_CHATSTATES, $state_tag ], {}, []] ]
841 =pod
843 =item *
845 C<set_session_state>($session, $state): The session's state is one
846 of several strings indicating what the state of the session is.
848 The first state is 'disconnected'. The user is disconnected, and
849 quite frankly we shouldn't even have a session unless they're
850 trying to connect right now.
852 The second state is 'logging in'; the user has indicated they
853 wish to log in and they are in the process of doing so. In this
854 state, everything that would normally go out the connection needs
855 to be stored away with the connection is being made, and if the
856 connection fails, actions may need to be taken.
858 The third state is 'online', which means the user is online and
859 all actions can be taken immediately.
861 In particular, calling C<set_session_state($session, 'online')>
862 will cause all the deferred processing to take place, which is
863 the primary purpose of this method. Going from "logging in"
864 to "login failed" will cause all deferred error processing to
865 occur, then the state will be switched to "disconnected".
867 =cut
869 sub set_session_state {
870 my $self = shift;
871 my $session = shift;
872 my $state = shift;
874 my $current_state = $session->{protocol_state};
875 $session->{protocol_state} = $state;
877 if ($state eq 'online' && $current_state eq 'logging in') {
878 succeeded('legacy_login_' . $session->{internal_id});
881 if ($state eq 'login failed' && $current_state eq 'logging in') {
882 failed('legacy_login_' . $session->{internal_id});
886 =pod
888 =item *
890 C<user_presence_update($session, $type, $show, $status)>: One of our
891 XMPP users has sent us a presence update, and we need to reflect
892 that back out to the transport.
894 For whatever action you need to take, you really ought to use
895 C<do_when_logged_in>, so you wait until the user is fully
896 online before trying to send the update.
898 This is the "general" presence update.
900 =cut
902 sub user_presence_update {
903 my $self = shift;
904 die "user_presence_update not implemented in " . ref($self);
907 =pod
909 =item *
911 C<user_targeted_presence_update($session, $type, $show, $status,
912 $target_user)>: The XMPP user has sent a targetted presence update,
913 at the $target_user (which will already be converted to the
914 legacy user name).
916 Note that targeted presence updates I<generally> accompany
917 general presence updates (as processed in C<user_presence_update>),
918 so the naive implementation that sets the presence on the legacy
919 user is probably not desirable; you should do something more
920 intelligent, though I'm still not sure exactly what.
922 =cut
924 sub user_targeted_presence_update {
925 my $self = shift;
926 die "user_targeted_presence_update not implemented in "
927 . ref($self);
930 =pod
932 =item *
934 C<legacy_presence_update($session, $legacy_name, $type, $show,
935 $status)>: A legacy user has sent a presence update of some
936 kind. Translate it into the $type, $show, and $status of XMPP,
937 and call this method. Implemented in Thrasher::Protocol and
938 probably doesn't need to be overridden.
940 =cut
942 sub legacy_presence_update {
943 my $self = shift;
944 my $session = shift;
945 my $legacy_name = shift;
946 my $type = shift;
947 my $show = shift;
948 my $status = shift;
950 # If the XMPP user has still not accepted this presence,
951 # store it away in the session
952 if ((my $state = $self->{backend}->get_roster_user_state
953 ($session->{jid}, $legacy_name)) !=
954 $self->{backend}->subscribed) {
955 log("Storing presence information for $legacy_name because state is $state");
956 $session->{presence_waiting_for_subscribe}->{$legacy_name} =
957 [$type, $show, $status];
958 return;
959 } else {
960 log("Not storing presence info for $legacy_name because state is $state");
963 $session->{component}->send_presence
964 ($session->{jid}, $legacy_name, $type, $show, $status);
967 =pod
969 =item *
971 C<disconnecting>($session): Call this when the protocol is
972 disconnecting from the remote service, regardless of the reason.
973 Thrasher will work out whether the user asked for it or not,
974 and take appropriate action.
976 =cut
978 sub disconnecting {
979 my $self = shift;
980 my $session = shift;
982 my $state = $session->{protocol_state};
984 # If the user is already at "disconnected", then the user
985 # requested this and life is good.
986 if ($state eq 'disconnected') {
987 return;
989 if ($state eq 'logging in') {
990 # FIXME: Login evidently failed, shouldn't we find out
991 # more explicitly?
993 if ($state eq 'online') {
994 # Note we can only get here if we successfully logged in,
995 # so we shouldn't see the error case of using all
996 # connections because the user gave the wrong password.
997 log("Connection for " . $session->{full_jid} .
998 "unexpectedly dropped, scheduling re-connection.");
1000 $session->{component}->login($session->{full_jid});
1004 =pod
1006 =back
1008 =head2 Protocol Services
1010 These are methods that multiple protocols will likely need to use,
1011 so we centralize them here. However, you are not required to use them.
1013 =over
1015 =item *
1017 C<set_current_legacy_roster>($session, $current_roster): The backend is
1018 required to maintain a copy of the legacy roster, as reflected
1019 in the user's XMPP roster. (Which can go out of sync if they fiddle
1020 with it while not connected to the gateway, or if the gateway isn't
1021 connected, but we can't do anything about in a standards-complaint
1022 way, so far as I know.) If you call this with the current roster,
1023 with the roster working as defined in L<Thrasher::Roster> (a hash
1024 with the legacy user names as keys and values corresponding to
1025 their current subscription state), this will compare it to the
1026 roster as stored in the backend, and issue the necesary <presence>
1027 tags to bring the user's XMPP roster up-to-date, and also handle
1028 the logic necessary to initiate some changes that need to
1029 go longer term (if the user is subscribed remotely and unsubscribed
1030 locally, the move to want_subscribe, not subscribed).
1032 =cut
1034 my $subscribed = Thrasher::Roster::subscribed;
1035 my $unsubscribed = Thrasher::Roster::unsubscribed;
1036 my $want_subscribe = Thrasher::Roster::want_subscribe;
1038 # The actions to take for a given presence transition
1039 my %presence_table =
1041 "$subscribed,$unsubscribed" =>
1042 [['unsubscribed', 'unsubscribe'], $unsubscribed],
1043 # This is a bit crazy, but it's also an unlikely transition
1044 "$subscribed,$want_subscribe" =>
1045 [['unsubscribe', 'unsubscribed', 'subscribe'], $want_subscribe],
1046 "$unsubscribed,$subscribed" =>
1047 [['subscribe', 'subscribed'], $want_subscribe],
1048 "$unsubscribed,$want_subscribe" =>
1049 [['subscribe'], $want_subscribe],
1050 "$want_subscribe,$subscribed" =>
1051 [['subscribe', 'subscribed'], $want_subscribe],
1052 "$want_subscribe,$unsubscribed" =>
1053 [['unsubscribe', 'unsubscribed'], $unsubscribed]
1056 sub set_current_legacy_roster {
1057 my $self = shift;
1058 my $session = shift;
1059 my $current_legacy_roster = shift;
1061 my $jid = $session->{jid};
1063 my $current_roster =
1064 $self->{backend}->get_roster($jid);
1066 my $roster_diffs = roster_diff($current_roster,
1067 $current_legacy_roster);
1069 my $component = $session->{component};
1071 for my $legacy_username (sort keys %$roster_diffs) {
1072 my $key = join ",", @{$roster_diffs->{$legacy_username}};
1074 # FIXME: We need to centralize the process of unsubscribing
1075 # and make that a callback, so that avatar handling can
1076 # catch unsubscriptions and remove dead avatars.
1078 my ($presence_to_send, $new_state) =
1079 @{$presence_table{$key}};
1081 for my $presence (@$presence_to_send) {
1082 $component->send_presence($jid, $legacy_username,
1083 $presence);
1086 # Set the actual current value as processed through the
1087 # logic
1088 $current_legacy_roster->{$legacy_username} = $new_state;
1091 $self->{backend}->set_roster($jid, $current_legacy_roster);
1094 =pod
1096 =item *
1098 C<set_displayname>($jid, $legacy_username, $displayname_value):
1099 Stores the displayname of the legacy user as it should be presented to $jid.
1101 If this is overridden, get_displayname() should be, too.
1103 =cut
1105 sub set_displayname {
1106 my ($self, $jid, $legacy_username, $displayname) = @_;
1108 debug("set_displayname($jid, $legacy_username, $displayname) called\n");
1110 if (not $displayname) {
1111 # Forget it!
1112 delete($self->{'displayname'}->{$jid}->{$legacy_username});
1113 return $displayname;
1116 my $displayname_was = $self->get_displayname($jid, $legacy_username);
1117 if ($displayname_was && $displayname_was eq $displayname) {
1118 # Don't send update if unchanged.
1119 return $displayname;
1122 $self->{'displayname'}->{$jid}->{$legacy_username} = $displayname;
1124 # Push change into $jid's roster.
1125 my $component = $self->{'component'};
1126 my $legacy_jid = $component->legacy_name_to_xmpp($jid, $legacy_username);
1127 $component->set_roster_name($jid, $legacy_jid, $displayname);
1129 return $displayname;
1132 =pod
1134 =back
1136 =head2 PRE-CANNED ERRORS
1138 Some errors in the protocol that need to be reflected back to the user
1139 are fairly common across protocols. These methods can be called
1140 to provide certain canned error messages back out to the user.
1141 If you're I<really> lucky, they'll even be localized!
1143 These will also handle logging out the user if appropriate
1144 (specifically, the component showing as "offline") and setting
1145 an appropriate detailed presence for the component.
1147 =over 4
1149 =item *
1151 C<wrong_authentication>($jid): Will send a message back of the form "The
1152 username and password you have tried to use is being reported as
1153 invalid by the remote service."
1155 =cut
1157 # These will eventually be localized as well
1158 sub _handle_error {
1159 my $self = shift;
1160 my $jid = shift;
1161 my $error = shift;
1162 my $error_type = shift;
1164 log("Handling error: $error");
1166 $self->{component}->send_error_message($jid, $error, $error_type);
1169 sub wrong_authentication {
1170 my $self = shift;
1171 my $jid = shift;
1173 $self->_handle_error($jid, 'The username and password you have '
1174 .'tried to use is being reported as '
1175 .'invalid by the remote service.',
1176 'forbidden');
1179 =pod
1181 =item *
1183 C<name_in_use>($jid): "The name you are using with this service is
1184 logged in at another location."
1186 Note that if the protocol in question already automatically sends
1187 out a message like this, you should not send another. (AIM for
1188 instance will automatically send this out.)
1190 =cut
1192 sub name_in_use {
1193 my $self = shift;
1194 my $jid = shift;
1196 $self->_handle_error($jid, 'The name you are using with this '
1197 .'service is logged in at another location.',
1198 'bad_request');
1201 =pod
1203 =item *
1205 C<invalid_username>($jid): "The username you are trying to
1206 use with this service is an invalid username, according to
1207 the service."
1209 Use this when the username is syntactically invalid. Incorrect
1210 authentication credentials for an otherwise syntactically-valid
1211 username is C<wrong_authentication>.
1213 =cut
1215 sub invalid_username {
1216 my $self = shift;
1217 my $jid = shift;
1219 $self->_handle_error
1220 ($jid, "The username you are trying to use with "
1221 ."this service is an invalid username, according "
1222 ."to this service.", 'not_acceptable');
1225 =pod
1227 =item *
1229 C<network_error>($jid): "There was a network error while
1230 attempting to connect to the remote service."
1232 =cut
1234 sub network_error {
1235 my $self = shift;
1236 my $jid = shift;
1238 $self->_handle_error($jid, 'There was a network error while '
1239 .'attempting to connect to the remote '
1240 .'service.', 'service_unavailable');
1243 =pod
1245 =item *
1247 C<process_remote_username>($username): This gives the backend an
1248 opportunity to munge the username if needed. This gives you the
1249 opportunity to make a protocol-specific hack for things like
1250 case sensitivity or other protocol-specific things. AIM, for instance,
1251 will very freely send "Prof Gilzot", "profgilzot", and "ProfGilzot",
1252 all within the same session, but in general Thrasher will (properly,
1253 IMHO) consider that three separate names unless you normalize
1254 them with this function.
1256 The default, of course, is to do nothing to the string.
1258 =cut
1260 sub process_remote_username {
1261 my $self = shift;
1262 my $username = shift;
1264 return $username;
1267 =pod
1269 =item *
1271 C<fake_up_a_legacy_name>($user_jid, $jid, $legacy_guess): Gives the
1272 protocol a chance to change the backend's $legacy_guess for $jid's
1273 legacy ID. The default is to accept the guess the backend has already
1274 made; a protocol may override this and return a different string to
1275 improve upon that guess.
1277 The default is to do return the backend's guess unmodified.
1279 =cut
1281 sub fake_up_a_legacy_name {
1282 my ($self, $user_jid, $jid, $legacy_guess) = @_;
1284 return $legacy_guess;
1287 =pod
1289 =back
1291 =cut