html/newsline.pl


   1 # by Stefan "tommie" Tomanek
   2 # 
   3 use strict;
   4 
   5 use vars qw($VERSION %IRSSI);
   6 $VERSION = '2003021101';
   7 %IRSSI = (
   8     authors     => 'Stefan \'tommie\' Tomanek',
   9     contact     => 'stefan@pico.ruhr.de',
  10     name        => 'Newsline',
  11     description => 'brings various newstickers to Irssi (Slashdot, Freshmeat, Heise etc.)',
  12     license     => 'GPLv2',
  13     changed     => $VERSION,
  14     modules     => 'Data::Dumper XML::RSS LWP::UserAgent Unicode::String Text::Wrap',
  15     depends     => 'openurl',
  16     sbitems     => 'newsline_ticker',
  17     commands	=> 'newsline'
  18 );  
  19 
  20 use Irssi 20020324;
  21 use Irssi::TextUI;
  22 
  23 use Data::Dumper;
  24 use XML::RSS;
  25 use LWP::UserAgent;
  26 use POSIX;
  27 use Unicode::String qw(utf8 latin1);
  28 use Text::Wrap;
  29 
  30 use vars qw(@ticker $timestamp $slide $index $timer_cycle $timer_update %sites $forked);
  31 
  32 $index = 0;
  33 # Just to have some data for the first startup
  34 %sites = ( Heise=>{page => 'http://www.heise.de/newsticker/heise.rdf', enable => 1, title=>'', description=>'', maxnews=>0},
  35            'Freshmeat'=>{'page' => 'http://freshmeat.net/backend/fm.rdf', 'enable' => 1, title=>'', description=>'', maxnews=>0}
  36 );
  37 
  38 sub show_help() {
  39     my $help = "newsline $VERSION
  40 /newsline
  41         List the downloaded headlines
  42 /newsline <number>
  43         Open the entry indicated by <number> via openurl.
  44         Openurl.pl is available at http://irssi.org/scripts/.
  45 /newsline description <number>
  46         Display a brief summary of the article if available
  47 /newsline paste <number>
  48         Write the headline and link to the current channel or query,
  49         add 'description' to a diplay the description as well
  50 /newsline fetch
  51         Retrieve new data from all enabled sources
  52 /newsline reload
  53         Reload configuration and sites
  54 /newsline save
  55         Save configration to ~/.irssi/newsline_sites
  56 /newsline list
  57         List all available sources
  58 /newsline toggle <Source>
  59         Enable or disable the source
  60 /newsline add <name> <url-to-rdf>
  61         Add a new source
  62 "; 
  63     my $text='';
  64     foreach (split(/\n/, $help)) {
  65 	$_ =~ s/^\/(.*)$/%9\/$1%9/;
  66 	$text .= $_."\n";
  67     }
  68     print CLIENTCRAP &draw_box("Newsline", $text, "newsline help", 1);
  69 }
  70 
  71 sub fork_get() {
  72     my ($rh, $wh);
  73     pipe($rh, $wh);
  74     return if $forked;
  75     $forked = 1;
  76     my $pid = fork();
  77     if ($pid > 0) {
  78 	close $wh;
  79 	Irssi::pidwait_add($pid);
  80 	my $pipetag;
  81 	my @args = ($rh, \$pipetag);
  82 	$pipetag = Irssi::input_add(fileno($rh), INPUT_READ, \&pipe_input, \@args);
  83     } else {
  84 	my (%siteinfo, @items);
  85 	eval {
  86 	    foreach (sort keys %sites) {
  87 		eval {
  88 		my $site = $sites{$_};
  89 		next unless $site->{'enable'};
  90 		my $maxnews = -1;
  91 		$maxnews = $site->{maxnews} if defined $site->{maxnews};
  92 		my $url = $site->{'page'};
  93 		my $ua = LWP::UserAgent->new(env_proxy=>1, keep_alive=>1, timeout=>30);
  94 		my $request = HTTP::Request->new('GET', $url);
  95 		#$request->if_modified_since($timestamp) if $timestamp;
  96 		my $response = $ua->request($request);
  97 		if ($response->is_success) {
  98 		    my $data = $response->content();
  99 		    ### FIXME I hate myself for this :)
 100 		    $data =~ s/encoding="ISO-8859-15"/encoding="ISO-8859-1"/i;
 101 		    my $rss = new XML::RSS();
 102 		    $rss->parse($data);
 103 		    my $title = $rss->{channel}->{title};
 104 		    my $description = de_umlaut($rss->{channel}->{description});
 105 		    my $link = de_umlaut($rss->{channel}->{link});
 106 		    $siteinfo{$_} = {title=>$title, description=>$description, link=>$link};
 107 		    foreach my $item (@{$rss->{items}}) {
 108 			next unless defined($item->{title}) && defined($item->{'link'});
 109 			my $title = de_umlaut($item->{title});
 110 			$title =~ s/\n/ /g;
 111 			my %story = ('title' => $title, 'link' => $item->{link}, 'source' => $_);
 112 			$story{description} = de_umlaut($item->{description}) if $item->{description};
 113 			push @items, \%story;
 114 			$maxnews--;
 115 			last if $maxnews == 0;
 116 		    }
 117 		};
 118 		}
 119 	    }
 120 	    my %result = (news=>\@items, siteinfo=>\%siteinfo);
 121 	    my $dumper = Data::Dumper->new([\%result]);
 122 	    $dumper->Purity(1)->Deepcopy(1);
 123 	    my $data = $dumper->Dump;
 124 	    print($wh $data);
 125 	};
 126 	close($wh);
 127 	POSIX::_exit(1);
 128     }
 129 }
 130 
 131 sub pipe_input {
 132     my ($rh, $pipetag) = @{$_[0]};
 133     my $text;
 134     $text .= $_ foreach (<$rh>);
 135     close($rh);
 136     Irssi::input_remove($$pipetag);
 137     return unless($text);
 138     no strict;
 139     my %result = %{ eval "$text" };
 140     my @items = @{$result{news}};
 141     my %siteinfo = %{$result{siteinfo}};
 142     @ticker = @items;
 143     foreach (sort keys %siteinfo) {
 144 	$sites{$_}->{title} = $siteinfo{$_}->{title};
 145 	$sites{$_}->{description} = $siteinfo{$_}->{description};
 146 	$sites{$_}->{link} = $siteinfo{$_}->{link};
 147     }
 148     $forked = 0;
 149 }
 150 
 151 sub draw_box ($$$$) {
 152     my ($title, $text, $footer, $colour) = @_;
 153     my $box = ''; 
 154     $box .= '%R,--[%n%9%U'.$title.'%U%9%R]%n'."\n";
 155     foreach (split(/\n/, $text)) {
 156 	$box .= '%R|%n '.$_."\n";
 157     }
 158     $box .= '%R`--<%n'.$footer.'%R>->%n';
 159     $box =~ s/%.//g unless $colour;
 160     return $box;
 161 }
 162 
 163 sub cmd_newsline ($$$) {
 164     my ($args, $server, $witem) = @_;
 165     $args =~ s/^\ +//;
 166     my @arg = split(/\ +/, $args);
 167     if (scalar(@arg) == 0) {
 168 	show_ticker(@ticker);
 169     } elsif ($arg[0] eq 'paste') {
 170 	# paste tickernews
 171 	shift(@arg);
 172 	my $desc = 0;
 173 	if (defined $arg[0] && $arg[0] eq 'description') {
 174 	    $desc = 1;
 175 	    shift(@arg);
 176 	}
 177 	foreach (@arg) {
 178 	    if (defined $ticker[$_-1]) {
 179 		my $message = $ticker[$_-1]->{'title'};
 180 		my $text = '['.$ticker[$_-1]->{'source'}.'] "'.$message.'" -> '.$ticker[$_-1]->{'link'};
 181 		$Text::Wrap::columns = 50;
 182 		my $article = wrap("","",$ticker[$_-1]->{description}) if ($desc && defined $ticker[$_-1]->{description});
 183 		my $text2 = draw_box($message, $article, $ticker[$_-1]->{source}, 0) if (defined $article);
 184 		if (($witem) and (($witem->{type} eq "CHANNEL") or ($witem->{type} eq "QUERY"))) {
 185 		    $witem->command("MSG ".$witem->{name}." ".$text);
 186 		    if (defined $text2) {
 187 			$witem->command("MSG ".$witem->{name}." ".$_) foreach (split /\n/, $text2);
 188 		    }
 189 		}
 190 	    }
 191 	}
 192     } elsif ($arg[0] eq 'description') {
 193 	shift(@arg);
 194 	foreach (@arg) {
 195 	    next unless defined $ticker[$_-1] and defined $ticker[$_-1]->{description};
 196 	    $Text::Wrap::columns = 50;
 197 	    my $filter = $ticker[$_-1]->{description};
 198 	    $filter =~ s/<.*?>//g;
 199 	    my $article = wrap("", "", $filter);
 200 	    my $text = '';
 201 	    print CLIENTCRAP draw_box($ticker[$_-1]->{title}, $article, $ticker[$_-1]->{source}, 1);
 202 	}
 203     } elsif ($arg[0] eq 'help') {
 204 	show_help();
 205     } elsif ($arg[0] eq 'fetch') {
 206 	fork_get()
 207     } elsif ($arg[0] eq 'reload') {
 208 	reload_config();
 209     } elsif ($arg[0] eq 'save') {
 210 	save_config();
 211     } elsif ($arg[0] eq 'add') {
 212 	if (defined($arg[1]) && defined($arg[2])) {
 213 	    my $source = $arg[1];
 214 	    my $page = $arg[2];
 215 	    $sites{$source} = {page => $page, enable => 1, maxnews=>0};
 216 	    print CLIENTCRAP '%R>>%n Added new source "'.$arg[1].'"';
 217 	    $timestamp = undef;
 218 	}
 219     } elsif ($arg[0] eq 'delete') {
 220 	if (defined $arg[1] && defined $sites{$arg[1]}) {
 221 	    delete $sites{$arg[1]};
 222 	    print CLIENTCRAP "%R>>%n ".$arg[1]." deleted";
 223 	}
 224     } elsif ($arg[0] eq 'toggle') {
 225 	# Toggle site
 226 	if (defined $arg[1] && defined $sites{$arg[1]}) {
 227 	    if ($sites{$arg[1]}{'enable'} == 0) {
 228 		$sites{$arg[1]}{'enable'} = 1;
 229 		print CLIENTCRAP "%R>>%n ".$arg[1]." enabled";
 230 	    } else {
 231 		$sites{$arg[1]}{'enable'} = 0;
 232 		print CLIENTCRAP "%R>>%n ".$arg[1]." disabled";
 233 	    }
 234 	}
 235     } elsif ($arg[0] eq 'limit') {
 236         if (defined $arg[1] && defined $sites{$arg[1]}) {
 237 	    if (defined $arg[2] && $arg[2] =~ /\d+/) {
 238                 $sites{$arg[1]}{'maxnews'} = $arg[2];
 239                 print CLIENTCRAP "%R>>%n ".$arg[1]." limited to ".$arg[2]." articles";
 240             }
 241         }
 242     } elsif ($arg[0] eq 'list') {
 243 	my $text = "";
 244 	foreach (sort keys %sites) {
 245 	    my %site = %{$sites{$_}};
 246 	    $text .= "%9[".$_.']%9'."\n";
 247 	    $text .= " %9|-[page  ]->%9 ".$site{'page'}."\n";
 248 	    #$text .= " %9|-[desc  ]->%9 ".$site{'description'}."\n" if defined $site{'description'};
 249 	    $Text::Wrap::columns = 60;
 250 	    my $filter = $site{'description'};
 251 	    $filter =~ s/<.*?>//;
 252 	    my $desc = wrap(" %9|-[desc  ]->%9 ",' %9|%9<tab>', $filter);
 253 	    $desc =~ s/<tab>/            /g;
 254 	    $text .= $desc."\n" if $site{'description'};
 255 	    $text .= " %9|-[limit ]->%9 ".$site{'maxnews'}."\n";
 256 	    $text .= " %9`-[enable]->%9 ".$site{'enable'}."\n";
 257 	}
 258 	print CLIENTCRAP draw_box("Newsline", $text, "newsline sources", 1);
 259 	
 260     } else {
 261 	foreach (@arg) {
 262 	    if (defined $sites{$_}) {
 263 		call_openurl($sites{$_}->{'link'}) if defined $sites{$_}->{'link'};
 264 	    } elsif (/\d+/ && defined $ticker[$_-1]) {
 265 		call_openurl($ticker[$_-1]->{'link'});
 266 	    }
 267 	}
 268     }
 269 }
 270 
 271 sub show_ticker (@) {
 272     my (@ticker) = @_;
 273     my $i = 1;
 274     my $text = '';
 275     foreach (@ticker) {
 276 	my $space = ' 'x(length(scalar(@ticker))-length($i));
 277 	my $newsitem = '%r'.$space.$i.'->%n['.$$_{source}.'] %9'.$$_{title}.'%9';
 278 	$newsitem .= ' %9[*]%9' if defined($$_{description});
 279 	$text .= $newsitem."\n";
 280 	$text .= "  %B`->%n%U".$$_{link}."%U \n" if Irssi::settings_get_bool('newsline_show_url');
 281 	$i++;
 282     }
 283     print CLIENTCRAP draw_box("Newsline", $text, "headlines", 1);
 284 }
 285 
 286 sub call_openurl ($) {
 287     my ($url) = @_;
 288     no strict "refs";
 289     # check for a loaded openurl
 290     if (defined %{ "Irssi::Script::openurl::" }) {
 291 	&{ "Irssi::Script::openurl::launch_url" }($url);
 292     } else {
 293 	print CLIENTCRAP "%R>>%n Please install openurl.pl";
 294     }
 295     use strict "refs";
 296 }
 297 sub newsline_ticker ($$) {
 298     my ($item, $get_size_only) = @_;
 299     if (Irssi::settings_get_bool('newsline_ticker_scroll')) {
 300 	draw_tape($item, $get_size_only);
 301     } else {
 302 	draw_ticker($item, $get_size_only);
 303     }
 304 }
 305     
 306 sub draw_ticker ($$) {
 307     my ($item, $get_size_only) = @_;
 308     if ($index >= scalar(@ticker)) {
 309 	$index = 0
 310     }
 311     my $tape;
 312     $tape .= '%F%Y<Fetching>%n' if $forked;
 313     if (scalar(@ticker) > 0) {
 314 	my $title = $ticker[$index]->{'title'};
 315 	my $source = $ticker[$index]->{'source'};
 316 	$tape .= '>'.($index+1).': ['.$source.'] '.$title;
 317 	$tape .= ' [*]' if defined($ticker[$index]->{description});
 318 	$tape .= '<';
 319     } else {
 320 	$tape .= '>Enter "/newsline fetch" to retrieve tickerdata>' unless $forked;
 321     }
 322     $tape = substr($tape, 0, Irssi::settings_get_int('newsline_ticker_max_width'));
 323     my $format = "{sb ".$tape."}";
 324     $item->{min_size} = $item->{max_size} = length($tape)+2;
 325     $item->default_handler($get_size_only, $format, 0, 1);
 326 }
 327 
 328 sub rotate ($$) {
 329     my ($text, $rot) = @_;
 330     return($text) if length($text) < 1;
 331     for (0..$rot) {
 332 	my $letter = substr($text, 0, 1);
 333 	$text = substr($text, 1);
 334 	$text = $text.$letter;
 335     }
 336     return($text);
 337 }
 338 
 339 sub draw_tape ($$) {
 340     my ($item, $get_size_only) = @_;
 341     my $tape;
 342     if (scalar(@ticker) > 0) {
 343 	my $i=1;
 344 	foreach (@ticker) {
 345 	    my $title = $_->{'title'};
 346 	    my $source = $_->{'source'};
 347 	    $tape .= '>'.($i).': ['.$source.'] '.$title.'|';
 348 	    $i++;
 349 	}
 350 	$tape = $tape;
 351 	$slide = 0 if $slide >= length($tape); 
 352 	$tape = rotate($tape, $slide);
 353 	$tape = substr($tape, 0, Irssi::settings_get_int('newsline_ticker_max_width'));
 354     } else {
 355 	$tape .= 'Use "/newsline -f" to fetch tickerdata';
 356     }
 357     my $format = "{sb ".$tape."}";
 358     $item->{min_size} = $item->{max_size} = length($tape)+2;
 359     $item->default_handler($get_size_only, $format, 0, 1);
 360 }
 361 
 362 sub cycle_ticker () {
 363     $index++;
 364     if ($index >= scalar(@ticker)) {
 365 	$index = 0
 366     }
 367     $slide++;
 368     Irssi::statusbar_items_redraw('newsline_ticker');
 369 }
 370 
 371 sub update_ticker () {
 372     fork_get();
 373 }
 374 
 375 sub reload_config() {
 376     my $filename = Irssi::settings_get_str('newsline_sites_file');
 377     my $text;
 378     if (-e $filename) {
 379 	local *F;
 380 	open F, "<".$filename;
 381 	$text .= $_ foreach (<F>);
 382 	close F;
 383 	if ($text) {
 384 	    no strict;
 385 	    my %pages = %{ eval "$text" };
 386 	    if (%pages) {
 387 		%sites = ();
 388 		foreach (keys %pages) {
 389 		    $sites{$_} = $pages{$_};
 390 		}
 391 	    }
 392 	}
 393     }
 394     Irssi::timeout_remove($timer_cycle) if defined $timer_cycle;
 395     Irssi::timeout_remove($timer_update) if defined $timer_update;
 396     $timer_cycle = Irssi::timeout_add(Irssi::settings_get_int('newsline_ticker_cycle_delay'), 'cycle_ticker', undef) if Irssi::settings_get_int('newsline_ticker_cycle_delay') > 0;
 397     $timer_update = Irssi::timeout_add(Irssi::settings_get_int('newsline_fetch_interval')*1000, 'update_ticker', undef) if Irssi::settings_get_int('newsline_fetch_interval') > 0;
 398     Irssi::statusbar_items_redraw('newsline_ticker');
 399     print CLIENTCRAP '%R>>%n Newsline sites loaded from '.$filename;
 400 }
 401 
 402 sub save_config() {
 403     local *F;
 404     my $filename = Irssi::settings_get_str('newsline_sites_file');
 405     open(F, ">$filename");
 406     my $dumper = Data::Dumper->new([\%sites], ['sites']);
 407     $dumper->Purity(1)->Deepcopy(1);
 408     my $data = $dumper->Dump;
 409     print (F $data);
 410     close(F);
 411     print CLIENTCRAP '%R>>%n Newsline sites saved to '.$filename;
 412 }
 413 
 414 sub de_umlaut ($) {
 415     my ($data) = @_;
 416     Unicode::String->stringify_as('utf8');
 417     my $s = new Unicode::String($data);
 418     my $result = $s->latin1();
 419     return($result);
 420 }
 421 
 422 sub sig_complete_word ($$$$$) {
 423     my ($list, $window, $word, $linestart, $want_space) = @_;
 424     return unless $linestart =~ /^.newsline (toggle|delete|add|limit)/;
 425     foreach (keys %sites) {
 426 	push @$list, $_ if /^(\Q$word\E.*)?$/;
 427     }
 428     Irssi::signal_stop();
 429 }
 430 
 431 Irssi::signal_add_first('complete word', \&sig_complete_word);
 432 Irssi::signal_add('setup saved', \&save_config);
 433 
 434 Irssi::command_bind('newsline', \&cmd_newsline);
 435 foreach my $cmd ('description', 'paste', 'paste description', 'fetch', 'reload', 'save', 'list', 'toggle', 'add', 'delete', 'help', 'limit') {
 436     Irssi::command_bind('newsline '.$cmd =>
 437 	sub { cmd_newsline("$cmd ".$_[0], $_[1], $_[2]); } );
 438 }
 439 
 440 Irssi::settings_add_int($IRSSI{'name'}, 'newsline_fetch_interval', 600);
 441 
 442 Irssi::settings_add_int($IRSSI{'name'}, 'newsline_ticker_max_width', 50);
 443 
 444 Irssi::settings_add_int($IRSSI{'name'}, 'newsline_ticker_cycle_delay', 3000);
 445 Irssi::settings_add_str($IRSSI{'name'}, 'newsline_sites_file', Irssi::get_irssi_dir()."/newsline_sites");
 446 Irssi::settings_add_bool($IRSSI{'name'}, 'newsline_show_url', 1);
 447 Irssi::settings_add_bool($IRSSI{'name'}, 'newsline_ticker_scroll', 0);
 448 
 449 Irssi::statusbar_item_register('newsline_ticker', 0, 'newsline_ticker');
 450 
 451 reload_config();
 452 update_ticker();
 453 print CLIENTCRAP '%B>>%n '.$IRSSI{name}.' '.$VERSION.' loaded: /newsline help for help';