Last active
October 2, 2024 15:35
-
-
Save Ovid/5db4290f4fbc46ec8fe7318065a8f60e to your computer and use it in GitHub Desktop.
Quick hack to post to Mastodon from the command line
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env perl | |
# vim: ft=perl | |
use Modern::Perl; | |
use Mastodon::Client; | |
use Config::Tiny; | |
use Getopt::Long; | |
use JSON::PP 'decode_json'; | |
use File::HomeDir; | |
use utf8::all; | |
use autodie ':all'; | |
use Term::ANSIColor 'colored'; | |
use Const::Fast; | |
use experimental 'signatures'; | |
GetOptions( 'verbose' => \( my $verbose = 0 ), ) | |
or die "Bad options"; | |
my $mastodon = get_mastodon_client(); | |
my $max_characters = eval { | |
# this should fetch the data from /api/va/instance, but in case | |
# it's malformed, we use an eval to trap and ignore the error | |
$mastodon->get('instance'); | |
my $config = decode_json( $mastodon->latest_response->content ); | |
$config->{configuration}{statuses}{max_characters}; | |
}; | |
# don't use // in case we have an empty string | |
const my $MAX_POST_LENGTH => $max_characters || 500; | |
const my $POST_LENGTH => $MAX_POST_LENGTH - 6; | |
my $post = get_post_raw_text(@ARGV); | |
my @posts = rewrite_raw_text_into_posts($post); | |
if ( @posts > 1 ) { | |
my $total = @posts; | |
my $count = 1; | |
foreach (@posts) { | |
$_ .= " $count/$total"; | |
$count++; | |
if ( adjusted_length($_) > $MAX_POST_LENGTH ) { | |
my $postlength = length($_); | |
die | |
"bad line. Post length of $postlength is greater than $MAX_POST_LENGTH: $_"; | |
} | |
} | |
} | |
say colored( ['white on_black'], "\nYour posts will read as follows\n" ); | |
foreach (@posts) { | |
my $length = length($_); | |
print colored( ['white on_black'], $_ ); | |
say $verbose ? " length: $length characters\n" : "\n"; | |
} | |
print "Is this OK? [y/N] "; | |
my $response = <STDIN>; | |
exit unless $response =~ /^\s*[yY]/; | |
my $last_id; | |
my $count = @posts; | |
my $current = 1; | |
foreach (@posts) { | |
say colored( ['bright_red on_black'], "Sending post $current of $count" ); | |
$current++; | |
my $response = | |
defined $last_id | |
? $mastodon->post_status( $_, | |
{ in_reply_to_id => $last_id, visibility => 'unlisted' } ) | |
: $mastodon->post_status($_); | |
$last_id = $response->{id}; | |
say "Last id: $last_id" if $verbose; | |
} | |
sub rewrite_raw_text_into_posts ($text) { | |
# Lingua::EN::Sentence appears to discard the newlines, | |
# so we use this, er, heuristic. | |
my @sentences = split /((?<=[.!?])\s+|\n)/, $text; | |
my @chunks; | |
my $chunk = ''; | |
foreach my $sentence (@sentences) { | |
# Check for a line of dashes, forcing a new chunk | |
if ( $sentence =~ /^[-–]+\n?$/ ) { | |
# Save the current chunk and start a new one, if there's content | |
push @chunks, $chunk if $chunk ne ''; | |
$chunk = ''; | |
next; # Skip adding the dash line to any chunk | |
} | |
# Calculate adjusted length considering URLs | |
my $adjusted_length = adjusted_length($sentence); | |
if ( length($chunk) + $adjusted_length > $POST_LENGTH ) { | |
# Save the current chunk and start a new one, if there's content | |
push @chunks, $chunk if $chunk ne ''; | |
$chunk = $sentence; | |
} | |
else { | |
# Add the part to the current chunk | |
$chunk .= $sentence; | |
} | |
} | |
# Don't forget the last chunk! | |
push @chunks, $chunk if $chunk ne ''; | |
@chunks = grep { /\S/ } # Sometimes we get whitespace-only chunks:w | |
map { s/^\s+//; s/\s+$//r } # trim our chunks | |
@chunks; | |
return @chunks; | |
} | |
sub get_post_raw_text (@argv) { | |
my $post = ''; | |
if (@argv) { | |
# This is a better way of handling multi-line posts because I can more | |
# easily edit what I'm writing. | |
my $file = shift @argv; | |
open my $fh, '<', $file; | |
$post = do { local $/; <$fh> }; | |
} | |
else { | |
say colored( ['white on_black'], 'Enter post stream:' ); | |
while ( chomp( my $input = <STDIN> ) ) { | |
last unless $input =~ /\w/; | |
$post .= " $input"; | |
} | |
} | |
say "Raw input: $post"; | |
return $post; | |
} | |
sub get_mastodon_client ( $config_file = undef ) { | |
$config_file = | |
File::HomeDir->my_home . ( $config_file // "/.config/toot/config.ini" ); | |
die "$config_file is missing\n" if not -e $config_file; | |
my $config = Config::Tiny->read( $config_file, 'utf8' ); | |
my $mastodon = Mastodon::Client->new( | |
instance => $config->{mastodon}{instance}, | |
name => 'masto', | |
client_id => $config->{mastodon}{client_id}, | |
client_secret => $config->{mastodon}{client_secret}, | |
access_token => $config->{mastodon}{access_token}, | |
coerce_entities => 1, | |
); | |
} | |
sub adjusted_length { | |
my ($text) = @_; | |
my $length = length($text); | |
# Find all URLs in the text | |
while ( $text =~ m!(https?://[^\s]+)!g ) { | |
my $url = $1; | |
# Subtract the actual URL length, then add 23 | |
$length -= length($url); | |
$length += 23; | |
} | |
return $length; | |
} | |
__END__ | |
=head1 NAME | |
posts - A tool to post "streams" of posts to Mastodon | |
=head1 SYNOPSIS | |
perl masto filename # reads text in file | |
perl masto # reads from standard input | |
The first version reads your posts from a file. The second version allows you | |
to type your posts and accepts input until it encounters a line not matching | |
C<\w>. | |
The text is then broken up into one or more posts. If more than one post, | |
each post will be numbered C<$post_number/$total_posts>. Assumes you don't | |
have more than 99 posts in a single stream. | |
If you have a line with only one or more dashes on it, it will force a break | |
into a new Mastodon post: | |
This is my first post. | |
--- | |
This is my second post. | |
If posts are more than 500 characters, will create a follow-up post, with | |
posts numbered sequentially as C<$index/$total>. Note that URLs are only | |
23 characters in length, regardless of the length of the URL. | |
Requires a configuration file at C<~/.config/toot/config.ini> with the | |
following structure: | |
[mastodon] | |
instance = $instance_name | |
username = $username | |
client_id = $client_id | |
client_secret = $client_secret | |
access_token = $access_token | |
To get that information, click on "Settings", "Development", and then the "New | |
Application" button. Fill in the info and you'll get the the information above. | |
# VIM | |
If you use vim, you can write Mastodon posts natively with it. In C<.vim/filetype.vim>, | |
add a mapping for C<*.masto> files: | |
autocmd! BufRead,BufNewFile *.masto call s:MastoMappings() | |
Then, add the following function to that file (assumes you can call this script as C<masto>: | |
function! s:MastoMappings() | |
match ErrorMsg '\%>475v.\+' " Highlight lines that are too long | |
set textwidth=500 " 500 characters is Fosstodon limit :/ | |
set wrap " Wrap long lines so they don't scroll off the terminal | |
set linebreak " Don't break words when wrapping | |
set spell " Spell check | |
" post to Mastodon | |
noremap <buffer> <leader>r :!masto %<CR> | |
endfunction | |
With the above, you can type <leader>r and it will automatically run this script | |
with your current buffer as the argument. Just type "y" to post, or anything else | |
to not post. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment