83f1fe0e4af05b2568645e89a1152f08bcd97863
[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: 309 $' =~ /(\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 %twits;
28 my $user;
29 my $poll;
30 my %nicks;
31 my %friends;
32 my $last_poll = time - 300;
33
34 sub cmd_direct {
35     my ( $data, $server, $win ) = @_;
36
37     unless ($twit) {
38         &notice("Not logged in!  Use /twitter_login username pass!");
39         return;
40     }
41
42     my ( $target, $text ) = split ' ', $data, 2;
43     unless ( $target and $text ) {
44         &notice("Usage: /dm <nick> <message>");
45         return;
46     }
47
48     &cmd_direct_as( "$user $data", $server, $win );
49 }
50
51 sub cmd_direct_as {
52     my ( $data, $server, $win ) = @_;
53
54     unless ($twit) {
55         &notice("Not logged in!  Use /twitter_login username pass!");
56         return;
57     }
58
59     my ( $username, $target, $text ) = split ' ', $data, 3;
60     unless ( $username and $target and $text ) {
61         &notice("Usage: /dm_as <username> <nick> <message>");
62         return;
63     }
64
65     unless ( exists $twits{$username} ) {
66         &notice("Unknown username $username");
67         return;
68     }
69
70     unless ( $twits{$username}
71         ->new_direct_message( { user => $target, text => $text } ) )
72     {
73         &notice("DM to $target failed");
74         return;
75     }
76
77     &notice("DM sent to $target");
78     $nicks{$target} = time;
79 }
80
81 sub cmd_tweet {
82     my ( $data, $server, $win ) = @_;
83
84     unless ($twit) {
85         &notice("Not logged in!  Use /twitter_login username pass!");
86         return;
87     }
88
89     $data =~ s/^\s+|\s+$//;
90     unless ($data) {
91         &notice("Usage: /tweet <update>");
92         return;
93     }
94
95     &cmd_tweet_as( "$user $data", $server, $win );
96 }
97
98 sub cmd_tweet_as {
99     my ( $data, $server, $win ) = @_;
100
101     unless ($twit) {
102         &notice("Not logged in!  Use /twitter_login username pass!");
103         return;
104     }
105
106     $data =~ s/^\s+|\s+$//;
107     my ( $username, $data ) = split ' ', $data, 2;
108
109     unless ( $username and $data ) {
110         &notice("Usage: /tweet_as <username> <update>");
111         return;
112     }
113
114     unless ( exists $twits{$username} ) {
115         &notice("Unknown username $username");
116         return;
117     }
118
119     foreach my $url ( $data =~ /(https?:\/\/\S+[\w\/])/g ) {
120         eval { my $short = makeashorterlink($url); $data =~ s/\Q$url/$short/g; };
121     }
122
123     unless ( $twits{$username}->update($data) ) {
124         &notice("Update failed");
125         return;
126     }
127
128     foreach ( $data =~ /@([-\w]+)/ ) {
129         $nicks{$1} = time;
130     }
131
132     my $away = 0;
133     if (    Irssi::settings_get_bool("tweet_to_away")
134         and $data !~ /\@\w/
135         and $data !~ /^[dD] / )
136     {
137         my $server =
138           Irssi::server_find_tag( Irssi::settings_get_str("bitlbee_server") );
139         if ($server) {
140             $server->send_raw("away :$data");
141             $away = 1;
142         } else {
143             &notice( "Can't find bitlbee server.",
144                 "Update bitlbee_server or disalbe tweet_to_away" );
145         }
146     }
147
148     &notice( "Update sent" . ( $away ? " (and away msg set)" : "" ) );
149 }
150
151 sub gen_cmd {
152     my ( $usage_str, $api_name, $post_ref ) = @_;
153
154     return sub {
155         my ( $data, $server, $win ) = @_;
156
157         unless ($twit) {
158             &notice("Not logged in!  Use /twitter_login username pass!");
159             return;
160         }
161
162         $data =~ s/^\s+|\s+$//;
163         unless ($data) {
164             &notice("Usage: $usage_str");
165             return;
166         }
167
168         unless ( $twit->$api_name($data) ) {
169             &notice("$api_name failed");
170             return;
171         }
172
173         &$post_ref($data) if $post_ref;
174       }
175 }
176
177 sub cmd_switch {
178     my ( $data, $server, $win ) = @_;
179
180     $data =~ s/^\s+|\s+$//g;
181     if ( exists $twits{$data} ) {
182         &notice("Switching to $data");
183         $twit = $twits{$data};
184         $user = $data;
185     } else {
186         &notice("Unknown user $data");
187     }
188 }
189
190 sub cmd_logout {
191     my ( $data, $server, $win ) = @_;
192
193     $data =~ s/^\s+|\s+$//g;
194     if ( $data and exists $twits{$data} ) {
195         &notice("Logging out $data...");
196         $twits{$data}->end_session();
197         delete $twits{$data};
198     } elsif ($data) {
199         &notice("Unknown username '$data'");
200     } else {
201         &notice("Logging out $user...");
202         $twit->end_session();
203         undef $twit;
204         delete $twits{$user};
205         if ( keys %twits ) {
206             &cmd_switch( ( keys %twits )[0], $server, $win );
207         } else {
208             Irssi::timeout_remove($poll) if $poll;
209             undef $poll;
210         }
211     }
212 }
213
214 sub cmd_login {
215     my ( $data, $server, $win ) = @_;
216     my $pass;
217     ( $user, $pass ) = split ' ', $data, 2;
218
219     %friends = %nicks = ();
220
221     $twit = Net::Twitter->new(
222         username => $user,
223         password => $pass,
224         source   => "twirssi"
225     );
226
227     unless ( $twit->verify_credentials() ) {
228         &notice("Login failed");
229         $twit = undef;
230         return;
231     }
232
233     if ($twit) {
234         $twits{$user} = $twit;
235         Irssi::timeout_remove($poll) if $poll;
236         $poll = Irssi::timeout_add( 300 * 1000, \&get_updates, "" );
237         &notice("Logged in as $user, loading friends list...");
238         &load_friends;
239         &notice( "loaded friends: ", scalar keys %nicks );
240         %nicks = %friends;
241         $nicks{$user} = 0;
242         &get_updates;
243     } else {
244         &notice("Login failed");
245     }
246 }
247
248 sub load_friends {
249     my $page = 1;
250     my %new_friends;
251     while (1) {
252         my $friends = $twit->friends( { page => $page } );
253         last unless $friends;
254         $new_friends{ $_->{screen_name} } = time foreach @$friends;
255         $page++;
256         last if @$friends == 0 or $page == 10;
257         $friends = $twit->friends( page => $page );
258     }
259
260     foreach ( keys %new_friends ) {
261         next if exists $friends{$_};
262         $friends{$_} = time;
263     }
264
265     foreach ( keys %friends ) {
266         delete $friends{$_} unless exists $new_friends{$_};
267     }
268 }
269
270 sub get_updates {
271     $window =
272       Irssi::window_find_name( Irssi::settings_get_str('twitter_window') );
273     unless ($window) {
274         Irssi::active_win()
275           ->print( "Can't find a window named '"
276               . Irssi::settings_get_str('twitter_window')
277               . "'.  Create it or change the value of twitter_window" );
278     }
279     unless ($twit) {
280         &notice("Not logged in!  Use /twitter_login username pass!");
281         return;
282     }
283
284     my ( $fh, $filename ) = File::Temp::tempfile();
285     my $pid = fork();
286
287     if ($pid) {    # parent
288         Irssi::timeout_add_once( 5000, 'monitor_child', [$filename] );
289     } elsif ( defined $pid ) {    # child
290         close STDIN;
291         close STDOUT;
292         close STDERR;
293
294         my $new_poll = time;
295
296         &do_updates( $fh, $user, $twit );
297         foreach ( keys %twits ) {
298             next if $_ eq $user;
299             &do_updates( $fh, $_, $twits{$_} );
300         }
301
302         print $fh "__friends__\n";
303         &load_friends;
304         foreach ( sort keys %friends ) {
305             print $fh "$_ $friends{$_}\n";
306         }
307         print $fh $new_poll;
308         close $fh;
309         exit;
310     }
311 }
312
313 sub do_updates {
314     my ( $fh, $username, $obj ) = @_;
315
316     print scalar localtime, " - Polling for updates for $username" if DEBUG;
317     my $tweets =
318       $obj->friends_timeline( { since => HTTP::Date::time2str($last_poll) } )
319       || [];
320     foreach my $t ( reverse @$tweets ) {
321         my $text = decode_entities( $t->{text} );
322         $text =~ s/%/%%/g;
323         $text =~ s/(^|\W)\@([-\w]+)/$1%B\@$2%n/g;
324         my $prefix = "";
325         if (    Irssi::settings_get_bool("show_reply_context")
326             and $t->{in_reply_to_screen_name} ne $username
327             and $t->{in_reply_to_screen_name}
328             and not exists $friends{ $t->{in_reply_to_screen_name} } )
329         {
330             $nicks{ $t->{in_reply_to_screen_name} } = time;
331             my $context = $obj->show_status( $t->{in_reply_to_status_id} );
332             if ($context) {
333                 my $ctext = decode_entities( $context->{text} );
334                 $ctext =~ s/%/%%/g;
335                 $ctext =~ s/(^|\W)\@([-\w]+)/$1%B\@$2%n/g;
336                 printf $fh "[%s%%B\@%s%%n] %s\n",
337                   ( $username ne $user ? "$username: " : "" ),
338                   $context->{user}{screen_name}, $ctext;
339                 $prefix = "\--> ";
340             }
341         }
342         next
343           if $t->{user}{screen_name} eq $username
344               and not Irssi::settings_get_bool("show_own_tweets");
345         printf $fh "%s[%s%%B\@%s%%n] %s\n",
346           $prefix,
347           ( $username ne $user ? "$username: " : "" ),
348           $t->{user}{screen_name},
349           $text;
350     }
351
352     print scalar localtime, " - Polling for replies" if DEBUG;
353     $tweets = $obj->replies( { since => HTTP::Date::time2str($last_poll) } )
354       || [];
355     foreach my $t ( reverse @$tweets ) {
356         next
357           if exists $friends{ $t->{user}{screen_name} };
358
359         my $text = decode_entities( $t->{text} );
360         $text =~ s/%/%%/g;
361         $text =~ s/(^|\W)\@([-\w]+)/$1%B\@$2%n/g;
362         printf $fh "[%s%%B\@%s%%n] %s\n",
363           ( $username ne $user ? "$username: " : "" ),
364           $t->{user}{screen_name},
365           $text;
366     }
367
368     print scalar localtime, " - Polling for DMs" if DEBUG;
369     $tweets =
370       $obj->direct_messages( { since => HTTP::Date::time2str($last_poll) } )
371       || [];
372     foreach my $t ( reverse @$tweets ) {
373         my $text = decode_entities( $t->{text} );
374         $text =~ s/%/%%/g;
375         $text =~ s/(^|\W)\@([-\w]+)/$1%B\@$2%n/g;
376         printf $fh "[%s%%B\@%s%%n (%%WDM%%n)] %s\n",
377           ( $username ne $user ? "$username: " : "" ),
378           $t->{sender_screen_name},
379           $text;
380     }
381     print scalar localtime, " - Done" if DEBUG;
382 }
383
384 sub monitor_child {
385     my $data     = shift;
386     my $filename = $data->[0];
387
388     print scalar localtime, " - checking child log at $filename" if DEBUG;
389     if ( open FILE, $filename ) {
390         my @lines;
391         while (<FILE>) {
392             chomp;
393             last if /^__friends__/;
394             push @lines, $_ unless /^__friends__/;
395         }
396
397         %friends = ();
398         while (<FILE>) {
399             if (/^\d+$/) {
400                 $last_poll = $_;
401                 last;
402             }
403             my ( $f, $t ) = split ' ', $_;
404             $nicks{$f} = $friends{$f} = $t;
405         }
406
407         print "new last_poll = $last_poll" if DEBUG;
408         foreach my $line (@lines) {
409             chomp $line;
410             $window->print( $line, MSGLEVEL_PUBLIC );
411             foreach ( $line =~ /\@([-\w]+)/ ) {
412                 $nicks{$1} = time;
413             }
414         }
415
416         close FILE;
417         unlink $filename or warn "Failed to remove $filename: $!";
418         return;
419     }
420
421     Irssi::timeout_add_once( 5000, 'monitor_child', [$filename] );
422 }
423
424 sub notice {
425     $window->print( "%R***%n @_", MSGLEVEL_PUBLIC );
426 }
427
428 sub sig_complete {
429     my ( $complist, $window, $word, $linestart, $want_space ) = @_;
430
431     return unless $linestart =~ /^\/(?:tweet|dm)/;
432     return if $linestart eq '/tweet' and $word !~ s/^@//;
433     push @$complist, grep /^\Q$word/i,
434       sort { $nicks{$b} <=> $nicks{$a} } keys %nicks;
435     @$complist = map { "\@$_" } @$complist if $linestart eq '/tweet';
436 }
437
438 Irssi::settings_add_str( "twirssi", "twitter_window",     "twitter" );
439 Irssi::settings_add_str( "twirssi", "bitlbee_server",     "bitlbee" );
440 Irssi::settings_add_str( "twirssi", "short_url_provider", "TinyURL" );
441 Irssi::settings_add_bool( "twirssi", "tweet_to_away",      0 );
442 Irssi::settings_add_bool( "twirssi", "show_reply_context", 0 );
443 Irssi::settings_add_bool( "twirssi", "show_own_tweets",    1 );
444 $window = Irssi::window_find_name( Irssi::settings_get_str('twitter_window') );
445 if ($window) {
446     Irssi::command_bind( "dm",             "cmd_direct" );
447     Irssi::command_bind( "tweet",          "cmd_tweet" );
448     Irssi::command_bind( "dm_as",          "cmd_direct_as" );
449     Irssi::command_bind( "tweet_as",       "cmd_tweet_as" );
450     Irssi::command_bind( "twitter_login",  "cmd_login" );
451     Irssi::command_bind( "twitter_logout", "cmd_logout" );
452     Irssi::command_bind( "twitter_switch", "cmd_switch" );
453     Irssi::command_bind(
454         "twirssi_version",
455         sub {
456             &notice(
457 "Twirssi v$VERSION (r$REV).  See details at http://tinyurl.com/twirssi"
458             );
459         }
460     );
461     Irssi::command_bind(
462         "twitter_friend",
463         &gen_cmd(
464             "/twitter_friend <username>",
465             "create_friend",
466             sub { &notice("Following $_[0]"); $nicks{ $_[0] } = time; }
467         )
468     );
469     Irssi::command_bind(
470         "twitter_unfriend",
471         &gen_cmd(
472             "/twitter_unfriend <username>",
473             "destroy_friend",
474             sub { &notice("Stopped following $_[0]"); delete $nicks{ $_[0] }; }
475         )
476     );
477     Irssi::command_bind( "twitter_updates", "get_updates" );
478     Irssi::signal_add_last( 'complete word' => \&sig_complete );
479
480     &notice("  %Y<%C(%B^%C)%N                   TWIRSSI v%R$VERSION%N (r$REV)");
481     &notice("   %C(_(\\%N        http://tinyurl.com/twirssi for full docs");
482     &notice(
483         "    %Y||%C `%N Log in with /twitter_login, send updates with /tweet");
484
485     if ( my $provider = Irssi::settings_get_str("short_url_provider") ) {
486         eval "use WWW::Shorten::$provider;";
487
488         if ($@) {
489             &notice(
490 "Failed to load WWW::Shorten::$provider - either clear short_url_provider or install the CPAN module"
491             );
492         }
493     }
494 } else {
495     Irssi::active_win()
496       ->print( "Create a window named "
497           . Irssi::settings_get_str('twitter_window')
498           . " or change the value of twitter_window.  Then, reload twirssi." );
499 }
500