File Coverage

blib/lib/Circle/Net/Matrix/Room.pm
Criterion Covered Total %
statement 30 229 13.1
branch 0 42 0.0
condition 0 48 0.0
subroutine 10 44 22.7
pod 0 31 0.0
total 40 394 10.1


line stmt bran cond sub pod time code
1             # You may distribute under the terms of the GNU General Public License
2             #
3             # (C) Paul Evans, 2014-2017 -- leonerd@leonerd.org.uk
4              
5             package Circle::Net::Matrix::Room;
6              
7 1     1   1044 use strict;
  1         2  
  1         25  
8 1     1   2 use warnings;
  1         2  
  1         33  
9 1     1   5 use base qw( Tangence::Object Circle::WindowItem );
  1         2  
  1         159  
10              
11             our $VERSION = '0.02';
12              
13 1     1   4 use Data::Dump qw( pp );
  1         1  
  1         38  
14 1     1   3 use Scalar::Util qw( weaken );
  1         1  
  1         38  
15              
16 1     1   438 use Net::Async::Matrix::Utils qw( parse_formatted_message );
  1         145544  
  1         65  
17 1     1   6 use Circle::TaggedString;
  1         1  
  1         1532  
18              
19             # To allow for out-of-tree development, use an inline Tangence class
20             # declaration instead of a .tan file
21             #
22             # class Circle.Net.Matrix.Room {
23             # isa Circle.WindowItem;
24             #
25             # smashed prop name = str;
26             # smashed prop topic = str;
27             # }
28              
29             sub DECLARE_TANGENCE
30             {
31 0     0 0   Tangence::Class->declare( __PACKAGE__,
32             props => {
33             name => {
34             dim => Tangence::Constants::DIM_SCALAR,
35             type => 'str',
36             },
37             },
38              
39             superclasses => [qw( Circle::WindowItem )],
40             );
41             }
42              
43             sub WEAKSELF_EVAL
44             {
45 0     0 0   my ( $self, $method ) = @_;
46 0 0   0     my $code = $self->can( $method ) or return sub {};
47              
48 0           weaken( $self );
49             return sub {
50 0     0     my @args = @_;
51 0 0         eval { $self->$code( @args ); 1 } or
  0            
  0            
52             warn $@;
53 0           };
54             }
55              
56 0     0 0   sub init_prop_topic { "" }
57              
58             sub new
59             {
60 0     0 0   my $class = shift;
61 0           my %args = @_;
62              
63 0           my $self = $class->SUPER::new( @_ );
64              
65 0           my $room = $self->{room} = $args{room};
66              
67 0           $self->set_prop_name( $room->room_id );
68 0           $self->set_prop_tag( $room->room_id );
69              
70 0           $self->{root} = $args{root};
71 0           $self->{net} = $args{net};
72              
73 0           weaken( my $weakself = $self );
74 0           $room->configure(
75             on_synced_state => $self->WEAKSELF_EVAL( 'on_synced_state' ),
76              
77             on_message => $self->WEAKSELF_EVAL( 'on_message' ),
78             on_membership => $self->WEAKSELF_EVAL( 'on_membership' ),
79             on_state_changed => $self->WEAKSELF_EVAL( 'on_state_changed' ),
80             on_members_typing => $self->WEAKSELF_EVAL( 'on_members_typing' ),
81             on_read_receipt => $self->WEAKSELF_EVAL( 'on_read_receipt' ),
82             );
83              
84             # TODO: this entire state-watching system likely wants to be migrated into
85             # NaMatrix itself
86             $self->{state_watches} = {
87 0           'm.room.name' => $self->can( 'on_state_name' ),
88             'm.room.topic' => $self->can( 'on_state_topic' ),
89             'm.room.member*' => $self->can( 'on_state_members' ),
90              
91             'm.room.join_rules' => $self->can( 'on_state_permission' ),
92             'm.room.history_visibility' => $self->can( 'on_state_permission' ),
93             };
94              
95 0           return $self;
96             }
97              
98             # Convenience accessor
99             sub name
100             {
101 0     0 0   my $self = shift;
102 0           return $self->get_prop_name;
103             }
104              
105             sub enumerable_name
106             {
107 0     0 0   my $self = shift;
108 0           return $self->name;
109             }
110              
111             sub get_prop_tag
112             {
113 0     0 0   my $self = shift;
114 0           return $self->name;
115             }
116              
117             sub parent
118             {
119 0     0 0   my $self = shift;
120 0           return $self->{net};
121             }
122              
123             sub commandable_parent
124             {
125 0     0 0   my $self = shift;
126 0           return $self->parent;
127             }
128              
129             sub on_synced_state
130             {
131 0     0 0   my $self = shift;
132              
133 0           my $room = $self->{room};
134              
135 0           my $state = $room->live_state;
136              
137 0           my $watches = $self->{state_watches};
138 0           foreach my $type ( keys %$watches ) {
139             # TODO: avoid doing the name ones twice in here somehow
140 0           my $code = $watches->{$type};
141              
142 0 0         if( $type =~ m/^(.*)\*$/ ) {
143 0           $code->( $self, $state->get_events( $1 ) );
144             }
145             else {
146 0           $code->( $self, $state->get_event( $type ) );
147             }
148             }
149              
150 0           my $name = $self->_generate_roomname( $state );
151              
152 0           $self->set_prop_name( $name );
153 0           $self->set_prop_tag( $name );
154              
155 0           $self->SUPER::set_prop_level( 0 );
156             }
157              
158             sub on_state_name
159             {
160 0     0 0   my $self = shift;
161              
162 0           my $state = $self->{room}->live_state;
163              
164 0           my $name = $self->_generate_roomname( $state );
165              
166 0           $self->set_prop_name( $name );
167 0           $self->set_prop_tag( $name );
168             }
169              
170             sub _generate_roomname
171             {
172 0     0     my $self = shift;
173 0           my ( $state ) = @_;
174              
175 0           my $event;
176              
177             $event = $state->get_event( 'm.room.name' ) and
178 0 0         return $event->{content}{name};
179              
180 0           my $myself = $self->{net}{matrix}->myself;
181 0           my @others = grep { $_->user->user_id ne $myself->user_id } $state->members;
  0            
182              
183 0 0         return $others[0]->displayname if @others == 1;
184              
185 0           return "TODO";
186             }
187              
188             sub on_state_topic
189             {
190 0     0 0   my $self = shift;
191 0           my ( $event ) = @_;
192              
193 0           my $topic = $event->{content}{topic};
194              
195 0           $self->get_widget_topic->set_prop_text( $topic );
196             }
197              
198             sub on_state_members
199             {
200 0     0 0   my $self = shift;
201 0           my ( $eventmap ) = @_;
202              
203             # For now lets just count members by membership, but at some point we'd
204             # like to consider powerlevel or something too
205 0           my %members_by_membership;
206              
207 0           $members_by_membership{ $_->{content}{membership} }++ for values %$eventmap;
208              
209 0           my $count = "$members_by_membership{join} members";
210 0 0         $count .= " + $members_by_membership{invite} invited" if $members_by_membership{invite};
211             # Ignore "leave"
212              
213 0           $self->get_widget_membercount->set_prop_text( $count );
214              
215 0           my $my_userid = $self->{net}{matrix}->myself->user_id;
216              
217             $self->get_widget_displayname->set_prop_text(
218 0   0       $eventmap->{$my_userid }{content}{displayname} // $my_userid
219             );
220             }
221              
222             sub on_state_permission
223             {
224 0     0 0   my $self = shift;
225             # Ignore the passed event because we combine multiple
226              
227 0           my $state = $self->{room}->live_state;
228              
229 0           my $ev;
230             my @parts;
231              
232             # TODO: something about my own power level?
233              
234 0   0       push @parts, "J=" . ( $state->join_rule // "?" );
235              
236 0           $ev = $state->get_event( "m.room.history_visibility" );
237 0   0       push @parts, "V=" . ( ( $ev && $ev->{content}{history_visibility} ) // "?" );
      0        
238              
239 0           $self->get_widget_permission->set_prop_text( join "|", @parts );
240             }
241              
242             sub on_message
243             {
244 0     0 0   my $self = shift; shift;
  0            
245 0           my ( $member, $content, $event ) = @_;
246 0           my $member_id = $member->user->user_id;
247              
248 0           $self->{latest_event_id} = $event->{event_id};
249              
250 0           my $tstamp = $event->{origin_server_ts} / 1000;
251 0           my $type = $content->{msgtype};
252              
253 0           my $formatted_body = parse_formatted_message( $content );
254 0           my $text = Circle::TaggedString->new_from_formatting( $formatted_body );
255              
256 0           my ( $etype, $args );
257 0 0         if( $type eq "m.text" ) {
    0          
    0          
258 0           ( $etype, $args ) = ( "matrix.text" => {
259             name => $member->displayname,
260             user_id => $member_id,
261             text => $text,
262             });
263             }
264             elsif( $type eq "m.notice" ) {
265 0           ( $etype, $args ) = ( "matrix.notice" => {
266             name => $member->displayname,
267             user_id => $member_id,
268             text => $text,
269             });
270             }
271             elsif( $type eq "m.emote" ) {
272 0           ( $etype, $args ) = ( "matrix.emote" => {
273             name => $member->displayname,
274             user_id => $member_id,
275             text => $text,
276             });
277             }
278             else {
279 0           ( $etype, $args ) = ( "text" => {
280             text => "Unrecognised Matrix event msgtype <$type>"
281             });
282             }
283              
284 0           $self->push_displayevent( $etype, $args, time => $tstamp );
285 0           $self->bump_level( 2 );
286             }
287              
288             sub on_membership
289             {
290 0     0 0   my $self = shift; shift;
  0            
291 0           my ( $member, $event, $subject, %changes ) = @_;
292 0           my $member_id = $member->user->user_id;
293              
294 0           $self->{latest_event_id} = $event->{event_id};
295              
296 0           $self->on_state_members( $self->{room}->live_state->get_events( "m.room.member" ) );
297              
298 0           my $tstamp = $event->{origin_server_ts} / 1000;
299              
300 0 0         if( my $membership = $changes{membership} ) {
301 0 0 0       if( ( $membership->[0]//"") ne "join" and $membership->[1] eq "join" ) {
      0        
302 0   0       $self->push_displayevent( "matrix.join" => {
303             name => $member->displayname // "[$member_id]",
304             user_id => $member_id,
305             }, time => $tstamp );
306 0           $self->bump_level( 1 );
307             return
308 0           }
309 0 0 0       if( $membership->[0] eq "join" and !defined $membership->[1] ) {
310 0   0       $self->push_displayevent( "matrix.leave" => {
311             name => $member->displayname // "[$member_id]",
312             user_id => $member_id,
313             });
314 0           $self->bump_level( 1 );
315             return
316 0           }
317             }
318              
319             # As a text-only client we don't care about avatar_url
320 0           delete $changes{avatar_url};
321              
322 0 0         if( my $displayname = $changes{displayname} ) {
    0          
    0          
323 0           $self->push_displayevent( "matrix.rename" => {
324             oldname => $displayname->[0],
325             newname => $displayname->[1],
326             user_id => $member_id,
327             }, time => $tstamp );
328 0           $self->bump_level( 1 );
329             }
330             elsif( my $state = $changes{state} ) {
331 0   0       my $message = $changes{status_msg} && $changes{status_msg}[1];
332              
333 0 0 0       $self->push_displayevent( "matrix.state" => {
334             state => $state->[1],
335             message => ( defined $message ? "($message)" : "" ),
336             name => $member->displayname // "[$member_id]",
337             }, time => $tstamp );
338 0           $self->bump_level( 1 );
339             }
340             elsif( keys %changes ) { # ignore "empty" changes e.g. avatar_url
341             # TODO for debugging
342 0           $self->push_displayevent( text => {
343 0           text => "Member $member changed to ${\pp \%changes}"
344             }, time => $tstamp );
345              
346 0           $self->bump_level( 2 );
347             }
348             }
349              
350             sub on_state_changed
351             {
352 0     0 0   my $self = shift; shift;
  0            
353 0           my ( $member, $event, %changes ) = @_;
354 0 0         my $member_id = $member ? $member->user->user_id : undef;
355              
356 0           $self->{latest_event_id} = $event->{event_id};
357              
358 0           my $watches = $self->{state_watches};
359 0 0         if( my $code = $watches->{ $event->{type} } ) {
360 0           $code->( $self, $event );
361             }
362              
363 0           my $tstamp = $event->{origin_server_ts} / 1000;
364              
365 0           if( 1 ) {
366             # TODO for debugging
367 0           $self->push_displayevent( text => {
368 0           text => "Member $member changed room state ${\pp \%changes}"
369             }, time => $tstamp );
370             }
371             }
372              
373             sub on_members_typing
374             {
375 0     0 0   my $self = shift; shift;
  0            
376 0           my @members = @_;
377              
378 0           my $widget = $self->get_widget_typing;
379              
380 0 0         if( !@members ) {
381 0           $widget->set_prop_text( "" );
382 0           return;
383             }
384              
385             $widget->set_prop_text( "(Typing: " .
386 0   0       join( ", ", map { $_->displayname // $_->user->user_id } @members ) .
  0            
387             ")"
388             );
389             }
390              
391             sub on_read_receipt
392             {
393 0     0 0   my $self = shift; shift;
  0            
394 0           my ( $member, $event_id, $content ) = @_;
395              
396             # TODO: maybe someday we'll care about other users, but not today
397 0           my $my_userid = $self->{net}{matrix}->myself->user_id;
398 0 0         return unless $member->user->user_id eq $my_userid;
399              
400 0 0         if( $event_id eq $self->{latest_event_id} ) {
401             # Reset level to zero; because another client has read up until latest
402             # message
403 0           $self->set_prop_level( 0 );
404             }
405             }
406              
407             sub method_reset_level
408             {
409 0     0 0   my $self = shift;
410 0           $self->SUPER::method_reset_level;
411              
412 0           my $room = $self->{room};
413              
414             my $f = $room->send_read_receipt(
415             event_id => $self->{latest_event_id},
416 0           );
417 0           $room->adopt_future( $f );
418             }
419              
420             sub enter_text
421             {
422 0     0 0   my $self = shift;
423 0           my ( $text ) = @_;
424              
425 0           my $room = $self->{room};
426 0           my $f = $room->send_message( $text );
427              
428 0           $room->adopt_future( $f );
429             }
430              
431             sub command_leave
432             : Command_description("Leave the room")
433             {
434 0     0 0 0 my $self = shift;
435 0         0 my ( $cinv ) = @_;
436              
437 0         0 my $matrix = $self->{net}{matrix};
438              
439 0         0 $matrix->leave_room( $self->{room}->room_id );
440              
441 0         0 return;
442 1     1   5 }
  1         1  
  1         7  
443              
444             sub command_say
445             : Command_description("Quote text directly as a text message")
446             : Command_arg('text', eatall => 1)
447             {
448 0     0 0 0 my $self = shift;
449 0         0 my ( $text ) = @_;
450              
451 0         0 $self->enter_text( $text );
452              
453 0         0 return;
454 1     1   364 }
  1         2  
  1         3  
455              
456             sub command_me
457             : Command_description("Send an emote message")
458             : Command_arg('text', eatall => 1)
459             {
460 0     0 0   my $self = shift;
461 0           my ( $text ) = @_;
462              
463 0           my $room = $self->{room};
464 0           my $f = $room->send_message( type => "m.emote", body => $text );
465              
466 0           $room->adopt_future( $f );
467              
468 0           return;
469 1     1   330 }
  1         2  
  1         4  
470              
471             sub get_widget_topic
472             {
473 0     0 0   my $self = shift;
474             return $self->{topic_widget} //= $self->{registry}->construct(
475 0   0       "Circle::Widget::Entry",
476             classes => [qw( topic )],
477             # on_enter => sub { $self->topic( $_[0] ) },
478             );
479             }
480              
481             sub get_widget_displayname
482             {
483 0     0 0   my $self = shift;
484             return $self->{displayname_widget} //= $self->{registry}->construct(
485 0   0       "Circle::Widget::Label",
486             );
487             }
488              
489             sub get_widget_permission
490             {
491 0     0 0   my $self = shift;
492             return $self->{permission_widget} //= $self->{registry}->construct(
493 0   0       "Circle::Widget::Label",
494             );
495             }
496              
497             sub get_widget_typing
498             {
499 0     0 0   my $self = shift;
500             return $self->{typing_widget} //= $self->{registry}->construct(
501 0   0       "Circle::Widget::Label",
502             classes => [qw( transient )],
503             );
504             }
505              
506             sub get_widget_membercount
507             {
508 0     0 0   my $self = shift;
509             return $self->{membercount_widget} //= $self->{registry}->construct(
510 0   0       "Circle::Widget::Label",
511             );
512             }
513              
514             sub make_widget_pre_scroller
515             {
516 0     0 0   my $self = shift;
517 0           my ( $box ) = @_;
518              
519 0           $box->add( $self->get_widget_topic );
520             }
521              
522             sub get_widget_statusbar
523             {
524 0     0 0   my $self = shift;
525              
526 0           my $registry = $self->{registry};
527 0           my $net = $self->{net};
528              
529 0           my $statusbar = $registry->construct(
530             "Circle::Widget::Box",
531             classes => [qw( status )],
532             orientation => "horizontal",
533             );
534              
535 0           $statusbar->add( $net->get_widget_netname );
536              
537 0           $statusbar->add( $self->get_widget_displayname );
538              
539 0           $statusbar->add( $self->get_widget_permission );
540              
541 0           $statusbar->add_spacer( expand => 1 );
542              
543 0           $statusbar->add( $self->get_widget_typing );
544              
545 0           $statusbar->add( $self->get_widget_membercount );
546              
547 0           return $statusbar;
548             }
549              
550             0x55AA;