File Coverage

blib/lib/App/MatrixTool.pm
Criterion Covered Total %
statement 51 204 25.0
branch 0 46 0.0
condition 0 23 0.0
subroutine 17 46 36.9
pod 0 21 0.0
total 68 340 20.0


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