Posting a tweet picked from a file at random
March 8, 2019
Today I finished a small Perl program that I started on yesterday; a
program to pick a tweet from a file at random and post it to
Twitter. The idea is to post automatically, using cron
, 3 or 4
tweets a day to promote my blog.
Edit: A slightly better written version is available on GitHub.
Authentication file format
Instead of hardcoding the authentication information, a bad practice I have seen on a few occasions, this program reads the required information from a file. This makes it easy to support multiple accounts.
The tokens and keys obtained from Twitter must be entered in a file using the following format:
#
# Configuration for the john_bokma Twitter account
#
consumer_key = xxxxxxxxxxxxxxxxxxxxxxxxx
consumer_secret = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
access_token = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
access_token_secret = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Lines starting with a #
are comments and are ignored.
I name this file the same as my Twitter account using the conf
extension to make clear it's a configuration file; john_bokma.conf
.
The sub
that reads this file is the most complicated part of this Perl script
because I considered it a good idea to add additional steps that catch possible
mistakes:
sub read_conf {
my $filename = shift;
my %conf;
my @keys = qw( consumer_key consumer_secret
access_token access_token_secret );
my %required;
@required{ @keys } = ();
my $fh = path( $filename )->openr_utf8();
while ( my $line = <$fh> ) {
chomp $line;
if ( my ( $key, $value ) = $line =~ /^([a-z_]+)\s*[:=]\s*(\S+)$/ ) {
exists $required{ $key } or
die "Unexpected key $key at $filename line $.\n";
!exists $conf{ $key } or
die "Duplicate key $key at $filename line $.\n";
$conf{ $key } = $value;
}
}
exists $conf{ $_ } or die "Missing key $_ at $filename line $.\n"
for @keys;
close( $fh );
return \%conf;
}
The sub
opens the file of which the filename is given as a parameter and reads line by line from the file. The chomp
function is used to remove the end-of-line and next the code checks if the line matches a certain pattern. If a match occurs a check is made if this is a required configuration key. If not, the program terminates, reporting that the key is unexpected.
Next, it checks if the key is encountered for the first time. If not, the program terminates, reporting that the key is a duplicate.
When all lines have been read, but before closing the file, the program checks if all keys have been provided. Since the program also reports the line number, $.
, this has to happen before the file is closed otherwise this
variable is reset to 0.
After the file has been closed, a reference to the configuration hash is returned.
Tweets file format
The file with tweets uses a format that's the same as a fortune cookies file, each tweet is separated by a %
character on a line of its own. An example of a file with 3 tweets:
An earth #snake on the road
#mexico #nature
http://johnbokma.com/blog/2017/07/07/an-earth-snake-on-the-road.html
%
Adding Color to a #LaTeX Resume
http://johnbokma.com/blog/2017/05/30/adding-color-to-latex-resume.html
%
A juvenile #opossum in the kitchen
#mexico #nature
http://johnbokma.com/blog/2017/04/29/a-juvenile-opossum-in-the-kitchen.html
I start the name of this text file with my Twitter account name: john_bokma-tweets.txt
.
The sub
that reads this file and splits it into tweets is very simple
because it uses the slurp_utf8
function of Path::Tiny
to read the entire file into memory and Perl's split
to obtain the separate tweets from this:
sub read_tweets {
my $filename = shift;
return [ split /^%\n/m, path( $filename )->slurp_utf8() ];
}
The sub
returns a reference to an array of tweets.
Posting a Tweet
The posting a tweet sub
is as follows:
sub tweet {
my ( $conf, $tweet ) = @_;
my $nt = Net::Twitter::Lite::WithAPIv1_1->new(
traits => [ 'API::RESTv1_1' ],
ssl => 1,
%$conf,
);
my $result;
my $error_message;
try {
$result = $nt->update( $tweet );
}
catch {
my $error = $_;
if ( blessed $error and $error->isa( 'Net::Twitter::Lite::Error' ) ) {
$error_message = $error->message . "\n" . $error->error . "<<<\n";
}
else {
$error_message = $error;
}
};
die $error_message if defined $error_message;
return;
}
The first parameter is the configuration read from a file which is used to authenticate the bot. The second parameter is the tweet itself.
The posting of the tweet, using the update
method, is done in a try
block. If an exception (error) is thrown the catch
block handles this. If the exception is a blessed reference and a Net::Twitter::Lite::Error
the error message is build by calling methods on this object. Otherwise it's assumed the message is plain text or can be stringified to plain text.
The complete Perl program
The complete program, tweetfile.pl
, is given below. Note that you don't
have to make any changes to the program itself as the authentication
configuration and tweets are read from files which must be specified
on the command line.
In order to obtain help about the program start it with the --help
option
as follows: perl tweetfile.pl --help
.
#!/usr/bin/perl
#
# (c) John Bokma, 2019
#
# This program is free software; you can redistribute it and/or modify it
# under the same terms as Perl itself.
use strict;
use warnings;
use Try::Tiny;
use Path::Tiny;
use Getopt::Long;
use Net::Twitter::Lite::WithAPIv1_1;
use Scalar::Util 'blessed';
my $conf_filename;
my $tweets_filename;
my $help = 0;
my $quiet = 0;
my $dry_run = 0;
my $first;
GetOptions(
'conf=s' => \$conf_filename,
'tweets=s' => \$tweets_filename,
'help' => \$help,
'quiet' => \$quiet,
'dry-run' => \$dry_run,
'first' => \$first,
);
if ( $help ) {
show_help();
exit;
}
if ( !defined $conf_filename || !defined $tweets_filename ) {
warn "Error: both --conf and --tweets must be given\n\n";
show_help();
exit( 1 );
}
$quiet or binmode STDOUT, ':encoding(UTF-8)';
$quiet or print "Reading configuration from $conf_filename\n";
my $conf = read_conf( $conf_filename );
$quiet or print "Reading tweets from $tweets_filename\n";
my $tweets = read_tweets( $tweets_filename );
my $count = @$tweets;
$count or die "No tweets found";
$quiet or print "Found $count tweets\n";
my $index = defined $first ? 0 : rand $count;
my $tweet = $tweets->[ $index ];
$quiet or print "Going to tweet:\n\n$tweet\n";
tweet( $conf, $tweet ) unless $dry_run;
sub read_conf {
my $filename = shift;
my %conf;
my @keys = qw( consumer_key consumer_secret
access_token access_token_secret );
my %required;
@required{ @keys } = ();
my $fh = path( $filename )->openr_utf8();
while ( my $line = <$fh> ) {
chomp $line;
if ( my ( $key, $value ) = $line =~ /^([a-z_]+)\s*[:=]\s*(\S+)$/ ) {
exists $required{ $key } or
die "Unexpected key $key at $filename line $.\n";
!exists $conf{ $key } or
die "Duplicate key $key at $filename line $.\n";
$conf{ $key } = $value;
}
}
exists $conf{ $_ } or die "Missing key $_ at $filename line $.\n"
for @keys;
close( $fh );
return \%conf;
}
sub read_tweets {
my $filename = shift;
return [ split /^%\n/m, path( $filename )->slurp_utf8() ];
}
sub tweet {
my ( $conf, $tweet ) = @_;
my $nt = Net::Twitter::Lite::WithAPIv1_1->new(
traits => [ 'API::RESTv1_1' ],
ssl => 1,
%$conf,
);
my $result;
my $error_message;
try {
$result = $nt->update( $tweet );
}
catch {
my $error = $_;
if ( blessed $error and $error->isa( 'Net::Twitter::Lite::Error' ) ) {
$error_message = $error->message . "\n" . $error->error . "<<<\n";
}
else {
$error_message = $error;
}
};
die $error_message if defined $error_message;
return;
}
sub show_help {
print <<'END_HELP';
NAME
tweetfile.pl - Posts a random tweet from a file to twitter
SYNOPSIS
tweetfile.pl [--quiet] [--dry-run] [--first]
--conf=<your.conf> --tweets=<your-tweets.txt>
tweetfile.pl --help
DESCRIPTION
Tweets a single message picked at random from a file given by
the --tweets argument. In this file, each tweet must be separated by
a % character on a line by itself.
The authentication keys and tokens must be made available in a file
given by the --conf argument. In this file, the following names must
be available: consumer_key, consumer_secret, access_token, and
access_token_secret. Each name must be followed by either a : or
a = character and its value as provided by Twitter.
The --quiet option prevents the program from printing information
regarding the progress and which tweet it's going to post.
The --dry-run option prevents the program from actually posting
the selected tweet to Twitter. This is useful in testing.
The --first option picks the first tweet instead of a random one.
This is useful if you add each new tweet to the top of the file.
The --help option shows this information.
END_HELP
return;
}
Installing missing Perl modules
If you run the Perl program for the first time and get an error message similar to the one below:
Can't locate Path/Tiny.pm in @INC (you may need to install the Path::Tiny module
) (@INC contains: ...
you have to install one or more missing Perl modules. If you are on Ubuntu in order to obtain the package name make each directory lower case and replace each /
with a -
and append the lowecase version of the filename without the extension, In this example you get:
path-tiny
Next, place lib
in front and add -perl
to the end and use apt-cache search
to verify that this is the correct name of the package, i.e.:
$ apt-cache search libpath-tiny-perl
libpath-tiny-perl - file path utility
Install the package as follows:
$ sudo apt-get install libpath-tiny-perl
Note: in these examples do not copy the $
character but the part following it.
If you don't get any result try to remove the filename. For example:
Can't locate Net/Twitter/Lite/WithAPIv1_1.pm in @INC (you may need to install th
e Net::Twitter::Lite::WithAPIv1_1 module) (@INC contains: ...
would per above recipe result in:
$ apt-cache search libnet-twitter-lite-withapiv1_1-perl
which gives no results. Removing the filename part results in:
$ apt-cache search libnet-twitter-lite-perl
libnet-twitter-lite-perl - interface to the Twitter API
This is the correct package. It can be installed as follows:
$ sudo apt-get install libnet-twitter-lite-perl
On my test virtual machine, running Ubuntu 18.10, installing those two packages was enough to make the Perl program work.
Note that instead of adding modules to your system's Perl installation I do recommend to use Perlbrew instead.
Running the bot at specific times
The cron
program can be used to run the tweet bot at specific times, see
Running a Perl program via cron for more information.
Related
- Access tokens — Twitter Developers.
- Getopt::Long - module documentation.
- Path::Tiny - module documentation.
- Net::Twitter::Lite::WithAPIv1_1 - module documentation.
- Try::Tiny - module documentation.
- Perlbrew - an admin-free perl installation management tool.