summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--twitlist.pl155
1 files changed, 120 insertions, 35 deletions
diff --git a/twitlist.pl b/twitlist.pl
index 21c3ce5..067e5c3 100644
--- a/twitlist.pl
+++ b/twitlist.pl
@@ -9,13 +9,60 @@ use JSON;
use Try::Tiny;
use Safe::Isa;
use open ':std',':locale';
+use Data::Printer;
-my $WRITING=0;
+=head1 NAME
-use Data::Printer;
+twitlist - create & update Twitter lists
+
+=head1 SYNOPSIS
+
+ $ twitlist > lists.txt
+ $ edit lists.txt
+ $ twitlist lists.txt
+
+=head1 DESCRIPTION
+
+This program makes it a bit easier to create Twitter lists, and manage the people in them.
+
+Before you use this program, you need to create a JSON file in this same directory, containing your access credentials.
+It should look like this:
+
+ {
+ "consumer_key": "1234",
+ "consumer_secret": "5678",
+ "access_token": "9abc",
+ "access_token_secret": "def0"
+ }
+
+You can get the correct values by registering an application at
+L<https://apps.twitter.com/>.
+
+Once you've done that, run this script with no arguments:
+
+ $ twitlist > lists.txt
+
+This will produce, on standard output, a text file with one Twitter account per line, and the lists they belong to.
+It will look like this:
+
+ @name The Full Name : list_one, list_two
+
+You can then edit that file, adding or removing list names after the colon.
+If you add the name of a list that doesn't exist, it will be created.
+
+To apply your changes to your Twitter lists, re-run the script passing the text file as argument:
+
+ $ twitlist lists.txt
+I<NOTE>: this script will not actually write to Twitter unles you set the C<TWITTER_WRITE> environment variable to a true value.
+
+=cut
+
+my $WRITING=$ENV{TWITTER_WRITE} // 0;
+
+# create a Net::Twitter object with out tokens
sub get_twitter {
- my $config_file = $0 =~ s{\.[^.]+$}{.json}r;
+ my $config_file = path(__FILE__)->basename('.pl') . '.json';
my $conf = decode_json(path($config_file)->slurp_raw);
return Net::Twitter->new(traits=>[
'API::RESTv1_1',
@@ -29,6 +76,13 @@ sub get_twitter {
],%$conf);
}
+# returns a hashref shaped like:
+# {
+# $list_id => {
+# name => $list_name,
+# members => { $user_screen_name => 1, ... },
+# }, ...
+# }
sub fetch_lists_info {
my ($tw) = @_;
@@ -37,7 +91,7 @@ sub fetch_lists_info {
});
my %lists_info = map {
$_->{id} => {
- name => $_->{name},
+ $_->%{qw(name)},
},
} $lists->{lists}->@*;
@@ -49,12 +103,20 @@ sub fetch_lists_info {
include_entities => 0,
});
$lists_info{$list_id}->{members}={
- map { $_->{id} => 1 } $members->{users}->@*,
+ map { $_->{screen_name} => 1 } $members->{users}->@*,
};
}
return \%lists_info;
}
+
+# returns a hashref shaped like:
+# {
+# $user_id => {
+# name => $user_name,
+# screen_name => $user_screen_name,
+# }, ...
+# }
sub fetch_friends_info {
my ($tw) = @_;
@@ -71,7 +133,7 @@ sub fetch_friends_info {
return \%friends_info;
}
-sub cache_file { $0 =~ s{\.[^.]+$}{-cache.json}r };
+sub cache_file { path(__FILE__)->basename('.pl') .'-cache.json' };
sub load_info {
return try { decode_json(path(cache_file)->slurp_raw) };
@@ -85,6 +147,9 @@ sub save_info {
return;
}
+# from { $id => { name => $n, ...}, ... }
+# to ( { id => $id, name => $n, ...}, ... )
+# sorted by name
sub to_list {
sort {
$a->{name} cmp $b->{name}
@@ -93,16 +158,22 @@ sub to_list {
} keys $_[0]->%*
}
+# sets the ->{dn} slot to the name, or the screen name + name
+# returns the maximum length of the dns set
sub set_display_name {
my $max=0;
for my $e ($_[0]->@*) {
- $e->{dn} = sprintf '%s (%d)',$e->@{qw(name id)};
+ $e->{dn} = $e->{screen_name}
+ ? sprintf '@%s (%s)',$e->@{qw(screen_name name)}
+ : sprintf '%s',$e->@{qw(name)};
my $l = length($e->{dn});
$max = $l if $max<$l;
}
return $max;
}
+# given the output of fetch_lists_info and fetch_friends_info
+# prints the text file
sub print_friends_lists_matrix {
my ($li,$fi) = @_;
@@ -115,12 +186,13 @@ sub print_friends_lists_matrix {
printf '%-*s : ',$friend_width,$f->{dn};
print join ', ',
map { $_->{dn} }
- grep { $_->{members}->{$f->{id}} }
+ grep { $_->{members}->{$f->{screen_name}} }
@lists;
print "\n";
}
}
+# given a filename, parses users & lists out of it
sub parse_friends_lists_matrix {
my ($fn) = @_;
@@ -128,30 +200,30 @@ sub parse_friends_lists_matrix {
my %lists;
for my $l (@lines) {
- $l =~ s{\A.*?\((\d+)\)\s+:}{} or next;
- my $friend_id = $1;
+ # the line should start with a @screen_screen,
+ # followed by a name in parentheses
+ $l =~ s{\A\@(\S+)\s+\(.*?\)\s*:}{} or next;
+ my $friend_name = $1;
+ # after the colon there should be a comma-separated list of list names
+ # (the map trims spaces out of each name)
my @lists = map { s{\A\s*(.*?)\s*\z}{$1}r } split ',',$l;
for my $list (@lists) {
- my ($list_name,$list_id) = ($list =~ m{\A(.*?)(?:\s+\((\d+)\))?\z});
- next unless $list_name;
- my $list_data = $lists{$list_name}||={};
- if ($list_id && $list_data->{id}) {
- if ($list_data->{id} != $list_id) {
- warn "List $list_name id conflict for friend $friend_id";
- }
- }
- else {
- $list_data->{id} ||= $list_id;
- }
- $list_data->{members}->{$friend_id} = 1;
+ # we might get some 0-length strings
+ # if there's nothing after the colon
+ next unless $list;
+ # add the user to the members of the list
+ my $list_data = $lists{$list}||={};
+ $list_data->{members}->{$friend_name} = 1;
}
}
return \%lists;
}
+# this is the function that does most of the work
sub make_it_so {
my ($tw,$current_lists_by_id,$wanted_lists) = @_;
+ # we'll need to access the lists by name, let's build that hash
my %current_lists_by_name = map {
$current_lists_by_id->{$_}{name} => {
id => $_,
@@ -159,16 +231,9 @@ sub make_it_so {
};
} keys $current_lists_by_id->%*;
- my @operations;
# first, lists to create
for my $list (keys $wanted_lists->%*) {
- if ($current_lists_by_name{$list}) {
- $wanted_lists->{$list}{id} ||= $current_lists_by_name{$list}{id};
- if ($wanted_lists->{$list}{id} != $current_lists_by_name{$list}{id}) {
- warn "list $list has conflicting ids!\n";
- }
- }
- else {
+ unless ($current_lists_by_name{$list}) {
warn "creating $list\n";
my $list_data;
if ($WRITING) {
@@ -183,6 +248,7 @@ sub make_it_so {
};
}
}
+
# then, set members
for my $list (keys $wanted_lists->%*) {
warn "operating on $list\n";
@@ -196,7 +262,7 @@ sub make_it_so {
if ($WRITING) {
$tw->remove_list_members({
list_id => $current_lists_by_name{$list}{id},
- user_id => \@these,
+ screen_name => \@these,
});
}
}
@@ -206,38 +272,57 @@ sub make_it_so {
if ($WRITING) {
$tw->add_list_members({
list_id => $current_lists_by_name{$list}{id},
- user_id => \@these,
+ screen_name => \@these,
});
}
}
}
}
+# main program!
my $tw = get_twitter;
+
try {
my $lists_info;
my $friends_info;
+
+ # try loading from the cache
my $info = load_info();
if ($info) {
($lists_info,$friends_info) = $info->@{qw(lists friends)},
}
else {
+ # otherwise load from Twitter and save the cache
$lists_info = fetch_lists_info($tw);
$friends_info = fetch_friends_info($tw);
save_info($lists_info,$friends_info);
}
+
+ # something to parse?
if (@ARGV) {
+ # parse it
my $wanted_lists = parse_friends_lists_matrix($ARGV[0]);
+ # and update Twitter
make_it_so($tw,$lists_info,$wanted_lists);
}
else {
+ # no, just fetch and print
print_friends_lists_matrix($lists_info,$friends_info);
}
-} catch {
+}
+catch {
+ # error handling: is it a Twitter exception?
if ($_->$_isa('Net::Twitter::Error')) {
- my $limit = $tw->rate_limit_status;
- p $limit;
+ # print it out
p $_;
+ # have we reached the rate limit?
+ if ($_->code == 429 or (
+ $_->has_twitter_error and $_->twitter_error_code == 88)) {
+ # retrieve the limits and print them
+ my $limit = $tw->rate_limit_status;
+ p $limit;
+ }
}
+ # no, just re-throw
else { local $@=$_;die }
};