File Coverage

blib/lib/Net/Async/Matrix.pm
Criterion Covered Total %
statement 266 397 67.0
branch 45 102 44.1
condition 23 62 37.1
subroutine 59 87 67.8
pod 19 27 70.3
total 412 675 61.0


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