File Coverage

blib/lib/App/MatrixTool.pm
Criterion Covered Total %
statement 51 206 24.7
branch 0 46 0.0
condition 0 23 0.0
subroutine 17 47 36.1
pod 0 22 0.0
total 68 344 19.7


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::MatrixTool;
7              
8 1     1   521 use strict;
  1         1  
  1         27  
9 1     1   4 use warnings;
  1         1  
  1         69  
10              
11             our $VERSION = '0.08';
12              
13 1     1   724 use Getopt::Long qw( GetOptionsFromArray );
  1         8894  
  1         4  
14 1     1   846 use Future;
  1         8500  
  1         35  
15 1     1   6 use List::Util 1.29 qw( pairmap );
  1         24  
  1         142  
16 1     1   470 use MIME::Base64 qw( encode_base64 );
  1         637  
  1         58  
17 1     1   548 use Module::Pluggable::Object;
  1         8401  
  1         33  
18 1     1   6 use Module::Runtime qw( use_package_optimistically );
  1         1  
  1         5  
19 1     1   38 use Scalar::Util qw( blessed );
  1         7  
  1         49  
20 1     1   586 use Socket qw( getnameinfo AF_INET AF_INET6 AF_UNSPEC NI_NUMERICHOST NI_NUMERICSERV );
  1         3122  
  1         178  
21 1     1   461 use Struct::Dumb qw( readonly_struct );
  1         1210  
  1         4  
22              
23 1     1   575 use Protocol::Matrix::HTTP::Federation;
  1         17981  
  1         1333  
24              
25             require JSON;
26             my $JSON_pretty = JSON->new->utf8(1)->pretty(1);
27              
28             my $opt_parser = Getopt::Long::Parser->new(
29             config => [qw( require_order no_ignore_case )],
30             );
31              
32             =head1 NAME
33              
34             C - commands to interact with a Matrix home-server
35              
36             =head1 SYNOPSIS
37              
38             Usually this would be used via the F command
39              
40             $ matrixtool server-key matrix.org
41              
42             =head1 DESCRIPTION
43              
44             Provides the base class and basic level support for commands that interact
45             with a Matrix home-server. See individual command modules, found under the
46             C namespace, for details on specific commands.
47              
48             =cut
49              
50             readonly_struct ArgSpec => [qw( name print_name optional eatall )];
51             sub ARGSPECS
52             {
53             map {
54 0     0 0   my $name = $_;
  0            
55 0           my $optional = $name =~ s/\?$//;
56 0           my $eatall = $name =~ m/\.\.\.$/;
57              
58 0           ( my $print_name = uc $name ) =~ s/_/-/g;
59              
60 0   0       ArgSpec( $name, $print_name, $optional||$eatall, $eatall )
61             } shift->ARGUMENTS;
62             }
63              
64             readonly_struct OptSpec => [qw( name print_name shortname getopt description )];
65             sub OPTSPECS
66             {
67             pairmap {
68 0     0     my ( $name, $desc ) = ( $a, $b ); # allocate new SVtPVs to placate odd COW-related bug in 5.18
69              
70 0           my $getopt = $name;
71              
72 0           $name =~ s/=.*//;
73              
74 0           my $shortname;
75 0 0         $name =~ s/^(.)\|// and $shortname = $1;
76              
77 0           my $printname = $name;
78 0           $name =~ s/-/_/g;
79              
80 0           OptSpec( $name, $printname, $shortname, $getopt, $desc )
81 0     0 0   } shift->OPTIONS;
82             }
83              
84             sub new
85             {
86 0     0 0   my $class = shift;
87 0           return bless { @_ }, $class;
88             }
89              
90             sub sock_family
91             {
92 0     0 0   my $self = shift;
93 0 0         return AF_INET if $self->{inet4};
94 0 0         return AF_INET6 if $self->{inet6};
95 0           return AF_UNSPEC;
96             }
97              
98             sub _pkg_for_command
99             {
100 0     0     my $self = shift;
101 0           my ( $cmd ) = @_;
102              
103 0   0       my $class = ref $self || $self;
104              
105 0 0         my $base = $class eq __PACKAGE__ ? "App::MatrixTool::Command" : $class;
106              
107             # Allow hyphens in command names
108 0           $cmd =~ s/-/_/g;
109              
110 0           my $pkg = "${base}::${cmd}";
111 0           use_package_optimistically( $pkg );
112             }
113              
114             sub run
115             {
116 0     0 0   my $self = shift;
117 0           my @args = @_;
118              
119 0           my %global_opts;
120             $opt_parser->getoptionsfromarray( \@args,
121             'inet4|4' => \$global_opts{inet4},
122             'inet6|6' => \$global_opts{inet6},
123             'print-request' => \$global_opts{print_request},
124             'print-response' => \$global_opts{print_response},
125 0 0         ) or return 1;
126              
127 0 0         my $cmd = @args ? shift @args : "help";
128              
129 0           my $pkg = $self->_pkg_for_command( $cmd );
130 0 0         $pkg->can( "new" ) or
131             return $self->error( "No such command '$cmd'" );
132              
133 0           my $runner = $pkg->new( %global_opts );
134              
135 0           $self->run_command_in_runner( $runner, @args );
136             }
137              
138             sub run_command_in_runner
139             {
140 0     0 0   my $self = shift;
141 0           my ( $runner, @args ) = @_;
142              
143 0           my @argvalues;
144              
145 0 0         if( $runner->can( "OPTIONS" ) ) {
146 0           my %optvalues;
147              
148             $opt_parser->getoptionsfromarray( \@args,
149 0 0         map { $_->getopt => \$optvalues{ $_->name } } $runner->OPTSPECS
  0            
150             ) or exit 1;
151              
152 0           push @argvalues, \%optvalues;
153             }
154              
155 0           my @argspecs = $runner->ARGSPECS;
156 0           while( @argspecs ) {
157 0           my $spec = shift @argspecs;
158              
159 0 0         if( !@args ) {
160 0 0         last if $spec->optional;
161              
162 0           return $self->error( "Required argument '${\ $spec->print_name }' missing" );
  0            
163             }
164              
165 0 0         if( $spec->eatall ) {
166 0           push @argvalues, @args;
167 0           @args = ();
168             }
169             else {
170 0           push @argvalues, shift @args;
171             }
172             }
173 0 0         @args and return $self->error( "Found extra arguments" );
174              
175 0           my $ret = $runner->run( @argvalues );
176 0 0 0       $ret = $ret->get if blessed $ret and $ret->isa( "Future" );
177 0   0       $ret //= 0;
178              
179 0           return $ret;
180             }
181              
182             sub output
183             {
184 0     0 0   my $self = shift;
185 0           print @_, "\n";
186              
187 0           return 0;
188             }
189              
190             # Some nicer-formatted outputs for terminals
191             sub output_ok
192             {
193 0     0 0   my $self = shift;
194 0           $self->output( "\e[32m", "[OK]", "\e[m", " ", @_ );
195             }
196              
197             sub output_info
198             {
199 0     0 0   my $self = shift;
200 0           $self->output( "\e[36m", "[INFO]", "\e[m", " ", @_ );
201             }
202              
203             sub output_warn
204             {
205 0     0 0   my $self = shift;
206 0           $self->output( "\e[33m", "[WARN]", "\e[m", " ", @_ );
207             }
208              
209             sub output_fail
210             {
211 0     0 0   my $self = shift;
212 0           $self->output( "\e[31m", "[FAIL]", "\e[m", " ", @_ );
213             }
214              
215             sub format_binary
216             {
217 0     0 0   my $self = shift;
218 0           my ( $bin ) = @_;
219              
220             # TODO: A global option to pick the format here
221 0           return "base64::" . do { local $_ = encode_base64( $bin, "" ); s/=+$//; $_ };
  0            
  0            
  0            
222             }
223              
224             sub format_hostport
225             {
226 0     0 0   my $self = shift;
227 0           my ( $host, $port ) = @_;
228              
229 0 0         return "[$host]:$port" if $host =~ m/:/; # IPv6
230 0           return "$host:$port";
231             }
232              
233             sub format_addr
234             {
235 0     0 0   my $self = shift;
236 0           my ( $addr ) = @_;
237 0           my ( $err, $host, $port ) = getnameinfo( $addr, NI_NUMERICHOST|NI_NUMERICSERV );
238 0 0         $err and die $err;
239              
240 0           return $self->format_hostport( $host, $port );
241             }
242              
243             sub error
244             {
245 0     0 0   my $self = shift;
246 0           print STDERR @_, "\n";
247              
248 0           return 1;
249             }
250              
251             ## Command support
252              
253             sub federation
254             {
255 0     0 0   my $self = shift;
256              
257 0   0       return $self->{federation} ||= Protocol::Matrix::HTTP::Federation->new;
258             }
259              
260             sub http_client
261             {
262 0     0 0   my $self = shift;
263              
264 0   0       return $self->{http_client} ||= do {
265 0           require App::MatrixTool::HTTPClient;
266             App::MatrixTool::HTTPClient->new(
267             family => $self->sock_family,
268 0           map { $_ => $self->{$_} } qw( print_request print_response ),
  0            
269             );
270             };
271             }
272              
273 0     0 0   sub server_key_store_path { "$ENV{HOME}/.matrix/server-keys" }
274              
275             sub server_key_store
276             {
277 0     0 0   my $self = shift;
278              
279 0   0       return $self->{server_key_store} ||= do {
280 0           require App::MatrixTool::ServerIdStore;
281 0           App::MatrixTool::ServerIdStore->new(
282             path => $self->server_key_store_path
283             );
284             };
285             }
286              
287 0     0 0   sub client_token_store_path { "$ENV{HOME}/.matrix/client-tokens" }
288              
289             sub client_token_store
290             {
291 0     0 0   my $self = shift;
292              
293 0   0       return $self->{client_token_store} ||= do {
294 0           require App::MatrixTool::ServerIdStore;
295 0           App::MatrixTool::ServerIdStore->new(
296             path => $self->client_token_store_path,
297             encode => "raw", # client tokens are already base64 encoded
298             );
299             };
300             }
301              
302 0     0 0   sub JSON_pretty { $JSON_pretty }
303              
304             ## Builtin commands
305              
306             package
307             App::MatrixTool::Command::help;
308 1     1   6 use base qw( App::MatrixTool );
  1         2  
  1         73  
309              
310 1     1   5 use List::Util qw( max );
  1         2  
  1         58  
311              
312 1     1   5 use constant DESCRIPTION => "Display help information about commands";
  1         1  
  1         121  
313 1     1   5 use constant ARGUMENTS => ( "command...?" );
  1         1  
  1         49  
314              
315 1     1   4 use Struct::Dumb qw( readonly_struct );
  1         1  
  1         9  
316             readonly_struct CommandSpec => [qw( name description argspecs optspecs package )];
317              
318             sub commands
319             {
320 0     0     my $mp = Module::Pluggable::Object->new(
321             require => 1,
322             search_path => [ "App::MatrixTool::Command" ],
323             );
324              
325 0           my @commands;
326              
327 0           foreach my $module ( sort $mp->plugins ) {
328 0 0         $module->can( "DESCRIPTION" ) or next;
329              
330 0           my $cmd = $module;
331 0           $cmd =~ s/^App::MatrixTool::Command:://;
332 0           $cmd =~ s/_/-/g;
333 0           $cmd =~ s/::/ /g;
334              
335 0 0         push @commands, CommandSpec(
336             $cmd,
337             $module->DESCRIPTION,
338             [ $module->ARGSPECS ],
339             $module->can( "OPTIONS" ) ? [ $module->OPTSPECS ] : undef,
340             $module,
341             );
342             }
343              
344 0           return @commands;
345             }
346              
347             my $GLOBAL_OPTS = <<'EOF';
348             Global options:
349             -4 --inet4 Use only IPv4
350             -6 --inet6 Use only IPv6
351             --print-request Print sent HTTP requests in full
352             --print-response Print received HTTP responses in full
353             EOF
354              
355             sub help_summary
356             {
357 0     0     my $self = shift;
358              
359 0           $self->output( <<'EOF' . $GLOBAL_OPTS );
360             matrixtool [] []
361              
362             EOF
363              
364 0           my @commands = $self->commands;
365              
366 0           my $namelen = max map { length $_->name } @commands;
  0            
367              
368             $self->output( "Commands:\n" .
369 0           join "\n", map { sprintf " %-*s %s", $namelen, $_->name, $_->description } @commands
  0            
370             );
371             }
372              
373             sub _argdesc
374             {
375 0     0     shift;
376 0           my ( $argspec ) = @_;
377 0           my $name = $argspec->print_name;
378 0 0         return $argspec->optional ? "[$name]" : $name;
379             }
380              
381             sub _optdesc
382             {
383 0     0     my ( $optspec, $namelen ) = @_;
384              
385 0           my $shortname = $optspec->shortname;
386              
387 0 0         join "",
388             ( defined $shortname ? "-$shortname " : " " ),
389             sprintf( "--%-*s", $namelen, $optspec->print_name ),
390             " ",
391             $optspec->description,
392             }
393              
394             sub help_detailed
395             {
396 0     0     my $self = shift;
397 0           my ( $cmd ) = @_;
398              
399 0           my $pkg = App::MatrixTool->_pkg_for_command( $cmd );
400 0 0         $pkg->can( "new" ) or
401             return $self->error( "No such command '$cmd'" );
402              
403 0           my @argspecs = $pkg->ARGSPECS;
404              
405 0           $self->output( join " ", "matrixtool",
406             "[]",
407             $cmd,
408             ( map $self->_argdesc($_), @argspecs ),
409             "[]"
410             );
411 0           $self->output( $pkg->DESCRIPTION );
412 0           $self->output();
413              
414 0           $self->output( $GLOBAL_OPTS );
415              
416 0 0         if( $pkg->can( "OPTIONS" ) ) {
417 0           my @optspecs = $pkg->OPTSPECS;
418              
419 0           my $namelen = max map { length $_->name } @optspecs;
  0            
420              
421 0           $self->output( "Options:" );
422 0           foreach my $optspec ( sort { $a->name cmp $b->name } @optspecs ) {
  0            
423 0           $self->output( " " . _optdesc( $optspec, $namelen ) );
424             }
425 0           $self->output();
426             }
427             }
428              
429             sub run
430             {
431 0     0     my $self = shift;
432 0           my ( @cmd ) = @_;
433              
434 0 0         if( @cmd ) {
435 0           $self->help_detailed( join "::", @cmd );
436             }
437             else {
438 0           $self->help_summary;
439             }
440             }
441              
442             =head1 AUTHOR
443              
444             Paul Evans
445              
446             =cut
447              
448             0x55AA;