Net::REPL::Client: Allow read/print portions to be overridden separately.
[thrasher.git] / perl / lib / Thrasher / Session.pm
blob6c0a610fd392b4981a4cbd256a05ed4494a809e2
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 $comp->send_presence($self->{jid}, $legacy_username);
207 log("Using stored presence information for $legacy_username");
208 $comp->send_presence($self->{jid}, $legacy_username,
209 @$stored_presence);
210 } else {
211 log("no stored presence information for $legacy_username found.");
214 # 4.6 #5
215 $comp->send_presence_xml($this_jid, 'subscribe', $target_jid);
217 # 4.6 #6, WTF? You can't do this, the legacy user doesn't
218 # get XML stanzas...? Psi seems to agree, so
219 # XEP-0100 violation, we don't send this.
220 # FIXME: Mail the standards list about this.
221 #$comp->send_presence_xml($target_jid, 'subscribed', $this_jid);
223 # Update the roster information
224 my $legacy_username =
225 $comp->{protocol}->{backend}->jid_to_legacy_name($self->{jid},
226 $target_jid);
227 $comp->{protocol}->{backend}->set_roster_user_state
228 ($self->{jid}, $legacy_username,
229 $comp->{protocol}->{backend}->subscribed);
230 } else {
231 # Unsuccessful subscription, assumed to be because the
232 # legacy user rejected it. Section 4.6.2.
233 $comp->send_presence_xml($this_jid, 'unsubscribed', $target_jid);
237 my $legacy_id =
238 $self->{protocol}->{backend}->jid_to_legacy_name($self->{jid}, $jid);
240 $self->{protocol}->subscribe($self, $legacy_id, $handle_subscription);
243 # Implementing section 4.7
244 sub unsubscribe {
245 my $self = shift;
246 my $jid = shift;
248 # FIXME: Should use the protocol for name translation here
249 my ($user_name) = split(/\@/, $jid);
250 if (!defined($user_name)) {
251 # FIXME: Error
254 # We assume subscription is successful, because XMPP assumes it is
255 my $handle_unsubscription = sub {
256 my $comp = $self->{component};
257 my $this_jid = $self->{jid};
258 my $target_jid = $jid;
260 $comp->send_presence_xml($target_jid, 'unsubscribe', $this_jid);
261 $comp->send_presence_xml($target_jid, 'unsubscribed', $this_jid);
262 $comp->send_presence_xml($this_jid, 'unavailable', $target_jid);
264 # Update the roster information
265 my $legacy_username =
266 $comp->{protocol}->{backend}->jid_to_legacy_name($self->{jid},
267 $target_jid);
268 $comp->{protocol}->{backend}->set_roster_user_state
269 ($self->{jid}, $legacy_username,
270 $comp->{protocol}->{backend}->unsubscribed);
273 my $legacy_id =
274 $self->{protocol}->{backend}->jid_to_legacy_name($self->{jid}, $jid);
276 $self->{protocol}->unsubscribe($self, $legacy_id, $handle_unsubscription);
279 sub is_registered {
280 my $self = shift;
281 return $self->{protocol}->{backend}->registered($self->{jid});
284 sub on_connection_complete {
285 my ($self, $callback) = @_;
287 do_when('legacy_login_' . $self->{'internal_id'},
288 $callback);