use strict;
use warnings;
use 5.020;
use experimental 'postderef';
use Net::Twitter;
use Path::Tiny;
use JSON;
use Try::Tiny;
use Safe::Isa;
use open ':std',':locale';
use Data::Printer;
my $WRITING=$ENV{TWITTER_WRITE} // 0;
sub get_twitter {
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',
'AutoCursor',
'AutoCursor' => {
max_calls => 15,
force_cursor => 1,
array_accessor => 'users',
methods => [qw(friends followers)],
},
],%$conf);
}
sub fetch_lists_info {
my ($tw) = @_;
my $lists = $tw->list_ownerships({
count => 200,
});
my %lists_info = map {
$_->{id} => {
$_->%{qw(name)},
},
} $lists->{lists}->@*;
for my $list_id (sort keys %lists_info) {
my $members = $tw->list_members({
list_id => $list_id,
count => 2000,
skip_status => 1,
include_entities => 0,
});
$lists_info{$list_id}->{members}={
map { $_->{screen_name} => 1 } $members->{users}->@*,
};
}
return \%lists_info;
}
sub fetch_friends_info {
my ($tw) = @_;
my $friends = $tw->friends({
count => 200,
skip_status => 1,
include_user_entities => 0,
});
my %friends_info = map {
$_->{id} => { $_->%{qw(name screen_name)} }
} $friends->@*;
return \%friends_info;
}
sub cache_file { path(__FILE__)->basename('.pl') .'-cache.json' };
sub load_info {
return try { decode_json(path(cache_file)->slurp_raw) };
}
sub save_info {
path(cache_file)->spew_raw(encode_json({
lists=>$_[0],
friends=>$_[1],
}));
return;
}
sub to_list {
sort {
$a->{name} cmp $b->{name}
} map {
{ id => $_, $_[0]->{$_}->%* }
} keys $_[0]->%*
}
sub set_display_name {
my $max=0;
for my $e ($_[0]->@*) {
$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;
}
sub print_friends_lists_matrix {
my ($li,$fi) = @_;
my @lists = to_list($li);
set_display_name(\@lists);
my @friends = to_list($fi);
my $friend_width = set_display_name(\@friends);
for my $f (@friends) {
printf '%-*s : ',$friend_width,$f->{dn};
print join ', ',
map { $_->{dn} }
grep { $_->{members}->{$f->{screen_name}} }
@lists;
print "\n";
}
}
sub parse_friends_lists_matrix {
my ($fn) = @_;
my @lines = path($fn)->lines_utf8;
my %lists;
for my $l (@lines) {
$l =~ s{\A\@(\S+)\s+\(.*?\)\s*:}{} or next;
my $friend_name = $1;
my @lists = map { s{\A\s*(.*?)\s*\z}{$1}r } split ',',$l;
for my $list (@lists) {
next unless $list;
my $list_data = $lists{$list}||={};
$list_data->{members}->{$friend_name} = 1;
}
}
return \%lists;
}
sub make_it_so {
my ($tw,$current_lists_by_id,$wanted_lists) = @_;
my %current_lists_by_name = map {
$current_lists_by_id->{$_}{name} => {
id => $_,
$current_lists_by_id->{$_}->%*,
};
} keys $current_lists_by_id->%*;
for my $list (keys $wanted_lists->%*) {
unless ($current_lists_by_name{$list}) {
warn "creating $list\n";
my $list_data;
if ($WRITING) {
$list_data = $tw->create_list({name=>$list,mode=>'private'});
}
else {
$list_data = { id => int(rand(10000)) };
}
$current_lists_by_name{$list} = {
$list_data->%{id},
members => {},
};
}
}
for my $list (keys $wanted_lists->%*) {
warn "operating on $list\n";
my %current_members = $current_lists_by_name{$list}->{members}->%*;
my %wanted_members = $wanted_lists->{$list}{members}->%*;
my %to_add = %wanted_members; delete @to_add{keys %current_members};
my %to_remove = %current_members; delete @to_remove{keys %wanted_members};
my @to_remove=keys %to_remove;
while (my @these = splice @to_remove,0,100) {
warn "Removing @these from $list\n";
if ($WRITING) {
$tw->remove_list_members({
list_id => $current_lists_by_name{$list}{id},
screen_name => \@these,
});
}
}
my @to_add=keys %to_add;
while (my @these = splice @to_add,0,100) {
warn "Adding @these to $list\n";
if ($WRITING) {
$tw->add_list_members({
list_id => $current_lists_by_name{$list}{id},
screen_name => \@these,
});
}
}
}
}
my $tw = get_twitter;
try {
my $lists_info;
my $friends_info;
my $info = load_info();
if ($info) {
($lists_info,$friends_info) = $info->@{qw(lists friends)},
}
else {
$lists_info = fetch_lists_info($tw);
$friends_info = fetch_friends_info($tw);
save_info($lists_info,$friends_info);
}
if (@ARGV) {
my $wanted_lists = parse_friends_lists_matrix($ARGV[0]);
make_it_so($tw,$lists_info,$wanted_lists);
}
else {
print_friends_lists_matrix($lists_info,$friends_info);
}
}
catch {
if ($_->$_isa('Net::Twitter::Error')) {
p $_;
if ($_->code == 429 or (
$_->has_twitter_error and $_->twitter_error_code == 88)) {
my $limit = $tw->rate_limit_status;
p $limit;
}
}
else { local $@=$_;die }
};