Push public IM aliases as XMPP roster names and vCard full names.
[thrasher.git] / perl / lib / Thrasher / Session.pm
blob8745fe4803ce708719e89f84f79429015aef14c2
1 package Thrasher::Session;
2 use strict;
3 use warnings;
5 =head1 NAME
7 Thrasher::Session - abstracts out a single session to a legacy
8 service
10 =head1 DESCRIPTION
12 Thrasher::Session abstracts out the parts of code that deal with
13 a specific session, which is a (jid, service, registration) tuple.
14 This session matches the "session" described in section 4.4 of
15 XEP-0100.
17 A Thrasher::Session is created specifically when a connection is
18 made and only has to do with things that must be done when connected
19 to a session, such as sending and receiving messages, subscribing
20 and unsubscribing legacy users, etc. Once the connection to the legacy
21 service is terminated, the Session is over. (I'm emphasizing this
22 because without this information, you might expect Thrasher::Session
23 to do more than it does.)
25 Thrasher::Protocols are responsible for creating Thrasher::Session
26 objects and returning them back to the main components for storage.
28 Sessions are intended to work in conjunction with other objects to
29 do their work; a Session can't send a message without the assistence
30 of a Protocol, for instance. Consequently, ideally, there won't be
31 a need for another Thrasher::Session implementation, since all
32 the protocol-varying stuff should live in the Thrasher::Protocol
33 implementations.
35 The design of this object is a bit weird, since it sort of ends
36 up filling in the cracks between Protocols, Backends, and the
37 Component, where the first two are designed primarily to be easily
38 replacable, not in accordance with any other OO design
39 principles. This badly distorts the design, and Thrasher::Session is
40 what ends up paying the piper.
42 As an example, see C<add_contact>, which implements the I<logic> for
43 adding a contact, but has to do it entirely in terms of methods
44 offered by the Protocol, Component, and Backend objects.
46 =cut
48 use Thrasher::XML qw(strip_resource extract_disco_info);
49 use Thrasher::Constants qw(:all);
50 use Thrasher::Callbacks qw(:all);
51 use Thrasher::Log qw(:all);
53 use Data::Dumper;
55 my $id_counter = 1;
57 sub new {
58 my $class = shift;
59 my $self = {};
60 bless $self, $class;
62 $self->{full_jid} = shift;
63 $self->{jid} = strip_resource($self->{full_jid});
64 $self->{component} = shift;
65 $self->{protocol} = shift;
66 $self->{legacy_login} = shift;
67 $self->{status} = '';
69 $self->{internal_id} = $id_counter++;
71 # When a session is created, fire a discovery request at the
72 # creating JID.
73 $self->{component}->iq_query
74 ([[$NS_COMPONENT, 'iq'],
75 {to => $self->{full_jid},
76 from => $self->{component}->{component_name},
77 type => 'get'},
78 [[[$NS_DISCO_INFO, 'query'], {}, []]]],
79 sub {
80 my $component = shift;
81 my $iq_params = shift;
82 my $iq_packet = shift;
84 if ($iq_params->{type} eq 'error') {
85 failed("client_discovery_" . $self->{internal_id});
86 return;
89 # And process the answer; check for 'error'
90 my ($identities, $features) =
91 extract_disco_info($iq_params->{query});
92 my %features_hash = map { $_ => 1 } @$features;
94 $self->{client_identities} = $identities;
95 $self->{client_features} = \%features_hash;
97 succeeded("client_discovery_" . $self->{internal_id});
98 });
100 return $self;
103 sub set_lang {
104 my $self = shift;
105 my $lang = shift;
106 if (!$self->{_xml_lang}) {
107 $self->{_xml_lang} = $lang;
111 sub get_lang {
112 my $self = shift;
113 # Sorry, everybody else... but there can only be one default.
114 return $self->{_xml_lang} || 'en';
117 sub logout {
118 my $self = shift;
120 # Cleanup its usage of the callback for this session, so that
121 # hash entry doesn't leak.
122 event_superceded("client_discovery_" . $self->{internal_id});
123 event_superceded("legacy_login_" . $self->{internal_id});
126 # Do something, if and only if we find that the given feature
127 # is supported.
128 sub do_if_feature {
129 my $self = shift;
130 my $feature_or_features = shift;
131 my $success_function = shift;
132 my $failure_function = shift;
134 my $features_discovered = sub {
135 if (ref($feature_or_features) eq 'ARRAY') {
136 for my $feature (@$feature_or_features) {
137 if (!$self->{client_features}->{$feature}) {
138 if ($failure_function) {
139 $failure_function->('missing_feature',
140 $feature);
142 return;
145 } elsif (!$self->{client_features}->{$feature_or_features}) {
146 if ($failure_function) {
147 $failure_function->('missing_feature',
148 $feature_or_features);
150 return;
153 $success_function->();
156 my $features_not_discovered = sub {
157 if ($failure_function) {
158 $failure_function->('no_disco');
162 do_when("client_discovery_" . $self->{internal_id},
163 $features_discovered, $features_not_discovered);
166 # Implementing section 4.6
167 sub subscribe {
168 my $self = shift;
169 my $jid = shift;
171 # FIXME - should use the protocol for translating names here
172 my $legacy_username = $self->{protocol}->{backend}->jid_to_legacy_name
173 ($self->{jid}, $jid);
174 if (!defined($legacy_username)) {
175 # FIXME: Error out.
176 return;
179 # Handle the subscription request, if it is successful
180 my $handle_subscription = sub {
181 my $subscription_successful = shift;
183 my $comp = $self->{component};
184 my $this_jid = $self->{jid};
185 my $target_jid = $jid;
187 if ($subscription_successful) {
188 # 4.6 #3, subscription successful
189 $comp->send_presence_xml($this_jid, 'subscribed', $target_jid);
191 # Spec violation: XEP-0100 assumes that the user is
192 # online, we actually pass through the real presence we got.
193 if (my $stored_presence =
194 delete
195 $self->{presence_waiting_for_subscribe}->{$legacy_username})
197 if ($stored_presence->[0]
198 && $stored_presence->[0] eq 'unavailable') {
199 # 4.6 #4, send available. But only *if* we're
200 # sending unavailable immediately after. Client
201 # may become suspicious and show the subscription
202 # as "ask"/waiting for auth if a contact that
203 # supposedly authorized was never online until
204 # that contact was next seen login.
205 debug("Faking out buggy clients with available before subscribe");
206 $comp->send_presence($self->{jid}, $legacy_username);
208 log("Using stored presence information for $legacy_username");
209 $comp->send_presence($self->{jid}, $legacy_username,
210 @$stored_presence);
211 $self->resend_displayname($legacy_username);
212 } else {
213 log("no stored presence information for $legacy_username found.");
216 # 4.6 #5
217 $comp->send_presence_xml($this_jid, 'subscribe', $target_jid);
219 # 4.6 #6, WTF? You can't do this, the legacy user doesn't
220 # get XML stanzas...? Psi seems to agree, so
221 # XEP-0100 violation, we don't send this.
222 # FIXME: Mail the standards list about this.
223 #$comp->send_presence_xml($target_jid, 'subscribed', $this_jid);
225 # Update the roster information
226 my $legacy_username =
227 $comp->{protocol}->{backend}->jid_to_legacy_name($self->{jid},
228 $target_jid);
229 $comp->{protocol}->{backend}->set_roster_user_state
230 ($self->{jid}, $legacy_username,
231 $comp->{protocol}->{backend}->subscribed);
232 } else {
233 # Unsuccessful subscription, assumed to be because the
234 # legacy user rejected it. Section 4.6.2.
235 $comp->send_presence_xml($this_jid, 'unsubscribed', $target_jid);
239 my $legacy_id =
240 $self->{protocol}->{backend}->jid_to_legacy_name($self->{jid}, $jid);
242 $self->{protocol}->subscribe($self, $legacy_id, $handle_subscription);
245 sub resend_displayname {
246 my ($self, $legacy_username) = @_;
248 my $displayname = $self->{'protocol'}->get_displayname($self->{'jid'},
249 $legacy_username);
250 if ($displayname) {
251 $self->{'protocol'}->set_displayname($self->{'jid'},
252 $legacy_username,
253 '');
254 $self->{'protocol'}->set_displayname($self->{'jid'},
255 $legacy_username,
256 $displayname);
260 # Implementing section 4.7
261 sub unsubscribe {
262 my $self = shift;
263 my $jid = shift;
265 # FIXME: Should use the protocol for name translation here
266 my ($user_name) = split(/\@/, $jid);
267 if (!defined($user_name)) {
268 # FIXME: Error
271 # We assume subscription is successful, because XMPP assumes it is
272 my $handle_unsubscription = sub {
273 my $comp = $self->{component};
274 my $this_jid = $self->{jid};
275 my $target_jid = $jid;
277 $comp->send_presence_xml($target_jid, 'unsubscribe', $this_jid);
278 $comp->send_presence_xml($target_jid, 'unsubscribed', $this_jid);
279 $comp->send_presence_xml($this_jid, 'unavailable', $target_jid);
281 # Update the roster information
282 my $legacy_username =
283 $comp->{protocol}->{backend}->jid_to_legacy_name($self->{jid},
284 $target_jid);
285 $comp->{protocol}->{backend}->set_roster_user_state
286 ($self->{jid}, $legacy_username,
287 $comp->{protocol}->{backend}->unsubscribed);
290 my $legacy_id =
291 $self->{protocol}->{backend}->jid_to_legacy_name($self->{jid}, $jid);
293 $self->{protocol}->unsubscribe($self, $legacy_id, $handle_unsubscription);
296 sub is_registered {
297 my $self = shift;
298 return $self->{protocol}->{backend}->registered($self->{jid});
301 sub on_connection_complete {
302 my ($self, $callback) = @_;
304 do_when('legacy_login_' . $self->{'internal_id'},
305 $callback);