File Coverage

blib/lib/AnyEvent/MSN.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package AnyEvent::MSN;
2             { $AnyEvent::MSN::VERSION = 0.002 }
3 2     2   2153 use lib '../../lib';
  2         5  
  2         18  
4 2     2   260 use 5.012;
  2         7  
  2         58  
5 2     2   857 use Moose;
  0            
  0            
6             use Moose::Util::TypeConstraints;
7             use AnyEvent qw[];
8             use AnyEvent::Handle qw[];
9             use AnyEvent::HTTP qw[];
10             use Try::Tiny;
11             use XML::Twig;
12             use AnyEvent::MSN::Protocol;
13             use AnyEvent::MSN::Types;
14             use MIME::Base64 qw[];
15              
16             #
17             #use Data::Dump;
18             #
19             our $DEBUG = 0;
20             sub DEBUG {$DEBUG}
21              
22             # XXX - During dev only
23             #use Data::Printer;
24             sub DEMOLISH {
25             my $s = shift;
26             $s->handle->destroy if $s->_has_handle && $s->handle;
27             $s->_clear_soap_requests;
28             }
29              
30             # Basic connection info
31             has host => (is => 'ro',
32             writer => '_set_host',
33             isa => 'Str',
34             default => 'messenger.hotmail.com'
35             );
36             has port => (is => 'ro',
37             writer => '_set_port',
38             isa => 'Int',
39             default => 1863
40             );
41              
42             # Authentication info from user
43             has passport => (
44             is => 'ro',
45             isa => 'AnyEvent::MSN::Types::Passport',
46             required => 1,
47             handles => {
48             username => sub {
49             shift->passport =~ m[^(.+)\@.+$];
50             $1;
51             },
52             userhost => sub { shift->passport =~ m[^.+\@(.+)$]; $1 }
53             }
54             );
55             has password => (is => 'ro', isa => 'Str', required => 1);
56              
57             # Extra stuff from user
58             has [qw[friendly_name personal_message]] =>
59             (is => 'ro', isa => 'Str', default => '');
60             has status => (
61             is => 'ro',
62             isa => 'AnyEvent::MSN::Types::OnlineStatus',
63             default => 'NLN',
64             writer => 'set_status' # exposed publicly
65             );
66              
67             # Client info for MSNP21
68             has protocol_version => (
69             is => 'ro',
70             isa => subtype(
71             as 'Str' => where {m[^(?:MSNP\d+\s*)+$]} => message {
72             'Protocol versions look like: MSNP18 MSNP21';
73             }
74             ),
75             writer => '_set_protocol_version',
76             clearer => '_reset_protocol_version',
77             default => 'MSNP21',
78             lazy => 1
79             );
80             map { has $_->[0] => (is => 'ro', isa => 'Str', default => $_->[1]) }
81             [qw[product_id PROD0120PW!CCV9@]],
82             [qw[product_key C1BX{V4W}Q3*10SM]],
83             [qw[locale_id 0x0409]],
84             [qw[os_type winnt]],
85             [qw[os_ver 6.1.1]],
86             [qw[arch i386]],
87             [qw[client_name MSNMSGR]],
88             [qw[client_version 15.4.3508.1109]],
89             [qw[client_string MSNMSGR]];
90             has guid => (
91             is => 'ro',
92             => isa => subtype(
93             as 'Str' => where {
94             my $hex = qr[[\da-f]];
95             m[{$hex{8}(?:-$hex{4}){3}-$hex{12}}$];
96             } => message {
97             'Malformed GUID. Should look like: {12345678-abcd-1234-abcd-123456789abc}';
98             }
99             ),
100             builder => '_build_guid'
101             );
102              
103             sub _build_guid {
104             state $r //= sub {
105             join '', map { ('a' .. 'f', 0 .. 9)[rand 15] } 1 .. shift;
106             };
107             sprintf '{%8s-%4s-%4s-%4s-%12s}', $r->(8), $r->(4), $r->(4), $r->(4),
108             $r->(12);
109             }
110             has location => (is => 'ro', isa => 'Str', default => 'Perl/AnyEvent::MSN');
111              
112             # Internals
113             has handle => (
114             is => 'ro',
115             isa => 'Object',
116              
117             # weak_ref => 1,
118             predicate => '_has_handle',
119             writer => '_set_handle',
120             clearer => '_reset_handle',
121             handles => {
122             send => sub {
123             my $s = shift;
124             $s->handle->push_write('AnyEvent::MSN::Protocol' => @_)
125             if $s->_has_handle; # XXX - Else mention it...
126             }
127             }
128             );
129             has tid => (is => 'ro',
130             isa => 'Int',
131             lazy => 1,
132             clearer => '_reset_tid',
133             builder => '_build_tid',
134             traits => ['Counter'],
135             handles => {'_inc_tid' => 'inc'}
136             );
137             sub _build_tid {0}
138             after tid => sub { shift->_inc_tid }; # Auto inc
139             has ping_timer => (is => 'ro',
140             isa => 'Ref', # AE::timer
141             writer => '_set_ping_timer'
142             );
143              
144             # Server configuration
145             has policies => (
146             is => 'bare',
147             isa => 'HashRef',
148             clearer => '_reset_policies',
149             writer => '_set_policies',
150             traits => ['Hash'],
151             handles => {_add_policy => 'set',
152             _del_policy => 'delete',
153             policy => 'get',
154             policies => 'kv' # XXX - Really?
155             }
156             );
157              
158             # SOAP
159             has SSOsites => (
160             is => 'ro', # Single Sign On
161             isa => 'ArrayRef[ArrayRef]',
162             traits => ['Array'],
163             default => sub {
164             [['http://Passport.NET/tb', ''],
165             ['messengerclear.live.com', 'MBI_KEY_OLD'],
166             ['messenger.msn.com', '?id=507'],
167             ['messengersecure.live.com', 'MBI_SSL'],
168             ['contacts.msn.com', 'MBI'],
169             ['storage.msn.com', 'MBI'],
170             ['sup.live.com', 'MBI']
171             ];
172             }
173             );
174             has auth_tokens => (is => 'bare',
175             isa => 'HashRef',
176             clearer => '_reset_auth_tokens',
177             writer => '_set_auth_tokens',
178             traits => ['Hash'],
179             handles => {_add_auth_token => 'set',
180             _del_auth_token => 'delete',
181             auth_token => 'get',
182             auth_tokens => 'kv'
183             }
184             );
185             has contacts => (is => 'ro',
186             isa => 'HashRef',
187             clearer => '_reset_contacts',
188             writer => '_set_contacts',
189             traits => ['Hash'],
190             );
191              
192             # Simple callbacks
193             has 'on_' . $_ => (
194             traits => ['Code'],
195             is => 'ro',
196             isa => 'CodeRef',
197             default => sub {
198             sub {1}
199             },
200             handles => {'_trigger_' . $_ => 'execute_method'},
201             )
202             for qw[
203             im nudge
204             error fatal_error connect
205             addressbook_update
206             buddylist_update
207             user_notification
208             create_circle
209             ];
210             has connected => (
211             is => 'ro',
212             isa => 'Bool',
213             traits => ['Bool'],
214             default => 0,
215             handles => {_set_connected => 'set', _unset_connected => 'unset'}
216             );
217             has redirect => (
218             is => 'ro',
219             isa => 'Str',
220             predicate => '_has_redirect',
221             writer => '_set_redirect',
222             clearer => '_reset_redirect' # XXX - Currently unused internally
223             );
224              
225             # Auto connect
226             sub BUILD {
227             my ($s, $p) = @_;
228             return if $p->{no_autoconnect};
229             $s->connect;
230             }
231              
232             sub connect {
233             my $s = shift;
234             $s->_unset_connected;
235             $s->_set_handle(
236             AnyEvent::Handle->new(
237             connect => [$s->host, $s->port],
238             on_connect => sub {
239              
240             # Get ready to read data from server
241             $s->handle->push_read(
242             'AnyEvent::MSN::Protocol' => sub {
243             my ($cmd, $tid, @data) = @_;
244             my $method = $s->can('_handle_packet_' . lc($cmd));
245             $method ||= sub {
246             $s->_trigger_error(
247             'Unhandled command type: ' . $cmd,
248             0);
249             };
250             if ($cmd =~ m[^(?:GCF|MSG|NFY|NOT|SDG|UBX|PUT)$])
251             { # payload types
252             $s->handle->unshift_read(
253             chunk => $data[-1] // $tid, # GFC:0, MSG:2
254             sub {
255             my ($_h, $_c) = @_;
256             $s->$method(
257             $tid, @data,
258             $cmd =~ m[GCF] ? $s->_parse_xml($_c)
259             : $cmd =~ m[(?:MSG|NFY|SDG)] ?
260             AnyEvent::MSN::Protocol::__parse_msn_headers(
261             $_c)
262             : $_c
263             );
264             }
265             );
266             }
267             elsif ($cmd =~ m[^\d+$]) { # Error!
268             $s->_trigger_error(
269             AnyEvent::MSN::Protocol::err2str($cmd, @data)
270             );
271             }
272             else {
273             $s->$method($tid, @data);
274             }
275             }
276             );
277              
278             # Send version negotiation
279             $s->send('VER %d %s CVR0', $s->tid, $s->protocol_version);
280              
281             # Schedule first PNG in two mins
282             $s->_set_ping_timer(AE::timer 120,
283             180, sub { $s->send('PNG') });
284             },
285             on_connect_error =>
286             sub { shift; $s->_trigger_fatal_error(shift) },
287             on_error => sub {
288             my $h = shift;
289             $s->_trigger_fatal_error(reverse @_);
290             $h->destroy;
291             },
292             on_eof => sub {
293             $_[0]->destroy;
294             $s->cleanup('connection closed');
295             }
296             )
297             );
298             }
299              
300             # Commands from notification server
301             sub _handle_packet_adl {
302             my $s = shift;
303              
304             # ACK for outgoing ADL
305             # $s->send('BLP %d AL', $s->tid);
306             }
307              
308             sub _handle_packet_chl { # Official client challenge
309             my ($s, $tid, @data) = @_;
310             my $data =
311             AnyEvent::MSN::Protocol::CreateQRYHash($data[0], $s->product_id,
312             $s->product_key);
313             $s->send("QRY %d %s %d\r\n%s",
314             $s->tid, $s->product_id, length($data), $data);
315             }
316              
317             sub _handle_packet_cvr { # Client version recommendation
318             my ($s, $tid, $r, $min_a, $min_b, $url_dl, $url_info) = @_;
319              
320             # We don't do anything with this yet but...
321             # The first parameter is a recommended version of
322             # the client for you to use, or "1.0.0000" if your
323             # client information is not recognised.
324             # The second parameter is identical to the first.
325             # The third parameter is the minimum version of the
326             # client it's safe for you to use, or the current
327             # version if your client information is not
328             # recognised.
329             # The fourth parameter is a URL you can download the
330             # recommended version of the client from.
331             # The fifth parameter is a URL the user can go to to
332             # get more information about the client.
333             $s->send('USR %d SSO I %s', $s->tid, $s->passport);
334             }
335              
336             sub _handle_packet_gcf { # Get config
337             my ($s, $tid, $len, $r) = @_;
338             if ($tid == 0) { # probably Policy list
339             $s->_set_policies($r->{Policy});
340              
341             #for (@{$s->policy('SHIELDS')->{config}{block}{regexp}{imtext}}) {
342             # my $regex = MIME::Base64::decode_base64($_);
343             # warn 'Blocking ' . qr[$regex];
344             #}
345             }
346             else {
347             ...;
348             }
349             }
350              
351             sub _handle_packet_msg {
352             my ($s, $from, $about, $len, $head, $body) = @_;
353             given ($head->{'Content-Type'}) {
354             when (m[text/x-msmsgsprofile]) {
355              
356             #
357             # http://msnpiki.msnfanatic.com/index.php/MSNP8:Messages#Profile_Messages
358             # My profile message. Expect no body.
359             }
360             when (m[text/x-msmsgsinitialmdatanotification]) { # Expect no body
361             }
362             when (m[text/x-msmsgsoimnotification]) {
363              
364             # Offline Message Waiting.
365             # Expect no body
366             # XXX - How do I request it?
367             }
368             when (m[text/x-msmsgsactivemailnotification]) {
369              
370             #warn 'You\'ve got mail!/aol'
371             }
372             when (m[text/x-msmsgsinitialmdatanotification]) {
373              
374             #warn 'You\'ve got mail!/aol'
375             }
376             default { $s->_trigger_error('Unknown message type: ' . $_) }
377             }
378             }
379              
380             sub _handle_packet_nfy {
381             my ($s, $type, $len, $headers, $data) = @_;
382              
383             =begin comment
384             use Data::Printer;
385             dd $type, $len, $headers, $data;
386             dd $s->_parse_xml($data);
387             =cut
388             given ($headers->{Uri}) {
389             when ('/user') {
390             given ($type) {
391             when ('PUT') {
392             my $xml = $s->_parse_xml($data);
393             if ((!defined $headers->{By})
394             && $headers->{From} eq '1:' . $s->passport)
395             { # Without guid
396             $s->set_status($s->status)
397             ; # Not fully logged in until sent
398             $s->_set_connected();
399             $s->_trigger_connect;
400             }
401             else {
402             $s->_trigger_user_notification($headers, $xml);
403             }
404             }
405             when ('DEL') {
406              
407             # Remove from list
408             }
409             default {...}
410             }
411             }
412             when ('/circle') {
413             my $xml = $s->_parse_xml($data);
414             $s->_trigger_create_circle($headers, $xml);
415             }
416             default {...}
417             }
418             }
419             sub _handle_packet_not { my $s = shift; }
420             sub _handle_packet_out { my $s = shift; }
421              
422             sub _handle_packet_put {
423             my $s = shift;
424              
425             # ACK for our PUT packets
426             }
427              
428             sub _handle_packet_qng {
429             my ($s, $next) = @_;
430              
431             # PONG in reply to our PNG
432             $s->_set_ping_timer(AE::timer $next, $next, sub { $s->send('PNG') });
433             }
434              
435             sub _handle_packet_qry {
436             my ($s, $tid) = @_;
437              
438             #
439             my $token = $s->auth_token('contacts.msn.com')
440             ->{'wst:RequestedSecurityToken'}{'wsse:BinarySecurityToken'}{content};
441             $token =~ s/&/&/sg;
442             $token =~ s/</&lt;/sg;
443             $token =~ s/>/&gt;/sg;
444             $token =~ s/"/&quot;/sg;
445              
446             # Reply to good challenge. Expect no body.
447             $s->_soap_request(
448             'https://local-bay.contacts.msn.com/abservice/SharingService.asmx',
449             { 'content-type' => 'text/xml; charset=utf-8',
450             SOAPAction =>
451             '"http://www.msn.com/webservices/AddressBook/FindMembership"'
452             },
453             sprintf(<<'XML', $token),
454             <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
455             <soap:Header>
456             <ABApplicationHeader xmlns="http://www.msn.com/webservices/AddressBook">
457             <ApplicationId>CFE80F9D-180F-4399-82AB-413F33A1FA11</ApplicationId>
458             <IsMigration>false</IsMigration>
459             <PartnerScenario>Initial</PartnerScenario>
460             </ABApplicationHeader>
461             <ABAuthHeader xmlns="http://www.msn.com/webservices/AddressBook">
462             <TicketToken>%s</TicketToken>
463             <ManagedGroupRequest>false</ManagedGroupRequest>
464             </ABAuthHeader>
465             </soap:Header>
466             <soap:Body>
467             <FindMembership xmlns="http://www.msn.com/webservices/AddressBook">
468             <ServiceFilter>
469             <Types>
470             <Space></Space>
471             <SocialNetwork></SocialNetwork>
472             <Profile></Profile>
473             <Invitation></Invitation>
474             <Messenger></Messenger>
475             </Types>
476             </ServiceFilter>
477             </FindMembership>
478             </soap:Body>
479             </soap:Envelope>
480             XML
481             sub {
482             my $contacts = shift;
483              
484             # XXX - Do something with these contacts
485             #...
486             }
487             );
488             $s->_soap_request(
489             'https://local-bay.contacts.msn.com/abservice/abservice.asmx',
490             { 'content-type' => 'text/xml; charset=utf-8',
491             SOAPAction =>
492             '"http://www.msn.com/webservices/AddressBook/ABFindContactsPaged"'
493             },
494             sprintf(<<'XML', $token),
495             <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
496             <soap:Header>
497             <ABApplicationHeader xmlns="http://www.msn.com/webservices/AddressBook">
498             <ApplicationId>3794391A-4816-4BAC-B34B-6EC7FB5046C6</ApplicationId>
499             <IsMigration>false</IsMigration>
500             <PartnerScenario>Initial</PartnerScenario>
501             </ABApplicationHeader>
502             <ABAuthHeader xmlns="http://www.msn.com/webservices/AddressBook">
503             <TicketToken>%s</TicketToken>
504             <ManagedGroupRequest>false</ManagedGroupRequest>
505             </ABAuthHeader>
506             </soap:Header>
507             <soap:Body>
508             <ABFindall xmlns="http://www.msn.com/webservices/AddressBook">
509             <abID>00000000-0000-0000-0000-000000000000</abID>
510             </ABFindall>
511             <ABFindContactsPaged xmlns="http://www.msn.com/webservices/AddressBook">
512             <extendedContent>AB AllGroups CircleResult</extendedContent>
513             <abView>MessengerClient8</abView>
514             <filterOptions>
515             <DeltasOnly>false</DeltasOnly>
516             <ContactFilter>
517             <IncludeShellContacts>true</IncludeShellContacts>
518             <IncludeHiddenContacts>true</IncludeHiddenContacts>
519             </ContactFilter>
520             <LastChanged>0001-01-01T00:00:00.00-08:00</LastChanged>
521             </filterOptions>
522             <pageContext>
523             <PageSize>1500</PageSize>
524             <Direction>Forward</Direction>
525             </pageContext>
526             </ABFindContactsPaged>
527             </soap:Body>
528             </soap:Envelope>
529             XML
530             sub {
531             my $contacts = shift;
532              
533             # XXX - Do something with these contacts
534             $s->_set_contacts($contacts);
535             my $ticket
536             = __html_unescape(
537             $s->contacts->{'soap:Body'}{'ABFindContactsPagedResponse'}
538             {'ABFindContactsPagedResult'}{'CircleResult'}
539             {'CircleTicket'});
540             $s->send('USR %d SHA A %s',
541             $s->tid, MIME::Base64::encode_base64($ticket, ''));
542              
543             #
544             my $x = # XML modules get it wrong if we only have 1 buddy
545             $s->contacts->{'soap:Body'}{'ABFindContactsPagedResponse'}
546             {'ABFindContactsPagedResult'}{'Contacts'}{'Contact'};
547             $x = [$x] if ref $x ne 'ARRAY';
548             $s->add_temporary_contact(map { $_->{contactInfo}{passportName} }
549             @$x);
550             }
551             );
552             }
553              
554             sub _handle_packet_rml {
555             my ($s, $tid, $ok) = @_;
556              
557             =begin comment
558             use Data::Printer;
559             dd @_;
560             =cut
561             ...;
562             }
563              
564             sub _handle_packet_sbs {
565             my $s = shift;
566              
567             # No one seems to know what this is. Official client ignores it?
568             }
569              
570             sub _handle_packet_sdg {
571             my ($s, $tid, $size, $head, $body) = @_;
572              
573             #dd [$head, $body];
574             given ($head->{'Message-Type'}) {
575             when ('Text') {
576             given ($head->{'Service-Channel'}) {
577             $s->_trigger_im($head, $body) when 'IM/Online';
578             $s->_trigger_im($head, $body) when undef;
579             warn 'Offline Msg!' when 'IM/Offline';
580             default {
581             warn 'unknown IM!!!!!'
582             }
583             }
584             }
585             $s->_trigger_nudge($head) when 'Nudge';
586             when ('Wink') { warn 'Wink' }
587             when ('CustomEmoticon') { warn 'Custom Emoticon' }
588             when ('Control/Typing') { warn 'Typing!' }
589             when ('Data') {
590             my ($header, $packet, $footer);
591             if ($head->{To} !~ m[{.+}]) {
592              
593             # 0 1 2 3 4 5
594             # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2
595             # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
596             # | SID | ID | Data Offset | Total Size |Length | Flags | AckID |AckUID | Ack Data Size |DATA....
597             # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
598             #
599             # The 48-byte binary header consists of 6 DWORDs and 3 QWORDS, which are all in little endian (little end first) order, where a DWORD is a 32-bit (4 byte) unsigned integer and QWORD is 64 bits (8 bytes).
600             #
601             # 1 0 DWORD SessionID The SessionID, which is zero when the Clients are negotiating about the session.
602             # 2 4 DWORD Identifier The first message you receive from the other client is the BaseIndentifier, the other messages contains a number near the BaseIdentifier.
603             # 3 8 QWORD Data offset Explained under Splitting big messages. Most often the messages are not split, and this value is 0.
604             # 4 16 QWORD Total data size Case 1: The byte size of all data sent between the header and footer of all of the message parts. This is the same independent of how many pieces the message is split in. Case 2: If this is an acknowledgement, this field is a copy of the same field in the message acknowledged. Sending acknowledgements
605             # 5 24 DWORD Message length The byte size of the data between the header and footer of this particular message.
606             # 6 28 DWORD Flag Identifies the message type. See the flags section
607             # 7 32 DWORD Acknowledged identifier In case the message is an acknowledgement, this is a copy of the Identifier of the acknowledged message. Else this is some random generated number.
608             # 8 36 DWORD Acknowledged unique ID In case the message is an acknowledgement, this is a copy of the previous field of the acknowledged message. Else this is 0.
609             # 9 40 QWORD Acknowledged data size In case the message is an acknowledgement, this is a copy of the Total data size field of the acknowledged message. Else this is 0.
610             sub _quad {
611             state $little//= unpack 'C', pack 'S', 1;
612             my $str = shift;
613             my $big;
614             if (!eval { $big = unpack('Q', $str); 1; }) {
615             my ($lo, $hi) = unpack 'LL', $str;
616             ($hi, $lo) = ($lo, $hi) if !$little;
617             $big = $lo + $hi * (1 + ~0);
618             if ($big + 1 == $big) {
619             warn 'A-pprox-i-mate!';
620             }
621             }
622             return $big;
623             }
624             (my ($sessionid, $identifier, $offset,
625             $total_size, $msg_len, $flag,
626             $ack_id, $ack_uid, $ack_data_size
627             ),
628             $packet
629             ) = unpack 'NNa8a8NNNNa8a*', $body;
630             ($packet, $footer)
631             = unpack 'a' . (_quad($total_size)) . ' a',
632             $packet;
633             $header = {sessionid => $sessionid,
634             identifier => $identifier,
635             offset => _quad($offset),
636             total_size => _quad($total_size),
637             msg_len => $msg_len,
638             flag => $flag,
639             ack_id => $ack_id,
640             ack_uid => $ack_uid,
641             ack_data_size => _quad($ack_data_size)
642             };
643             }
644             else {
645              
646             # 0 1 2 3
647             # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
648             # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
649             # |L|O|Len|Base ID|if L>8 then TLVs = read(L - 8) else skip ....
650             # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
651             # | if Len > 0 then Payload = (DH and D) else skip ....
652             # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
653             #
654             # For all of them - (DWORD is a 32-bit, 4 byte, unsigned integer and QWORD is 64 bits, 8 bytes.)
655             #
656             # 1 BYTE HL Length of header.
657             # 2 BYTE OP Operation code. 0: None, 2: Ack, 3: Init session.
658             # 3 WORD ML Message length without header length. (but included the header's message length)
659             # 4 DWORD BaseID Initially random (?) To get the next one add the payload length.
660             # TLVs BYTE[HL-8] TLV Data TLV list consists of TLV-encoded pairs (type, length, value). A whole TLV list is padded with zeros to fit 4-byte boundary. TLVs: T=0x1(1) L=0xc(12): IPv6 address of sender/receiver. T=0x2(2) L=0x4(4): ACK identifier.
661             # DH DHL Data Header
662             # BYTE DHL: Data header length
663             # BYTE TFCombination: 0x1=First, 0x4=Msn object (display picture, emoticon etc), 0x6=File transfer
664             # WORD PackageNumber: Package number
665             # DWORD SessionID: Session Identifier
666             # BYTE[DHL-8] Data packets TLVs: if (DHL>8) then read bytes(DHL - 8). T=0x1(1) L=0x8(8): Data remaining.
667             # D ML-DHL Data Packet SLP messsage or data packet
668             # F DWORD Footer The footer.
669             p $body;
670             my ($hl, $op, $ml, $baseid, $etc) = unpack 'CCnNa*', $body;
671              
672             #warn sprintf 'HL = %d', $hl;
673             #warn sprintf 'OP = %d (%s)', $op,
674             # ( $op == 0 ? 'None'
675             # : $op == 2 ? 'Ack'
676             # : $op == 3 ? 'Init'
677             # : 'BROKEN'
678             # );
679             #warn sprintf 'ML = %d', $ml;
680             #warn sprintf 'BaseID = %s', $baseid;
681             #
682             my $_tlv_len = $hl - 8;
683             $_tlv_len += $_tlv_len % 8;
684             my ($tlv, $moar) = unpack "a$_tlv_len a*", $etc;
685              
686             #warn sprintf 'TLV = %s', $tlv;
687             sub _tlv {
688             my ($t, $v, $m) = unpack 'CC/a', shift;
689             { shift // (), t => $t, v => $v, $m ? _tlv($m) : () }
690             }
691             my ($dhlen, $tf_combo, $pac, $ses, $XXX)
692             = unpack 'CCnNa*',
693             $moar;
694             warn length($moar);
695             ($packet, $footer) = unpack 'a' . ($ml - $dhlen) . 'a*', $XXX
696             if $XXX;
697             $header = {tlv => ($tlv ? _tlv($tlv) : ()),
698             header_len => $hl,
699             operation => $op,
700             ( $op == 0 ? 'None'
701             : $op == 2 ? 'Ack'
702             : $op == 3 ? 'Init'
703             : 'BROKEN'
704             ),
705             base_id => $baseid,
706             msg_len => $ml
707             };
708              
709             #
710             }
711              
712             #dd $header;
713             #p($packet =~ m[^(.+?)\r\n(.+)\r\n\r\n(.)$]s);
714             my ($p2p_action, $p2p_head, $p2p_body)
715             = ($packet =~ m[^(.+?)\r\n(.+)\r\n\r\n(.)$]s);
716              
717             #dd $head, $p2p_action,
718             # AnyEvent::MSN::Protocol::__parse_msn_headers($p2p_head),
719             # $p2p_body;
720             #warn 'Data'
721             # XXX - trigger a callback of some sort
722             }
723             when ('Signal/P2P') { warn 'P2P' }
724             when ('Signal/ForceAbchSync') { }
725             when ('Signal/CloseIMWindow') { }
726             when ('Signal/MarkIMWindowRead') { }
727             when ('Signal/Turn') { };
728             when ('Signal/AudioMeta') { }
729             when ('Signal/AudioTunnel') { }
730             default {...}
731             }
732             }
733              
734             sub _handle_packet_usr {
735             my ($s, $tid, $subtype, $_s, $policy, $nonce) = @_;
736             if ($subtype eq 'OK') {
737              
738             # Sent after we send ADL command. Lastcommand in the logon?
739             }
740             elsif ($subtype eq 'SSO') {
741             my $x = 1;
742             my @tokens = map {
743             sprintf <<'TOKEN', $x++, $_->[0], $_->[1] } @{$s->SSOsites};
744             <wst:RequestSecurityToken Id="RST%d">
745             <wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType>
746             <wsp:AppliesTo>
747             <wsa:EndpointReference>
748             <wsa:Address>%s</wsa:Address>
749             </wsa:EndpointReference>
750             </wsp:AppliesTo>
751             <wsse:PolicyReference URI="%s"></wsse:PolicyReference>
752             </wst:RequestSecurityToken>
753             TOKEN
754             $s->_soap_request(
755             ($s->passport =~ m[\@msn.com$]i
756             ?
757             'https://msnia.login.live.com/pp550/RST.srf'
758             : 'https://login.live.com/RST.srf'
759             ),
760             {}, # headers
761             sprintf(<<'XML', $s->password, $s->passport, join '', @tokens),
762             <Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsse="http://schemas.xmlsoap.org/ws/2003/06/secext" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:wsp="http://schemas.xmlsoap.org/ws/2002/12/policy" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/03/addressing" xmlns:wssc="http://schemas.xmlsoap.org/ws/2004/04/sc" xmlns:wst="http://schemas.xmlsoap.org/ws/2004/04/trust">
763             <Header>
764             <wsse:Security>
765             <wsse:UsernameToken Id="user">
766             <wsse:Password>%s</wsse:Password>
767             <wsse:Username>%s</wsse:Username>
768             </wsse:UsernameToken>
769             </wsse:Security>
770             <ps:AuthInfo Id="PPAuthInfo" xmlns:ps="http://schemas.microsoft.com/Passport/SoapServices/PPCRL">
771             <ps:Cookies></ps:Cookies>
772             <ps:UIVersion>1</ps:UIVersion>
773             <ps:HostingApp>{7108E71A-9926-4FCB-BCC9-9A9D3F32E423}</ps:HostingApp>
774             <ps:BinaryVersion>4</ps:BinaryVersion>
775             <ps:RequestParams>AQAAAAIAAABsYwQAAAAxMDMz</ps:RequestParams>
776             </ps:AuthInfo>
777             </Header>
778             <Body>
779             <ps:RequestMultipleSecurityTokens Id="RSTS" xmlns:ps="http://schemas.microsoft.com/Passport/SoapServices/PPCRL">
780             %s </ps:RequestMultipleSecurityTokens>
781             </Body>
782             </Envelope>
783             XML
784             sub {
785             my $d = shift;
786             for my $token (
787             @{ $d->{'S:Body'}
788             {'wst:RequestSecurityTokenResponseCollection'}
789             {'wst:RequestSecurityTokenResponse'}
790             }
791             )
792             { $s->_add_auth_token(
793             $token->{'wsp:AppliesTo'}{'wsa:EndpointReference'}
794             {'wsa:Address'},
795             $token
796             );
797             }
798              
799             #
800             if ($policy =~ m[MBI]) {
801             my $token = $s->auth_token('messengerclear.live.com')
802             ; # or http://Passport.NET/tb
803             my $token_
804             = __html_escape($token->{'wst:RequestedSecurityToken'}
805             {'wsse:BinarySecurityToken'}{'content'});
806             $s->send('USR %d SSO S %s %s %s',
807             $s->tid,
808             $token->{'wst:RequestedSecurityToken'}
809             {'wsse:BinarySecurityToken'}{'content'},
810             AnyEvent::MSN::Protocol::SSO(
811             $nonce,
812             $token->{'wst:RequestedProofToken'}
813             {'wst:BinarySecret'}
814             ),
815             $s->guid
816             );
817             }
818             elsif ($policy =~ m[^\?]) {
819             ...;
820             }
821             }
822             );
823             }
824             elsif ($subtype eq 'OK') {
825              
826             # XXX - logged in okay. What now?
827             }
828             else {
829             ...;
830             }
831             }
832              
833             sub _handle_packet_ubx { # Buddy has changed something
834             my ($s, $passport, $len, $payload) = @_;
835             my $xml = $s->_parse_xml($payload);
836             if ($len == 0 && $passport eq '1:' . $s->passport) {
837             }
838             else {
839              
840             #dd $xml;
841             my ($user) = ($passport =~ m[:(.+)$]);
842             $s->_add_temporary_contact($user, $xml);
843             }
844             }
845              
846             sub _handle_packet_uux { # ACK for UUX
847             }
848              
849             sub _handle_packet_ver { # Negotiated protocol version
850             my ($s, $tid, $r) = @_;
851             $s->_set_protocol_version($r);
852              
853             # Send basic client info
854             $s->send('CVR %d %s %s %s %s %s %s %s %s%s',
855             $s->tid,
856             $s->locale_id,
857             $s->os_type,
858             $s->os_ver,
859             $s->arch,
860             $s->client_name,
861             $s->client_version,
862             $s->client_string,
863             $s->passport,
864             (' ' . ($s->_has_redirect ? $s->redirect : ' 0'))
865             );
866             }
867              
868             sub _handle_packet_xfr { # Transver to another switchboard
869             my $s = shift;
870             my ($tid, $type, $addr, $u, $d, $redirect) = @_;
871             $s->send('OUT');
872             $s->handle->destroy;
873             my ($host, $port) = ($addr =~ m[^(.+):(\d+)$]);
874             $s->_set_host($host);
875             $s->_set_port($port);
876             $s->_set_redirect($redirect);
877             $s->connect;
878             }
879              
880             # SOAP client
881             has soap_requests => (isa => 'HashRef[AnyEvent::Util::guard]',
882             traits => ['Hash'],
883             handles => {_add_soap_request => 'set',
884             _del_soap_request => 'delete',
885             _clear_soap_requests => 'clear'
886             }
887             );
888              
889             sub _soap_request {
890             my ($s, $uri, $headers, $content, $cb) = @_;
891             my %headers = (
892             'user-agent' => 'MSNPM 1.0',
893             'content-type' => 'application/soap+xml; charset=utf-8; action=""',
894             'Expect' => '100-continue',
895             'connection' => 'Keep-Alive'
896             );
897              
898             #warn $content;
899             @headers{keys %$headers} = values %$headers;
900             $s->_add_soap_request(
901             $uri,
902             AnyEvent::HTTP::http_request(
903             POST => $uri,
904             headers => \%headers,
905             timeout => 15,
906             persistent => 1,
907             body => $content,
908             sub {
909             my ($body, $hdr) = @_;
910             my $xml = $s->_parse_xml($body);
911             $s->_del_soap_request($uri);
912             return $cb->($xml)
913             if $hdr->{Status} =~ /^2/
914             && !defined $xml->{'S:Fault'};
915              
916             #dd $hdr;
917             #dd $xml;
918             $s->_trigger_error(
919             $xml->{'soap:Body'}{'soap:Fault'}{'soap:Reason'}
920             {'soap:Text'}{'content'}
921             // $xml->{'soap:Body'}{'soap:Fault'}{'faultstring'}
922             // $hdr->{Reason});
923             }
924             )
925             );
926             }
927              
928             # Methods exposed publicly
929             sub disconnect { # cleanly disconnect from switchboard
930             my $s = shift;
931             $s->send('OUT');
932             $s->handle->on_drain(
933             sub {
934             $s->handle->destroy;
935             }
936             );
937             $s->_clear_redirect; # Start from scratch next time
938             }
939              
940             sub send_message {
941             my ($s, $to, $msg, $format) = @_;
942             $to = '1:' . $to if $to !~ m[^\d+:];
943             $format //= 'FN=Segoe%20UI; EF=; CO=0; CS=1; PF=0';
944              
945             # FN: Font name (url safe)
946             # EF: String containing...
947             # - B for Bold
948             # - U for Underline
949             # - I for Italics
950             # - S for Strikethrough
951             # CO: Color (hex without #)
952             my $data
953             = sprintf
954             qq[Routing: 1.0\r\nTo: %s\r\nFrom: 1:%s;epid=%s\r\n\r\nReliability: 1.0\r\n\r\nMessaging: 2.0\r\nMessage-Type: Text\r\nContent-Type: text/plain; charset=UTF-8\r\nContent-Length: %d\r\nX-MMS-IM-Format: %s\r\n\r\n%s],
955             $to, $s->passport, $s->guid, length($msg), $format, $msg;
956             $s->send(qq'SDG 0 %d\r\n%s', length($data), $data);
957             }
958              
959             sub nudge {
960             my ($s, $to) = @_;
961             $to = '1:' . $to if $to !~ m[^\d+:];
962             my $data
963             = sprintf
964             qq[Routing: 1.0\r\nTo: %s\r\nFrom: 1:%s;epid=%s\r\n\r\nReliability: 1.0\r\n\r\nMessaging: 2.0\r\nMessage-Type: Nudge\r\nService-Channel: IM/Online\r\nContent-Type: text/plain; charset=UTF-8\r\nContent-Length: 0\r\n\r\n],
965             $to, $s->passport, $s->guid;
966             $s->send("SDG 0 %d\r\n%s", length($data), $data);
967             }
968              
969             sub add_contact {
970             my ($s, $contact) = @_;
971              
972             #
973             my $token = $s->auth_token('contacts.msn.com')
974             ->{'wst:RequestedSecurityToken'}{'wsse:BinarySecurityToken'}{content};
975             $token =~ s/&/&amp;/sg;
976             $token =~ s/</&lt;/sg;
977             $token =~ s/>/&gt;/sg;
978             $token =~ s/"/&quot;/sg;
979              
980             #
981             $s->_soap_request(
982             'https://local-bay.contacts.msn.com/abservice/abservice.asmx',
983             { 'content-type' => 'text/xml; charset=utf-8',
984             SOAPAction =>
985             '"http://www.msn.com/webservices/AddressBook/ABContactAdd"'
986             },
987             sprintf(<<'XML', $token, $contact),
988             <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
989             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
990             xmlns:xsd="http://www.w3.org/2001/XMLSchema"
991             xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
992             <soap:Header>
993             <ABApplicationHeader xmlns="http://www.msn.com/webservices/AddressBook">
994             <ApplicationId>CFE80F9D-180F-4399-82AB-413F33A1FA11</ApplicationId>
995             <IsMigration>false</IsMigration>
996             <PartnerScenario>ContactSave</PartnerScenario>
997             </ABApplicationHeader>
998             <ABAuthHeader xmlns="http://www.msn.com/webservices/AddressBook">
999             <TicketToken>%s</TicketToken>
1000             <ManagedGroupRequest>false</ManagedGroupRequest>
1001             </ABAuthHeader>
1002             </soap:Header>
1003             <soap:Body>
1004             <ABContactAdd xmlns="http://www.msn.com/webservices/AddressBook">
1005             <abId>
1006             00000000-0000-0000-0000-000000000000
1007             </abId>
1008             <contacts>
1009             <Contact xmlns="http://www.msn.com/webservices/AddressBook">
1010             <contactInfo>
1011             <contactType>LivePending</contactType>
1012             <passportName>%s</passportName>
1013             <isMessengerUser>true</isMessengerUser>
1014             <MessengerMemberInfo>
1015             <DisplayName>minimum clorpvfgt</DisplayName>
1016             </MessengerMemberInfo>
1017             </contactInfo>
1018             </Contact>
1019             </contacts>
1020             <options>
1021             <EnableAllowListManagement>
1022             true
1023             </EnableAllowListManagement>
1024             </options>
1025             </ABContactAdd>
1026              
1027             </soap:Body>
1028             </soap:Envelope>
1029             XML
1030             sub {
1031              
1032             #dd @_;
1033             $s->add_temporary_contact($contact);
1034             }
1035             );
1036             }
1037              
1038             sub remove_contact {
1039             my ($s, $contact) = @_;
1040              
1041             #
1042             my $token = $s->auth_token('contacts.msn.com')
1043             ->{'wst:RequestedSecurityToken'}{'wsse:BinarySecurityToken'}{content};
1044             $token =~ s/&/&amp;/sg;
1045             $token =~ s/</&lt;/sg;
1046             $token =~ s/>/&gt;/sg;
1047             $token =~ s/"/&quot;/sg;
1048              
1049             #
1050             $s->_soap_request(
1051             'https://contacts.msn.com/abservice/abservice.asmx',
1052             {'content-type' => 'text/xml; charset=utf-8',
1053             SOAPAction =>
1054             '"http://www.msn.com/webservices/AddressBook/ABContactDelete"'
1055             },
1056             sprintf(<<'XML', $token, $contact),
1057             <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
1058             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1059             xmlns:xsd="http://www.w3.org/2001/XMLSchema"
1060             xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
1061             <soap:Header>
1062             <ABApplicationHeader xmlns="http://www.msn.com/webservices/AddressBook">
1063             <ApplicationId>CFE80F9D-180F-4399-82AB-413F33A1FA11</ApplicationId>
1064             <IsMigration>false</IsMigration>
1065             <PartnerScenario>ContactSave</PartnerScenario>
1066             </ABApplicationHeader>
1067             <ABAuthHeader xmlns="http://www.msn.com/webservices/AddressBook">
1068             <TicketToken>%s</TicketToken>
1069             <ManagedGroupRequest>false</ManagedGroupRequest>
1070             </ABAuthHeader>
1071             </soap:Header>
1072             <soap:Body>
1073             <ABContactAdd xmlns="http://www.msn.com/webservices/AddressBook">
1074             <abId>
1075             00000000-0000-0000-0000-000000000000
1076             </abId>
1077             <contacts>
1078             <Contact xmlns="http://www.msn.com/webservices/AddressBook">
1079             <contactInfo>
1080             <contactType>LivePending</contactType>
1081             <passportName>%s</passportName>
1082             <isMessengerUser>true</isMessengerUser>
1083             <MessengerMemberInfo>
1084             <DisplayName>minimum clorpvfgt</DisplayName>
1085             </MessengerMemberInfo>
1086             </contactInfo>
1087             </Contact>
1088             </contacts>
1089             <options>
1090             <EnableAllowListManagement>
1091             true
1092             </EnableAllowListManagement>
1093             </options>
1094             </ABContactAdd>
1095             </soap:Body>
1096             </soap:Envelope>
1097             XML
1098             sub {
1099              
1100             #dd @_;
1101             $s->remove_temporary_contact($contact);
1102             ...;
1103             }
1104             );
1105             }
1106              
1107             # Remove a contact:
1108             # RML 12 112\r\n
1109             # <ml><d n="penilecolada.com"><c n="junk" t="1"><s l="3" n="IM" /><s l="3" n="PE" /><s l="3" n="PF"/></c></d></ml>
1110             sub add_temporary_contact {
1111             my $s = shift;
1112             my %contacts;
1113             for my $contact (@_) {
1114             my ($user, $domain) = split /\@/, $contact, 2;
1115             push @{$contacts{$domain}}, $user;
1116             }
1117             my $data = sprintf '<ml%s>%s</ml>', ($s->connected ? '' : ' l="1"'),
1118             join '', map {
1119             sprintf '<d n="%s">%s</d>', $_, join '', map {
1120             sprintf '<c n="%s" t="1">%s</c>', $_, join '',
1121             map {"<s l='3' n='$_' />"}
1122             qw[IM PE PF]
1123             } sort @{$contacts{$_}}
1124             } sort keys %contacts;
1125             my $tid = $s->tid;
1126             $s->send("ADL %d %d\r\n%s", $tid, length($data), $data);
1127             $tid;
1128             }
1129              
1130             sub remove_buddy {
1131             my $s = shift;
1132             my $data = sprintf <<'', reverse split '@', shift, 2;
1133             <ml>
1134             <d n="%s">
1135             <c n="%s" t="1">
1136             <s l="3" n="IM" />
1137             <s l="3" n="PE" />
1138             <s l="3" n="PF" />
1139             </c>
1140             </d>
1141             </ml>
1142              
1143             my $tid = $s->tid;
1144             $s->send("RML %d %d\r\n%s", $tid, length($data), $data);
1145             $tid;
1146             }
1147             after set_status => sub {
1148             my ($s, $status) = @_;
1149             my $body = sprintf '<user>' . '<s n="PE">
1150             <UserTileLocation>0</UserTileLocation><FriendlyName>%s</FriendlyName><PSM>%s</PSM><RUM></RUM><RLT>0</RLT></s>'
1151             . '<s n="IM"><Status>%s</Status><CurrentMedia></CurrentMedia></s>'
1152             . '<sep n="PD"><ClientType>1</ClientType><EpName>%s</EpName><Idle>false</Idle><State>%s</State></sep>'
1153             . '<sep n="PE" epid="%s"><VER>MSNMSGR:15.4.3508.1109</VER><TYP>1</TYP><Capabilities>2952790016:557056</Capabilities></sep>'
1154             . '<sep n="IM"><Capabilities>2953838624:132096</Capabilities></sep>'
1155             . '</user>', __html_escape($s->friendly_name),
1156             __html_escape($s->personal_message),
1157             $status,
1158             __html_escape($s->location), $status, $s->guid;
1159             my $out
1160             = sprintf
1161             qq[To: 1:%s\r\nRouting: 1.0\r\nFrom: 1:%s;epid=%s\r\n\r\nStream: 1\r\nFlags: ACK\r\nReliability: 1.0\r\n\r\nContent-Length: %d\r\nContent-Type: application/user+xml\r\nPublication: 1.0\r\nUri: /user\r\n\r\n%s],
1162             $s->passport,
1163             $s->passport, $s->guid, length($body), $body;
1164             $s->send("PUT %d %d\r\n%s", $s->tid, length($out), $out);
1165             };
1166              
1167             # Testing/Incomplete stuff
1168             sub create_group_chat {
1169             my $s = shift;
1170             my $body = ''; # For now.
1171             my $out
1172             = sprintf
1173             qq[To: 10:00000000-0000-0000-0000-000000000000\@live.com\r\nRouting: 1.0\r\nFrom: 1:%s;epid=%s\r\n\r\nStream: 1\r\nFlags: ACK\r\nReliability: 1.0\r\n\r\nContent-Length: %d\r\nContent-Type: application/multiparty+xml\r\nPublication: 1.0\r\nUri: /circle\r\n\r\n%s],
1174             $s->passport, $s->guid, length($body), $body;
1175             $s->send("PUT %d %d\r\n%s", $s->tid, length($out), $out);
1176             }
1177              
1178             # Random private methods
1179             sub _parse_xml {
1180             my ($s, $data) = @_;
1181             state $xml_twig //= XML::Twig->new();
1182             my $xml = {};
1183             use Carp;
1184              
1185             =begin comment Carp::confess('...') if ! length $data ;
1186             =cut
1187             try {
1188             $xml_twig->parse($data);
1189             $xml = $xml_twig->simplify(keyattr => [qw[type id value]]);
1190             }
1191             catch { $s->_trigger_fatal_error(qq[parsing XML: $_]) };
1192             $xml;
1193             }
1194              
1195             # Non-OOP utility functions
1196             sub __html_escape {
1197             my $x = shift;
1198             $x =~ s[&][&amp;]sg;
1199             $x =~ s[<][&lt;]sg;
1200             $x =~ s[>][&gt;]sg;
1201             $x =~ s["][&quot;]sg;
1202             $x;
1203             }
1204              
1205             sub __html_unescape {
1206             my $x = shift;
1207             $x =~ s[&lt;][<]sg;
1208             $x =~ s[&gt;][>]sg;
1209             $x =~ s[&quot;]["]sg;
1210             $x =~ s[&amp;][&]sg;
1211             $x;
1212             }
1213              
1214             #
1215             __PACKAGE__->meta->make_immutable();
1216             no Moose;
1217             1;
1218              
1219             =pod
1220              
1221             =head1 NAME
1222              
1223             AnyEvent::MSN - Simple, Less Annoying Client for Microsoft's Windows Live Messenger Network
1224              
1225             =head1 Synopsis
1226              
1227             use AnyEvent::MSN;
1228             my $msn = AnyEvent::MSN->new(
1229             passport => 'you@hotmail.com',
1230             password => 'sekrit',
1231             on_im => sub { # Simiple echo bot
1232             my ($msn, $head, $body) = @_;
1233             $msn->im($head->{From}, $body)
1234             }
1235             );
1236             AnyEvent->condvar->recv;
1237              
1238             =head1 Description
1239              
1240             TODO
1241              
1242             =head1 Methods
1243              
1244             Well, the public bits anyway...
1245              
1246             =over
1247              
1248             =item new
1249              
1250             my $msn = AnyEvent::MSN->new(passport => 'you@hotmail.com', password => 'password');
1251              
1252             This constructs a new L<AnyEvent::MSN> object. Required parameters are:
1253              
1254             =over
1255              
1256             =item C<passport>
1257              
1258             This is an email address.
1259              
1260             Microsoft calls them C<passport>s in some documentation, C<username> and plain
1261             ol' C<address> in other places. For future versions of the API (think 1.0),
1262             I'm leaning towards the least awkward: C<username>. Just... keep that in mind.
1263              
1264             =item C<password>
1265              
1266             It's... your... password.
1267              
1268             =back
1269              
1270             Optional parameters to C<new> include...
1271              
1272             =over
1273              
1274             =item C<status>
1275              
1276             This will be used as your initial online status. Please see the section
1277             entitled L<Online Status|/"Online Status"> for more information.
1278              
1279             =item C<friendly_name>
1280              
1281             This sets the display or friendly name for the client. This is what your
1282             friends see on their buddylists.
1283              
1284             =item C<personal_message>
1285              
1286             This is the short message typically shown below the friendly name.
1287              
1288             =item C<no_autoconnect>
1289              
1290             Normally, L<AnyEvent::MSN-E<gt>new( ... )|/new> automatically initiates the
1291             L<client login|/connect> stage. If this is set to a true value, that doesn't
1292             happen and you'll need to call L<connect|/connect> yourself.
1293              
1294             =item C<on_connect>
1295              
1296             This is callback is triggered when we have completed the login stage but
1297             before we set our initial status.
1298              
1299             =item C<on_im>
1300              
1301             This callback is triggered when we receive an instant message. It is passed
1302             the raw headers (which contain a 'From' value) and the actual message.
1303              
1304             =item C<on_nudge>
1305              
1306             This callback is triggered when we recieve a nudge. The callback is passed the
1307             raw headers (which contain a 'From' value).
1308              
1309             =item C<on_error>
1310              
1311             This callback is triggered when we meet any sort of non-fatal error. This
1312             callback is passed a texual message for display.
1313              
1314             =item C<on_fatal_error>
1315              
1316             This callback is triggered when we meet an error which prevents normal client
1317             operations. This could be a major SOAP error or even an unexpected disconnect.
1318             This callback is passed a textual message for display.
1319              
1320             =item C<on_user_notification>
1321              
1322             ...
1323             on_user_notification => sub { my ($s, $head, $presence) = @_; ... }
1324             ...
1325              
1326             This callback is triggered when a contact updates their public information.
1327             Simple Online/Offline status changes are included in this as well as friendly
1328             name changes and current media (now playing) status.
1329              
1330             =back
1331              
1332             =item connect
1333              
1334             Initiates the logon process. You should only need to call this if you passed
1335             C<no_autoconnect =E<gt> 1> to L<the constructor|/new>.
1336              
1337             =item im
1338              
1339             $msn->send_message('buddy@hotmail.com', 'oh hai!');
1340              
1341             This sends an instant message.
1342              
1343             C<send_message( ... )> supports a third parameter, a string to indicate how
1344             the message shoud be displayed. The default is
1345             C<FN=Segoe%20UI; EF=; CO=0; CS=1; PF=0>. Uh, we break that down a little in
1346             L<the notes|/"Text Format"> below.
1347              
1348             =item nudge
1349              
1350             $msn->nudge('buddy@hotmail.com');
1351              
1352             This sends a nudge to the other person. It's called nudge in the protocol
1353             itself and in pidgin but in the the official client it's called 'Attention'
1354             and may (depending on the buddy's settings) make the IM window jiggle on
1355             screen for a second. ...which, I suppose, won the contest for the most
1356             annoying behaviour they could come up with.
1357              
1358             =item add_contact
1359              
1360             $msn->add_contact('silas@live.com'); # Temporary
1361             $msn->add_contact('mark@hotmail.com'); # Persistant
1362              
1363             This adds a buddy to your temporary list of contacts.
1364              
1365             'Add List' command, uses XML to identify each contact, and works as a payload
1366             message. Each ADL command may contain up to 150 contacts (a payload of roughly
1367             7500 bytes). The format of the payload looks like this:
1368             <ml l="1">
1369             <d n="domain">
1370             <c n="email" l="3" t="1" />
1371             </d>
1372             </ml>
1373             Elements:
1374             ml: the meaning of l is unknown (thought to mean initial listing due to the
1375             fact that it is only sent in the initial ADL)
1376             d (domain): contacts are grouped by domain, where n is the domain name (the
1377             part after the @ symbol in the email address)
1378             c (contact): n is the name or the part before the @ symbol in the email
1379             address, l is the list bit flag (i.e. 1 for FL, 2 for AL, 4 for BL) and t
1380             is the contact type (1 for a Passport, 4 for a mobile phone, other values
1381             are still unknown)
1382             Note: you can send all your contacts in just one ADL command by putting
1383             multiple 'd' elements (with the sub-elements of course) for each contact
1384             e.g.:
1385             <ml l="1">
1386             <d n="domain1">
1387             <c n="email1" l="3" t="1" />
1388             </d>
1389             <d n="domain2">
1390             <c n="email2" l="5" t="4" />
1391             </d>
1392             </ml>
1393             Sending ADL to the server:
1394             >>> ADL (TrId) (PayloadLength)\r\n
1395             Then send your payload:
1396             >>> <ml l="1"><d n="domain"><c n="email" l="3" t="1" /></d></ml>
1397             The payload must not contain any 'whitespace' characters (i.e. returns, tabs or spaces) between tags or at the beginning or end, or the server will reply with error 240 or 241.
1398             The server responds to a successful ADL command with:
1399             ADL (TrId) OK
1400             Initial ADL listing
1401             Once the client has retrieved the contact list with a new set of SOAP requests (see MSNP13:Contact_List), it will send the information about the contacts on the list to the server with an ADL command. In this ADL, the <ml> node often seems to contain the attribute l, set to 1. However, the client does not always appear to send this attribute in the official listing!
1402             You must include everyone on your Forward List (FL), Allow List (AL) and Block List (BL). If you don't, anyone you fail to include will be removed from their respective lists. Also note that the official client does not include contacts on the RL and PL in the initial listing. In fact, if you send the RL and PL bits in the ADL, the server will reject your ADL command, and possibly disconnect you.
1403             You MUST send your privacy settings (BLP command), then ADL and finally your display name (PRP command) in that order or the server will ignore your ADL. These are retrieved using the ABFindAll SOAP request.
1404             After receiving ADL (TrId) OK, you must set your initial presence (CHG command). If you send CHG before ADL, the servers will not dispatch your presence to other clients.
1405              
1406             =item remove_buddy
1407              
1408             $msn->remove_buddy('buddy@hotmail.com');
1409              
1410             The remove contacts from your lists. Note that you may only remove people from
1411             the FL, AL and BL using this command (which makes sense, seeing as you can
1412             also only add people to the FL, AL and BL with the L<add_contact|/add_contact>
1413             command). Also note that the contact will not be removed from your server-side
1414             address book - for this, you will have to use the ABContactDelete SOAP
1415             request. ...which we don't support yet.
1416              
1417             =back
1418              
1419             =head1 Notes
1420              
1421             This is where random stuff will go. The sorts of things which may make life
1422             somewhat easier for you but are easily skipped.
1423              
1424             =head2 Online Status
1425              
1426             Your online status not only affects your appearance on other's buddylists, but
1427             can change how your buddies are shown.
1428              
1429             =over
1430              
1431             =item NLN
1432              
1433             Make the client appear Online (after logging in) and send and receive
1434             notifications about buddies.
1435              
1436             This is the default.
1437              
1438             =item FLN
1439              
1440             Make the client Offline. If the client is already online, offline
1441             notifications will be sent to users on the RL. No message activity is allowed.
1442             In this state, the client can only synchronize the lists as described above.
1443              
1444             =item HDN
1445              
1446             Make the client appear Hidden/Invisible. If the client is already
1447             online, offline notifications will be sent to users on the RL. The client will
1448             appear as Offline to others but can receive online/offline notifications from
1449             other users, and can also synchronize the lists. Clients cannot receive any
1450             instant messages in this state.
1451              
1452             =item BSY
1453              
1454             Make the client appear Busy. This is a sub-state of NLN.
1455              
1456             =item IDL
1457              
1458             Make the client appear Idle. This is a sub-state of NLN.
1459              
1460             =item BRB
1461              
1462             Make the client say they'll Be Right Back. This is a sub-state of NLN.
1463              
1464             =item AWY
1465              
1466             Make the client appear to be Away from their computer. This is a sub-state of
1467             NLN.
1468              
1469             =item PHN
1470              
1471             Makes the client appear to be on the Phone. This is a sub-state of NLN.
1472              
1473             =item LUN
1474              
1475             Makes the client appear to be out to Lunch. This is a sub-state of NLN.
1476              
1477             =back
1478              
1479             =back
1480              
1481             =head1 Notes
1482              
1483             Get by with a little help...
1484              
1485             =head2 Text Format
1486              
1487             Messages sent and recieved may contain a special parameter defining how the
1488             message should be displayed. The message format specifies the font (FN),
1489             effect (EF), color (CO), character set (CS) and pitch and family (PF) used for
1490             rendering the text message. The value of the Format element is a string of the
1491             following key/value pairs:
1492              
1493             =for url http://msdn.microsoft.com/en-us/library/bb969558(v=office.12).aspx
1494              
1495             =over
1496              
1497             =item FN
1498              
1499             Specifies a font name. The font name must be URL-encoded. For example, to have
1500             a font of "MS Sans Serif", you would have to specify C<FN=MS%20Sans%20Serif>.
1501             Font names are not case-sensitive and only spaces should be URL-encoded.
1502             URL-encoding other characters such as numbers and letters cause unpredictable
1503             results in other clients.
1504              
1505             According to MS, if the receiving client does not have the specified font, it
1506             should make judgment based on the PF and CS parameters. Basically, the client
1507             should select whichever available font supports the character set specified in
1508             CS and is closest to the category specified in PF. If those parameters are not
1509             present, the client should just use a default font.
1510              
1511             =item EF
1512              
1513             Specifies optional style effects. Possible effects are bold, italic,
1514             underline, and strikethrough. Each effect is referred to by its first letter.
1515             For example, to make bold-italic text, include the parameter C<EF=IB> or
1516             C<EF=BI>. The order does not matter. Any unknown effects are to be ignored.
1517             If there are no effects, just leave the parameter value blank.
1518              
1519             =item CO
1520              
1521             Specifies a font color. The value of the CO field is a six-character
1522             hex BGR (Note that this is I<blue-green-red>, the I<reverse> of the standard
1523             RGB order seen in HTML) string. The first two characters represent a hex
1524             number from C<00> to C<ff> for the intensity of blue, the second two are for
1525             green, and the third two are for red. For example, to make a full red color,
1526             send C<CO=0000ff>.
1527              
1528             Again, this should be in BGR; the I<reverse> of the standard RGB order seen in
1529             HTML.
1530              
1531             =item CS
1532              
1533             Character sets are identified in the CS parameter with one or two hexadecimal
1534             digits (leading zeros are dropped by the official client and are ignored if
1535             present), representing the numerical value Windows uses for the character set.
1536             The following table shows the full list of the predefined character sets that
1537             are included with the Microsoft® Windows® operating system.
1538              
1539             Val Description
1540             -------------------------------------------------------------------------
1541             00 ANSI characters
1542             01 Font is chosen based solely on name and size. If the described font is
1543             not available on the system, you should substitute another font.
1544             02 Standard symbol set
1545             4d Macintosh characters
1546             80 Japanese shift-JIS characters
1547             81 Korean characters (Wansung)
1548             82 Korean characters (Johab)
1549             86 Simplified Chinese characters (China)
1550             88 Traditional Chinese characters (Taiwan)
1551             a1 Greek characters
1552             a2 Turkish characters
1553             a3 Vietnamese characters
1554             b1 Hebrew characters
1555             b2 Arabic characters
1556             ba Baltic characters
1557             cc Cyrillic characters
1558             de Thai characters
1559             ee Sometimes called the "Central European" character set, this includes
1560             diacritical marks for Eastern European countries
1561             ff Depends on the codepage of the operating system
1562              
1563             You should not assume that clients receiving your messages understand all
1564             character sets. This character set is arbitrary, but it is advisable to make
1565             it the one that causes the most characters to be displayed correctly.
1566              
1567             =item PF
1568              
1569             The PF family defines the category that the font specified in the FN parameter
1570             falls into. This parameter is used by the receiving client if it does not have
1571             the specified font installed. The value is a two-digit hexadecimal number.
1572             If you're familiar with the Windows APIs, this value is the PitchAndFamily
1573             value in RichEdit and LOGFONT.
1574              
1575             The first digit of the value represents the font family. Below is a list of
1576             numbers for the first digit and the font families they represent.
1577              
1578             First Digit Description
1579             -------------------------------------------------------------------------
1580             0_ Specifies a generic family name. This name is used when
1581             information about a font does not exist or does not
1582             matter. The default font is used.
1583             1_ Specifies a proportional (variable-width) font with
1584             serifs. An example is Times New Roman.
1585             2_ Specifies a proportional (variable-width) font without
1586             serifs. An example is Arial.
1587             3_ Specifies a Monospace font with or without serifs.
1588             Monospace fonts are usually modern; examples include Pica,
1589             Elite, and Courier New.
1590             4_ Specifies a font that is designed to look like
1591             handwriting; examples include Script and Cursive.
1592             5_ Specifies a novelty font. An example is Old English.
1593              
1594             The second digit represents the pitch of the font — in other words, whether it
1595             is monospace or variable-width.
1596              
1597             Second Digit Description
1598             -------------------------------------------------------------------------
1599             _0 Specifies a generic font pitch. This name is used when
1600             information about a font does not exist or does not
1601             matter. The default font pitch is used.
1602             _1 Specifies a fixed-width (Monospace) font. Examples are
1603             Courier New and Bitstream Vera Sans Mono.
1604             _2 Specifies a variable-width (proportional) font. Examples
1605             are Times New Roman and Arial.
1606              
1607             Below are some PF values and example fonts that fit the category.
1608              
1609             Examples of PF Value Description
1610             -------------------------------------------------------------------------
1611             12 Times New Roman, MS Serif, Bitstream Vera Serif
1612             22 Arial, Verdana, MS Sans Serif, Bitstream Vera Sans
1613             31 Courier New, Courier
1614             42 Comic Sans MS
1615              
1616             =head1 TODO
1617              
1618             These are things I have plans to do with L<AnyEvent::MSN> but haven't found
1619             the time to complete them. If you'd like to help or have a suggestion for new
1620             feature, see the project pages on
1621             L<GitHub|http://github.com/sanko/anyevent-msn>.
1622              
1623             =over
1624              
1625             =item P2P Transfers
1626              
1627             MSNP supports simple file transfers, handwritten IMs, voice chat, and even
1628             webcam sessions through the P2P protocol. The protocol changed between MSNP18
1629             and MSNP21 and I'll need to implement both. ...I'll get to it eventually.
1630              
1631             =item Group Chat
1632              
1633             MSNP21 redefinied the switchboard concept including how group chat sessions
1634             are initiated and handled.
1635              
1636             =item Internal State Cleanup
1637              
1638             Things like the address book are very difficult to use because (for now) I
1639             simply store the parsed XML Microsoft sends me.
1640              
1641             =item Correct Client Capabilities
1642              
1643             They (and a few other properties) are all hardcoded values taken from MSN 2011
1644             right now.
1645              
1646             =back
1647              
1648             =head1 See Also
1649              
1650             =over
1651              
1652             =item L<Net::MSN|Net::MSN>
1653              
1654             =item L<MSN::PersonalMessage|MSN::PersonalMessage>
1655              
1656             =item L<POE::Component::Client::MSN|POE::Component::Client::MSN>
1657              
1658             =item L<Net::Msmgr::Session|Net::Msmgr::Session>
1659              
1660             =back
1661              
1662             =head1 Author
1663              
1664             Sanko Robinson <sanko@cpan.org> - http://sankorobinson.com/
1665              
1666             CPAN ID: SANKO
1667              
1668             =head1 License and Legal
1669              
1670             Copyright (C) 2011-2012 by Sanko Robinson <sanko@cpan.org>
1671              
1672             This program is free software; you can redistribute it and/or modify it under
1673             the terms of
1674             L<The Artistic License 2.0|http://www.perlfoundation.org/artistic_license_2_0>.
1675             See the F<LICENSE> file included with this distribution or
1676             L<notes on the Artistic License 2.0|http://www.perlfoundation.org/artistic_2_0_notes>
1677             for clarification.
1678              
1679             When separated from the distribution, all original POD documentation is
1680             covered by the
1681             L<Creative Commons Attribution-Share Alike 3.0 License|http://creativecommons.org/licenses/by-sa/3.0/us/legalcode>.
1682             See the
1683             L<clarification of the CCA-SA3.0|http://creativecommons.org/licenses/by-sa/3.0/us/>.
1684              
1685             Some protocol descriptions taken from text Copyright 2011, Microsoft.
1686              
1687             Neither this module nor the L<Author|/Author> is affiliated with Microsoft.
1688              
1689             =cut