93cb8963e8f44d9aceea2c8e16563bc953e68bb8
[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: 300 $';
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           push @lines, $_ unless /^--friends:$/;
288         }
289
290         %friends = ();
291         while (<FILE>) {
292           if (/^\d+$/) {
293             $last_poll = $_;
294             last;
295           }
296           my ($f, $t) = split ' ', $_;
297           $friends{$f} = $t;
298         }
299
300         print "new last_poll = $last_poll" if DEBUG;
301         foreach my $line (@lines) {
302             chomp $line;
303             $window->print( $line, MSGLEVEL_PUBLIC );
304             foreach ( $line =~ /\@([-\w]+)/ ) {
305                 $nicks{$1} = time;
306             }
307         }
308
309         close FILE;
310         unlink $filename or warn "Failed to remove $filename: $!";
311         return;
312     }
313
314     Irssi::timeout_add_once( 5000, 'monitor_child', [$filename] );
315 }
316
317 sub notice {
318     $window->print( "%R***%n @_", MSGLEVEL_PUBLIC );
319 }
320
321 sub sig_complete {
322     my ( $complist, $window, $word, $linestart, $want_space ) = @_;
323
324     return unless $linestart =~ /^\/(?:tweet|dm)/;
325     return if $linestart eq '/tweet' and $word !~ s/^@//;
326     push @$complist, grep /^\Q$word/i,
327       sort { $nicks{$b} <=> $nicks{$a} } keys %nicks;
328     @$complist = map { "\@$_" } @$complist if $linestart eq '/tweet';
329 }
330
331 Irssi::settings_add_str( "twirssi", "twitter_window",     "twitter" );
332 Irssi::settings_add_str( "twirssi", "bitlbee_server",     "bitlbee" );
333 Irssi::settings_add_str( "twirssi", "short_url_provider", "TinyURL" );
334 Irssi::settings_add_bool( "twirssi", "tweet_to_away",      0 );
335 Irssi::settings_add_bool( "twirssi", "show_reply_context", 0 );
336 Irssi::settings_add_bool( "twirssi", "show_own_tweets",    1 );
337 $window = Irssi::window_find_name( Irssi::settings_get_str('twitter_window') );
338 if ($window) {
339     Irssi::command_bind( "dm",            "cmd_direct" );
340     Irssi::command_bind( "tweet",         "cmd_tweet" );
341     Irssi::command_bind( "twitter_login", "cmd_login" );
342     Irssi::command_bind(
343         "twirssi_version",
344         sub {
345             &notice(
346                 "Twirssi v$VERSION (r$REV).  See details at http://tinyurl.com/twirssi"
347             );
348         }
349     );
350     Irssi::command_bind(
351         "twitter_friend",
352         &gen_cmd(
353             "/twitter_friend <username>",
354             "create_friend",
355             sub { &notice("Following $_[0]"); $nicks{$_[0]} = time; }
356         )
357     );
358     Irssi::command_bind(
359         "twitter_unfriend",
360         &gen_cmd(
361             "/twitter_unfriend <username>",
362             "destroy_friend",
363             sub { &notice("Stopped following $_[0]"); delete $nicks{$_[0]}; }
364         )
365     );
366     Irssi::command_bind( "twitter_updates", "get_updates" );
367     Irssi::signal_add_last( 'complete word' => \&sig_complete );
368
369     &notice("  %Y<%C(%B^%C)%N                   TWIRSSI v%R$VERSION%N (r$REV)");
370     &notice("   %C(_(\\%N        http://tinyurl.com/twirssi for full docs");
371     &notice( "    %Y||%C `%N Log in with /twitter_login, send updates with /tweet");
372
373     if ( my $provider = Irssi::settings_get_str("short_url_provider") ) {
374         eval "use WWW::Shorten::$provider;";
375
376         if ($@) {
377             &notice(
378 "Failed to load WWW::Shorten::$provider - either clear short_url_provider or install the CPAN module"
379             );
380         }
381     }
382 } else {
383     Irssi::active_win()
384       ->print( "Create a window named "
385           . Irssi::settings_get_str('twitter_window')
386           . " or change the value of twitter_window.  Then, reload twirssi." );
387 }
388