File Coverage

blib/lib/App/MatrixClient.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


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, 2015 -- leonerd@leonerd.org.uk
5              
6             package App::MatrixClient;
7              
8 1     1   696 use strict;
  1         2  
  1         25  
9 1     1   5 use warnings;
  1         2  
  1         39  
10              
11             our $VERSION = '0.01';
12              
13 1     1   797 use curry;
  1         234  
  1         31  
14              
15 1     1   701 use Event::Distributor 0.04; # actions
  1         16438  
  1         28  
16 1     1   1090 use IO::Async::Loop;
  1         63825  
  1         42  
17              
18 1     1   818 use Tickit::Async;
  0            
  0            
19             use Tickit::Console 0.07; # time/datestamp format
20             use Tickit::Widgets qw( FloatBox Static VBox );
21             use String::Tagged 0.11; # ->clone
22              
23             # Presence list scrolling requires Tickit 0.48 to actually work properly
24             use Tickit 0.48;
25              
26             use Data::Dump 'pp';
27              
28             use App::MatrixClient::RoomTab;
29             use App::MatrixClient::Matrix;
30              
31             =head1 NAME
32              
33             C - a C-based terminal UI client for F
34              
35             =head1 EMITTED SIGNALS
36              
37             * start()
38              
39             do_upload( file => $file, content_type => $type )
40              
41             do_get_displayname( ?$user_id ) => $displayname
42             do_set_displayname( $displayname )
43              
44             do_set_presence( $state, ?$msg )
45              
46             do_add_alias( $alias, $room_id )
47             do_del_alias( $alias )
48              
49             do_room_create( $name ) => $event
50             do_room_join( $name )
51             do_room_leave( $room_id )
52             do_room_msg( $room_id, $message )
53              
54             =head1 CONSUMED SIGNALS
55              
56             * log( $str )
57             * logerr( $str )
58              
59             on_invite( $event )
60              
61             on_user_displayname( $user, $oldname )
62             on_user_presence( $user )
63              
64             on_room_new( $room )
65             on_room_del( $room )
66              
67             =head1 CONSTRUCTOR
68              
69             =cut
70              
71             =head2 new
72              
73             $client = App::MatrixClient->new( %args )
74              
75             Takes the following named arguments:
76              
77             =over 8
78              
79             =item loop => IO::Async::Loop
80              
81             =back
82              
83             =cut
84              
85             sub new
86             {
87             my $class = shift;
88             my %args = @_;
89              
90             my $loop = $args{loop} // IO::Async::Loop->new;
91             my $dist = $args{dist} // Event::Distributor->new;
92              
93             $dist->declare_signal( $_ ) for
94             qw( start log logerr );
95              
96             my $console = Tickit::Console->new(
97             timestamp_format => String::Tagged->new_tagged( "%H:%M ", fg => undef )
98             ->apply_tag( 0, 5, fg => "hi-blue" ),
99             datestamp_format => String::Tagged->new_tagged( "-- day is now %Y/%m/%d --",
100             fg => "grey" ),
101             );
102              
103             my $self = bless {
104             loop => $loop,
105             dist => $dist,
106             console => $console,
107              
108             ( map { $_ => $args{$_} } qw( server ssl ) ),
109             }, $class;
110              
111             $dist->subscribe_sync( log => sub {
112             shift;
113             $self->log( @_ );
114             });
115              
116             $dist->subscribe_sync( logerr => sub {
117             shift;
118             $self->append_line_colour( red => join " ", @_ );
119             });
120              
121             $dist->subscribe_sync( $_ => $self->${\"curry::$_"} ) for
122             qw( on_invite on_user_displayname on_user_presence on_room_new on_room_del );
123              
124             my $globaltab = $self->{globaltab} = $console->add_tab(
125             name => "Global",
126             on_line => sub {
127             my ( $tab, $line ) = @_;
128             $self->do_command( $line, $tab );
129             },
130             );
131              
132             $self->{tickit} = Tickit::Async->new( root => $console );
133             $loop->add( $self->{tickit} );
134              
135             my %tabs_by_roomid;
136              
137             push @{ $self->{components} }, App::MatrixClient::Matrix->new(
138             loop => $loop,
139             dist => $dist,
140              
141             ( map { $_ => $args{$_} } qw( server ssl user_id password ) ),
142             );
143              
144             {
145             # Much hackery in here...
146             my $entry = $console->{entry};
147              
148             my $old_on_key = Tickit::Widget::Entry->can( 'on_key' );
149             no warnings 'redefine';
150             *Tickit::Widget::Entry::on_key = sub {
151             my $ret = $old_on_key->( @_ );
152             if( $ret and $_[0] == $entry ) {
153             my $tab = $console->active_tab;
154             $tab->still_typing if $tab->can( 'still_typing' );
155             }
156             return $ret;
157             };
158             }
159              
160             return $self;
161             }
162              
163             sub run
164             {
165             my $self = shift;
166              
167             local $SIG{__WARN__} = sub {
168             my $msg = join " ", @_;
169             $self->append_line_colour( orange => join " ", @_ );
170             };
171              
172             $self->{dist}->fire_sync( start => );
173              
174             $self->{tickit}->run;
175             }
176              
177              
178             # Signal handlers
179              
180             sub on_invite
181             {
182             my $self = shift;
183             my ( undef, $event ) = @_;
184              
185             $self->{globaltab}->append_line( String::Tagged->new
186             ->append_tagged( " ** " )
187             ->append_tagged( $event->{inviter}, fg => "grey" )
188             ->append_tagged( " invites you to " )
189             ->append_tagged( $event->{room_id}, fg => "cyan" )
190             );
191              
192             # TODO: consider whether we should look up user displayname, room name,
193             # etc...
194             }
195              
196             sub on_user_displayname
197             {
198             my $self = shift;
199             my ( undef, $user, $oldname ) = @_;
200              
201             $self->append_line_colour( yellow => " * $oldname is now called " . make_username($user) );
202             }
203              
204             sub on_user_presence
205             {
206             my $self = shift;
207             my ( undef, $user ) = @_;
208              
209             $self->append_line_colour( yellow => " * " . make_username($user) . " now " . $user->presence );
210             }
211              
212             sub on_room_new
213             {
214             my $self = shift;
215             my ( undef, $room ) = @_;
216              
217             $self->new_room( $room );
218             }
219              
220             sub on_room_del
221             {
222             my $self = shift;
223             my ( undef, $room ) = @_;
224              
225             my $roomtab = delete $self->{tabs_by_roomid}{ $room->room_id } or return;
226              
227             $self->{console}->remove_tab( $roomtab );
228             }
229              
230              
231             # Internal API
232              
233             sub append_line_colour
234             {
235             my $self = shift;
236             my ( $fg, $text ) = @_;
237              
238             $self->{globaltab}->append_line(
239             String::Tagged->new( $text )->apply_tag( 0, -1, fg => $fg )
240             );
241             }
242              
243             sub log
244             {
245             my $self = shift;
246             my ( $line ) = @_;
247              
248             $self->append_line_colour( green => ">> $line" );
249             }
250              
251             sub new_room
252             {
253             my $self = shift;
254             my ( $room ) = @_;
255              
256             my $floatbox;
257             my $headline;
258              
259             # Until Tickit::Widget::Tabbed supports a 'tab_class' argument to add_tab,
260             # we'll have to cheat
261             no warnings 'redefine';
262             local *Tickit::Widget::Tabbed::TAB_CLASS = sub { "App::MatrixClient::RoomTab" };
263              
264             my $roomtab = $self->{console}->add_tab(
265             name => $room->room_id,
266             make_widget => sub {
267             my ( $scroller ) = @_;
268              
269             my $vbox = Tickit::Widget::VBox->new;
270              
271             $vbox->add( $headline = Tickit::Widget::Static->new(
272             text => "",
273             style => { bg => "blue" },
274             ),
275             expand => 0
276             );
277             $vbox->add( $scroller, expand => 1 );
278              
279             return $floatbox = Tickit::Widget::FloatBox->new(
280             base_child => $vbox,
281             );
282             },
283             on_line => sub {
284             my ( $tab, $line ) = @_;
285             if( $line =~ s{^/}{} ) {
286             my ( $cmd, @args ) = split m/\s+/, $line;
287             if( my $code = $tab->can( "cmd_$cmd" ) ) {
288             $room->adopt_future( $tab->$code( @args ) );
289             }
290             else {
291             $self->do_command( $line, $tab );
292             }
293             }
294             else {
295             $room->adopt_future( $room->send_message( $line ) );
296             $room->typing_stop;
297             }
298             },
299             );
300              
301             $self->{tabs_by_roomid}->{ $room->room_id } = $roomtab;
302              
303             $roomtab->_setup(
304             room => $room,
305             dist => $self->{dist},
306             url_base => ( $self->{ssl} ? "https" : "http" ) . "://$self->{server}",
307             floatbox => $floatbox,
308             headline => $headline,
309             );
310             }
311              
312             sub make_username
313             {
314             # function
315             my ( $user ) = @_;
316              
317             if( defined $user->displayname ) {
318             return "${\$user->displayname} (${\$user->user_id})";
319             }
320             else {
321             return $user->user_id;
322             }
323             }
324              
325             sub do_command
326             {
327             my $self = shift;
328             my ( $line, $tab ) = @_;
329              
330             # For now all commands are simple methods on __PACKAGE__
331             my ( $cmd, @args ) = split m/\s+/, $line;
332              
333             $tab->append_line(
334             String::Tagged->new( '$ ' . join " ", $cmd, @args )
335             ->apply_tag( 0, -1, fg => "cyan" )
336             );
337              
338             my $method = "cmd_$cmd";
339             $self->{cmd_f} = Future->call( sub { $self->$method( @args ) } )
340             ->on_done( sub {
341             my @result = @_;
342             $tab->append_line( $_ ) for @result;
343              
344             undef $self->{cmd_f};
345             })
346             ->on_fail( sub {
347             my ( $failure ) = @_;
348              
349             $tab->append_line(
350             String::Tagged->new( "Error: $failure" )
351             ->apply_tag( 0, -1, fg => "red" )
352             );
353              
354             undef $self->{cmd_f};
355             });
356             }
357              
358              
359             ## Command handlers
360              
361             sub cmd_dname_get
362             {
363             my $self = shift;
364             my ( $user_id ) = @_;
365              
366             $self->{dist}->fire_async( do_get_displayname => $user_id );
367             }
368              
369             sub cmd_dname_set
370             {
371             my $self = shift;
372             my ( $name ) = @_;
373              
374             $self->{dist}->fire_async( do_set_displayname => $name )
375             ->then_done( "Set" );
376             }
377              
378             sub cmd_offline
379             {
380             my $self = shift;
381              
382             $self->{dist}->fire_async( do_set_presence => "offline", @_ )
383             ->then_done( "Set" );
384             }
385              
386             sub cmd_busy
387             {
388             my $self = shift;
389              
390             $self->{dist}->fire_async( do_set_presence => "unavailable", "Busy" )
391             ->then_done( "Set" );
392             }
393              
394             sub cmd_away
395             {
396             my $self = shift;
397              
398             $self->{dist}->fire_async( do_set_presence => "unavailable", "Away" )
399             ->then_done( "Set" );
400             }
401              
402             sub cmd_online
403             {
404             my $self = shift;
405              
406             $self->{dist}->fire_async( do_set_presence => "online", @_ )
407             ->then_done( "Set" );
408             }
409              
410             sub cmd_createroom
411             {
412             my $self = shift;
413             my ( $room_name ) = @_;
414              
415             $self->{dist}->fire_async( do_room_create => $room_name )->then( sub {
416             my ( $response ) = @_;
417             Future->done( pp($response) );
418             });
419             }
420              
421             sub cmd_join
422             {
423             my $self = shift;
424             my ( $room_name ) = @_;
425              
426             $self->{dist}->fire_async( do_room_join => $room_name )
427             ->then_done( "Joined" );
428             }
429              
430             sub cmd_leave
431             {
432             my $self = shift;
433             my ( $roomid ) = @_;
434              
435             $self->{dist}->fire_async( do_room_leave => $roomid )
436             ->then_done( "Left" );
437             }
438              
439             sub cmd_msg
440             {
441             my $self = shift;
442             my ( $roomid, @msg ) = @_;
443              
444             my $msg = join " ", @msg;
445              
446             $self->{dist}->fire_async( do_room_msg => $roomid, $msg )
447             ->then_done(); # suppress output
448              
449             }
450              
451             =head1 AUTHOR
452              
453             Paul Evans
454              
455             =cut
456              
457             0x55AA;