File Coverage

blib/lib/Net/Async/Matrix.pm
Criterion Covered Total %
statement 244 375 65.0
branch 37 90 41.1
condition 20 56 35.7
subroutine 54 82 65.8
pod 18 26 69.2
total 373 629 59.3


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2014-2016 -- leonerd@leonerd.org.uk
5              
6             package Net::Async::Matrix;
7              
8 12     12   1609593 use strict;
  12         23  
  12         338  
9 12     12   45 use warnings;
  12         16  
  12         345  
10              
11 12     12   54 use base qw( IO::Async::Notifier );
  12         12  
  12         5990  
12             IO::Async::Notifier->VERSION( '0.63' ); # adopt_future
13              
14             our $VERSION = '0.18_002';
15             $VERSION = eval $VERSION;
16              
17 12     12   44324 use Carp;
  12         18  
  12         631  
18              
19 12     12   49 use Future;
  12         18  
  12         236  
20 12     12   5599 use Future::Utils qw( repeat );
  12         20897  
  12         795  
21 12     12   446 use JSON::MaybeXS qw( encode_json decode_json );
  12         4454  
  12         540  
22              
23 12     12   5495 use Data::Dump 'pp';
  12         49814  
  12         825  
24 12     12   5770 use File::stat;
  12         64018  
  12         48  
25 12     12   6094 use Struct::Dumb;
  12         15152  
  12         47  
26 12     12   630 use Time::HiRes qw( time );
  12         19  
  12         95  
27 12     12   1704 use URI;
  12         3176  
  12         470  
28              
29             struct User => [qw( user_id displayname presence last_active )];
30              
31 12     12   5705 use Net::Async::Matrix::Room;
  12         25  
  12         402  
32              
33 12     12   66 use constant PATH_PREFIX => "/_matrix/client/r0";
  12         12  
  12         740  
34 12     12   52 use constant LONGPOLL_TIMEOUT => 30;
  12         16  
  12         644  
35              
36             # This is only needed for the (undocumented) recaptcha bypass feature
37 12     12   48 use constant HAVE_DIGEST_HMAC_SHA1 => eval { require Digest::HMAC_SHA1; };
  12         14  
  12         18  
  12         5462  
38              
39             =head1 NAME
40              
41             C - use Matrix with L
42              
43             =head1 SYNOPSIS
44              
45             use Net::Async::Matrix;
46             use IO::Async::Loop;
47              
48             my $loop = IO::Async::Loop->new;
49              
50             my $matrix = Net::Async::Matrix->new(
51             server => "my.home.server",
52             );
53              
54             $loop->add( $matrix );
55              
56             $matrix->login(
57             user_id => '@my-user:home.server',
58             password => 'SeKr1t',
59             )->get;
60              
61             =head1 DESCRIPTION
62              
63             F is an new open standard for interoperable Instant Messaging and VoIP,
64             providing pragmatic HTTP APIs and open source reference implementations for
65             creating and running your own real-time communication infrastructure.
66              
67             This module allows an program to interact with a Matrix homeserver as a
68             connected user client.
69              
70             L
71              
72             =cut
73              
74             =head1 EVENTS
75              
76             The following events are invoked, either using subclass methods or C
77             references in parameters:
78              
79             =head2 on_log $message
80              
81             A request to write a debugging log message. This is provided temporarily for
82             development and debugging purposes, but will at some point be removed when the
83             code has reached a certain level of stability.
84              
85             =head2 on_presence $user, %changes
86              
87             Invoked on receipt of a user presence change event from the homeserver.
88             C<%changes> will map user state field names to 2-element ARRAY references,
89             each containing the old and new values of that field.
90              
91             =head2 on_room_new $room
92              
93             Invoked when a new room first becomes known about.
94              
95             Passed an instance of L.
96              
97             =head2 on_room_del $room
98              
99             Invoked when the user has now left a room.
100              
101             =head2 on_invite $event
102              
103             Invoked on receipt of a room invite. The C<$event> will contain the plain
104             Matrix event as received; with at least the keys C and C.
105              
106             =head2 on_unknown_event $event
107              
108             Invoked on receipt of any sort of event from the event stream, that is not
109             recognised by any of the other code. This can be used to handle new kinds of
110             incoming events.
111              
112             =cut
113              
114             =head1 PARAMETERS
115              
116             The following named parameters may be passed to C or C. In
117             addition, C references for event handlers using the event names listed
118             above can also be given.
119              
120             =head2 server => STRING
121              
122             Hostname and port number to contact the homeserver at. Given in the form
123              
124             $hostname:$port
125              
126             This string will be interpolated directly into HTTP request URLs.
127              
128             =head2 SSL => BOOL
129              
130             Whether to use SSL/TLS to communicate with the homeserver. Defaults false.
131              
132             =head2 SSL_* => ...
133              
134             Any other parameters whose names begin C will be stored for passing to
135             the HTTP user agent. See L for more detail.
136              
137             =head2 path_prefix => STRING
138              
139             Optional. Gives the path prefix to find the Matrix client API at. Normally
140             this should not need modification.
141              
142             =head2 on_room_member, on_room_message => CODE
143              
144             Optional. Sets default event handlers on new room objects.
145              
146             =head2 enable_events => BOOL
147              
148             Optional. Normally enabled, but if set to a defined-but-false value (i.e. 0 or
149             empty string) the event stream will be disabled. This will cause none of the
150             incoming event handlers to be invoked, because the server will not be polled
151             for events.
152              
153             This may be useful in simple send-only cases where the client has no interest
154             in receiveing any events, and wishes to reduce the load on the homeserver.
155              
156             =head2 longpoll_timeout => NUM
157              
158             Optional. Timeout in seconds for the C longpoll operation. Defaults
159             to 30 seconds if not supplied.
160              
161             =cut
162              
163             sub _init
164             {
165 11     11   1525 my $self = shift;
166 11         22 my ( $params ) = @_;
167              
168 11         77 $self->SUPER::_init( $params );
169              
170 11   33     85 $params->{ua} ||= do {
171 0         0 require Net::Async::HTTP;
172 0         0 Net::Async::HTTP->VERSION( '0.36' ); # SSL params
173 0         0 my $ua = Net::Async::HTTP->new(
174             fail_on_error => 1,
175             max_connections_per_host => 3, # allow 2 longpolls + 1 actual command
176             user_agent => __PACKAGE__,
177             pipeline => 0,
178             );
179 0         0 $self->add_child( $ua );
180 0         0 $ua
181             };
182              
183             # Injectable for unit tests, other event systems, etc..
184             # For now undocumented while I try to work out the wider design issues
185             $self->{make_delay} = delete $params->{make_delay} || $self->_capture_weakself( sub {
186 1     1   7 my ( $self, $secs ) = @_;
187 1         4 $self->loop->delay_future( after => $secs );
188 11   66     68 } );
189              
190 11         40 $self->{msgid_next} = 0;
191              
192 11         26 $self->{users_by_id} = {};
193 11         21 $self->{rooms_by_id} = {};
194              
195 11         26 $self->{path_prefix} = PATH_PREFIX;
196              
197 11         27 $self->{longpoll_timeout} = LONGPOLL_TIMEOUT;
198             }
199              
200             =head1 METHODS
201              
202             The following methods documented with a trailing call to C<< ->get >> return
203             L instances.
204              
205             =cut
206              
207             sub configure
208             {
209 26     26 1 412 my $self = shift;
210 26         60 my %params = @_;
211              
212 26         64 foreach (qw( server path_prefix ua SSL enable_events longpoll_timeout
213             on_log on_unknown_event on_presence on_room_new on_room_del on_invite
214             on_room_member on_room_message )) {
215 364 100       540 $self->{$_} = delete $params{$_} if exists $params{$_};
216             }
217              
218 26         40 my $ua = $self->{ua};
219 26         66 foreach ( grep { m/^SSL_/ } keys %params ) {
  14         59  
220 0         0 $ua->configure( $_ => delete $params{$_} );
221             }
222              
223 26         115 $self->SUPER::configure( %params );
224             }
225              
226             sub log
227             {
228 0     0 0 0 my $self = shift;
229 0         0 my ( $message ) = @_;
230              
231 0 0       0 $self->{on_log}->( $message ) if $self->{on_log};
232             }
233              
234             sub _uri_for_path
235             {
236 57     57   64 my $self = shift;
237 57         88 my ( $path, %params ) = @_;
238              
239 57 100       240 $path = "/$path" unless $path =~ m{^/};
240              
241 57         258 my $uri = URI->new;
242 57 50       46035 $uri->scheme( $self->{SSL} ? "https" : "http" );
243 57         29909 $uri->authority( $self->{server} );
244 57         1753 $uri->path( $self->{path_prefix} . $path );
245              
246 57 50       1293 $params{access_token} = $self->{access_token} if defined $self->{access_token};
247 57         215 $uri->query_form( %params );
248              
249 57         3204 return $uri;
250             }
251              
252             sub _do_GET_json
253             {
254 43     43   3714 my $self = shift;
255 43         104 my ( $path, %params ) = @_;
256              
257             $self->{ua}->GET( $self->_uri_for_path( $path, %params ) )->then( sub {
258 29     29   19909 my ( $response ) = @_;
259              
260 29 50       187 $response->content_type eq "application/json" or
261             return Future->fail( "Expected application/json response", matrix => );
262              
263 29         958 Future->done( decode_json( $response->content ), $response );
264 43         142 });
265             }
266              
267             sub _do_send_json
268             {
269 13     13   17 my $self = shift;
270 13         21 my ( $method, $path, $content ) = @_;
271              
272 13         34 my $req = HTTP::Request->new( $method, $self->_uri_for_path( $path ) );
273 13         595 $req->content( encode_json( $content ) );
274 13         219 $req->header( Content_length => length $req->content ); # ugh
275              
276 13         678 $req->header( Content_type => "application/json" );
277              
278             my $f = $self->{ua}->do_request(
279             request => $req,
280             )->then( sub {
281 13     13   16517 my ( $response ) = @_;
282              
283 13 50       46 $response->content_type eq "application/json" or
284             return Future->fail( "Expected application/json response", matrix => );
285              
286 13         286 my $content = $response->content;
287 13 50 33     149 if( length $content and $content ne q("") ) {
288 13 50       18 eval {
289 13         61 $content = decode_json( $content );
290 13         40 1;
291             } or
292             return Future->fail( "Unable to parse JSON response $content" );
293 13         38 return Future->done( $content, $response );
294             }
295             else {
296             # server yields empty strings sometimes... :/
297 0         0 return Future->done( undef, $response );
298             }
299 13         370 });
300              
301 13         1030 return $self->adopt_future( $f );
302             }
303              
304 4     4   9 sub _do_PUT_json { shift->_do_send_json( PUT => @_ ) }
305 9     9   40 sub _do_POST_json { shift->_do_send_json( POST => @_ ) }
306              
307             sub _do_DELETE
308             {
309 0     0   0 my $self = shift;
310 0         0 my ( $path, %params ) = @_;
311              
312             $self->{ua}->do_request(
313 0         0 method => "DELETE",
314             uri => $self->_uri_for_path( $path, %params ),
315             );
316             }
317              
318             sub _do_POST_file
319             {
320 1     1   3 my $self = shift;
321 1         5 my ( $path, %params ) = @_;
322              
323 1         3 my $uri = $self->_uri_for_path( "" );
324 1         4 $uri->path( "/_matrix" . $path );
325              
326 1         19 my $req = HTTP::Request->new( "POST" , $uri );
327 1         40 $req->header( Content_type => $params{content_type} );
328              
329 1         44 my $body;
330              
331 1 50 0     3 if( defined $params{content} ) {
    0          
332 1         5 $req->content( $params{content} );
333 1         16 $req->header( Content_length => length $req->content );
334             }
335             elsif( defined $params{file} or defined $params{fh} ) {
336 0         0 my $fh = $params{fh};
337             $fh or open $fh, "<", $params{file} or
338 0 0 0     0 return Future->fail( "Cannot read $params{file} - $!", open => );
339              
340             $body = sub {
341 0 0   0   0 $fh->read( my $buffer, 65536 ) or return undef;
342 0         0 return $buffer;
343 0         0 };
344              
345 0   0     0 $req->header( Content_length => $params{content_length} // ( stat $fh )->size );
346             }
347              
348             my $f = $self->{ua}->do_request(
349             request => $req,
350             request_body => $body,
351             )->then( sub {
352 1     1   3471 my ( $response ) = @_;
353 1 50       5 $response->content_type eq "application/json" or
354             return Future->fail( "Expected application/json response", matrix => );
355              
356 1         24 my $content = $response->content;
357 1         34 my $uri;
358 1 50 33     12 if( length $content and $content ne q("") ) {
359 1 50       2 eval {
360 1         8 $content = decode_json( $content );
361 1         3 1;
362             } or
363             return Future->fail( "Unable to parse JSON response " );
364 1         6 return Future->done( $content, $response );
365             }
366             else {
367 0         0 return Future->done( undef, $response );
368             }
369 1         35 });
370              
371 1         72 return $self->adopt_future( $f );
372             }
373              
374             =head2 login
375              
376             $matrix->login( %params )->get
377              
378             Performs the necessary steps required to authenticate with the configured
379             Home Server, actually obtain an access token and starting the event stream
380             (unless disabled by the C option being false). The returned
381             C will eventually yield the C<$matrix> object itself, so it can be
382             easily chained.
383              
384             There are various methods of logging in supported by Matrix; the following
385             sets of arguments determine which is used:
386              
387             =over 4
388              
389             =item user_id, password
390              
391             Log in via the C method.
392              
393             =item user_id, access_token
394              
395             Directly sets the C and C fields, bypassing the usual
396             login semantics. This presumes you already have an existing access token to
397             re-use, obtained by some other mechanism. This exists largely for testing
398             purposes.
399              
400             =back
401              
402             =cut
403              
404             sub login
405             {
406 14     14 1 43228 my $self = shift;
407 14         63 my %params = @_;
408              
409 14 100 66     111 if( defined $params{user_id} and defined $params{access_token} ) {
410 13         58 $self->{$_} = $params{$_} for qw( user_id access_token );
411 13         65 $self->configure( notifier_name => "uid=$params{user_id}" );
412             return ( ( $self->{enable_events} // 1 ) ? $self->start : Future->done )->then( sub {
413 12     12   9917 Future->done( $self )
414 13 100 100     256 });
415             }
416              
417             # Otherwise; try to obtain the login flow information
418             $self->_do_GET_json( "/login" )->then( sub {
419 1     1   68 my ( $response ) = @_;
420 1         2 my $flows = $response->{flows};
421              
422 1         1 my @supported;
423 1         3 foreach my $flow ( @$flows ) {
424 1 50       6 next unless my ( $type ) = $flow->{type} =~ m/^m\.login\.(.*)$/;
425 1         3 push @supported, $type;
426              
427 1 50       12 next unless my $code = $self->can( "_login_with_$type" );
428 1 50       4 next unless my $f = $code->( $self, %params );
429              
430 1         63 return $f;
431             }
432              
433 0         0 Future->fail( "Unsure how to log in (server supports @supported)", matrix => );
434 1         2 });
435             }
436              
437             sub _login_with_password
438             {
439 1     1   1 my $self = shift;
440 1         2 my %params = @_;
441              
442 1 50 33     10 return unless defined $params{user_id} and defined $params{password};
443              
444             $self->_do_POST_json( "/login",
445             { type => "m.login.password", user => $params{user_id}, password => $params{password} }
446             )->then( sub {
447 1     1   80 my ( $resp ) = @_;
448 1 50       7 return $self->login( %$resp, %params ) if defined $resp->{access_token};
449 0         0 return Future->fail( "Expected server to respond with 'access_token'", matrix => );
450 1         6 });
451             }
452              
453             =head2 register
454              
455             $matrix->register( %params )->get
456              
457             Performs the necessary steps required to create a new account on the
458             configured Home Server.
459              
460             =cut
461              
462             sub register
463             {
464 0     0 1 0 my $self = shift;
465 0         0 my %params = @_;
466              
467             $self->_do_GET_json( "/register" )->then( sub {
468 0     0   0 my ( $response ) = @_;
469 0         0 my $flows = $response->{flows};
470              
471 0         0 my @supported;
472             # Try to find a flow for which we can support all the stages
473 0         0 FLOW: foreach my $flow ( @$flows ) {
474             # Might or might not find a 'stages' key
475 0 0       0 my @stages = $flow->{stages} ? @{ $flow->{stages} } : ( $flow->{type} );
  0         0  
476              
477 0         0 push @supported, join ",", @stages;
478              
479 0         0 my @flowcode;
480 0         0 foreach my $stage ( @stages ) {
481 0 0       0 next FLOW unless my ( $type ) = $stage =~ m/^m\.login\.(.*)$/;
482 0         0 $type =~ s/\./_/g;
483              
484 0 0       0 next FLOW unless my $method = $self->can( "_register_with_$type" );
485 0 0       0 next FLOW unless my $code = $method->( $self, %params );
486              
487 0         0 push @flowcode, $code;
488             }
489              
490             # If we've got this far then we know we can implement all the stages
491 0         0 my $start = Future->new;
492 0         0 my $tail = $start;
493 0         0 $tail = $tail->then( $_ ) for @flowcode;
494              
495 0         0 $start->done();
496             return $tail->then( sub {
497 0         0 my ( $resp ) = @_;
498 0 0       0 return $self->login( %$resp ) if defined $resp->{access_token};
499 0         0 return Future->fail( "Expected server to respond with 'access_token'", matrix => );
500 0         0 });
501             }
502              
503 0         0 Future->fail( "Unsure how to register (server supports @supported)", matrix => );
504 0         0 });
505             }
506              
507             sub _register_with_password
508             {
509 0     0   0 my $self = shift;
510 0         0 my %params = @_;
511              
512 0 0       0 return unless defined( my $password = $params{password} );
513              
514             return sub {
515 0     0   0 my ( $resp ) = @_;
516              
517             $self->_do_POST_json( "/register", {
518             type => "m.login.password",
519             session => $resp->{session},
520              
521             user => $params{user_id},
522 0         0 password => $password,
523             } );
524             }
525 0         0 }
526              
527             sub _register_with_recaptcha
528             {
529 0     0   0 my $self = shift;
530 0         0 my %params = @_;
531              
532             return unless defined( my $secret = $params{captcha_bypass_secret} ) and
533 0 0 0     0 defined $params{user_id};
534              
535 0         0 warn "Cannot use captcha_bypass_secret to bypass m.register.recaptcha without Digest::HMAC_SHA1\n" and return
536             if !HAVE_DIGEST_HMAC_SHA1;
537              
538 0         0 my $digest = Digest::HMAC_SHA1::hmac_sha1_hex( $params{user_id}, $secret );
539              
540             return sub {
541 0     0   0 my ( $resp ) = @_;
542              
543             $self->_do_POST_json( "/register", {
544             type => "m.login.recaptcha",
545             session => $resp->{session},
546              
547             user => $params{user_id},
548 0         0 captcha_bypass_hmac => $digest,
549             } );
550 0         0 };
551             }
552              
553             =head2 sync
554              
555             $matrix->sync( %params )->get
556              
557             Performs a single C request on the server, returning the raw results
558             directly.
559              
560             Takes the following named parameters
561              
562             =over 4
563              
564             =item since => STRING
565              
566             Optional. Sync token from the previous request.
567              
568             =back
569              
570             =cut
571              
572             sub sync
573             {
574 14     14 1 24 my $self = shift;
575 14         27 my ( %params ) = @_;
576              
577 14         50 $self->_do_GET_json( "/sync", %params );
578             }
579              
580             sub await_synced
581             {
582 12     12 0 18 my $self = shift;
583 12   33     117 return $self->{synced_future} //= $self->loop->new_future;
584             }
585              
586             =head2 start
587              
588             $f = $matrix->start
589              
590             Performs the initial sync on the server, and starts the event stream to
591             begin receiving events.
592              
593             While this method does return a C it is not required that the caller
594             keep track of this; the object itself will store it. It will complete when the
595             initial sync has fininshed, and the event stream has started.
596              
597             If the initial sync has already been requested, this method simply returns the
598             future it returned the last time, ensuring that you can await the client
599             starting up simply by calling it; it will not start a second time.
600              
601             =cut
602              
603             sub start
604             {
605 16     16 1 3161 my $self = shift;
606              
607 16 50       50 defined $self->{access_token} or croak "Cannot ->start without an access token";
608              
609 16   66     63 return $self->{start_f} ||= do {
610 14         35 undef $self->{synced_future};
611              
612 14         21 foreach my $room ( values %{ $self->{rooms_by_id} } ) {
  14         48  
613 1         6 $room->_reset_for_sync;
614             }
615              
616             $self->sync->then( sub {
617 12     12   1130 my ( $sync ) = @_;
618              
619 12         50 $self->_incoming_sync( $sync );
620              
621 12         57 $self->start_longpoll( since => $sync->{next_batch} );
622              
623 12         267 return $self->await_synced->done;
624 14     1   45 })->on_fail( sub { undef $self->{start_f} });
  1         163  
625             };
626             }
627              
628             =head2 stop
629              
630             $matrix->stop
631              
632             Stops the event stream. After calling this you will need to use C again
633             to continue receiving events.
634              
635             =cut
636              
637             sub stop
638             {
639 4     4 1 2885 my $self = shift;
640              
641 4 50       55 ( delete $self->{start_f} )->cancel if $self->{start_f};
642 4         24 $self->stop_longpoll;
643             }
644              
645             ## Longpoll events
646              
647             sub start_longpoll
648             {
649 12     12 0 20 my $self = shift;
650 12         37 my %args = @_;
651              
652 12         77 $self->stop_longpoll;
653 12         25 $self->{longpoll_last_token} = $args{since};
654              
655             my $f = $self->{longpoll_f} = repeat {
656 28     28   620 my $last_token = $self->{longpoll_last_token};
657              
658             Future->wait_any(
659             $self->{make_delay}->( $self->{longpoll_timeout} + 5 )
660             ->else_fail( "Longpoll timed out" ),
661              
662             $self->_do_GET_json( "/sync",
663             since => $last_token,
664             timeout => $self->{longpoll_timeout} * 1000, # msec
665             )->then( sub {
666 16         1184 my ( $sync ) = @_;
667              
668 16         86 $self->_incoming_sync( $sync );
669              
670 16         27 $self->{longpoll_last_token} = $sync->{next_batch};
671              
672 16         47 Future->done();
673             }),
674             )->else( sub {
675 0         0 my ( $failure ) = @_;
676 0         0 warn "Longpoll failed - $failure\n";
677              
678 0         0 $self->{make_delay}->( 3 )
679 28         104 });
680 12     16   135 } while => sub { !shift->failure };
  16         2846  
681              
682             # Don't ->adopt_future this one as it makes it hard to grab to cancel it
683             # again, but apply the same on_fail => invoke_error logic
684             $f->on_fail( $self->_capture_weakself( sub {
685 0     0   0 my $self = shift;
686 0         0 $self->invoke_error( @_ );
687 12         3572 }));
688             }
689              
690             sub stop_longpoll
691             {
692 16     16 0 21 my $self = shift;
693              
694 16 100       59 ( delete $self->{longpoll_f} )->cancel if $self->{longpoll_f};
695             }
696              
697             sub _get_or_make_user
698             {
699 20     20   15 my $self = shift;
700 20         21 my ( $user_id ) = @_;
701              
702 20   66     76 return $self->{users_by_id}{$user_id} ||= User( $user_id, undef, undef, undef );
703             }
704              
705             sub _make_room
706             {
707 7     7   13 my $self = shift;
708 7         11 my ( $room_id ) = @_;
709              
710 7 50       32 $self->{rooms_by_id}{$room_id} and
711             croak "Already have a room with ID '$room_id'";
712              
713 7         10 my @args;
714 7         17 foreach (qw( message member )) {
715 14 50       90 push @args, "on_$_" => $self->{"on_room_$_"} if $self->{"on_room_$_"};
716             }
717              
718 7         35 my $room = $self->{rooms_by_id}{$room_id} = $self->make_room(
719             matrix => $self,
720             room_id => $room_id,
721             @args,
722             );
723 7         118 $self->add_child( $room );
724              
725 7         472 $self->maybe_invoke_event( on_room_new => $room );
726              
727 7         113 return $room;
728             }
729              
730             sub make_room
731             {
732 7     7 1 21 my $self = shift;
733 7         107 return Net::Async::Matrix::Room->new( @_ );
734             }
735              
736             sub _get_or_make_room
737             {
738 16     16   15 my $self = shift;
739 16         19 my ( $room_id ) = @_;
740              
741 16   66     64 return $self->{rooms_by_id}{$room_id} //
742             $self->_make_room( $room_id );
743             }
744              
745             =head2 myself
746              
747             $user = $matrix->myself
748              
749             Returns the user object representing the connected user.
750              
751             =cut
752              
753             sub myself
754             {
755 2     2 1 3 my $self = shift;
756 2         6 return $self->_get_or_make_user( $self->{user_id} );
757             }
758              
759             =head2 user
760              
761             $user = $matrix->user( $user_id )
762              
763             Returns the user object representing a user of the given ID, if defined, or
764             C.
765              
766             =cut
767              
768             sub user
769             {
770 0     0 1 0 my $self = shift;
771 0         0 my ( $user_id ) = @_;
772 0         0 return $self->{users_by_id}{$user_id};
773             }
774              
775             sub _incoming_sync
776             {
777 28     28   37 my $self = shift;
778 28         30 my ( $sync ) = @_;
779              
780 28         47 foreach my $category (qw( invite join leave )) {
781 84 100       278 my $rooms = $sync->{rooms}{$category} or next;
782 16         64 foreach my $room_id ( keys %$rooms ) {
783 16         20 my $roomsync = $rooms->{$room_id};
784              
785 16         39 my $room = $self->_get_or_make_room( $room_id );
786              
787 16         18 $room->${\"_incoming_sync_$category"}( $roomsync );
  16         75  
788             }
789             }
790              
791 28         34 foreach my $event ( @{ $sync->{presence}{events} } ) {
  28         69  
792 1         4 $self->_handle_event_m_presence( $event );
793             }
794              
795             # TODO: account_data
796             }
797              
798             sub _on_self_leave
799             {
800 0     0   0 my $self = shift;
801 0         0 my ( $room ) = @_;
802              
803 0         0 $self->maybe_invoke_event( on_room_del => $room );
804              
805 0         0 delete $self->{rooms_by_id}{$room->room_id};
806             }
807              
808             =head2 get_displayname
809              
810             =head2 set_displayname
811              
812             $name = $matrix->get_displayname->get
813              
814             $matrix->set_displayname( $name )->get
815              
816             Accessor and mutator for the user account's "display name" profile field.
817              
818             =cut
819              
820             sub get_displayname
821             {
822 0     0 1 0 my $self = shift;
823 0         0 my ( $user_id ) = @_;
824              
825 0   0     0 $user_id //= $self->{user_id};
826              
827             $self->_do_GET_json( "/profile/$user_id/displayname" )->then( sub {
828 0     0   0 my ( $content ) = @_;
829              
830 0         0 Future->done( $content->{displayname} );
831 0         0 });
832             }
833              
834             sub set_displayname
835             {
836 0     0 1 0 my $self = shift;
837 0         0 my ( $name ) = @_;
838              
839 0         0 $self->_do_PUT_json( "/profile/$self->{user_id}/displayname",
840             { displayname => $name }
841             );
842             }
843              
844             =head2 get_presence
845              
846             =head2 set_presence
847              
848             ( $presence, $msg ) = $matrix->get_presence->get
849              
850             $matrix->set_presence( $presence, $msg )->get
851              
852             Accessor and mutator for the user's current presence state and optional status
853             message string.
854              
855             =cut
856              
857             sub get_presence
858             {
859 0     0 1 0 my $self = shift;
860              
861             $self->_do_GET_json( "/presence/$self->{user_id}/status" )->then( sub {
862 0     0   0 my ( $status ) = @_;
863 0         0 Future->done( $status->{presence}, $status->{status_msg} );
864 0         0 });
865             }
866              
867             sub set_presence
868             {
869 0     0 1 0 my $self = shift;
870 0         0 my ( $presence, $msg ) = @_;
871              
872 0         0 my $status = {
873             presence => $presence,
874             };
875 0 0       0 $status->{status_msg} = $msg if defined $msg;
876              
877 0         0 $self->_do_PUT_json( "/presence/$self->{user_id}/status", $status )
878             }
879              
880             sub get_presence_list
881             {
882 0     0 0 0 my $self = shift;
883              
884             $self->_do_GET_json( "/presence_list/$self->{user_id}" )->then( sub {
885 0     0   0 my ( $events ) = @_;
886              
887 0         0 my @users;
888 0         0 foreach my $event ( @$events ) {
889 0         0 my $user = $self->_get_or_make_user( $event->{user_id} );
890 0         0 foreach (qw( presence displayname )) {
891 0 0       0 $user->$_ = $event->{$_} if defined $event->{$_};
892             }
893              
894 0         0 push @users, $user;
895             }
896              
897 0         0 Future->done( @users );
898 0         0 });
899             }
900              
901             sub invite_presence
902             {
903 0     0 0 0 my $self = shift;
904 0         0 my ( $remote ) = @_;
905              
906 0         0 $self->_do_POST_json( "/presence_list/$self->{user_id}",
907             { invite => [ $remote ] }
908             );
909             }
910              
911             sub drop_presence
912             {
913 0     0 0 0 my $self = shift;
914 0         0 my ( $remote ) = @_;
915              
916 0         0 $self->_do_POST_json( "/presence_list/$self->{user_id}",
917             { drop => [ $remote ] }
918             );
919             }
920              
921             =head2 create_room
922              
923             ( $room, $room_alias ) = $matrix->create_room( $alias_localpart )->get
924              
925             Requests the creation of a new room and associates a new alias with the given
926             localpart on the server. The returned C will return an instance of
927             L and a string containing the full alias that was
928             created.
929              
930             =cut
931              
932             sub create_room
933             {
934 0     0 1 0 my $self = shift;
935 0         0 my ( $room_alias ) = @_;
936              
937 0         0 my $body = {};
938 0 0       0 $body->{room_alias_name} = $room_alias if defined $room_alias;
939             # TODO: visibility?
940              
941             $self->_do_POST_json( "/createRoom", $body )->then( sub {
942 0     0   0 my ( $content ) = @_;
943              
944 0         0 my $room = $self->_get_or_make_room( $content->{room_id} );
945             $room->initial_sync
946 0         0 ->then_done( $room, $content->{room_alias} );
947 0         0 });
948             }
949              
950             =head2 join_room
951              
952             $room = $matrix->join_room( $room_alias_or_id )->get
953              
954             Requests to join an existing room with the given alias name or plain room ID.
955             If this room is already known by the C<$matrix> object, this method simply
956             returns it.
957              
958             =cut
959              
960             sub join_room
961             {
962 6     6 1 651 my $self = shift;
963 6         204 my ( $room_alias ) = @_;
964              
965             $self->_do_POST_json( "/join/$room_alias", {} )->then( sub {
966 6     6   541 my ( $content ) = @_;
967 6         13 my $room_id = $content->{room_id};
968              
969 6 50       23 if( my $room = $self->{rooms_by_id}{$room_id} ) {
970 0         0 return Future->done( $room );
971             }
972             else {
973 6         24 my $room = $self->_make_room( $room_id );
974 6         29 return $room->await_synced->then_done( $room );
975             }
976 6         40 });
977             }
978              
979             sub room_list
980             {
981 0     0 0 0 my $self = shift;
982              
983             $self->_do_GET_json( "/users/$self->{user_id}/rooms/list" )
984             ->then( sub {
985 0     0   0 my ( $response ) = @_;
986 0         0 Future->done( pp($response) );
987 0         0 });
988             }
989              
990             =head2 add_alias
991              
992             =head2 delete_alias
993              
994             $matrix->add_alias( $alias, $room_id )->get
995              
996             $matrix->delete_alias( $alias )->get
997              
998             Performs a directory server request to create the given room alias name, to
999             point at the room ID, or to remove it again.
1000              
1001             Note that this is likely only to be supported for alias names scoped within
1002             the homeserver the client is connected to, and that additionally some form of
1003             permissions system may be in effect on the server to limit access to the
1004             directory server.
1005              
1006             =cut
1007              
1008             sub add_alias
1009             {
1010 0     0 1 0 my $self = shift;
1011 0         0 my ( $alias, $room_id ) = @_;
1012              
1013 0         0 $self->_do_PUT_json( "/directory/room/$alias",
1014             { room_id => $room_id },
1015             )->then_done();
1016             }
1017              
1018             sub delete_alias
1019             {
1020 0     0 1 0 my $self = shift;
1021 0         0 my ( $alias ) = @_;
1022              
1023 0         0 $self->_do_DELETE( "/directory/room/$alias" )
1024             ->then_done();
1025             }
1026              
1027             =head2 upload
1028              
1029             $content_uri = $matrix->upload( %params )->get
1030              
1031             Performs a post to the server's media content repository, to upload a new
1032             piece of content, returning the content URI that points to it.
1033              
1034             The content can be specified in any of three ways, with the following three
1035             mutually-exclusive arguments:
1036              
1037             =over 4
1038              
1039             =item content => STRING
1040              
1041             Gives the content directly as an immediate scalar value.
1042              
1043             =item file => STRING
1044              
1045             Gives the path to a readable file on the filesystem containing the content.
1046              
1047             =item fh => IO
1048              
1049             Gives an opened IO handle the content can be read from.
1050              
1051             =back
1052              
1053             The following additional arguments are also recognised:
1054              
1055             =over 4
1056              
1057             =item content_type => STRING
1058              
1059             Gives the MIME type of the content data.
1060              
1061             =item content_length => INT
1062              
1063             Optional. If the content is being delivered from an opened filehandle (via the
1064             C argument), this gives the total length in bytes. This is required in
1065             cases such as reading from pipes, when the length of the content isn't
1066             immediately available such as by Cing the filehandle.
1067              
1068             =back
1069              
1070             =cut
1071              
1072             sub upload
1073             {
1074 1     1 1 114 my $self = shift;
1075 1         39 my %params = @_;
1076              
1077             defined $params{content_type} or
1078 1 50       6 croak "Require 'content_type'";
1079              
1080             defined $params{content} or defined $params{file} or defined $params{fh} or
1081 1 0 33     5 croak "Require 'content', 'file' or 'fh'";
      33        
1082              
1083             # This one takes ~full URL paths
1084             $self->_do_POST_file( "/media/v1/upload", %params )->then( sub {
1085 1     1   113 my ( $content, $response ) = @_;
1086 1         4 Future->done( $content->{content_uri} );
1087 1         7 });
1088             }
1089              
1090             ## Incoming events
1091              
1092             sub _handle_event_m_presence
1093             {
1094 1     1   2 my $self = shift;
1095 1         1 my ( $event ) = @_;
1096 1         3 my $content = $event->{content};
1097              
1098 1         3 my $user = $self->_get_or_make_user( $event->{sender} );
1099              
1100 1         45 my %changes;
1101 1         3 foreach (qw( presence displayname )) {
1102 2 100       13 next unless defined $content->{$_};
1103 1 50 33     4 next if defined $user->$_ and $content->{$_} eq $user->$_;
1104              
1105 1         12 $changes{$_} = [ $user->$_, $content->{$_} ];
1106 1         14 $user->$_ = $content->{$_};
1107             }
1108              
1109 1 50       9 if( defined $content->{last_active_ago} ) {
1110 0         0 my $new_last_active = time() - ( $content->{last_active_ago} / 1000 );
1111              
1112 0         0 $changes{last_active} = [ $user->last_active, $new_last_active ];
1113 0         0 $user->last_active = $new_last_active;
1114             }
1115              
1116             $self->maybe_invoke_event(
1117 1         12 on_presence => $user, %changes
1118             );
1119              
1120 1         26 foreach my $room ( values %{ $self->{rooms_by_id} } ) {
  1         7  
1121 0           $room->_handle_event_m_presence( $user, %changes );
1122             }
1123             }
1124              
1125             =head1 USER STRUCTURES
1126              
1127             Parameters documented as C<$user> receive a user struct, which supports the
1128             following methods:
1129              
1130             =head2 $user_id = $user->user_id
1131              
1132             User ID of the user.
1133              
1134             =head2 $displayname = $user->displayname
1135              
1136             Profile displayname of the user.
1137              
1138             =head2 $presence = $user->presence
1139              
1140             Presence state. One of C, C or C.
1141              
1142             =head2 $last_active = $user->last_active
1143              
1144             Epoch time that the user was last active.
1145              
1146             =cut
1147              
1148             =head1 SUBCLASSING METHODS
1149              
1150             The following methods are not normally required by users of this class, but
1151             are provided for the convenience of subclasses to override.
1152              
1153             =head2 $room = $matrix->make_room( %params )
1154              
1155             Returns a new instance of L.
1156              
1157             =cut
1158              
1159             =head1 SEE ALSO
1160              
1161             =over 4
1162              
1163             =item *
1164              
1165             L - matrix.org home page
1166              
1167             =item *
1168              
1169             L - matrix.org on github
1170              
1171             =back
1172              
1173             =cut
1174              
1175             =head1 AUTHOR
1176              
1177             Paul Evans
1178              
1179             =cut
1180              
1181             0x55AA;