File Coverage

blib/lib/App/MatrixClient/RoomTab.pm
Criterion Covered Total %
statement 11 11 100.0
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 15 15 100.0


line stmt bran cond sub pod time code
1             package App::MatrixClient::RoomTab;
2              
3 1     1   1244 use 5.014; # s///r
  1         4  
4 1     1   6 use strict;
  1         3  
  1         22  
5 1     1   5 use warnings;
  1         2  
  1         29  
6              
7 1     1   5 use base qw( Tickit::Console::Tab );
  1         2  
  1         794  
8              
9             use List::Util 1.33 qw( any );
10             use POSIX qw( strftime );
11              
12             use Convert::Color::XTerm;
13             use Future;
14             use Image::ExifTool;
15             use IO::Async::Timer::Countdown;
16             use Net::Async::Matrix::Utils qw( parse_formatted_message );
17              
18             use Tickit::Widgets qw( Frame GridBox ScrollBox Static VBox );
19             Tickit::Widget::Frame->VERSION( '0.31' ); # bugfix to linetypes in constructor
20              
21             use constant TYPING_GRACE_SECONDS => 5;
22              
23             my %PRESENCE_STATE_TO_COLOUR = (
24             offline => "grey",
25             unavailable => "orange",
26             online => "green",
27             );
28              
29             sub _setup
30             {
31             my $self = shift;
32             my %args = @_;
33              
34             my $room = $self->{room} = $args{room};
35             my $floatbox = $args{floatbox};
36              
37             $self->{$_} = $args{$_} for qw( dist url_base );
38              
39             $self->{headline} = $args{headline};
40              
41             $self->{presence_table} = my $presence_table = Tickit::Widget::GridBox->new(
42             col_spacing => 1,
43             );
44              
45             $self->{presence_userids} = \my @presence_userids;
46             $presence_table->add( 0, 0, Tickit::Widget::Static->new( text => "Name" ) );
47             $presence_table->add( 0, 1, Tickit::Widget::Static->new( text => "Since" ) );
48             $presence_table->add( 0, 2, Tickit::Widget::Static->new( text => "Lvl" ) );
49              
50             # Create an abstract widget tree during initial loading to avoid the
51             # O(n^2) overhead of resizing the gridbox after -every- user is added.
52             my $vbox = Tickit::Widget::VBox->new;
53              
54             $vbox->add(
55             Tickit::Widget::ScrollBox->new(
56             child => $presence_table,
57             vertical => "on_demand",
58             horizontal => 0,
59             ),
60             expand => 1,
61             );
62              
63             $vbox->add(
64             my $presence_summary = Tickit::Widget::Static->new( text => "" )
65             );
66              
67             my $presence_float;
68             my $visible = 0;
69             $self->bind_key( 'F2' => sub {
70             $visible ? ( $presence_float->hide, $visible = 0 )
71             : ( $presence_float->show, $visible = 1 );
72             });
73              
74             $room->configure(
75             on_synced_state => sub {
76             $self->set_name( $room->name );
77             $self->update_headline;
78              
79             # Fetch initial presence state of users
80             foreach my $member ( $room->joined_members ) {
81             $self->update_member_presence( $member );
82             }
83              
84             $presence_summary->set_text(
85             sprintf "Total: %d users", scalar $room->joined_members
86             );
87              
88             $room->paginate_messages( limit => 150 );
89              
90             # Only now should we add the presence table to the floatbox
91             $presence_float = $floatbox->add_float(
92             child => Tickit::Widget::Frame->new(
93             style => {
94             linetype => "none",
95             linetype_left => "single",
96              
97             frame_fg => "white", frame_bg => "purple",
98             },
99             child => $vbox,
100             ),
101              
102             top => 0, bottom => -1, right => -1,
103             left => -44,
104              
105             # Initially hidden
106             hidden => 1,
107             );
108             },
109              
110             on_message => sub {
111             my ( undef, $member, $content, $event ) = @_;
112              
113             $self->append_line( $self->format_message( $content, $member ),
114             indent => 10,
115             time => ( $event->{origin_server_ts} // $content->{hsob_ts} ) / 1000,
116             );
117             },
118             on_back_message => sub {
119             my ( undef, $member, $content, $event ) = @_;
120              
121             $self->prepend_line( $self->format_message( $content, $member ),
122             indent => 10,
123             time => ( $event->{origin_server_ts} // $content->{hsob_ts} ) / 1000,
124             );
125             },
126              
127             on_membership => sub {
128             my ( undef, $action_member, $event, $target_member, %changes ) = @_;
129              
130             $self->update_member_presence( $target_member );
131              
132             if( $changes{membership} and ( $changes{membership}[1] // "" ) eq "invite" ) {
133             $self->append_line( format_invite( $action_member, $target_member ),
134             time => ( $event->{origin_server_ts} // $event->{ts} ) / 1000,
135             );
136             }
137             elsif( $changes{membership} ) {
138             # On a LEAVE event they no longer have a displayname
139             $target_member->displayname = $changes{displayname}[0] if !defined $changes{membership}[1];
140              
141             $self->append_line( format_membership( $changes{membership}[1] // "leave", $target_member ),
142             time => ( $event->{origin_server_ts} // $event->{ts} ) / 1000,
143             );
144             }
145             elsif( $changes{displayname} ) {
146             $self->append_line( format_displayname_change( $target_member, @{ $changes{displayname} } ) );
147             }
148             elsif( $changes{level} ) {
149             $self->append_line( format_memberlevel_change( $action_member, $target_member, $changes{level}[1] ),
150             time => ( $event->{origin_server_ts} // $event->{ts} ) / 1000,
151             );
152             }
153              
154             $presence_summary->set_text(
155             sprintf "Total: %d users", scalar $room->joined_members
156             );
157             },
158             on_back_membership => sub {
159             my ( undef, $action_member, $event, $target_member, %changes ) = @_;
160              
161             if( $changes{membership} and ( $changes{membership}[0] // "" ) eq "invite" ) {
162             $self->prepend_line( format_invite( $action_member, $target_member ),
163             time => ( $event->{origin_server_ts} // $event->{ts} ) / 1000,
164             );
165             }
166             elsif( $changes{membership} ) {
167             # On a JOIN event they don't yet have a displayname
168             $target_member->displayname = $changes{displayname}[0] if $changes{membership}[0] // '' eq "join";
169              
170             $self->prepend_line( format_membership( $changes{membership}[0] // "leave", $target_member ),
171             time => ( $event->{origin_server_ts} // $event->{ts} ) / 1000,
172             );
173             }
174             elsif( $changes{displayname} ) {
175             $self->prepend_line( format_displayname_change( $target_member, reverse @{ $changes{displayname} } ),
176             time => ( $event->{origin_server_ts} // $event->{ts} ) / 1000,
177             );
178             }
179             elsif( $changes{level} ) {
180             $self->prepend_line( format_memberlevel_change( $action_member, $target_member, $changes{level}[0] ),
181             time => ( $event->{origin_server_ts} // $event->{ts} ) / 1000,
182             );
183             }
184             },
185              
186             on_state_changed => sub {
187             my ( undef, $member, $event, %changes ) = @_;
188              
189             if( $changes{name} ) {
190             $self->append_line( format_name_change( $member, $changes{name}[1] ),
191             time => ( $event->{origin_server_ts} // $event->{ts} ) / 1000,
192             );
193             $self->set_name( $room->name );
194             }
195             if( $changes{aliases} ) {
196             $self->append_line( $_,
197             time => ( $event->{origin_server_ts} // $event->{ts} ) / 1000,
198             ) for format_alias_changes( $event->{user_id}, @{ $changes{aliases} }[0,1] );
199             }
200             if( $changes{topic} ) {
201             $self->append_line( format_topic_change( $member, $changes{topic}[1] ),
202             time => ( $event->{origin_server_ts} // $event->{ts} ) / 1000,
203             );
204             $self->update_headline;
205             }
206             foreach ( map { m/^level\.(.*)/ ? ( $1 ) : () } keys %changes ) {
207             $self->append_line( format_roomlevel_change( $member, $_, $changes{"level.$_"}[1] ),
208             time => ( $event->{origin_server_ts} // $event->{ts} ) / 1000,
209             );
210             }
211             },
212             on_back_state_changed => sub {
213             my ( undef, $member, $event, %changes ) = @_;
214              
215             if( $changes{name} ) {
216             $self->prepend_line( format_name_change( $member, $changes{name}[0] ),
217             time => ( $event->{origin_server_ts} // $event->{ts} ) / 1000,
218             );
219             }
220             if( $changes{aliases} ) {
221             $self->prepend_line( $_,
222             time => ( $event->{origin_server_ts} // $event->{ts} ) / 1000,
223             ) for format_alias_changes( $event->{user_id}, @{ $changes{aliases} }[1,0] );
224             }
225             if( $changes{topic} ) {
226             $self->prepend_line( format_topic_change( $member, $changes{topic}[0] ),
227             time => ( $event->{origin_server_ts} // $event->{ts} ) / 1000,
228             );
229             }
230             foreach ( map { m/^level\.(.*)/ ? ( $1 ) : () } keys %changes ) {
231             $self->prepend_line( format_roomlevel_change( $member, $_, $changes{"level.$_"}[0] ),
232             time => ( $event->{origin_server_ts} // $event->{ts} ) / 1000,
233             );
234             }
235             },
236              
237             on_presence => sub {
238             my ( undef, $member, %changes ) = @_;
239             $self->update_member_presence( $member );
240             },
241              
242             on_members_typing => sub {
243             my ( undef, @members ) = @_;
244              
245             @members or
246             $self->set_typing_line( undef ), return;
247              
248             my $s = String::Tagged->new
249             ->append_tagged( " # currently typing: ", fg => "magenta" );
250              
251             my $last_member = pop @members;
252             $s->append_tagged( format_displayname( $_ ) )
253             ->append( ", " ) for @members;
254             $s->append_tagged( format_displayname( $last_member ) );
255              
256             $self->set_typing_line( $s );
257             },
258             );
259              
260             $room->add_child( $self->{typing_grace_timer} = IO::Async::Timer::Countdown->new(
261             delay => TYPING_GRACE_SECONDS,
262             on_expire => sub { $room->typing_stop },
263             ) );
264             }
265              
266             sub append_line
267             {
268             my $self = shift;
269              
270             if( $self->{typing_line} ) {
271             my @after = $self->{scroller}->pop;
272             $self->SUPER::append_line( @_ );
273             $self->{scroller}->push( @after );
274             }
275             else {
276             $self->SUPER::append_line( @_ );
277             }
278             }
279              
280             sub set_typing_line
281             {
282             my $self = shift;
283             my ( $line ) = @_;
284              
285             $self->{scroller}->pop if delete $self->{typing_line};
286              
287             # No timestamp
288             local $self->{timestamp_format};
289             $self->SUPER::append_line( $self->{typing_line} = $line ) if $line;
290             }
291              
292             sub still_typing
293             {
294             my $self = shift;
295              
296             my $timer = $self->{typing_grace_timer};
297             if( $timer->is_running ) {
298             $timer->reset;
299             }
300             else {
301             $self->{room}->typing_start;
302             $timer->start;
303             }
304             }
305              
306             sub update_headline
307             {
308             my $self = shift;
309             my $room = $self->{room};
310              
311             $self->{headline}->set_text( $room->topic // "" );
312             }
313              
314             sub update_member_presence
315             {
316             my $self = shift;
317             my ( $member ) = @_;
318              
319             return; # TODO
320              
321             my $user = $member->user;
322             my $user_id = $user->user_id;
323              
324             my $presence_userids = $self->{presence_userids};
325              
326             # Find an existing row if we can
327             my $rowidx;
328             $presence_userids->[$_] eq $user_id and $rowidx = $_, last
329             for 0 .. $#$presence_userids;
330              
331             my $presence_table = $self->{presence_table};
332              
333             if( defined $rowidx and !defined $member->membership ) {
334             splice @$presence_userids, $rowidx, 1, ();
335             $presence_table->delete_row( $rowidx+1 );
336             return;
337             }
338              
339             my ( $w_name, $w_since, $w_power );
340             if( defined $rowidx ) {
341             ( $w_name, $w_since, $w_power ) = $presence_table->get_row( $rowidx+1 );
342             }
343             else {
344             $presence_table->append_row( [
345             $w_name = Tickit::Widget::Static->new( text => "" ),
346             $w_since = Tickit::Widget::Static->new( text => "" ),
347             $w_power = Tickit::Widget::Static->new( text => "-", class => "level" ),
348             ] );
349             push @$presence_userids, $user_id;
350             }
351              
352             $w_name->set_style( fg => $PRESENCE_STATE_TO_COLOUR{$user->presence} )
353             if defined $user->presence;
354              
355             my $dname = defined $member->displayname ? $member->displayname : "[".$user->user_id."]";
356             $dname = substr( $dname, 0, 17 ) . "..." if length $dname > 20;
357             $w_name->set_text( $dname );
358              
359             if( defined $user->last_active ) {
360             $w_since->set_text( strftime "%Y/%m/%d %H:%M", localtime $user->last_active );
361             }
362             else {
363             $w_since->set_text( " -- " );
364             }
365              
366             if( defined( my $level = $self->{room}->member_level( $user_id ) ) ) {
367             $w_power->set_text( $level );
368             $w_power->set_style( fg => ( $level > 0 ) ? "yellow" : undef );
369             }
370             else {
371             $w_power->set_text( "-" );
372             }
373             }
374              
375             sub format_message
376             {
377             my $self = shift;
378             my ( $content, $member ) = @_;
379              
380             my $s = String::Tagged->new;
381              
382             my $formatted_body = parse_formatted_message( $content );
383             my $msgtype = $content->{msgtype};
384              
385             # Convert $body into something Tickit::Widget::Scoller will understand
386             my $body = String::Tagged->clone( $formatted_body,
387             only_tags => [qw( bold under italic reverse fg bg )],
388             convert_tags => {
389             bold => "b",
390             under => "u",
391             italic => "i",
392             reverse => "rv",
393             fg => sub { fg => $_[1]->as_xterm->index },
394             bg => sub { bg => $_[1]->as_xterm->index },
395             },
396             );
397              
398             my $content_url;
399             if( $content->{url} ) {
400             my $uri = URI->new( $content->{url} );
401             if( $uri->scheme eq "mxc" ) {
402             $content_url = $self->{url_base} . "/_matrix/media/v1/download/" . $uri->authority . $uri->path;
403             }
404             else {
405             $content_url = "$uri";
406             }
407             }
408              
409             if( $msgtype eq "m.text" ) {
410             return $s
411             ->append_tagged( "<", fg => "magenta" )
412             ->append( format_displayname( $member ) )
413             ->append_tagged( "> ", fg => "magenta" )
414             ->append ( $body );
415             }
416             elsif( $msgtype eq "m.emote" ) {
417             return $s
418             ->append_tagged( "* ", fg => "magenta" )
419             ->append( format_displayname( $member ) )
420             ->append_tagged( " " )
421             ->append ( $body );
422             }
423             elsif( $msgtype eq "m.notice" ) {
424             return $s
425             ->append_tagged( "--", fg => "red" )
426             ->append( format_displayname( $member ) )
427             ->append_tagged( "-- ", fg => "red" )
428             ->append ( $body );
429             }
430             # Handle all the four attachment-style messages similarly
431             elsif( any { $msgtype eq $_ } qw( m.image m.audio m.video m.file ) ) {
432             my $info = $content->{info} // $content->{body}; # cope with older message format
433              
434             $s->append_tagged( "[" )
435             ->append( format_displayname( $member ) )
436             ->append_tagged( "] " )
437             ->append_tagged( $msgtype =~ s/^m\.//r, fg => "yellow" );
438              
439             if( defined $info->{mimetype} ) {
440             $s->append_tagged( "; $info->{mimetype}", fg => "grey" );
441             }
442             if( defined( my $bytes = $info->{size} ) ) {
443             $s->append_tagged( "; " . format_bytes( $bytes ), fg => "grey" );
444             }
445              
446             if( $msgtype eq "m.image" ) {
447             $s->append_tagged( " ($info->{w}x$info->{h})", fg => "grey" );
448             }
449             elsif( $msgtype eq "m.audio" ) {
450             $s->append_tagged( " (" . format_msec( $info->{duration} ) . ")", fg => "grey" );
451             }
452             elsif( $msgtype eq "m.video" ) {
453             $s->append_tagged( " ($info->{w}x$info->{h}, " . format_msec( $info->{duration} ) . ")", fg => "grey" );
454             }
455              
456             $s->append_tagged( " " )
457             ->append_tagged( $content_url, fg => "hi-blue", u => 1 );
458              
459             # filename comes from content, not info
460             if( defined $content->{filename} ) {
461             $s->append_tagged( " - $content->{filename}" );
462             }
463              
464             return $s;
465             }
466             else {
467             return $s
468             ->append_tagged( "[" )
469             ->append_tagged( $msgtype, fg => "yellow" )
470             ->append_tagged( " from " )
471             ->append( format_displayname( $member ) )
472             ->append_tagged( "]: " )
473             ->append ( Data::Dump::pp $body );
474             }
475             }
476              
477             sub format_bytes
478             {
479             my ( $v ) = @_;
480             return sprintf "%d bytes", $v if $v < 1024; $v /= 1024;
481             return sprintf "%.1f KiB", $v if $v < 1024; $v /= 1024;
482             return sprintf "%.1f MiB", $v if $v < 1024; $v /= 1024;
483             return sprintf "%.1f GiB", $v if $v < 1024; $v /= 1024;
484             return sprintf "%.1f TiB", $v;
485             }
486              
487             sub format_msec
488             {
489             my ( $v ) = @_;
490             return sprintf "%.3f sec", $v / 1000 if $v < 1000*10; $v /= 1000;
491             return sprintf "%.1f sec", $v if $v < 60; $v /= 60;
492             return sprintf "%dm%02ds", $v / 60, $v % 60 if $v < 60; $v /= 60;
493             return sprintf "%dh%02dm", $v / 60, $v % 60;
494             }
495              
496             sub format_membership
497             {
498             my ( $membership, $member ) = @_;
499              
500             my $s = String::Tagged->new;
501              
502             if( $membership eq "join" ) {
503             return $s
504             ->append_tagged( " => ", fg => "magenta" )
505             ->append ( format_displayname( $member, 1 ) )
506             ->append ( " " )
507             ->append_tagged( "joined", fg => "green" );
508             }
509             elsif( $membership eq "leave" ) {
510             return $s
511             ->append_tagged( " <= ", fg => "magenta" )
512             ->append ( format_displayname( $member, 1 ) )
513             ->append ( " " )
514             ->append_tagged( "left", fg => "red" );
515             }
516             else {
517             return $s
518             ->append ( " [membership " )
519             ->append_tagged( $membership, fg => "yellow" )
520             ->append ( "] " )
521             ->append ( format_displayname( $member, 1 ) );
522             }
523             }
524              
525             sub format_invite
526             {
527             my ( $inviting_member, $invitee ) = @_;
528              
529             return String::Tagged->new
530             ->append ( " ** " )
531             ->append ( format_displayname( $inviting_member ) )
532             ->append ( " invites " )
533             ->append_tagged( $invitee->user->user_id, fg => "grey" );
534             }
535              
536             sub format_displayname_change
537             {
538             my ( $member, $oldname, $newname ) = @_;
539              
540             my $s = String::Tagged->new
541             ->append_tagged( " ** ", fg => "magenta" );
542              
543             defined $oldname ?
544             $s->append_tagged( $oldname, fg => "cyan" ) :
545             $s->append_tagged( "[".$member->user->user_id."]", fg => "grey" );
546              
547             $s->append_tagged( " is now called " );
548              
549             defined $newname ?
550             $s->append_tagged( $newname, fg => "cyan" ) :
551             $s->append_tagged( "[".$member->user->user_id."]", fg => "grey" );
552              
553             return $s;
554             }
555              
556             sub format_name_change
557             {
558             my ( $member, $name ) = @_;
559              
560             return String::Tagged->new
561             ->append ( " ** " )
562             ->append ( format_displayname( $member ) )
563             ->append ( " sets the room name to: " )
564             ->append_tagged( $name, fg => "cyan" );
565             }
566              
567             sub format_alias_changes
568             {
569             my ( $hs_domain, $old, $new ) = @_;
570              
571             my %deleted = map { $_ => 1 } @$old;
572             delete $deleted{$_} for @$new;
573              
574             my %added = map { $_ => 1 } @$new;
575             delete $added{$_} for @$old;
576              
577             return
578             ( map { String::Tagged->new
579             ->append_tagged( " # ", fg => "yellow" )
580             ->append_tagged( $hs_domain, fg => "red" )
581             ->append ( " adds room alias " )
582             ->append_tagged( $_, fg => "cyan" ) } sort keys %added ),
583             ( map { String::Tagged->new
584             ->append_tagged( " # ", fg => "yellow" )
585             ->append_tagged( $hs_domain, fg => "red" )
586             ->append ( " deletes room alias " )
587             ->append_tagged( $_, fg => "cyan" ) } sort keys %deleted );
588             }
589              
590             sub format_topic_change
591             {
592             my ( $member, $topic ) = @_;
593              
594             return String::Tagged->new
595             ->append ( " ** " )
596             ->append ( format_displayname( $member ) )
597             ->append ( " sets the topic to: " )
598             ->append_tagged( $topic, fg => "cyan" );
599             }
600              
601             sub format_roomlevel_change
602             {
603             my ( $member, $name, $level ) = @_;
604              
605             return String::Tagged->new
606             ->append ( " ** " )
607             ->append ( format_displayname( $member ) )
608             ->append ( " changes required level for " )
609             ->append_tagged( $name, fg => "green" )
610             ->append ( " to " )
611             ->append_tagged( $level, $level > 0 ? ( fg => "yellow" ) : () );
612             }
613              
614             sub format_memberlevel_change
615             {
616             my ( $changing_member, $target_member, $level ) = @_;
617              
618             return String::Tagged->new
619             ->append ( " ** " )
620             ->append ( format_displayname( $changing_member ) )
621             ->append ( " changes power level of " )
622             ->append ( format_displayname( $target_member ) )
623             ->append ( " to " )
624             ->append_tagged( $level, $level > 0 ? ( fg => "yellow" ) : () );
625             }
626              
627             sub format_displayname
628             {
629             my ( $member, $full ) = @_;
630              
631             if( defined $member->displayname ) {
632             my $s = String::Tagged->new
633             ->append_tagged( $member->displayname, fg => "cyan" );
634              
635             $s->append_tagged( " [".$member->user->user_id."]", fg => "grey" ) if $full;
636              
637             return $s;
638             }
639             else {
640             return String::Tagged->new
641             ->append_tagged ( $member->user->user_id, fg => "grey" );
642             }
643             }
644              
645             sub cmd_me
646             {
647             my $self = shift;
648             my ( @args ) = @_;
649              
650             my $text = join " ", @args;
651             my $room = $self->{room};
652              
653             $room->send_message( type => "m.emote", body => $text );
654             }
655              
656             sub cmd_notice
657             {
658             my $self = shift;
659             my ( @args ) = @_;
660              
661             my $text = join " ", @args;
662             my $room = $self->{room};
663              
664             $room->send_message( type => "m.notice", body => $text );
665             }
666              
667             sub cmd_image
668             {
669             my $self = shift;
670             my ( $file ) = @_;
671              
672             my $dist = $self->{dist};
673             my $room = $self->{room};
674              
675             unless( -e $file ) {
676             $self->append_line( "File $file not found!" );
677             return Future->done;
678             }
679              
680             my $exifTool = Image::ExifTool->new;
681             $exifTool->ImageInfo( $file );
682              
683             my $content_type = $exifTool->GetValue( 'MIMEType' ) //
684             "application/octet-stream";
685              
686             $dist->fire_async( do_upload => file => $file, content_type => $content_type )->then( sub {
687             my ( $uri ) = @_;
688              
689             $room->send_message( type => "m.image", body => "image attachment",
690             url => $uri,
691             filename => $file,
692             info => {
693             size => $exifTool->GetValue( "FileSize", "ValueConv" ),
694             w => $exifTool->GetValue( "ImageWidth" ),
695             h => $exifTool->GetValue( "ImageHeight" ),
696             mimetype => $content_type,
697             },
698             );
699             });
700             }
701              
702             sub cmd_audio
703             {
704             my $self = shift;
705             my ( $file ) = @_;
706              
707             my $dist = $self->{dist};
708             my $room = $self->{room};
709              
710             unless( -e $file ) {
711             $self->append_line( "File $file not found!" );
712             return Future->done;
713             }
714              
715             my $exifTool = Image::ExifTool->new;
716             $exifTool->ImageInfo( $file );
717              
718             my $content_type = $exifTool->GetValue( "MIMEType" ) //
719             "application/octet-stream";
720              
721             $dist->fire_async( do_upload => file => $file, content_type => $content_type )->then( sub {
722             my ( $uri ) = @_;
723              
724             $room->send_message( type => "m.audio", body => "audio attachment",
725             url => $uri,
726             filename => $file,
727             info => {
728             size => $exifTool->GetValue( "FileSize", "ValueConv" ),
729             duration => int( $exifTool->GetValue( "Duration", "ValueConv" ) * 1000 ), # msec
730             mimetype => $content_type,
731             },
732             );
733             });
734             }
735              
736             sub cmd_video
737             {
738             my $self = shift;
739             my ( $file ) = @_;
740              
741             my $dist = $self->{dist};
742             my $room = $self->{room};
743              
744             unless( -e $file ) {
745             $self->append_line( "File $file not found!" );
746             return Future->done;
747             }
748              
749             my $exifTool = Image::ExifTool->new;
750             $exifTool->ImageInfo($file);
751              
752             my $content_type = $exifTool->GetValue( "MIMEType" ) //
753             "application/octet-stream";
754              
755             $dist->fire_async( do_upload => file => $file, content_type => $content_type )->then( sub {
756             my ( $uri ) = @_;
757              
758             $room->send_message( type => "m.video", body => "video attachment",
759             url => $uri,
760             filename => $file,
761             info => {
762             size => $exifTool->GetValue( "FileSize", "ValueConv" ),
763             duration => int( $exifTool->GetValue( "Duration", "ValueConv" ) * 1000 ), # msec
764             w => $exifTool->GetValue( "ImageWidth" ),
765             h => $exifTool->GetValue( "ImageHeight" ),
766             mimetype => $content_type,
767             },
768             );
769             });
770             }
771              
772             sub cmd_file
773             {
774             my $self = shift;
775             my ( $file ) = @_;
776              
777             my $dist = $self->{dist};
778             my $room = $self->{room};
779              
780             unless( -e $file ) {
781             $self->append_line( "File $file not found!" );
782             return Future->done;
783             }
784              
785             my $exifTool = Image::ExifTool->new;
786             $exifTool->ImageInfo($file);
787              
788             my $content_type = $exifTool->GetValue( "MIMEType" ) //
789             "application/octet-stream";
790              
791             $dist->fire_async( do_upload => file => $file, content_type => $content_type )->then( sub {
792             my ( $uri ) = @_;
793              
794             $room->send_message( type => "m.file", body => "file attachment",
795             url => $uri,
796             filename => $file,
797             info => {
798             size => $exifTool->GetValue( "FileSize", "ValueConv" ),
799             mimetype => $content_type,
800             },
801             );
802             Future->done;
803             });
804             }
805              
806             sub cmd_leave
807             {
808             my $self = shift;
809              
810             my $room = $self->{room};
811             $room->leave;
812             }
813              
814             sub cmd_invite
815             {
816             my $self = shift;
817             my ( $user_id ) = @_;
818              
819             my $room = $self->{room};
820              
821             $room->invite( $user_id );
822             }
823              
824             sub cmd_level
825             {
826             my $self = shift;
827             my $delete = $_[0] eq "-del" ? shift : 0;
828             my ( $user_id, $level ) = @_;
829              
830             defined $level or $delete or
831             Future->fail( "Require a power level, or -del" );
832              
833             my $room = $self->{room};
834              
835             $room->change_member_levels( $user_id => $level );
836             }
837              
838             sub cmd_roomlevels
839             {
840             my $self = shift;
841              
842             my %levels;
843             foreach (@_) {
844             m/^(.*)=(\d+)$/ and $levels{$1} = $2;
845             }
846              
847             my $room = $self->{room};
848             $room->change_levels( %levels );
849             }
850              
851             sub cmd_topic
852             {
853             my $self = shift;
854             my $topic = join " ", @_; # TODO
855              
856             my $room = $self->{room};
857              
858             if( length $topic ) {
859             $room->set_topic( $topic )
860             }
861             else {
862             my $room_topic = $room->topic;
863             my $room_name = $room->name;
864             $self->append_line( "Topic for $room_name is: $room_topic" );
865             Future->done;
866             }
867             }
868              
869             sub cmd_roomname
870             {
871             my $self = shift;
872             my $name = join " ", @_; # TODO
873              
874             my $room = $self->{room};
875              
876             if( length $name ) {
877             $room->set_name( $name )
878             }
879             else {
880             my $room_name = $room->name;
881             $self->append_line( "Room name is: $room_name" );
882             Future->done;
883             }
884             }
885              
886             sub cmd_roomid
887             {
888             my $self = shift;
889              
890             my $roomid = $self->{room}->room_id;
891             $self->append_line( "Room ID is: $roomid" );
892              
893             Future->done;
894             }
895              
896             sub cmd_add_alias
897             {
898             my $self = shift;
899             my ( $alias ) = @_;
900              
901             my $room_id = $self->{room}->room_id;
902              
903             $self->{dist}->fire_async( do_add_alias => $alias, $room_id );
904             }
905              
906             sub cmd_list_aliases
907             {
908             my $self = shift;
909              
910             map { $self->append_line( "Room alias: $_" ) } $self->{room}->aliases;
911             Future->done;
912             }
913              
914             sub cmd_delete_alias
915             {
916             my $self = shift;
917             my ( $alias ) = @_;
918              
919             grep { $_ eq $alias } $self->{room}->aliases or
920             return Future->fail( "$alias is not an alias of this room" );
921              
922             $self->{dist}->fire_async( do_del_alias => $alias );
923             }
924              
925             0x55AA;