#! /usr/bin/perl -wT # Required Modules use strict; use warnings; use Data::Dumper; #use Scalar::Util qw(tainted); # Globals my %parameters = ( username => undef, password => undef, interval => 4, # seconds between tweets poll => 37, # time between polls, no more than 100 / hour exclude => undef, # Array sticky => undef, # Array include => undef, # Array number => 5, retrieve => 200, std_output => 0, #on/off 'format' => '%u: %t', config_file => undef, verbose_level => 0, #0-3 ); my %imgs = (); my $app = 'Growl+Twitter=trowel'; my @names = ('New Tweet'); my $previous_switch = ''; # Extract Parameters foreach my $para (@ARGV) { # Lets use a long if..elsif..else statement if($para =~ m/^\-h/) { &PrintHelp(); exit; } # Help elsif($previous_switch =~ m/^\-g/) { &Verbose("g switch found, loading launchd file: ".$para); &LaunchdLoad($para); exit; } # start launchd elsif($previous_switch =~ m/^\-u/) { &Verbose("setting username to: ".$para); $parameters{username} = $para; } # Twitter Username elsif($previous_switch =~ m/^\-p/) { &Verbose("setting password to: ".$para); $parameters{password} = $para; } # Twitter Password elsif($previous_switch =~ m/^\-i/) { &Verbose("setting interval to: ".$para);$parameters{interval} = $para; } # Tweet display interval elsif($previous_switch =~ m/^\-l/) { &Verbose("setting poll to: ".$para);$parameters{poll} = $para; } # Twitter poll interval elsif($previous_switch =~ m/^\-n/) { &Verbose("setting number to: ".$para);$parameters{number} = $para; } # #elsif($previous_switch =~ m/^\-o/) { $parameters{std_output} = 1; } # Output to STDOUT elsif($previous_switch =~ m/^\-f/) { &Verbose("setting format to: ".$para); $parameters{'format'} = $para; } #format elsif($previous_switch =~ m/^\-I(.*)/) { foreach my $i (split(/,/, $para)) { $parameters{include}{$i} = 1; } } elsif($previous_switch =~ m/^\-x(.*)/) { foreach my $x (split(/,/, $para)) { $parameters{exclude}{$x} = 1; } } elsif($previous_switch =~ m/^\-t(.*)/) { foreach my $t (split(/,/, $para)) { $parameters{sticky}{$t} = 1; } } elsif($para =~ m/^\-[v]{1}\b/) { $parameters{verbose_level} = 1; } # Verbose level 1 elsif($para =~ m/^\-[v]{2}\b/) { $parameters{verbose_level} = 2; } # Verbose level 2 elsif($para =~ m/^\-[v]{3}\b/) { $parameters{verbose_level} = 3; } # Verbose level 3 $previous_switch = $para; } # Overrides #if( $parameters{std_output} && ($parameters{verbose_level} < 3) ) { $parameters{verbose_level} = 0; } # Check for invalied input if(!$parameters{username}) { &PrintUsage(); exit; } #print STDERR "Username not found\n"; elsif(!$parameters{password}) { &PrintUsage(); exit }; #print STDERR "Password not found\n"; # Debug output # foreach my $d (keys %parameters) # { # if($parameters{$d}) # { # if($d eq 'sticky' || $d eq 'exclude' || $d eq 'include') # { # printf("[%d]\t %s : %s\n", $$, $d, join(" | ", keys %{$parameters{$d}})); # } # else # { # printf("[%d]\t %s : %s\n", $$, $d, $parameters{$d}); # } # } # } #exit; #sleep(10); #print STDOUT "Verbose $parameters{verbose_level}\n"; &Verbose("*** Entering poll loop ***"); #print "INCLUDE? : ". ( (scalar(keys %{$parameters{include}})) )."\n"; #print "EXCLUDE? : ". ( (scalar(keys %{$parameters{exclude}})) )."\n"; #print "HUH? : ". !( (scalar(keys %{$parameters{include}})) || (scalar(keys %{$parameters{exclude}})) )."\n"; &Loop(); #start poll loop exit; sub PrintHelp { my $help = 0; while() { if($_ =~ m/^__STOP__/g) { $help = 1; next; } if(!$help) { next; } else { print $_; }; } } sub PrintUsage { while() { if($_ =~ m/^__STOP__/g) { last; } print $_; } } sub Verbose { if($parameters{verbose_level} == 1) { printf("%s\n", $_[0]); } elsif($parameters{verbose_level} == 2) { printf("%d\t%s\n", $$, $_[0]); } elsif($parameters{verbose_level} == 3) { printf("%s %d\t%s\n", __FILE__, $$, $_[0]); } } sub Loop { use Mac::Growl ':all'; use Net::Twitter; use Date::Format; use LWP::Simple qw(get); my $encode = eval { require Encode; }; RegisterNotifications($app, \@names, [$names[0]]); my $tweet = Net::Twitter->new( username=>$parameters{username}, password=>$parameters{password} ); # print Dumper($tweet); my $last_id = &ReadTweetID(); my $switch = 0; &Verbose('Setting tweet id to: '.$last_id); while(1) { my @tt = (); if($last_id) { &Verbose("Get Tweets since ID ".$last_id); @tt = $tweet->friends_timeline({count => $parameters{retrieve}, since_id => $last_id }); } else { &Verbose("Get All tweets"); @tt = $tweet->friends_timeline({count => $parameters{number} }); } #print Dumper(@tt); #print time2str('%a %e %b %Y %H:%M:%S +%Z', time); foreach my $t (@{$tt[0]}) { if(!$switch) { &SaveTweetID($t->{id}); $last_id = $t->{id}; $switch++; }; if($parameters{include}{$t->{user}{screen_name}}) { &Verbose(sprintf("Included\t\t%s: %s", $t->{user}{screen_name}, $t->{text})); &AssemblePostTweet($t); } elsif($parameters{exclude}{$t->{user}{screen_name}}) { &Verbose(sprintf("Excluded\t\t%s: %s", $t->{user}{screen_name}, $t->{text})); #printf("Excluded - %s: %s\n", $t->{user}{screen_name}, $t->{text}); } elsif( !( (scalar(keys %{$parameters{include}})) || (scalar(keys %{$parameters{exclude}})) ) ) { &Verbose(sprintf("Tweet\t\t%s: %s", $t->{user}{screen_name}, $t->{text})); &AssemblePostTweet($t); } # &Verbose("Include: ".); # &Verbose("Excluded: ".!scalar(keys %{$parameters{exclude}})); #&SaveTweetID($t->{id}); } $switch = 0; ##$a = pop(@tt); ##print Dumper($a); # ->{text}."\n"; #print $tt[0][0]->{text}."\n -> "; #print "sleep ".$parameters{poll}."\n"; &Verbose("Wait ".$parameters{poll}." seconds"); sleep($parameters{poll}); } } sub AssemblePostTweet { my $img_path = &DownloadProfileImage($_[0]->{user}{profile_image_url}); my %data = ( username => Encode::encode('utf8', $_[0]->{user}{screen_name}), name => Encode::encode('utf8', $_[0]->{user}{name}), tweet => Encode::encode('utf8', $_[0]->{text}), date => Encode::encode('utf8', $_[0]->{created_at}), location => Encode::encode('utf8', $_[0]->{user}{location}), ); my $notification = $parameters{'format'}; $notification =~ s/%u/$data{username}/; $notification =~ s/%t/$data{tweet}/; $notification =~ s/%d/$data{date}/; $notification =~ s/%l/$data{location}/; $notification =~ s/%n/$data{name}/; if($parameters{std_output}) { printf("%s\n", $notification); } else { PostNotification($app, $names[0], $data{name}, $notification, $parameters{sticky}{$_[0]->{user}{screen_name}}?1:0, 0, $img_path); # PostNotification(appname, name, title, description[, sticky, priority, image_path]); } #print "sleep ".$parameters{interval}."\n"; &Verbose("Wait ".$parameters{interval}." seconds"); sleep($parameters{interval}); } sub DownloadProfileImage { my $img_path = undef; #print "Image: ".$_[0]."\n"; if($imgs{$_[0]}) { $img_path = $imgs{$_[0]}; } else { my $img_data = get($_[0]); my @imgp = split('/', $_[0]); $img_path = '/tmp/'.$imgp[$#imgp]; if($img_path =~ /^([\/\-\@\w.]+)$/) { my $file = $1; if(open(FHANDLE, '>>'.$file)) { print FHANDLE $img_data; close(FHANDLE); $img_path = $file; # $img[$#imgp-1]; } } else { # data should be thrown out die "Bad data in $img_path"; } } return $img_path; } sub LaunchdLoad { $ENV{"PATH"} = "/bin:/usr/local/bin:/usr/bin"; my $file = $_[0]; $file =~ /(.*)/; $file = $1; #print "\n\n$ENV{PATH} : ".tainted($ENV{PATH}); #print "\n$file : ".tainted($file); #print "\n\nStarting launchd\n"; system('launchctl load '.$file); } sub LaunchdUnload { # $ENV{"PATH"} = "/bin:/usr/local/bin:/usr/bin"; # my $file = $_[0]; $file =~ /(.*)/; $file = $1; # # print "\n\n$ENV{PATH} : ".tainted($ENV{PATH}); # print "\n$file : ".tainted($file); # # print "\n\nStarting launchd\n"; # system('launchctl load '.$file); } sub SaveTweetID { if(open(TWEETID, '>/tmp/tweet.id')) { print TWEETID $_[0]; &Verbose('Saving Tweet ID to file: '.$_[0]); close(TWEETID); } } sub ReadTweetID { my $id = 0; if(open(TWEETID, ') { $id = $_; } &Verbose('Reading Tweet ID from file: '.$id); close(TWEETID); } return $id; #first run; } __END__ Usage: trowel -u -p [-p -x -t -I -o -f -g -h -v -vv -vvv] __STOP__ NAME trowel - display Twitter messages with Growl. SYSOPSIS trowel -u -p [options] EXAMPLES trowel -u -p -i5 -l180 -f"%t %d" -ttwitter,stephenfry,rjstelling -Iguykawasaki,twitter,stephenfry,TechCrunch,rjstelling OPTIONS -u, --username Twitter username -p, --password Twitter passord, if omitted it will be requested interactively -i interval between displaying tweets -l, --poll time between polls of Twitter feed -x, --exclude list of users to exclude -t, --sticky list of users who's tweets are sticky, -x and -i will override this -I, --include list of users to include -o output to STDOUT only, by-passing Growl, use this for piping to another application -n initial number of Tweets to request, default is 5 -f"" format of the Tweet %u - user %t - tweet %d - date time %l - location -g, --config a configuration file that sets command line parameters, this function is not implemented -h, --help show this help -v verbose mode -vv very verbose mode -vvv debug verbose mode