$VERSION = "1.1"; - r303 - Make sure we keep the cache of nicks up to date
[twirssi-net-twitter-lite.git] / twirssi.pl
1 use strict;
2 use Irssi;
3 use Irssi::Irc;
4 use Net::Twitter;
5 use HTTP::Date;
6 use HTML::Entities;
7 use File::Temp;
8
9 use vars qw($VERSION %IRSSI);
10 use constant { DEBUG => 0 };
11
12 $VERSION = "1.1";
13 my ($REV) = '$Rev: 303 $' =~ /(\d+)/;
14 %IRSSI   = (
15     authors     => 'Dan Boger',
16     contact     => 'zigdon@gmail.com',
17     name        => 'twirssi',
18     description => 'Send twitter updates using /tweet.  '
19       . 'Can optionally set your bitlbee /away message to same',
20     license => 'GNU GPL v2',
21     url     => 'http://tinyurl.com/twirssi',
22     changed => 'Mon Dec  1 15:36:01 PST 2008',
23 );
24
25 my $window;
26 my $twit;
27 my $user;
28 my $poll;
29 my %nicks;
30 my %friends;
31 my $last_poll = time - 300;
32
33 sub cmd_direct {
34     my ( $data, $server, $win ) = @_;
35
36     unless ($twit) {
37         &notice("Not logged in!  Use /twitter_login username pass!");
38         return;
39     }
40
41     my ( $target, $text ) = split ' ', $data, 2;
42     unless ( $target and $text ) {
43         &notice("Usage: /dm <nick> <message>");
44         return;
45     }
46
47     unless ( $twit->new_direct_message( { user => $target, text => $text } ) ) {
48         &notice("DM to $target failed");
49         return;
50     }
51
52     &notice("DM sent to $target");
53     $nicks{$target} = time;
54 }
55
56 sub cmd_tweet {
57     my ( $data, $server, $win ) = @_;
58
59     unless ($twit) {
60         &notice("Not logged in!  Use /twitter_login username pass!");
61         return;
62     }
63
64     $data =~ s/^\s+|\s+$//;
65     unless ($data) {
66         &notice("Usage: /tweet <update>");
67         return;
68     }
69
70     foreach my $url ( $data =~ /(https?:\/\/\S+[\w\/])/g ) {
71         eval { my $short = makeashorterlink($url); $data =~ s/\Q$url/$short/g; };
72     }
73
74     unless ( $twit->update($data) ) {
75         &notice("Update failed");
76         return;
77     }
78
79     foreach ( $data =~ /@([-\w]+)/ ) {
80         $nicks{$1} = time;
81     }
82
83     my $away = 0;
84     if (    Irssi::settings_get_bool("tweet_to_away")
85         and $data !~ /\@\w/
86         and $data !~ /^[dD] / )
87     {
88         my $server =
89           Irssi::server_find_tag( Irssi::settings_get_str("bitlbee_server") );
90         if ($server) {
91             $server->send_raw("away :$data");
92             $away = 1;
93         } else {
94             &notice( "Can't find bitlbee server.",
95                 "Update bitlbee_server or disalbe tweet_to_away" );
96         }
97     }
98
99     &notice( "Update sent" . ( $away ? " (and away msg set)" : "" ) );
100 }
101
102 sub gen_cmd {
103     my ( $usage_str, $api_name, $post_ref ) = @_;
104
105     return sub {
106         my ( $data, $server, $win ) = @_;
107
108         unless ($twit) {
109             &notice("Not logged in!  Use /twitter_login username pass!");
110             return;
111         }
112
113         $data =~ s/^\s+|\s+$//;
114         unless ($data) {
115             &notice("Usage: $usage_str");
116             return;
117         }
118
119         unless ( $twit->$api_name($data) ) {
120             &notice("$api_name failed");
121             return;
122         }
123
124         &$post_ref($data) if $post_ref;
125       }
126 }
127
128 sub cmd_login {
129     my ( $data, $server, $win ) = @_;
130     my $pass;
131     ( $user, $pass ) = split ' ', $data, 2;
132
133     %friends = %nicks = ();
134
135     $twit = Net::Twitter->new(
136         username => $user,
137         password => $pass,
138         source   => "twirssi"
139     );
140
141     unless ( $twit->verify_credentials() ) {
142         &notice("Login failed");
143         $twit = undef;
144         return;
145     }
146
147     if ($twit) {
148         Irssi::timeout_remove($poll) if $poll;
149         $poll = Irssi::timeout_add( 300 * 1000, \&get_updates, "" );
150         &notice("Logged in as $user, loading friends list...");
151         &load_friends;
152         &notice( "loaded friends: ", scalar keys %nicks );
153         %nicks = %friends;
154         $nicks{$user} = 0;
155         &get_updates;
156     } else {
157         &notice("Login failed");
158     }
159 }
160
161 sub load_friends {
162     my $page = 1;
163     my %new_friends;
164     while (1) {
165         my $friends = $twit->friends( { page => $page } );
166         last unless $friends;
167         $new_friends{ $_->{screen_name} } = time
168           foreach @$friends;
169         $page++;
170         last if @$friends == 0 or $page == 10;
171         $friends = $twit->friends( page => $page );
172     }
173
174     foreach (keys %new_friends) {
175       next if exists $friends{$_};
176       $friends{$_} = time;
177     }
178
179     foreach (keys %friends) {
180       delete $friends{$_} unless exists $new_friends{$_};
181     }
182 }
183
184 sub get_updates {
185     $window =
186       Irssi::window_find_name( Irssi::settings_get_str('twitter_window') );
187     unless ($window) {
188         Irssi::active_win()
189           ->print( "Can't find a window named '"
190               . Irssi::settings_get_str('twitter_window')
191               . "'.  Create it or change the value of twitter_window" );
192     }
193     unless ($twit) {
194         &notice("Not logged in!  Use /twitter_login username pass!");
195         return;
196     }
197
198     my ( $fh, $filename ) = File::Temp::tempfile();
199     my $pid = fork();
200
201     if ($pid) {    # parent
202         Irssi::timeout_add_once( 5000, 'monitor_child', [$filename] );
203     } elsif ( defined $pid ) {    # child
204         close STDIN;
205         close STDOUT;
206         close STDERR;
207
208         my $new_poll = time;
209
210         print scalar localtime, " - Polling for updates" if DEBUG;
211         my $tweets = $twit->friends_timeline(
212             { since => HTTP::Date::time2str($last_poll) } )
213           || [];
214         foreach my $t ( reverse @$tweets ) {
215             my $text = decode_entities( $t->{text} );
216             $text =~ s/%/%%/g;
217             $text =~ s/(^|\W)\@([-\w]+)/$1%B\@$2%n/g;
218             my $prefix = "";
219             if (    Irssi::settings_get_bool("show_reply_context")
220                 and $t->{in_reply_to_screen_name} ne $user
221                 and $t->{in_reply_to_screen_name}
222                 and not exists $friends{ $t->{in_reply_to_screen_name} } )
223             {
224                 $nicks{ $t->{in_reply_to_screen_name} } = time;
225                 my $context = $twit->show_status( $t->{in_reply_to_status_id} );
226                 if ($context) {
227                     my $ctext = decode_entities( $context->{text} );
228                     $ctext =~ s/%/%%/g;
229                     $ctext =~ s/(^|\W)\@([-\w]+)/$1%B\@$2%n/g;
230                     printf $fh "[%%B\@%s%%n] %s\n",
231                       $context->{user}{screen_name}, $ctext;
232                     $prefix = "\--> ";
233                 }
234             }
235             next
236               if $t->{user}{screen_name} eq $user
237                   and not Irssi::settings_get_bool("show_own_tweets");
238             printf $fh "%s[%%B\@%s%%n] %s\n", $prefix, $t->{user}{screen_name},
239               $text;
240         }
241
242         print scalar localtime, " - Polling for replies" if DEBUG;
243         $tweets =
244           $twit->replies( { since => HTTP::Date::time2str($last_poll) } )
245           || [];
246         foreach my $t ( reverse @$tweets ) {
247             next
248               if exists $friends{ $t->{user}{screen_name} };
249
250             my $text = decode_entities( $t->{text} );
251             $text =~ s/%/%%/g;
252             $text =~ s/(^|\W)\@([-\w]+)/$1%B\@$2%n/g;
253             printf $fh "[%%B\@%s%%n] %s\n", $t->{user}{screen_name}, $text;
254         }
255
256         print scalar localtime, " - Polling for DMs" if DEBUG;
257         $tweets = $twit->direct_messages(
258             { since => HTTP::Date::time2str($last_poll) } )
259           || [];
260         foreach my $t ( reverse @$tweets ) {
261             my $text = decode_entities( $t->{text} );
262             $text =~ s/%/%%/g;
263             $text =~ s/(^|\W)\@([-\w]+)/$1%B\@$2%n/g;
264             printf $fh "[%%B\@%s%%n (%%WDM%%n)] %s\n", $t->{sender_screen_name},
265               $text;
266         }
267         print scalar localtime, " - Done" if DEBUG;
268         print $fh "__friends__\n";
269         &load_friends;
270         foreach (sort keys %friends) {
271           print $fh "$_ $friends{$_}\n";
272         }
273         print $fh $new_poll;
274         close $fh;
275         exit;
276     }
277 }
278
279 sub monitor_child {
280     my $data     = shift;
281     my $filename = $data->[0];
282
283     print scalar localtime, " - checking child log at $filename" if DEBUG;
284     if ( open FILE, $filename ) {
285         my @lines;
286         while (<FILE>) {
287           chomp;
288           last if /^__friends__/;
289           push @lines, $_ unless /^__friends__/;
290         }
291
292         %friends = ();
293         while (<FILE>) {
294           if (/^\d+$/) {
295             $last_poll = $_;
296             last;
297           }
298           my ($f, $t) = split ' ', $_;
299           $nicks{$f} = $friends{$f} = $t;
300         }
301
302         print "new last_poll = $last_poll" if DEBUG;
303         foreach my $line (@lines) {
304             chomp $line;
305             $window->print( $line, MSGLEVEL_PUBLIC );
306             foreach ( $line =~ /\@([-\w]+)/ ) {
307                 $nicks{$1} = time;
308             }
309         }
310
311         close FILE;
312         unlink $filename or warn "Failed to remove $filename: $!";
313         return;
314     }
315
316     Irssi::timeout_add_once( 5000, 'monitor_child', [$filename] );
317 }
318
319 sub notice {
320     $window->print( "%R***%n @_", MSGLEVEL_PUBLIC );
321 }
322
323 sub sig_complete {
324     my ( $complist, $window, $word, $linestart, $want_space ) = @_;
325
326     return unless $linestart =~ /^\/(?:tweet|dm)/;
327     return if $linestart eq '/tweet' and $word !~ s/^@//;
328     push @$complist, grep /^\Q$word/i,
329       sort { $nicks{$b} <=> $nicks{$a} } keys %nicks;
330     @$complist = map { "\@$_" } @$complist if $linestart eq '/tweet';
331 }
332
333 Irssi::settings_add_str( "twirssi", "twitter_window",     "twitter" );
334 Irssi::settings_add_str( "twirssi", "bitlbee_server",     "bitlbee" );
335 Irssi::settings_add_str( "twirssi", "short_url_provider", "TinyURL" );
336 Irssi::settings_add_bool( "twirssi", "tweet_to_away",      0 );
337 Irssi::settings_add_bool( "twirssi", "show_reply_context", 0 );
338 Irssi::settings_add_bool( "twirssi", "show_own_tweets",    1 );
339 $window = Irssi::window_find_name( Irssi::settings_get_str('twitter_window') );
340 if ($window) {
341     Irssi::command_bind( "dm",            "cmd_direct" );
342     Irssi::command_bind( "tweet",         "cmd_tweet" );
343     Irssi::command_bind( "twitter_login", "cmd_login" );
344     Irssi::command_bind(
345         "twirssi_version",
346         sub {
347             &notice(
348                 "Twirssi v$VERSION (r$REV).  See details at http://tinyurl.com/twirssi"
349             );
350         }
351     );
352     Irssi::command_bind(
353         "twitter_friend",
354         &gen_cmd(
355             "/twitter_friend <username>",
356             "create_friend",
357             sub { &notice("Following $_[0]"); $nicks{$_[0]} = time; }
358         )
359     );
360     Irssi::command_bind(
361         "twitter_unfriend",
362         &gen_cmd(
363             "/twitter_unfriend <username>",
364             "destroy_friend",
365             sub { &notice("Stopped following $_[0]"); delete $nicks{$_[0]}; }
366         )
367     );
368     Irssi::command_bind( "twitter_updates", "get_updates" );
369     Irssi::signal_add_last( 'complete word' => \&sig_complete );
370
371     &notice("  %Y<%C(%B^%C)%N                   TWIRSSI v%R$VERSION%N (r$REV)");
372     &notice("   %C(_(\\%N        http://tinyurl.com/twirssi for full docs");
373     &notice( "    %Y||%C `%N Log in with /twitter_login, send updates with /tweet");
374
375     if ( my $provider = Irssi::settings_get_str("short_url_provider") ) {
376         eval "use WWW::Shorten::$provider;";
377
378         if ($@) {
379             &notice(
380 "Failed to load WWW::Shorten::$provider - either clear short_url_provider or install the CPAN module"
381             );
382         }
383     }
384 } else {
385     Irssi::active_win()
386       ->print( "Create a window named "
387           . Irssi::settings_get_str('twitter_window')
388           . " or change the value of twitter_window.  Then, reload twirssi." );
389 }
390