Skip to content

Instantly share code, notes, and snippets.

@Gro-Tsen
Created January 3, 2023 11:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Gro-Tsen/4dd55cc6825d1ea19e06ca52e580858d to your computer and use it in GitHub Desktop.
Save Gro-Tsen/4dd55cc6825d1ea19e06ca52e580858d to your computer and use it in GitHub Desktop.
Twitter Perl scripts
#! /usr/local/bin/perl -w
# gettweet.pl: Retrieve the content of tweets by id.
# Usage: getweet.pl -i <comma-separated-list>
# saves each in a file called <fulldate>-<id>.txt
# Requires API keys to be stored in ~/.twitterkeys (or $TWITTERKEYS)
# syntax being four lines starting "API-Key: ", "API-Key-Secret: ",
# "Access-Token: " and "Access-Token-Secret: " each followed by their value.
use strict;
use warnings;
use Getopt::Std;
use Net::Twitter;
use DateTime::Format::Strptime;
use Data::Dumper;
binmode STDERR, ":utf8";
binmode STDOUT, ":utf8";
# Get options
my %opts;
getopts("i:", \%opts);
# -i <ids>: retrieve specific tweets (comma separated list)
my $lookuplist;
if ( defined($opts{i}) ) {
die "Badly formed argument to -i (expecting a comma-separated list of integers)" unless $opts{i} =~ m/^[0-9]+(?:\,[0-9]+)*$/;
$lookuplist = [ split(/,/, $opts{i}) ];
} else {
die "Need to pass -i option";
}
# Read authentication file
my $keyfname = $ENV{"TWITTERKEYS"} // ($ENV{"HOME"}."/.twitterkeys");
my %keyfdata;
open my $keyf, "<", $keyfname or die "Can't open $keyfname: $!";
while (<$keyf>) {
s/\#.*//;
next if m/^\s+$/;
die "Bad key file format: $_" unless m/^\s*([A-Za-z][A-Za-z0-9\-]*)\:\s+(\S+)\s*$/;
# print STDERR "Read auth data \[$1\]=$2\n";
$keyfdata{$1} = $2;
}
close $keyf;
my $consumer_key = $keyfdata{"API-Key"} or die "Didn't find API-Key in key file";
my $consumer_secret = $keyfdata{"API-Key-Secret"} or die "Didn't find API-Key-Secret in key file";
my $access_token = $keyfdata{"Access-Token"} or die "Didn't find Access-Token in key file";
my $access_token_secret = $keyfdata{"Access-Token-Secret"} or die "Didn't find Access-Token-Secret in key file";
die "Badly formed Access-Token" unless $access_token =~ m/^([0-9]+)\-/;
my $self_id = $1;
# Open the Twitter connection.
my $nt = Net::Twitter->new(
traits => [qw/API::RESTv1_1/],
consumer_key => $consumer_key,
consumer_secret => $consumer_secret,
access_token => $access_token,
access_token_secret => $access_token_secret,
);
die "Couldn't open Twitter connection" unless $nt;
my $datetime_parser = DateTime::Format::Strptime->new(
pattern => "%a %b %d %T %z %Y", time_zone => "UTC", locale => "C",
on_error => "croak");
# Get self screen name
print STDERR "Getting information on self...\n";
my $slf = $nt->lookup_users({user_id => $self_id});
die "lookup_user didn't return exactly one user" unless scalar(@{$slf})==1;
die "Missing screen name" unless defined($slf->[0]->{"screen_name"});
my $self_screen_name = $slf->[0]->{"screen_name"};
my $self_name = $slf->[0]->{"name"};
my %paramdict;
my $tweetlist;
if ( 1 ) {
%paramdict = (trim_user => 0, tweet_mode => "extended");
$paramdict{id} = $lookuplist;
$tweetlist = $nt->lookup_statuses(\%paramdict);
}
# The following function performs a set of substitutions on a string
# (correctly adjusting the position of each subsequent substitution to
# take into account earlier ones).
sub substitute_in_string {
my $str = shift;
my @subs = sort { $a->[0] <=> $b->[0] } @{ shift() };
# Each entry of @subs is an array reference:
# [0]: first character of substring to modify
# [1]: length of substring to modify
# [2]: undefined, or substring itself, to check for consistency
# [3]: undefined, or substring to substitute
# [4]: undefined, or substring to insert before
# [5]: undefined, or substring to insert at end
my $corr = 0;
my $minbar = 0;
for my $sb ( @subs ) {
my $idx0 = $sb->[0] + $corr;
die "Attempting to overlap substitutions" unless $idx0>=$minbar;
my $len = $sb->[1];
if ( defined($sb->[2]) ) {
die(sprintf("Bad calibration: expecting \"%s\", got \"%s\"\n", $sb->[2], substr($str, $idx0, $len))) unless substr($str, $idx0, $len) eq $sb->[2];
}
my $repl = ($sb->[4] // "") . ($sb->[3] // substr($str, $idx0, $len)) . ($sb->[5] // "");
my $newlen = length($repl);
# printf STDERR "str: %s\nsubstituting (%d,%d) \"%s\"->\"%s\"\n", $str, $idx0, $len, substr($str, $idx0, $len), $repl;
substr($str, $idx0, $len) = $repl;
# printf STDERR "str: %s\n", $str;
$corr += ($newlen - $len);
$minbar = $idx0 + $newlen;
}
return $str;
}
# Now scan through tweetlist
for my $st ( @{$tweetlist} ) {
die "Missing id from tweetlist entry" unless defined($st->{"id"});
die "Bad id from tweetlist entry" unless $st->{"id"} =~ /^[0-9]+$/;
my $created_at = $datetime_parser->parse_datetime($st->{"created_at"});
my $timestamp = $created_at->strftime("%Y%m%d%H%M%S");
my $id = $st->{"id"};
printf STDERR "%s-%s\n", $timestamp, $id;
my $fname = $timestamp . "-" . $id . ".txt";
my $user_id = $st->{"user"}->{"id"};
my $user_screen_name = $user_id eq $self_id ? $self_screen_name : $st->{"user"}->{"screen_name"};
die "Didn't get user screen name" unless defined($user_screen_name);
my $permalink = sprintf("https://twitter.com/%s/status/%s", $user_screen_name, $id);
my $fulltext = $st->{"full_text"};
die "Text contains unescaped HTML" if $fulltext =~ m/[\<\>]|\&(?!(?:lt|gt|amp|apos|quot|\#(?:x[0-9A-Fa-f]+|[0-9]+))\;)/;
my @substitutions = ();
for my $ent ( @{$st->{"entities"}->{"urls"}} ) {
my $idx0 = $ent->{"indices"}->[0];
my $idx1 = $ent->{"indices"}->[1];
push @substitutions, [$idx0, $idx1-$idx0, $ent->{"url"}, $ent->{"expanded_url"}];
}
for my $ent ( @{$st->{"entities"}->{"media"}} ) {
my $idx0 = $ent->{"indices"}->[0];
my $idx1 = $ent->{"indices"}->[1];
push @substitutions, [$idx0, $idx1-$idx0, $ent->{"url"}, $ent->{"display_url"}];
}
$fulltext = substitute_in_string $fulltext, \@substitutions;
# Save text archive in a file
open my $f, ">:utf8", $fname or die "Cannot open $fname";
print $f "$timestamp\n$id ($user_id)\n$permalink\n\n$fulltext\n";
close $f;
}
#! /usr/local/bin/perl -w
# tweet.pl: Post a tweet on Twitter.
# Reads tweet content from stdin unless -c option is specified (giving content).
# Use -r id to reply to specified tweet ID. Use of -m is preferable (required?).
# Use -n to chomp final NL (useful when reading from stdin).
# The id of the posted tweet is written on stdout, various diagnostics on stderr.
# Requires API keys to be stored in ~/.twitterkeys (or $TWITTERKEYS)
# syntax being four lines starting "API-Key: ", "API-Key-Secret: ",
# "Access-Token: " and "Access-Token-Secret: " each followed by their value.
use strict;
use warnings;
use Encode;
use Getopt::Std;
use Net::Twitter;
# use DateTime::Format::Strptime;
# use Data::Dumper;
use open IN => ':utf8';
binmode STDIN, ":utf8";
binmode STDERR, ":utf8";
binmode STDOUT, ":utf8";
# Get options
my %opts;
getopts("r:c:mn", \%opts);
# -r <id>: reply to specified ID
# -c <string>: status content
# -m: set auto_populate_reply_metadata
# -n: chomp final NL
my $in_reply_to;
if ( defined($opts{r}) ) {
die "Badly formed argument to -r" unless $opts{r} =~ m/^(?:https?\:\/\/twitter\.com\/[^\/]*\/status\/)?([0-9]+)$/;
$in_reply_to = $1;
}
my $auto_populate_reply_metadata;
$auto_populate_reply_metadata = 1 if $opts{m};
# Read authentication file
my $keyfname = $ENV{"TWITTERKEYS"} // ($ENV{"HOME"}."/.twitterkeys");
my %keyfdata;
open my $keyf, "<", $keyfname or die "Can't open $keyfname: $!";
while (<$keyf>) {
s/\#.*//;
next if m/^\s+$/;
die "Bad key file format: $_" unless m/^\s*([A-Za-z][A-Za-z0-9\-]*)\:\s+(\S+)\s*$/;
# print STDERR "Read auth data \[$1\]=$2\n";
$keyfdata{$1} = $2;
}
close $keyf;
my $consumer_key = $keyfdata{"API-Key"} or die "Didn't find API-Key in key file";
my $consumer_secret = $keyfdata{"API-Key-Secret"} or die "Didn't find API-Key-Secret in key file";
my $access_token = $keyfdata{"Access-Token"} or die "Didn't find Access-Token in key file";
my $access_token_secret = $keyfdata{"Access-Token-Secret"} or die "Didn't find Access-Token-Secret in key file";
die "Badly formed Access-Token" unless $access_token =~ m/^([0-9]+)\-/;
my $self_id = $1;
# Open the Twitter connection.
my $nt = Net::Twitter->new(
traits => [qw/API::RESTv1_1/],
consumer_key => $consumer_key,
consumer_secret => $consumer_secret,
access_token => $access_token,
access_token_secret => $access_token_secret,
);
die "Couldn't open Twitter connection" unless $nt;
# my $datetime_parser = DateTime::Format::Strptime->new(
# pattern => "%a %b %d %T %z %Y", time_zone => "UTC", locale => "C",
# on_error => "croak");
# Read status to be tweeted
my $status;
if ( defined($opts{c}) ) {
$status = decode("utf-8", $opts{c});
} else {
print STDERR "Reading tweet content...\n";
{ local $/ = undef; $status = <>; }
}
chomp $status if defined($opts{n});
print STDERR "Replying to $in_reply_to\n" if $in_reply_to;
print STDERR "Tweet to be posted follows:\n$status\n";
my %hash;
$hash{status} = $status;
$hash{in_reply_to_status_id} = $in_reply_to if defined($in_reply_to);
$hash{auto_populate_reply_metadata} = 1 if $auto_populate_reply_metadata;
# Not sure if this is useful/desirable?
# <URL: https://developer.twitter.com/en/docs/tweets/tweet-updates.html >
$hash{tweet_mode} = "extended";
my $tweet = $nt->update(\%hash);
print STDERR "Posted\n";
print $tweet->{id} . "\n";
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment