It’s been well-over a year since I hacked the the first few lines of code for Trowel, the open source, Perl solution to your Twitter and Growl needs.
I must confess there was no great plan I just needed a solution, but looking at the comments the above posts generated, so did many of you.
Now Roland van Ipenburg has cleaned up the original code you can download the latest version. But for completeness I have reproduced it below.
#!/usr/bin/perl -w
# -*- cperl; cperl-indent-level: 4 -*-
use strict;
use warnings;
## no critic qw(ProhibitLongLines)
# $Id$
# $Revision$
# $HeadURL$
# $Date$
## use critic
use utf8;
use 5.008000;
our $VERSION = '0.01';
use Date::Format;
use Digest::MD5;
use Encode;
use File::HomeDir;
use File::Slurp;
use File::Spec;
use Getopt::Long qw(:config no_ignore_case);
use IO::File;
use Imager;
use LWP::Simple;
use Log::Log4perl qw(:easy get_logger);
use Mac::Growl;
use Net::Twitter;
use Pod::Usage;
use Set::Scalar;
use Readonly ();
## no critic qw(prohibitCallsToUnexportedSubs)
Readonly::Scalar my $EMPTY => q{};
Readonly::Scalar my $NEWLINE => qq{\n};
Readonly::Scalar my $COMMA => q{,};
Readonly::Scalar my $ESCAPE => q{%};
Readonly::Scalar my $APPLICATION => q{Growl+Twitter=trowel};
Readonly::Scalar my $NOTIFICATION_NAME => q{New Tweet};
Readonly::Scalar my $DEFAULT_FORMAT => q{%u: %t};
Readonly::Scalar my $MAX_TWEETS => 200;
Readonly::Scalar my $MAX_TWEETS_INIT => 5;
Readonly::Scalar my $DISPLAY_INTERVAL => 4;
Readonly::Scalar my $POLL_INTERVAL => 37;
Readonly::Scalar my $AVATAR_WIDTH => 32;
Readonly::Scalar my $AVATAR_HEIGHT => $AVATAR_WIDTH;
Readonly::Scalar my $CACHE_DIR => File::Spec->catdir( File::HomeDir->my_home(),
qw{Library Caches GrowlHelperApp} );
Readonly::Scalar my $TID_FILE => q{tweet.id};
Readonly::Scalar my $ENCODING => q{utf8};
Readonly::Scalar my $AVATAR_TYPE => q{jpeg};
Readonly::Scalar my $ERR_MODE_SLURP => q{quiet};
Readonly::Array my @CSV_OPTIONS => qw(exclude include sticky);
Readonly::Array my @NET_TWITTER_TRAITS => qw(InflateObjects Legacy);
## use critic
Log::Log4perl->easy_init($INFO);
my $log = get_logger();
my %options = (
format => $DEFAULT_FORMAT,
poll => $POLL_INTERVAL,
interval => $DISPLAY_INTERVAL,
);
foreach my $csv (@CSV_OPTIONS) {
$options{$csv} = [];
}
Getopt::Long::GetOptions(
\%options, q{username|s=s},
q{password|p=s}, q{interval|i=i},
q{poll|l=i}, q{exclude|x=s@},
q{sticky|t=s@}, q{include|I=s@},
q{output|o}, qq{initials|n:$MAX_TWEETS_INIT},
q{format|f=s}, q{config|g=s},
qq{retrieve|r:$MAX_TWEETS}, q{help|h},
q{verbose+}, q{exclude_self|X},
qq{width:$AVATAR_WIDTH}, qq{height:$AVATAR_HEIGHT},
q{man},
) or Pod::Usage::pod2usage(2);
$options{help} && Pod::Usage::pod2usage(1);
$options{man} && Pod::Usage::pod2usage( -verbose => 2 );
# Gather comma seperated items or items from multiple options into a set:
foreach my $csv (@CSV_OPTIONS) {
$options{$csv} =
Set::Scalar->new( split /$COMMA/xsm, join $COMMA, @{ $options{$csv} } );
}
$options{exclude_self} && $options{exclude}->insert( $options{username} );
my @names = ($NOTIFICATION_NAME);
$log->debug('Register notifications');
## no critic qw(prohibitCallsToUnexportedSubs)
Mac::Growl::RegisterNotifications( $APPLICATION, \@names, [ $names[0] ] );
## use critic
my $twitter = Net::Twitter->new(
username => $options{username},
password => $options{password},
traits => \@NET_TWITTER_TRAITS,
);
my $last_id =
## no critic qw(prohibitCallsToUnexportedSubs)
File::Slurp::read_file( File::Spec->catfile( $CACHE_DIR, $TID_FILE ),
err_mode => $ERR_MODE_SLURP );
## use critic
my $switch = 0;
$log->debug('Entering poll loop');
while (1) {
$log->debug('Getting timeline');
my $ar_timeline = $twitter->friends_timeline(
$last_id
? { count => $MAX_TWEETS, since_id => $last_id }
: { count => $options{initials} }
);
while ( my $tweet = shift @{$ar_timeline} ) {
$log->debug( 'Processing tweet ' . $tweet->id );
if ( !$switch ) {
$last_id = $tweet->id;
## no critic qw(prohibitCallsToUnexportedSubs)
File::Slurp::write_file(
File::Spec->catfile( $CACHE_DIR, $TID_FILE ), $last_id );
## use critic
$switch++;
}
next if ( skippable($tweet) );
my %tweet_data = get_data($tweet);
if ( $options{output} ) {
## no critic qw(RequireCheckedSyscalls)
print $tweet_data{body}, $NEWLINE;
## use critic
}
else {
growl( $tweet, \%tweet_data );
}
}
$switch = 0;
$log->debug( 'Wait ' . $options{poll} . ' seconds' );
sleep $options{poll};
}
sub skippable {
my $tweet = shift;
return ( $options{exclude}->has( $tweet->user->screen_name )
|| !$options{include}->is_null )
&& !$options{include}->has( $tweet->user->screen_name )
&& !$options{sticky}->has( $tweet->user->screen_name );
}
sub get_data {
my $tweet = shift;
my %tweet_data = (
n => $tweet->user->name,
u => $tweet->user->screen_name,
t => $tweet->text,
d => $tweet->created_at,
l => $tweet->user->location,
);
while ( my ( $key => $value ) = each %tweet_data ) {
( defined $value )
&& ( $tweet_data{$key} = Encode::encode( $ENCODING, $value ) );
}
$tweet_data{$ESCAPE} = $ESCAPE;
my $map =
qq{(?
$log->debug( 'Using map ' . $map );
my $re = qr{$map}imsx;
$tweet_data{body} = $options{format};
## no critic qw(ProhibitUselessRegexModifiers RequireLineBoundaryMatching)
$tweet_data{body} =~ s/$re/$tweet_data{$1}/gxs;
## use critic
return %tweet_data;
}
sub growl {
my ( $tweet, $hr_tweet_data ) = @_;
my $avatar = get_avatar( $tweet, $hr_tweet_data );
## no critic qw(prohibitCallsToUnexportedSubs)
Mac::Growl::PostNotification(
$APPLICATION,
$names[0],
## no critic qw(ProhibitAccessOfPrivateData)
$hr_tweet_data->{n},
$hr_tweet_data->{body},
## use critic
$options{sticky}->has( $tweet->user->screen_name ) ? 1 : 0,
0,
$avatar
);
## use critic
sleep $options{interval};
return;
}
sub get_avatar {
my ( $tweet, $hr_tweet_data ) = @_;
my $avatar_url = $tweet->user->profile_image_url->as_string;
my $avatar_file =
File::Spec->catfile( ($CACHE_DIR), Digest::MD5::md5_hex($avatar_url) );
$log->debug($avatar_file);
my $fh = IO::File->new();
if ( !$fh->open(qq{< $avatar_file}) ) {
LWP::Simple::mirror( $avatar_url, $avatar_file );
my $img = Imager->new( file => $avatar_file );
my $thumb = $img->scale(
xpixels => $options{width},
ypixels => $options{height}
);
$thumb->write( file => $avatar_file, type => $AVATAR_TYPE );
}
$fh->close();
return $avatar_file;
}
exit;
__END__
=encoding utf8
=head1 NAME
trowel - display Twitter messages with Growl.
=head1 VERSION
This is version 0.01. It's based on
L
=head1 SYNOPSIS
trowel -u -p [options]
=head1 DESCRIPTION
Shows the tweets entering a users Twitter timeline as Growl notifications,
including the avatar of the tweep. The format is the message in the
notification is configurable and the avatars are scaled to fit in the standard
Smoke Theme and cached locally.
=head1 DEPENDENCIES
L
L
L
L
L
L
L
L
L
L
L
L
L
L
L
Issue the following command in a Terminal to install these modules:
sudo /usr/bin/cpan -i Date::Format Digest::MD5 Encode File::Slurp \\
File::Spec Getopt::Long IO::File Imager LWP::Simple Log::Log4perl \\
Mac::Growl Net::Twitter Pod::Usage Set::Scalar Readonly \\
&& sudo /usr/bin/cpan -fi File::HomeDir
=head1 INCOMPATIBILITIES
=over 4
=item * File::HomeDir fails tests when it is being installed as root because
the user root doesn't have some special Folders only normal users have. It can
be installed as root by forcing the install with the -fi option.
=back
=head1 DIAGNOSTICS
This module uses Log::Log4perl for logging.
=head1 BUGS AND LIMITATIONS
=over 4
=item * This script aims to be compatible with the original version or
trowel, but it it not bug-compatible
=item * A missing password option isn't interactively requested later
=item * The format processing is improved so C<%%> can be used to display a
single C<%> and substitutes containing formats aren't clobbered.
=item * The location is not the location of the tweet, but of the account
=back
=head1 CONFIGURATION
To use this script you'll need an account at the Twitter micro-blogging
service. The avatars used are scaled to 32x32 to fit as graphic in the default
Smoke theme of Growl. The scaled avatars and the file containing the id of the
most recent tweet displayed are stored in the cache folder of GrowlhelperApp
in L<~/Library/Caches>.
=head1 USAGE
trowel -u
-h -v -vv -vvv]
=head1 REQUIRED ARGUMENTS
=over 4
=item * B<-u> B<--username> The username of the Twitter account to connect to
=item * B<-p> B<--password> The password of the Twitter account to connect to
=back
=head1 OPTIONS
=over 4
=item B<-u> B<--username> Twitter username
=item B<-p> B<--password> Twitter password
=item B<-i> interval between displaying tweets
=item B<-l> time between polls of Twitter feed
=item B<-x> list of users to exclude
=item B<-X> B<--exclude-self> exclude yourself
=item B<-t> list of users who's tweets are sticky, -x and -i will override
this
=item B<-l> list of users to include
=item B<-o> output to STDOUT only, by-passing Growl, use this for piping to
another application
=item B<-n> initial number of Tweets to request, default is 5
=item B<-f"
%u - user
%t - tweet
%d - date time
%l - location
=item B<-g> B<--config> a configuration file that sets command line
parameters, this function is not implemented
=item B<-h> B<-help>
=item B<-v> verbose mode
=item B<-vv> very verbose mode
=item B<-vvv> debug verbose mode
=item B<-man>
=back
=head1 EXIT STATUS
The exit status is determined by L
=over 4
=item * 1
=item * 2
=back
=head1 EXAMPLES
trowel -u
-ttwitter,stephenfry,rjstelling
-Iguykawasaki,twitter,stephenfry,TechCrunch,rjstelling
=head1 AUTHOR
=over 4
=item * Roland van Ipenburg C<<
=item * Echotech L
=back
=head1 LICENSE AND COPYRIGHT
Copyright (C) 2009 by Roland van Ipenburg
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.10.0 or,
at your option, any later version of Perl 5 you may have available.
=head1 DISCLAIMER OF WARRANTY
BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
NECESSARY SERVICING, REPAIR, OR CORRECTION.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENSE, BE
LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
=cut