v1.1, r302 - Fix a bug in async friends updates
[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: 302 $' =~ /(\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{$user} = 0;
154         &get_updates;
155     } else {
156         &notice("Login failed");
157     }
158 }
159
160 sub load_friends {
161     my $page = 1;
162     my %new_friends;
163     while (1) {
164         my $friends = $twit->friends( { page => $page } );
165         last unless $friends;
166         $new_friends{ $_->{screen_name} } = $nicks{ $_->{screen_name} } = time
167           foreach @$friends;
168         $page++;
169         last if @$friends == 0 or $page == 10;
170         $friends = $twit->friends( page => $page );
171     }
172
173     foreach (keys %new_friends) {
174       next if exists $friends{$_};
175       $friends{$_} = time;
176     }
177
178     foreach (keys %friends) {
179       delete $friends{$_} unless exists $new_friends{$_};
180     }
181 }
182
183 sub get_updates {
184     $window =
185       Irssi::window_find_name( Irssi::settings_get_str('twitter_window') );
186     unless ($window) {
187         Irssi::active_win()
188           ->print( "Can't find a window named '"
189               . Irssi::settings_get_str('twitter_window')
190               . "'.  Create it or change the value of twitter_window" );
191     }
192     unless ($twit) {
193         &notice("Not logged in!  Use /twitter_login username pass!");
194         return;
195     }
196
197     my ( $fh, $filename ) = File::Temp::tempfile();
198     my $pid = fork();
199
200     if ($pid) {    # parent
201         Irssi::timeout_add_once( 5000, 'monitor_child', [$filename] );
202     } elsif ( defined $pid ) {    # child
203         close STDIN;
204         close STDOUT;
205         close STDERR;
206
207         my $new_poll = time;
208
209         print scalar localtime, " - Polling for updates" if DEBUG;
210         my $tweets = $twit->friends_timeline(
211             { since => HTTP::Date::time2str($last_poll) } )
212           || [];
213         foreach my $t ( reverse @$tweets ) {
214             my $text = decode_entities( $t->{text} );
215             $text =~ s/%/%%/g;
216             $text =~ s/(^|\W)\@([-\w]+)/$1%B\@$2%n/g;
217             my $prefix = "";
218             if (    Irssi::settings_get_bool("show_reply_context")
219                 and $t->{in_reply_to_screen_name} ne $user
220                 and $t->{in_reply_to_screen_name}
221                 and not exists $friends{ $t->{in_reply_to_screen_name} } )
222             {
223                 $nicks{ $t->{in_reply_to_screen_name} } = time;
224                 my $context = $twit->show_status( $t->{in_reply_to_status_id} );
225                 if ($context) {
226                     my $ctext = decode_entities( $context->{text} );
227                     $ctext =~ s/%/%%/g;
228                     $ctext =~ s/(^|\W)\@([-\w]+)/$1%B\@$2%n/g;
229                     printf $fh "[%%B\@%s%%n] %s\n",
230                       $context->{user}{screen_name}, $ctext;
231                     $prefix = "\--> ";
232                 }
233             }
234             next
235               if $t->{user}{screen_name} eq $user
236                   and not Irssi::settings_get_bool("show_own_tweets");
237             printf $fh "%s[%%B\@%s%%n] %s\n", $prefix, $t->{user}{screen_name},
238               $text;
239         }
240
241         print scalar localtime, " - Polling for replies" if DEBUG;
242         $tweets =
243           $twit->replies( { since => HTTP::Date::time2str($last_poll) } )
244           || [];
245         foreach my $t ( reverse @$tweets ) {
246             next
247               if exists $friends{ $t->{user}{screen_name} };
248
249             my $text = decode_entities( $t->{text} );
250             $text =~ s/%/%%/g;
251             $text =~ s/(^|\W)\@([-\w]+)/$1%B\@$2%n/g;
252             printf $fh "[%%B\@%s%%n] %s\n", $t->{user}{screen_name}, $text;
253         }
254
255         print scalar localtime, " - Polling for DMs" if DEBUG;
256         $tweets = $twit->direct_messages(
257             { since => HTTP::Date::time2str($last_poll) } )
258           || [];
259         foreach my $t ( reverse @$tweets ) {
260             my $text = decode_entities( $t->{text} );
261             $text =~ s/%/%%/g;
262             $text =~ s/(^|\W)\@([-\w]+)/$1%B\@$2%n/g;
263             printf $fh "[%%B\@%s%%n (%%WDM%%n)] %s\n", $t->{sender_screen_name},
264               $text;
265         }
266         print scalar localtime, " - Done" if DEBUG;
267         print $fh "__friends__\n";
268         &load_friends;
269         foreach (sort keys %friends) {
270           print $fh "$_ $friends{$_}\n";
271         }
272         print $fh $new_poll;
273         close $fh;
274         exit;
275     }
276 }
277
278 sub monitor_child {
279     my $data     = shift;
280     my $filename = $data->[0];
281
282     print scalar localtime, " - checking child log at $filename" if DEBUG;
283     if ( open FILE, $filename ) {
284         my @lines;
285         while (<FILE>) {
286           chomp;
287           last if /^__friends__/;
288           push @lines, $_ unless /^__friends__/;
289         }
290
291         %friends = ();
292         while (<FILE>) {
293           if (/^\d+$/) {
294             $last_poll = $_;
295             last;
296           }
297           my ($f, $t) = split ' ', $_;
298           $friends{$f} = $t;
299         }
300
301         print "new last_poll = $last_poll" if DEBUG;
302         foreach my $line (@lines) {
303             chomp $line;
304             $window->print( $line, MSGLEVEL_PUBLIC );
305             foreach ( $line =~ /\@([-\w]+)/ ) {
306                 $nicks{$1} = time;
307             }
308         }
309
310         close FILE;
311         unlink $filename or warn "Failed to remove $filename: $!";
312         return;
313     }
314
315     Irssi::timeout_add_once( 5000, 'monitor_child', [$filename] );
316 }
317
318 sub notice {
319     $window->print( "%R***%n @_", MSGLEVEL_PUBLIC );
320 }
321
322 sub sig_complete {
323     my ( $complist, $window, $word, $linestart, $want_space ) = @_;
324
325     return unless $linestart =~ /^\/(?:tweet|dm)/;
326     return if $linestart eq '/tweet' and $word !~ s/^@//;
327     push @$complist, grep /^\Q$word/i,
328       sort { $nicks{$b} <=> $nicks{$a} } keys %nicks;
329     @$complist = map { "\@$_" } @$complist if $linestart eq '/tweet';
330 }
331
332 Irssi::settings_add_str( "twirssi", "twitter_window",     "twitter" );
333 Irssi::settings_add_str( "twirssi", "bitlbee_server",     "bitlbee" );
334 Irssi::settings_add_str( "twirssi", "short_url_provider", "TinyURL" );
335 Irssi::settings_add_bool( "twirssi", "tweet_to_away",      0 );
336 Irssi::settings_add_bool( "twirssi", "show_reply_context", 0 );
337 Irssi::settings_add_bool( "twirssi", "show_own_tweets",    1 );
338 $window = Irssi::window_find_name( Irssi::settings_get_str('twitter_window') );
339 if ($window) {
340     Irssi::command_bind( "dm",            "cmd_direct" );
341     Irssi::command_bind( "tweet",         "cmd_tweet" );
342     Irssi::command_bind( "twitter_login", "cmd_login" );
343     Irssi::command_bind(
344         "twirssi_version",
345         sub {
346             &notice(
347                 "Twirssi v$VERSION (r$REV).  See details at http://tinyurl.com/twirssi"
348             );
349         }
350     );
351     Irssi::command_bind(
352         "twitter_friend",
353         &gen_cmd(
354             "/twitter_friend <username>",
355             "create_friend",
356             sub { &notice("Following $_[0]"); $nicks{$_[0]} = time; }
357         )
358     );
359     Irssi::command_bind(
360         "twitter_unfriend",
361         &gen_cmd(
362             "/twitter_unfriend <username>",
363             "destroy_friend",
364             sub { &notice("Stopped following $_[0]"); delete $nicks{$_[0]}; }
365         )
366     );
367     Irssi::command_bind( "twitter_updates", "get_updates" );
368     Irssi::signal_add_last( 'complete word' => \&sig_complete );
369
370     &notice("  %Y<%C(%B^%C)%N                   TWIRSSI v%R$VERSION%N (r$REV)");
371     &notice("   %C(_(\\%N        http://tinyurl.com/twirssi for full docs");
372     &notice( "    %Y||%C `%N Log in with /twitter_login, send updates with /tweet");
373
374     if ( my $provider = Irssi::settings_get_str("short_url_provider") ) {
375         eval "use WWW::Shorten::$provider;";
376
377         if ($@) {
378             &notice(
379 "Failed to load WWW::Shorten::$provider - either clear short_url_provider or install the CPAN module"
380             );
381         }
382     }
383 } else {
384     Irssi::active_win()
385       ->print( "Create a window named "
386           . Irssi::settings_get_str('twitter_window')
387           . " or change the value of twitter_window.  Then, reload twirssi." );
388 }
389