Skip to content

Instantly share code, notes, and snippets.

@Ovid
Last active October 2, 2024 15:35
Show Gist options
  • Save Ovid/5db4290f4fbc46ec8fe7318065a8f60e to your computer and use it in GitHub Desktop.
Save Ovid/5db4290f4fbc46ec8fe7318065a8f60e to your computer and use it in GitHub Desktop.
Quick hack to post to Mastodon from the command line
#!/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