File Coverage

blib/lib/Net/Async/Matrix/Room.pm
Criterion Covered Total %
statement 212 382 55.5
branch 37 100 37.0
condition 30 84 35.7
subroutine 44 74 59.4
pod 24 25 96.0
total 347 665 52.1


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::Room;
7              
8 12     12   44 use strict;
  12         15  
  12         280  
9 12     12   38 use warnings;
  12         13  
  12         271  
10              
11             # Not really a Notifier but we like the ->maybe_invoke_event style
12 12     12   53 use base qw( IO::Async::Notifier );
  12         12  
  12         1115  
13              
14             our $VERSION = '0.19';
15             $VERSION = eval $VERSION;
16              
17 12     12   47 use Carp;
  12         15  
  12         519  
18              
19 12     12   41 use Future;
  12         15  
  12         222  
20 12     12   41 use Future::Utils qw( repeat );
  12         13  
  12         423  
21              
22 12     12   47 use List::Util qw( pairmap );
  12         14  
  12         508  
23 12     12   38 use Time::HiRes qw( time );
  12         18  
  12         53  
24              
25 12     12   4953 use Net::Async::Matrix::Room::State;
  12         19  
  12         454  
26             # TEMPORARY hack
27             *Member = \&Net::Async::Matrix::Room::State::Member;
28              
29 12     12   50 use Data::Dump 'pp';
  12         13  
  12         457  
30              
31 12     12   44 use constant TYPING_RESEND_SECONDS => 30;
  12         119  
  12         43680  
32              
33             =head1 NAME
34              
35             C - a single Matrix room
36              
37             =head1 DESCRIPTION
38              
39             An instances in this class are used by L to represent a
40             single Matrix room.
41              
42             =cut
43              
44             =head1 EVENTS
45              
46             The following events are invoked, either using subclass methods or C
47             references in parameters:
48              
49             =head2 on_synced_state
50              
51             Invoked after the initial sync of the room has been completed as far as the
52             state.
53              
54             =head2 on_message $member, $content, $event
55              
56             =head2 on_back_message $member, $content, $event
57              
58             Invoked on receipt of a new message from the given member, either "live" from
59             the event stream, or from backward pagination.
60              
61             =head2 on_membership $member, $event, $subject_member, %changes
62              
63             =head2 on_back_membership $member, $event, $subject_member, %changes
64              
65             Invoked on receipt of a membership change event for the given member, either
66             "live" from the event stream, or from backward pagination. C<%changes> will be
67             a key/value list of state field names that were changed, whose values are
68             2-element ARRAY references containing the before/after values of those fields.
69              
70             on_membership: $field_name => [ $old_value, $new_value ]
71             on_back_membership: $field_name => [ $new_value, $old_value ]
72              
73             Note carefully that the second value in each array gives the "updated" value,
74             in the direction of the change - that is, for C it gives the
75             new value after the change but for C it gives the old value
76             before. Fields whose values did not change are not present in the C<%changes>
77             list; the values of these can be inspected on the C<$member> object.
78              
79             It is unspecified what values the C<$member> object has for fields present in
80             the change list - client code should not rely on these fields.
81              
82             In most cases when users change their own membership status (such as normal
83             join or leave), the C<$member> and C<$subject_member> parameters refer to the
84             same object. In other cases, such as invites or kicks, the C<$member>
85             parameter refers to the member performing the change, and the
86             C<$subject_member> refers to member that the change is about.
87              
88             =head2 on_state_changed $member, $event, %changes
89              
90             =head2 on_back_state_changed $member, $event, %changes
91              
92             Invoked on receipt of a change of room state (such as name or topic).
93              
94             In the special case of room aliases, because they are considered "state" but
95             are stored per-homeserver, the changes value will consist of three fields; the
96             old and new values I, and a list of the known aliases
97             from all the other servers:
98              
99             on_state_changed: aliases => [ $old, $new, $other ]
100             on_back_state_changed: aliases => [ $new, $old, $other ]
101              
102             This allows a client to detect deletions and additions by comparing the before
103             and after lists, while still having access to the full set of before or after
104             aliases, should it require it.
105              
106             =head2 on_presence $member, %changes
107              
108             Invoked when a member of the room changes membership or presence state. The
109             C<$member> object will already be in the new state. C<%changes> will be a
110             key/value list of state fields names that were changed, and references to
111             2-element ARRAYs containing the old and new values for this field.
112              
113             =head2 on_typing $member, $is_typing
114              
115             Invoked on receipt of a typing notification change, when the given member
116             either starts or stops typing.
117              
118             =head2 on_members_typing @members
119              
120             Invoked on receipt of a typing notification change to give the full set of
121             currently-typing members. This is invoked after the individual C
122             events.
123              
124             =head2 on_read_receipt $member, $event_id, $content
125              
126             Invoked on receipt of a C type of receipt message.
127              
128             =cut
129              
130             sub _init
131             {
132 7     7   72 my $self = shift;
133 7         11 my ( $params ) = @_;
134 7         34 $self->SUPER::_init( $params );
135              
136 7         37 $self->{matrix} = delete $params->{matrix};
137 7         12 $self->{room_id} = delete $params->{room_id};
138              
139             # Server gives us entire sets of typing user_ids all at once. We have to
140             # remember state
141 7         14 $self->{typing_members} = {};
142              
143 7         51 $self->{live_state} = Net::Async::Matrix::Room::State->new( $self );
144             }
145              
146             sub configure
147             {
148 10     10 1 1170 my $self = shift;
149 10         20 my %params = @_;
150              
151 10         20 foreach (qw( on_message on_back_message on_membership on_back_membership
152             on_presence on_synced_state on_state_changed on_back_state_changed
153             on_typing on_members_typing on_read_receipt )) {
154 110 100       159 $self->{$_} = delete $params{$_} if exists $params{$_};
155             }
156              
157 10         44 $self->SUPER::configure( %params );
158             }
159              
160             =head1 METHODS
161              
162             =cut
163              
164             # FUNCTION
165             sub _delete_null_changes
166             {
167 9     9   9 my ( $changes ) = @_;
168              
169 9         15 foreach ( keys %$changes ) {
170 12         10 my ( $old, $new ) = @{ $changes->{$_} };
  12         14  
171              
172 12 100 100     84 delete $changes->{$_} if !defined $old and !defined $new or
      66        
      100        
      66        
173             defined $old and defined $new and $old eq $new;
174             }
175             }
176              
177             # FUNCTION
178             sub _pushdown_changes
179             {
180 1     1   2 my ( $ch ) = @_;
181 1         2 my ( $oldhash, $newhash ) = @$ch;
182              
183 1         2 my %changes;
184              
185 1         2 foreach ( keys %$oldhash ) {
186 1         2 my $old = $oldhash->{$_};
187 1 50       3 if( !exists $newhash->{$_} ) {
188 0 0       0 $changes{$_} = [ $old, undef ] if defined $old;
189 0         0 next;
190             }
191              
192 1         1 my $new = $newhash->{$_};
193              
194 1 50 33     20 $changes{$_} = [ $old, $new ] unless !defined $old and !defined $new or
      33        
      33        
      33        
195             defined $old and defined $new and $old eq $new;
196             }
197              
198 1         2 foreach ( keys %$newhash ) {
199 1         2 my $new = $newhash->{$_};
200 1 50       5 next if exists $oldhash->{$_};
201              
202 0 0       0 $changes{$_} = [ undef, $new ] if defined $new;
203             }
204              
205 1 50       4 return keys %changes ? \%changes : undef;
206             }
207              
208             sub _do_GET_json
209             {
210 0     0   0 my $self = shift;
211 0         0 my ( $path, @args ) = @_;
212              
213 0         0 $self->{matrix}->_do_GET_json( "/rooms/$self->{room_id}" . $path, @args );
214             }
215              
216             sub _do_PUT_json
217             {
218 4     4   7 my $self = shift;
219 4         5 my ( $path, $content ) = @_;
220              
221 4         22 $self->{matrix}->_do_PUT_json( "/rooms/$self->{room_id}" . $path, $content );
222             }
223              
224             sub _do_POST_json
225             {
226 2     2   3 my $self = shift;
227 2         3 my ( $path, $content ) = @_;
228              
229 2         10 $self->{matrix}->_do_POST_json( "/rooms/$self->{room_id}" . $path, $content );
230             }
231              
232             =head2 await_synced
233              
234             $f = $room->await_synced
235              
236             Returns a L stored within the room that will complete (with no value)
237             once the room initial state sync has been completed. This completes just
238             I the C event.
239              
240             =cut
241              
242             sub _reset_for_sync
243             {
244 1     1   1 my $self = shift;
245              
246 1         3 undef $self->{synced_future};
247             }
248              
249             sub _incoming_sync_invite
250             {
251 0     0   0 my $self = shift;
252 0         0 my ( $sync ) = @_;
253 0         0 warn "TODO handle incoming sync data in invite state";
254             }
255              
256             sub _incoming_sync_join
257             {
258 16     16   19 my $self = shift;
259 16         19 my ( $sync ) = @_;
260              
261 16         30 my $initial = not $self->await_synced->is_done;
262              
263             # Toplevel fields for now I'm ignoring
264             # account_data
265             # unread_notifications
266              
267 16         884 my $live_state = $self->live_state;
268              
269 16 100 66     68 if( $sync->{state} and $sync->{state}{events} and @{ $sync->{state}{events} } ) {
  7   66     28  
270 5         7 foreach my $event ( @{ $sync->{state}{events} } ) {
  5         12  
271 11         29 $live_state->handle_event( $event );
272             }
273             }
274              
275 16         18 foreach my $event ( @{ $sync->{timeline}{events} } ) {
  16         37  
276 8 100       18 if( defined $event->{state_key} ) {
277 7         16 my $old_event = $live_state->get_event( $event->{type}, $event->{state_key} );
278 7         12 $live_state->handle_event( $event );
279 7         12 $self->_handle_state_event( $old_event, $event, $live_state );
280             }
281             else {
282 1         4 $self->_handle_event( forward => $event );
283             }
284             }
285              
286 16         135 foreach my $event ( @{ $sync->{ephemeral}{events} } ) {
  16         36  
287 1         3 $self->_handle_event( ephemeral => $event );
288             }
289              
290 16 100       60 if( $initial ) {
291 7         15 $self->await_synced->done;
292 7         503 $self->maybe_invoke_event( on_synced_state => );
293             }
294             }
295              
296             sub _incoming_sync_leave
297             {
298 0     0   0 my $self = shift;
299 0         0 my ( $sync ) = @_;
300             # don't care for now
301             }
302              
303             sub await_synced
304             {
305 30     30 1 839 my $self = shift;
306 30   66     144 return $self->{synced_future} //= $self->loop->new_future;
307             }
308              
309             =head2 live_state
310              
311             $state = $room->live_state
312              
313             Returns a L instance representing the current
314             live-tracking state of the room.
315              
316             This instance will mutate and change as new state events are received.
317              
318             =cut
319              
320             sub live_state
321             {
322 31     31 1 26 my $self = shift;
323 31         69 return $self->{live_state};
324             }
325              
326             sub _handle_state_event
327             {
328 7     7   7 my $self = shift;
329 7         7 my ( $old_event, $new_event, $state ) = @_;
330              
331 7         7 my $old_content = $old_event->{content};
332 7         7 my $new_content = $new_event->{content};
333              
334 7         7 my %changes;
335 7         21 $changes{$_}->[0] = $old_content->{$_} for keys %$old_content;
336 7         16 $changes{$_}->[1] = $new_content->{$_} for keys %$new_content;
337              
338 7   50     18 $_->[1] //= undef for values %changes; # Ensure deleted key values become undef
339              
340 7         13 _delete_null_changes \%changes;
341              
342 7         19 my $member = $state->member( $new_event->{sender} );
343              
344 7         39 my $type = $new_event->{type};
345              
346 7         24 $type =~ m/^m\.room\.(.*)$/;
347 7 50       31 my $method = $1 ? "_handle_state_event_" . join( "_", split m/\./, $1 ) : undef;
348              
349 7 100 66     49 if( $method and my $code = $self->can( $method ) ) {
350 4         11 $self->$code( $member, $new_event, $state, %changes );
351             }
352             else {
353 3         16 $self->maybe_invoke_event( on_state_changed =>
354             $member, $new_event, %changes
355             );
356             }
357             }
358              
359             sub _handle_event
360             {
361 2     2   3 my $self = shift;
362 2         2 my ( $direction, $event ) = @_;
363              
364 2 50       14 $event->{type} =~ m/^(m\.room\.)?(.*)$/ or return;
365              
366 2 100       7 my $base = $1 ? "_handle_roomevent_" : "_handle_event_";
367 2         9 my $method = $base . join( "_", split( m/\./, $2 ), $direction );
368              
369 2 50       11 if( my $code = $self->can( $method ) ) {
370 2         6 $code->( $self, $event );
371             }
372             else {
373 0         0 warn "TODO: $direction event $event->{type}\n";
374             }
375             }
376              
377             sub _handle_state_backward
378             {
379 0     0   0 my $self = shift;
380 0         0 my ( $field, $event ) = @_;
381              
382 0         0 my $newvalue = $event->{content}{$field};
383 0         0 my $oldvalue = $event->{prev_content}{$field};
384              
385             $self->maybe_invoke_event( on_back_state_changed =>
386 0         0 $self->{back_members_by_userid}{$event->{user_id}}, $event,
387             $field => [ $newvalue, $oldvalue ]
388             );
389             }
390              
391             =head2 room_id
392              
393             $id = $room->room_id
394              
395             Returns the opaque room ID string for the room. Usually this would not be
396             required, except for long-term persistence uniqueness purposes, or for
397             inclusion in direct protocol URLs.
398              
399             =cut
400              
401             sub room_id
402             {
403 1     1 1 569 my $self = shift;
404 1         4 return $self->{room_id};
405             }
406              
407             =head2 name
408              
409             $name = $room->name
410              
411             Returns the room name, if defined, otherwise the opaque room ID.
412              
413             =cut
414              
415             sub _handle_roomevent_name_backward
416             {
417 0     0   0 my $self = shift;
418 0         0 my ( $event ) = @_;
419 0         0 $self->_handle_state_backward( name => $event );
420             }
421              
422             sub name
423             {
424 2     2 1 12 my $self = shift;
425 2   33     4 return $self->live_state->name || $self->room_id;
426             }
427              
428             =head2 set_name
429              
430             $room->set_name( $name )->get
431              
432             Requests to set a new room name.
433              
434             =cut
435              
436             sub set_name
437             {
438 0     0 1 0 my $self = shift;
439 0         0 my ( $name ) = @_;
440              
441 0         0 $self->_do_PUT_json( "/state/m.room.name", { name => $name } )
442             ->then_done();
443             }
444              
445             =head2 aliases
446              
447             @aliases = $room->aliases
448              
449             Returns a list of all the known room alias names taken from the
450             C events. Note that these are simply names I to have
451             aliases from the alias events; a client ought to still check that these are
452             valid before presenting them to the user as such, or in other ways relying on
453             their values.
454              
455             =cut
456              
457             sub _handle_state_event_aliases
458             {
459 1     1   1 my $self = shift;
460 1         3 my ( $member, $event, $state, %changes ) = @_;
461              
462 1         1 my $homeserver = $event->{state_key};
463              
464 0         0 my @others = map { $_->{content}{aliases} }
465 1         4 grep { $_->{state_key} ne $homeserver }
466 1         2 values %{ $state->get_events( "m.room.aliases" ) };
  1         34  
467              
468 1         2 $changes{aliases}[2] = \@others;
469              
470 1         4 $self->maybe_invoke_event( on_state_changed =>
471             $member, $event, %changes
472             );
473             }
474              
475             sub _handle_roomevent_aliases_backward
476             {
477 0     0   0 my $self = shift;
478 0         0 my ( $event ) = @_;
479              
480 0         0 my $homeserver = $event->{state_key};
481              
482 0   0     0 my $new = $event->{prev_content}{aliases} // [];
483 0   0     0 my $old = $event->{content}{aliases} // [];
484              
485 0         0 $self->{back_aliases_by_hs}{$homeserver} = [ @$new ];
486              
487 0         0 my @others = map { @{ $self->{back_aliases_by_hs}{$_} } }
  0         0  
488 0         0 grep { $_ ne $homeserver }
489 0         0 keys %{ $self->{back_aliases_by_hs} };
  0         0  
490              
491             $self->maybe_invoke_event( on_back_state_changed =>
492 0         0 $self->{back_members_by_userid}{$event->{user_id}}, $event,
493             aliases => [ $old, $new, \@others ]
494             );
495             }
496              
497             sub aliases
498             {
499 2     2 1 1115 my $self = shift;
500 2         4 return $self->live_state->aliases;
501             }
502              
503             =head2 join_rule
504              
505             $rule = $room->join_rule
506              
507             Returns the current C for the room; a string giving the type of
508             access new members may get:
509              
510             =over 4
511              
512             =item * public
513              
514             Any user may join without further permission
515              
516             =item * invite
517              
518             Users may only join if explicitly invited
519              
520             =item * knock
521              
522             Any user may send a knock message to request access; may only join if invited
523              
524             =item * private
525              
526             No new users may join the room
527              
528             =back
529              
530             =cut
531              
532             sub _handle_roomevent_join_rules_backward
533             {
534 0     0   0 my $self = shift;
535 0         0 my ( $event ) = @_;
536 0         0 $self->_handle_state_backward( join_rule => $event );
537             }
538              
539             sub join_rule
540             {
541 2     2 1 1297 my $self = shift;
542 2         6 return $self->live_state->join_rule;
543             }
544              
545             =head2 topic
546              
547             $topic = $room->topic
548              
549             Returns the room topic, if defined
550              
551             =cut
552              
553             sub _handle_roomevent_topic_backward
554             {
555 0     0   0 my $self = shift;
556 0         0 my ( $event ) = @_;
557 0         0 $self->_handle_state_backward( topic => $event );
558             }
559              
560             sub topic
561             {
562 2     2 1 1065 my $self = shift;
563 2         5 return $self->live_state->topic;
564             }
565              
566             =head2 set_topic
567              
568             $room->set_topic( $topic )->get
569              
570             Requests to set a new room topic.
571              
572             =cut
573              
574             sub set_topic
575             {
576 0     0 1 0 my $self = shift;
577 0         0 my ( $topic ) = @_;
578              
579 0         0 $self->_do_PUT_json( "/state/m.room.topic", { topic => $topic } )
580             ->then_done();
581             }
582              
583             =head2 levels
584              
585             %levels = $room->levels
586              
587             Returns a key/value list of the room levels; that is, the member power level
588             required to perform each of the named actions.
589              
590             =cut
591              
592             sub _handle_generic_level
593             {
594 0     0   0 my $self = shift;
595 0         0 my ( $phase, $level, $convert, $event ) = @_;
596              
597 0         0 foreach my $k (qw( content prev_content )) {
598 0 0       0 next unless my $levels = $event->{$k};
599              
600             $event->{$k} = {
601 0         0 map { $convert->{$_} => $levels->{$_} } keys %$convert
  0         0  
602             };
603             }
604              
605 0 0       0 if( $phase eq "initial" ) {
    0          
    0          
606 0         0 my $levels = $event->{content};
607              
608 0         0 $self->{levels}{$_} = $levels->{$_} for keys %$levels;
609             }
610             elsif( $phase eq "forward" ) {
611 0         0 my $newlevels = $event->{content};
612 0         0 my $oldlevels = $event->{prev_content};
613              
614 0         0 my %changes;
615 0         0 foreach ( keys %$newlevels ) {
616 0         0 $self->{levels}{$_} = $newlevels->{$_};
617              
618             $changes{"level.$_"} = [ $oldlevels->{$_}, $newlevels->{$_} ]
619 0 0 0     0 if !defined $oldlevels->{$_} or $oldlevels->{$_} != $newlevels->{$_};
620             }
621              
622 0         0 my $member = $self->member( $event->{sender} );
623 0         0 $self->maybe_invoke_event( on_state_changed =>
624             $member, $event, %changes
625             );
626             }
627             elsif( $phase eq "backward" ) {
628 0         0 my $newlevels = $event->{content};
629 0         0 my $oldlevels = $event->{prev_content};
630              
631 0         0 my %changes;
632 0         0 foreach ( keys %$newlevels ) {
633             $changes{"level.$_"} = [ $newlevels->{$_}, $oldlevels->{$_} ]
634 0 0 0     0 if !defined $oldlevels->{$_} or $oldlevels->{$_} != $newlevels->{$_};
635             }
636              
637 0         0 my $member = $self->{back_members_by_userid}{$event->{user_id}};
638 0         0 $self->maybe_invoke_event( on_back_state_changed =>
639             $member, $event, %changes
640             );
641             }
642             }
643              
644             sub levels
645             {
646 0     0 1 0 my $self = shift;
647 0         0 return %{ $self->{levels} };
  0         0  
648             }
649              
650             =head2 change_levels
651              
652             $room->change_levels( %levels )->get
653              
654             Performs a room levels change, submitting new values for the given keys while
655             leaving other keys unchanged.
656              
657             =cut
658              
659             sub change_levels
660             {
661 0     0 1 0 my $self = shift;
662 0         0 my %levels = @_;
663              
664             # Delete null changes
665 0         0 foreach ( keys %levels ) {
666 0 0       0 delete $levels{$_} if $self->{levels}{$_} == $levels{$_};
667             }
668              
669 0         0 my %events;
670              
671             # These go in their own event with the content key 'level'
672 0         0 foreach (qw( send_event add_state )) {
673 0 0       0 $events{"${_}_level"} = { level => $levels{$_} } if exists $levels{$_};
674             }
675              
676             # These go in an 'ops_levels' event
677 0         0 foreach (qw( ban kick redact )) {
678 0 0       0 $events{ops_levels}{"${_}_level"} = $levels{$_} if exists $levels{$_};
679             }
680              
681             # Fill in remaining 'ops_levels' keys
682 0 0       0 if( $events{ops_levels} ) {
683 0   0     0 $events{ops_levels}{"${_}_level"} //= $self->{levels}{$_} for qw( ban kick redact );
684             }
685              
686             Future->needs_all(
687 0         0 map { $self->_do_PUT_json( "/state/m.room.$_", $events{$_} ) } keys %events
  0         0  
688             )->then_done();
689             }
690              
691             =head2 members
692              
693             @members = $room->members
694              
695             Returns a list of member structs containing the currently known members of the
696             room, in no particular order. This list will include users who are not yet
697             members of the room, but simply have been invited.
698              
699             =cut
700              
701             sub _handle_roomevent_member_backward
702             {
703 0     0   0 my $self = shift;
704 0         0 my ( $event ) = @_;
705              
706             # $self->_handle_roomevent_member( on_back_membership => $event,
707             # $self->{back_members_by_userid}, $event->{content}, $event->{prev_content} );
708             }
709              
710             sub _handle_state_event_member
711             {
712 2     2   2 my $self = shift;
713 2         5 my ( $member, $event, $state, %changes ) = @_;
714              
715             # Currently, the server "deletes" users from the room by setting their
716             # membership to "leave". It's neater if we consider an empty content in
717             # that case.
718 2         4 foreach my $idx ( 0, 1 ) {
719 4 50 100     17 next unless ( $changes{membership}[$idx] // "" ) eq "leave";
720              
721 0         0 undef $changes{$_}[$idx] for keys %changes;
722             }
723              
724 2         4 my $user_id = $event->{state_key}; # == user the change applies to
725              
726 2 50 0     5 my $target_member = $state->member( $user_id ) or
727             warn "ARGH: roomevent_member with unknown user id '$user_id'" and return;
728              
729 2         49 _delete_null_changes \%changes;
730              
731 2         8 $self->maybe_invoke_event( on_membership => $member, $event, $target_member, %changes );
732             }
733              
734             sub members
735             {
736 3     3 1 1547 my $self = shift;
737 3         8 return $self->live_state->members;
738             }
739              
740             sub member
741             {
742 2     2 0 2 my $self = shift;
743 2         3 my ( $user_id ) = @_;
744 2         4 return $self->live_state->member( $user_id );
745             }
746              
747             =head2 joined_members
748              
749             @members = $room->joined_members
750              
751             Returns the subset of C who actually in the C<"join"> state -
752             i.e. are not invitees, or have left.
753              
754             =cut
755              
756             sub joined_members
757             {
758 0     0 1 0 my $self = shift;
759 0   0     0 return grep { ( $_->membership // "" ) eq "join" } $self->members;
  0         0  
760             }
761              
762             =head2 member_level
763              
764             $level = $room->member_level( $user_id )
765              
766             Returns the current cached value for the power level of the given user ID, or
767             the default value if no specific value exists for the given ID.
768              
769             =cut
770              
771             sub _handle_roomevent_power_levels_backward
772             {
773 0     0   0 my $self = shift;
774 0         0 my ( $event ) = @_;
775              
776             # $self->_handle_roomevent_power_levels( on_back_membership =>
777             # $event, $self->{back_members_by_userid}, $event->{content}, $event->{prev_content}
778             # );
779             }
780              
781             sub _handle_state_event_power_levels
782             {
783 1     1   2 my $self = shift;
784 1         2 my ( $member, $event, $state, %changes ) = @_;
785              
786             # Before we go any further we should also clean up null changes in 'users'
787             # and 'events' hashes by pushing the 'old+new' diff ARRAYrefs down into the
788             # hashes
789 1   66     5 $_ and $_ = _pushdown_changes $_ for $changes{users}, $changes{events};
790              
791 1 50       3 if( my $users = $changes{users} ) {
792             # TODO: handle default changes
793 1         2 my $default = $event->{content}{user_default};
794              
795 1         2 foreach my $user_id ( keys %$users ) {
796 1 50       2 my $target = $state->member( $user_id ) or next;
797 1         8 my ( $oldlevel, $newlevel ) = @{ $users->{$user_id} };
  1         2  
798              
799 1   33     3 $oldlevel //= $default;
800 1   33     2 $newlevel //= $default;
801              
802 1         5 $self->maybe_invoke_event( on_membership =>
803             $member, $event, $target, level => [ $oldlevel, $newlevel ]
804             );
805             }
806             }
807             }
808              
809             sub member_level
810             {
811 2     2 1 1849 my $self = shift;
812 2         2 my ( $user_id ) = @_;
813 2         4 return $self->live_state->member_level( $user_id );
814             }
815              
816             =head2 change_member_levels
817              
818             $room->change_member_levels( %levels )->get
819              
820             Performs a member power level change, submitting new values for user IDs to
821             the home server. As there is no server API to make individual mutations, this
822             is done by taking the currently cached values, applying the changes given by
823             the C<%levels> key/value list, and submitting the resulting whole as the new
824             value for the C room state.
825              
826             The C<%levels> key/value list should provide new values for keys giving user
827             IDs, or the special user ID of C to change the overall default value
828             for users not otherwise mentioned. Setting the special value of C for a
829             user ID will remove that ID from the set, reverting them to the default.
830              
831             =cut
832              
833             sub change_member_levels
834             {
835 0     0 1 0 my $self = shift;
836              
837             # Can't just edit the local cache as maybe the server will reject it. Clone
838             # it and if the server accepts our modification the cache will be updated
839             # on the incoming event.
840              
841 0         0 my %user_levels = %{ $self->{powerlevels}{users} };
  0         0  
842              
843 0         0 while( @_ ) {
844 0         0 my $user_id = shift;
845 0         0 my $value = shift;
846              
847 0 0       0 if( defined $value ) {
848 0         0 $user_levels{$user_id} = $value;
849             }
850             else {
851 0         0 delete $user_levels{$user_id};
852             }
853             }
854              
855             $self->_do_PUT_json( "/state/m.room.power_levels",
856 0         0 { %{ $self->{powerlevels} }, users => \%user_levels }
  0         0  
857             )->then_done();
858             }
859              
860             =head2 leave
861              
862             $room->leave->get
863              
864             Requests to leave the room. After this completes, the user will no longer be
865             a member of the room.
866              
867             =cut
868              
869             sub leave
870             {
871 0     0 1 0 my $self = shift;
872 0         0 $self->_do_POST_json( "/leave", {} );
873             }
874              
875             =head2 invite
876              
877             $room->invite( $user_id )->get
878              
879             Sends an invitation for the user with the given User ID to join the room.
880              
881             =cut
882              
883             sub invite
884             {
885 0     0 1 0 my $self = shift;
886 0         0 my ( $user_id ) = @_;
887              
888 0         0 $self->_do_POST_json( "/invite", { user_id => $user_id } )
889             ->then_done();
890             }
891              
892             =head2 kick
893              
894             $room->kick( $user_id, $reason )->get
895              
896             Requests to remove the user with the given User ID from the room.
897              
898             Optionally, a textual description reason can also be provided.
899              
900             =cut
901              
902             sub kick
903             {
904 0     0 1 0 my $self = shift;
905 0         0 my ( $user_id, $reason ) = @_;
906              
907 0         0 $self->_do_POST_json( "/kick", { user_id => $user_id, reason => $reason } )
908             ->then_done();
909             }
910              
911             =head2 send_message
912              
913             $event_id = $room->send_message( %args )->get
914              
915             Sends a new message to the room. Requires a C named argument giving the
916             message type. Depending on the type, further keys will be required that
917             specify the message contents:
918              
919             =over 4
920              
921             =item m.text, m.emote, m.notice
922              
923             Require C
924              
925             =item m.image, m.audio, m.video, m.file
926              
927             Require C
928              
929             =item m.location
930              
931             Require C
932              
933             =back
934              
935             If an additional argument called C is provided, this is used as the
936             transaction ID for the message, which is then sent as a C request instead
937             of a C.
938              
939             $event_id = $room->send_message( $text )->get
940              
941             A convenient shortcut to sending an C message with a body string and
942             no additional content.
943              
944             =cut
945              
946             my %MSG_REQUIRED_FIELDS = (
947             'm.text' => [qw( body )],
948             'm.emote' => [qw( body )],
949             'm.notice' => [qw( body )],
950             'm.image' => [qw( url )],
951             'm.audio' => [qw( url )],
952             'm.video' => [qw( url )],
953             'm.file' => [qw( url )],
954             'm.location' => [qw( geo_uri )],
955             );
956              
957             sub send_message
958             {
959 2     2 1 836 my $self = shift;
960 2 50       9 my %args = ( @_ == 1 ) ? ( type => "m.text", body => shift ) : @_;
961              
962             my $type = $args{msgtype} = delete $args{type} or
963 2 50       9 croak "Require a 'type' field";
964              
965 2 50       5 $MSG_REQUIRED_FIELDS{$type} or
966             croak "Unrecognised message type '$type'";
967              
968 2         2 foreach (@{ $MSG_REQUIRED_FIELDS{$type} } ) {
  2         4  
969 2 50       5 $args{$_} or croak "'$type' messages require a '$_' field";
970             }
971              
972 2 100       6 if( defined( my $txn_id = $args{txn_id} ) ) {
973             $self->_do_PUT_json( "/send/m.room.message/$txn_id", \%args )->then( sub {
974 1     1   75 my ( $response ) = @_;
975 1         3 Future->done( $response->{event_id} );
976 1         4 });
977             }
978             else {
979             $self->_do_POST_json( "/send/m.room.message", \%args )->then( sub {
980 1     1   75 my ( $response ) = @_;
981 1         3 Future->done( $response->{event_id} );
982 1         3 });
983             }
984             }
985              
986             =head2 paginate_messages
987              
988             $room->paginate_messages( limit => $n )->get
989              
990             Requests more messages of back-pagination history.
991              
992             There is no need to maintain a reference on the returned C; it will be
993             adopted by the room object.
994              
995             =cut
996              
997             sub paginate_messages
998             {
999 0     0 1 0 my $self = shift;
1000 0         0 my %args = @_;
1001              
1002 0   0     0 my $limit = $args{limit} // 20;
1003 0   0     0 my $from = $self->{pagination_token} // "END";
1004              
1005 0 0       0 croak "Cannot paginate_messages any further since we're already at the start"
1006             if $from eq "START";
1007              
1008             # Since we're now doing pagination, we'll need a second set of member
1009             # objects
1010             $self->{back_members_by_userid} //= {
1011 0   0 0   0 pairmap { $a => Member( $b->user, $b->displayname, $b->membership ) } %{ $self->{members_by_userid} }
  0         0  
  0         0  
1012             };
1013             $self->{back_aliases_by_hs} //= {
1014 0   0 0   0 pairmap { $a => [ @$b ] } %{ $self->{aliases_by_hs} }
  0         0  
  0         0  
1015             };
1016              
1017             my $f = $self->_do_GET_json( "/messages",
1018             from => $from,
1019             dir => "b",
1020             limit => $limit,
1021             )->then( sub {
1022 0     0   0 my ( $response ) = @_;
1023              
1024 0         0 foreach my $event ( @{ $response->{chunk} } ) {
  0         0  
1025 0 0       0 next unless my ( $subtype ) = ( $event->{type} =~ m/^m\.room\.(.*)$/ );
1026 0         0 $subtype =~ s/\./_/g;
1027              
1028 0 0       0 if( my $code = $self->can( "_handle_roomevent_${subtype}_backward" ) ) {
1029 0         0 $code->( $self, $event );
1030             }
1031             else {
1032 0         0 $self->{matrix}->log( "TODO: Handle room pagination event $subtype" );
1033             }
1034             }
1035              
1036 0         0 $self->{pagination_token} = $response->{end};
1037 0         0 Future->done( $self );
1038 0         0 });
1039 0         0 $self->adopt_future( $f );
1040             }
1041              
1042             =head2 typing_start
1043              
1044             $room->typing_start
1045              
1046             Sends a typing notification that the user is currently typing in this room.
1047             This notification will periodically be re-sent as required by the protocol
1048             until the C method is called.
1049              
1050             =cut
1051              
1052             sub typing_start
1053             {
1054 1     1 1 280 my $self = shift;
1055              
1056 1 50       4 return if $self->{typing_timer};
1057              
1058 1         4 my $user_id = $self->{matrix}->myself->user_id;
1059              
1060             my $f = $self->{typing_timer} = repeat {
1061             $self->_do_PUT_json( "/typing/$user_id", {
1062             typing => 1,
1063             timeout => ( TYPING_RESEND_SECONDS + 5 ) * 1000, # msec
1064             })->then( sub {
1065 2         161 $self->{matrix}->{make_delay}->( TYPING_RESEND_SECONDS );
1066 2     2   39 });
1067 1     1   46 } while => sub { !shift->failure };
  1         399  
1068              
1069             $f->on_fail( $self->_capture_weakself( sub {
1070 0     0   0 my $self = shift;
1071 0         0 $self->invoke_error( @_ );
1072 1         92 }));
1073             }
1074              
1075             =head2 typing_stop
1076              
1077             $room->typing_stop
1078              
1079             Sends a typing notification that the user is no longer typing in this room.
1080             This method also cancels the repeating re-send behaviour created by
1081             C.
1082              
1083             =cut
1084              
1085             sub typing_stop
1086             {
1087 1     1 1 41 my $self = shift;
1088              
1089 1 50       4 return unless my $f = $self->{typing_timer};
1090              
1091 1         4 $f->cancel;
1092 1         75 undef $self->{typing_timer};
1093              
1094 1         4 my $user_id = $self->{matrix}->myself->user_id;
1095              
1096 1         15 $self->adopt_future(
1097             $self->_do_PUT_json( "/typing/$user_id", {
1098             typing => 0,
1099             })
1100             );
1101             }
1102              
1103             =head2 send_read_receipt
1104              
1105             $room->send_read_receipt( event_id => $event_id, ... )->get
1106              
1107             Sends a C receipt to the given room for the given event ID.
1108              
1109             =cut
1110              
1111             sub send_read_receipt
1112             {
1113 1     1 1 2469 my $self = shift;
1114 1         3 my %args = @_;
1115              
1116 1 50       4 my $event_id = $args{event_id} or croak "Require event_id";
1117              
1118 1         6 $self->_do_POST_json( "/receipt/m.read/$event_id", {} );
1119             }
1120              
1121             sub _handle_roomevent_create_forward
1122             {
1123 0     0   0 my $self = shift;
1124 0         0 my ( $event ) = @_;
1125              
1126             # Nothing interesting here...
1127             }
1128             *_handle_roomevent_create_initial = \&_handle_roomevent_create_forward;
1129              
1130             sub _handle_roomevent_create_backward
1131             {
1132 0     0   0 my $self = shift;
1133              
1134             # Stop now
1135 0         0 $self->{pagination_token} = "START";
1136             }
1137              
1138             sub _handle_roomevent_message_forward
1139             {
1140 1     1   2 my $self = shift;
1141 1         1 my ( $event ) = @_;
1142              
1143 1         2 my $user_id = $event->{sender};
1144 1 50 0     4 my $member = $self->member( $user_id ) or
1145             warn "TODO: Unknown member '$user_id' for forward message" and return;
1146              
1147 1         35 $self->maybe_invoke_event( on_message => $member, $event->{content}, $event );
1148             }
1149              
1150             sub _handle_roomevent_message_backward
1151             {
1152 0     0   0 my $self = shift;
1153 0         0 my ( $event ) = @_;
1154              
1155 0         0 my $user_id = $event->{user_id};
1156 0 0 0     0 my $member = $self->{back_members_by_userid}{$user_id} or
1157             warn "TODO: Unknown member '$user_id' for backward message" and return;
1158              
1159 0         0 $self->maybe_invoke_event( on_back_message => $member, $event->{content}, $event );
1160             }
1161              
1162             sub _handle_event_m_presence
1163             {
1164 0     0   0 my $self = shift;
1165 0         0 my ( $user, %changes ) = @_;
1166 0 0       0 my $member = $self->member( $user->user_id ) or return;
1167              
1168             $changes{$_} and $member->$_ = $changes{$_}[1]
1169 0   0     0 for qw( displayname );
1170              
1171 0         0 $self->maybe_invoke_event( on_presence => $member, %changes );
1172             }
1173              
1174             sub _handle_event_m_typing_ephemeral
1175             {
1176 0     0   0 my $self = shift;
1177 0         0 my ( $event ) = @_;
1178              
1179 0         0 my $typing = $self->{typing_members};
1180 0         0 my %not_typing = %$typing;
1181              
1182 0         0 foreach my $user_id ( @{ $event->{content}{user_ids} } ) {
  0         0  
1183 0         0 delete $not_typing{$user_id};
1184 0 0       0 next if $typing->{$user_id};
1185              
1186 0         0 $typing->{$user_id}++;
1187 0 0       0 my $member = $self->member( $user_id ) or next;
1188 0         0 $self->maybe_invoke_event( on_typing => $member, 1 );
1189             }
1190              
1191 0         0 foreach my $user_id ( keys %not_typing ) {
1192 0 0       0 my $member = $self->member( $user_id ) or next;
1193 0         0 $self->maybe_invoke_event( on_typing => $member, 0 );
1194 0         0 delete $typing->{$user_id};
1195             }
1196              
1197 0         0 my @members = map { $self->member( $_ ) } keys %$typing;
  0         0  
1198 0         0 $self->maybe_invoke_event( on_members_typing => grep { defined } @members );
  0         0  
1199             }
1200              
1201             sub _handle_event_m_receipt_ephemeral
1202             {
1203 1     1   1 my $self = shift;
1204 1         5 my ( $event ) = @_;
1205              
1206 1         1 my $content = $event->{content};
1207 1         3 foreach my $event_id ( keys %$content ) {
1208 1         1 my $receipt = $content->{$event_id};
1209 1 50       3 my $read_receipt = $receipt->{"m.read"} or next;
1210              
1211 1         2 foreach my $user_id ( keys %$read_receipt ) {
1212 1         2 my $content = $read_receipt->{$user_id};
1213 1 50       4 my $member = $self->member( $user_id ) or next;
1214              
1215 1         33 $self->maybe_invoke_event( on_read_receipt => $member, $event_id, $content );
1216             }
1217             }
1218             }
1219              
1220             =head1 MEMBERSHIP STRUCTURES
1221              
1222             Parameters documented as C<$member> receive a membership struct, which
1223             supports the following methods:
1224              
1225             =head2 $user = $member->user
1226              
1227             User object of the member.
1228              
1229             =head2 $displayname = $member->displayname
1230              
1231             Profile displayname of the user.
1232              
1233             =head2 $membership = $member->membership
1234              
1235             Membership state. One of C or C.
1236              
1237             =head1 AUTHOR
1238              
1239             Paul Evans
1240              
1241             =cut
1242              
1243             0x55AA;