logo

ap-client

CLI-based client / toolbox for ActivityPub Client-to-Servergit clone https://hacktivis.me/git/ap-client.git
commit: 1f6251fe2a81cea9dc75e903ee4f34edd5b31f9e
parent 04b5573d848bc599a03f055e30547da4dee20dc3
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date:   Mon, 27 Mar 2023 23:54:41 +0200

Modularize

Diffstat:

MMakefile.PL28+++++++++++++++++++---------
Dap-backup.pl119-------------------------------------------------------------------------------
Dap-fetch.pl59-----------------------------------------------------------
Dap-represent.pl67-------------------------------------------------------------------
Alib/ActivityPub/PrettyPrint.pm72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ascript/ap-backup127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ascript/ap-fetch97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ascript/ap-represent36++++++++++++++++++++++++++++++++++++
8 files changed, 351 insertions(+), 254 deletions(-)

diff --git a/Makefile.PL b/Makefile.PL @@ -1,16 +1,26 @@ +#!/usr/bin/env perl +# AP-Client: CLI-based client / toolbox for ActivityPub +# Copyright © 2020-2023 AP-Client Authors <https://hacktivis.me/git/ap-client/> +# SPDX-License-Identifier: BSD-3-Clause +use strict; +use warnings; use ExtUtils::MakeMaker; WriteMakefile( - NAME => 'App::ApClient', - ABSTRACT => 'CLI-based client / toolbox for ActivityPub Client-to-Server', - AUTHOR => 'Haelwenn (lanodan) Monnier <contact+ap-client@hacktivis.me>', - LICENSE => 'bsd', - VERSION => '0.1', - EXE_FILES => ['ap-fetch.pl', 'ap-represent.pl', 'ap-backup.pl'], + NAME => 'App::ApClient', + ABSTRACT => 'CLI-based client / toolbox for ActivityPub Client-to-Server', + AUTHOR => 'Haelwenn (lanodan) Monnier <contact+ap-client@hacktivis.me>', + LICENSE => 'bsd', + VERSION => 'v0.1.0', + EXE_FILES => + [ 'script/ap-fetch', 'script/ap-represent', 'script/ap-backup' ], + PREREQ_PM => { + 'JSON' => 0, + }, META_MERGE => { - 'meta-spec' => { version => 2 }, + 'meta-spec' => {version => 2}, release_status => 'stable', - resources => { + resources => { repository => { type => 'git', url => 'https://hacktivis.me/git/ap-client.git/', @@ -18,4 +28,4 @@ WriteMakefile( }, }, }, -) +); diff --git a/ap-backup.pl b/ap-backup.pl @@ -1,119 +0,0 @@ -#!/usr/bin/env perl -# AP-Client: CLI-based client / toolbox for ActivityPub -# Copyright © 2020-2021 AP-Client Authors <https://hacktivis.me/git/ap-client/> -# SPDX-License-Identifier: BSD-3-Clause -use strict; -use utf8; -no warnings; # Wide Character… - -use Getopt::Std; -use LWP::UserAgent; -use MIME::Base64; -use JSON::PP; - -my %options=(); -my $auth; -my $ua = LWP::UserAgent->new; - -$ua->agent("AP-Client Backup <https://hacktivis.me/git/ap-client/>"); - -sub save_collection { - my ($items) = @_; - my $filename; - - foreach my $item (@{$items}) { - if($item->{"id"}) { - $filename = $item->{"id"}; - # replace / in URLs with _ - $filename =~ tr/\//_/; - } else { - die "id property undefined" if not $_->{"id"}; - } - - #print "Saving ", $item->{"id"}, " to ", $filename, "\n"; - - open (my $fh, '>', $filename) or die "couldn't open", $filename; - print $fh encode_json($item); - close $fh; - } -} - -sub apc2s_backup { - my ($url) = @_; - - my $req = HTTP::Request->new(GET => $url); - $req->header('Accept' => 'application/activity+json'); - $req->header('Authorization' => $auth); - - my $res = $ua->request($req); - - if($res->is_success) { - print "Got $url\n"; - my $content_type = $res->header("Content-Type"); - my $content_match = /application\/([^+]*+)?json(; .*)?/; - if($content_type =~ $content_match) { - my $response = decode_json($res->content); - - if($response->{"type"} eq "OrderedCollection") { - if(not $response->{"first"}) { - die "“first” property of OrderedCollection undefined"; - } - - print "Fetching first property: ", $response->{"first"}, "\n"; - apc2s_backup($response->{"first"}); - } elsif($response->{"type"} eq "OrderedCollectionPage") { - if($response->{"orderedItems"}) { - save_collection($response->{"orderedItems"}); - print "next: ", $response->{"next"}, "\n" if $response->{"next"}; - print "prev: ", $response->{"prev"}, "\n" if $response->{"prev"}; - - if($response->{"next"}) { - print "Fetching next property\n"; - apc2s_backup($response->{"next"}) - } else { - print "No “next” property defined, done?\n"; - } - } else { - die "OrderedCollectionPage without “orderedItems” defined is unsupported"; - } - } elsif($response->{"type"} eq "Person") { - if($response->{"outbox"}) { - print "Fetching outbox property: ", $response->{"outbox"}, "\n"; - apc2s_backup($response->{"outbox"}); - } else { - die "Person actor with no outbox"; - } - } else { - die "Unknown type: ", $response->{"type"}; - } - } else { - die "Got ", $content_type, " instead of ", $content_match; - } - } else { - die "Got ", $res->status_line, " instead of 2xx"; - } -} - -getopts("u:o:", \%options); - -if($#ARGV != 0) { - print "usage: ap-backup.pl <-u user:password|-o OAuth-Bearer-Token> <url>\n"; - print "This tool is used to backup an account, authentication is required.\n"; - print " -u HTTP Basic Auth\n"; - print " -o OAuth2 Bearer Token\n"; - print " url ActivityPub user URL or outbox URL\n"; - print "Known to work against Pleroma.\n"; - print "Activities are saved in the current working directory via their id, it's recommended to launch in a dedicated directory.\n"; - exit 1; -} - -if(defined $options{u}) { - $auth = "Basic ".encode_base64($options{u}); -} -if(defined $options{o}) { - $auth = "Bearer ".$options{o}; -} - -print "Authorization: $auth"; -print "Fetching: ", $ARGV[0], "\n"; -apc2s_backup($ARGV[0]); diff --git a/ap-fetch.pl b/ap-fetch.pl @@ -1,59 +0,0 @@ -#!/usr/bin/env perl -# AP-Client: CLI-based client / toolbox for ActivityPub -# Copyright © 2020-2021 AP-Client Authors <https://hacktivis.me/git/ap-client/> -# SPDX-License-Identifier: BSD-3-Clause -use strict; -use utf8; -no warnings; # Wide Character… - -use Getopt::Std; - -use LWP::UserAgent; -use HTTP::Request::Common; - -my %options=(); -my $ua = LWP::UserAgent->new; - - -getopts("rju:", \%options); - -if($#ARGV != 0) { - print "usage: ap-fetch.pl [-r|-j|-u user:pass] <url>\n"; - print " -j Pipe into jq(1)\n"; - print " -r Raw output\n"; - print " -u user:pass HTTP Basic Auth credentials\n"; - print "By default, when -j and -r are absent it pipes the data into ap-represent.pl.\n"; - exit 1; -} - -$ua->agent("AP-Client fetch <https://hacktivis.me/git/ap-client/>"); -my $req = HTTP::Request->new(GET => $ARGV[0]); -$req->header('Accept' => 'application/activity+json'); - -if(defined $options{u}) { - my ($user, $password) = split(/:/, $options{u}); - $req->authorization_basic($user, $password); -} - -my $res = $ua->request($req); - -if($res->is_success) { - my $content_type = $res->header("Content-Type"); - my $content_match = /application\/([^+]*+)?json(; .*)?/; - - if($content_type =~ $content_match) { - if(defined $options{r}) { - print $res->content; - } elsif(defined $options{j}) { - open(my $pipe_out, '|-', 'jq .') or die "Couldn't open a pipe into jq: $!"; - print $pipe_out $res->content; - } else { - open(my $pipe_out, '|-', 'ap-represent.pl') or die "Couldn't open a pipe into ap-represent.pl: $!"; - print $pipe_out $res->content; - } - } else { - print "Got $content_type instead of $content_match"; - } -} else { - print "Got ", $res->status_line, " instead of 2xx\n"; -} diff --git a/ap-represent.pl b/ap-represent.pl @@ -1,67 +0,0 @@ -#!/usr/bin/env perl -# AP-Client: CLI-based client / toolbox for ActivityPub -# Copyright © 2020 AP-Client Authors <https://hacktivis.me/git/ap-client/> -# SPDX-License-Identifier: BSD-3-Clause -use strict; -use utf8; -no warnings; # Wide Character… -use Scalar::Util qw(reftype); - -use JSON::MaybeXS; -use Data::Dumper; - -my $json = JSON::MaybeXS->new(utf8 => 1); - -sub print_object_key { - my ($indent, $object, $key) = @_; - - if($object->{$key}) { - print_ref($indent, $object->{$key}, $key); - } -} - -sub print_object { - my ($indent, $object) = @_; - - printf "%*s %s", $indent, '⇒', $object->{"type"}; - printf ' id:<%s>', $object->{"id"} if $object->{"id"}; - printf ' href:<%s>', $object->{"href"} if $object->{"href"}; - printf ' “%s”', $object->{"name"} if $object->{"name"}; - printf ' @%s', $object->{"preferredUsername"} if $object->{"preferredUsername"}; - printf ' ⚠' if ($object->{"sensitive"} eq JSON->true); - foreach("url", "subtitleLanguage", "context", "inbox", "outbox", "prev", "next", "published", "updated", "summary", "content", "bcc", "bto", "to", "cc", "object", "attachment", "tag", "orderedItems", "mediaType") { - print_object_key($indent, $object, $_); - } -} - -sub print_ref { - my ($indent, $object, $name) = @_; - - my $ref_type = reftype($object); - - if($ref_type eq 'HASH') { - printf "\n%*s%s: \n", $indent, ' ', $name; - print_object($indent+4, $object); - } elsif($ref_type eq 'ARRAY') { - printf "\n%*s%s: ", $indent, ' ', $name if @{$object}; - foreach(@{$object}) { - if(reftype($_) eq 'HASH') { - print "\n"; - print_object($indent+4, $_); - } else { - printf "%s ; ", $_; - } - } - } else { - printf "\n%*s%s: %s", $indent, ' ', $name, $object; - } -} - -my $blob; -{ - undef $/; - $blob = $json->decode(<STDIN>); -} - -print_object(1, $blob); -print "\n"; diff --git a/lib/ActivityPub/PrettyPrint.pm b/lib/ActivityPub/PrettyPrint.pm @@ -0,0 +1,72 @@ +#!/usr/bin/env perl +# AP-Client: CLI-based client / toolbox for ActivityPub +# Copyright © 2020-2023 AP-Client Authors <https://hacktivis.me/git/ap-client/> +# SPDX-License-Identifier: BSD-3-Clause +package ActivityPub::PrettyPrint; +our $VERSION = '0.1.0'; +use strict; +use utf8; +use open ":std", ":encoding(UTF-8)"; + +use Scalar::Util qw(reftype); + +use Exporter 'import'; + +our @EXPORT_OK = qw(print_object); + +sub print_object_key { + my ($indent, $object, $key) = @_; + + if ($object->{$key}) { + print_ref($indent, $object->{$key}, $key); + } +} + +sub print_object { + my ($indent, $object) = @_; + + printf "%*s %s", $indent, '⇒', $object->{"type"}; + printf ' id:<%s>', $object->{"id"} if $object->{"id"}; + printf ' href:<%s>', $object->{"href"} if $object->{"href"}; + printf ' “%s”', $object->{"name"} if $object->{"name"}; + printf ' @%s', $object->{"preferredUsername"} + if $object->{"preferredUsername"}; + printf ' ⚠' if ($object->{"sensitive"} eq JSON->true); + foreach ( + "url", "subtitleLanguage", "context", + "inbox", "outbox", "prev", + "next", "published", "updated", + "summary", "content", "bcc", + "bto", "to", "cc", + "object", "attachment", "tag", + "orderedItems", "mediaType" + ) + { + print_object_key($indent, $object, $_); + } +} + +sub print_ref { + my ($indent, $object, $name) = @_; + + my $ref_type = reftype($object); + + if ($ref_type eq 'HASH') { + printf "\n%*s%s: \n", $indent, ' ', $name; + print_object($indent + 4, $object); + } elsif ($ref_type eq 'ARRAY') { + printf "\n%*s%s: ", $indent, ' ', $name if @{$object}; + foreach (@{$object}) { + if (reftype($_) eq 'HASH') { + print "\n"; + print_object($indent + 4, $_); + } else { + printf "%s ; ", $_; + } + } + } else { + printf "\n%*s%s: %s", $indent, ' ', $name, $object; + } +} + +1; diff --git a/script/ap-backup b/script/ap-backup @@ -0,0 +1,127 @@ +#!/usr/bin/env perl +# AP-Client: CLI-based client / toolbox for ActivityPub +# Copyright © 2020-2021 AP-Client Authors <https://hacktivis.me/git/ap-client/> +# SPDX-License-Identifier: BSD-3-Clause +use strict; +use utf8; +no warnings; # Wide Character… + +use Getopt::Std; +use LWP::UserAgent; +use MIME::Base64; +use JSON::PP; + +my %options = (); +my $auth; +my $ua = LWP::UserAgent->new; + +$ua->agent("AP-Client Backup <https://hacktivis.me/git/ap-client/>"); + +sub save_collection { + my ($items) = @_; + my $filename; + + foreach my $item (@{$items}) { + if ($item->{"id"}) { + $filename = $item->{"id"}; + + # replace / in URLs with _ + $filename =~ tr/\//_/; + } else { + die "id property undefined" if not $_->{"id"}; + } + + #print "Saving ", $item->{"id"}, " to ", $filename, "\n"; + + open(my $fh, '>', $filename) or die "couldn't open", $filename; + print $fh encode_json($item); + close $fh; + } +} + +sub apc2s_backup { + my ($url) = @_; + + my $req = HTTP::Request->new(GET => $url); + $req->header('Accept' => 'application/activity+json'); + $req->header('Authorization' => $auth); + + my $res = $ua->request($req); + + if ($res->is_success) { + print "Got $url\n"; + my $content_type = $res->header("Content-Type"); + my $content_match = /application\/([^+]*+)?json(; .*)?/; + if ($content_type =~ $content_match) { + my $response = decode_json($res->content); + + if ($response->{"type"} eq "OrderedCollection") { + if (not $response->{"first"}) { + die "“first” property of OrderedCollection undefined"; + } + + print "Fetching first property: ", $response->{"first"}, "\n"; + apc2s_backup($response->{"first"}); + } elsif ($response->{"type"} eq "OrderedCollectionPage") { + if ($response->{"orderedItems"}) { + save_collection($response->{"orderedItems"}); + print "next: ", $response->{"next"}, "\n" + if $response->{"next"}; + print "prev: ", $response->{"prev"}, "\n" + if $response->{"prev"}; + + if ($response->{"next"}) { + print "Fetching next property\n"; + apc2s_backup($response->{"next"}); + } else { + print "No “next” property defined, done?\n"; + } + } else { + die +"OrderedCollectionPage without “orderedItems” defined is unsupported"; + } + } elsif ($response->{"type"} eq "Person") { + if ($response->{"outbox"}) { + print "Fetching outbox property: ", $response->{"outbox"}, + "\n"; + apc2s_backup($response->{"outbox"}); + } else { + die "Person actor with no outbox"; + } + } else { + die "Unknown type: ", $response->{"type"}; + } + } else { + die "Got ", $content_type, " instead of ", $content_match; + } + } else { + die "Got ", $res->status_line, " instead of 2xx"; + } +} + +getopts("u:o:", \%options); + +if ($#ARGV != 0) { + print + "usage: ap-backup.pl <-u user:password|-o OAuth-Bearer-Token> <url>\n"; + print + "This tool is used to backup an account, authentication is required.\n"; + print " -u HTTP Basic Auth\n"; + print " -o OAuth2 Bearer Token\n"; + print " url ActivityPub user URL or outbox URL\n"; + print "Known to work against Pleroma.\n"; + print +"Activities are saved in the current working directory via their id, it's recommended to launch in a dedicated directory.\n"; + exit 1; +} + +if (defined $options{u}) { + $auth = "Basic " . encode_base64($options{u}); +} +if (defined $options{o}) { + $auth = "Bearer " . $options{o}; +} + +print "Authorization: $auth"; +print "Fetching: ", $ARGV[0], "\n"; +apc2s_backup($ARGV[0]); diff --git a/script/ap-fetch b/script/ap-fetch @@ -0,0 +1,97 @@ +#!/usr/bin/env perl +# AP-Client: CLI-based client / toolbox for ActivityPub +# Copyright © 2020-2023 AP-Client Authors <https://hacktivis.me/git/ap-client/> +# SPDX-License-Identifier: BSD-3-Clause +package App::ApClient; + +use strict; +use utf8; +no warnings; # Wide Character… + +use Getopt::Std; +use LWP::UserAgent; +use HTTP::Request::Common; +use JSON; +use ActivityPub::PrettyPrint qw(print_object); + +=head1 NAME + +ap-fetch - Fetch ActivityStream object, optionally pretty printing it + +=head1 SYNOPSIS + +B<ap-fetch> [-r|-j|-u <user:pass>] <URI> + +=head1 DESCRIPTION + +ap-fetch fetches an URI, decodes it as an ActivityStream object. + +=item -j + +Pipe into jq(1) + +=item -r + +Raw output, print server's output without any decoding + +=item -u user:pass + +Pass username and password for HTTP Basic Auth. + +=head1 LICENSE + +BSD-3-Clause + +=cut + +my %options = (); +my $ua = LWP::UserAgent->new; + +getopts("rju:", \%options); + +if ($#ARGV != 0) { + print "usage: ap-fetch.pl [-r|-j|-u user:pass] <url>\n"; + print " -j Pipe into jq(1)\n"; + print " -r Raw output\n"; + print " -u user:pass HTTP Basic Auth credentials\n"; + print +"By default, when -j and -r are absent it pipes the data into ap-represent.pl.\n"; + exit 1; +} + +$ua->agent("AP-Client fetch <https://hacktivis.me/git/ap-client/>"); +my $req = HTTP::Request->new(GET => $ARGV[0]); +$req->header('Accept' => 'application/activity+json'); + +if (defined $options{u}) { + my ($user, $password) = split(/:/, $options{u}); + $req->authorization_basic($user, $password); +} + +my $res = $ua->request($req); + +if ($res->is_success) { + my $content_type = $res->header("Content-Type"); + my $content_match = /application\/([^+]*+)?json(; .*)?/; + + if ($content_type =~ $content_match) { + if (defined $options{r}) { + print $res->content; + } elsif (defined $options{j}) { + open(my $pipe_out, '|-', 'jq .') + or die "Couldn't open a pipe into jq: $!"; + print $pipe_out $res->content; + close($pipe_out); + } else { + my $object = decode_json($res->content); + print_object(1, $object); + print "\n"; + } + } else { + print STDERR "Got $content_type instead of $content_match\n"; + exit 1; + } +} else { + print STDERR "Got $res->status_line instead of 2xx\n"; + exit 1; +} diff --git a/script/ap-represent b/script/ap-represent @@ -0,0 +1,36 @@ +#!/usr/bin/env perl +# AP-Client: CLI-based client / toolbox for ActivityPub +# Copyright © 2020-2023 AP-Client Authors <https://hacktivis.me/git/ap-client/> +# SPDX-License-Identifier: BSD-3-Clause +use strict; +use utf8; +no warnings; # Wide Character… + +use JSON; +use ActivityPub::PrettyPrint qw(print_object); + +=head1 NAME + +ap-represent - Pretty-print ActivityStreams data + +=head1 SYNOPSIS + +B<ap-represent> + +=head1 DESCRIPTION + +ap-represent takes JSON-formatted ActivityStreams data from standard input and +pretty prints it to stdout. + +Said output isn't made to be readable by machines, only humans. + +=head1 LICENSE + +BSD-3-Clause + +=cut + +undef $/; +my $blob = decode_json(<STDIN>); +print_object(1, $blob); +print "\n";