1# by Stefan "tommie" Tomanek 2# 3use strict; 4 5use vars qw($VERSION %IRSSI); 6$VERSION = '2017040101'; 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 20use Irssi 20020324; 21use Irssi::TextUI; 22 23use Data::Dumper; 24use XML::RSS; 25use LWP::UserAgent; 26use POSIX; 27use Unicode::String qw(utf8 latin1); 28use Text::Wrap; 29 30use 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 38sub 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 71sub 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 131sub 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 151sub 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 163sub 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 271sub 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 286sub call_openurl ($) { 287 my ($url) = @_; 288 no strict "refs"; 289 # check for a loaded openurl 290 if (my $code = Irssi::Script::openurl::->can('launch_url')) { 291 $code->($url); 292 } else { 293 print CLIENTCRAP "%R>>%n Please install openurl.pl"; 294 } 295 use strict "refs"; 296} 297sub 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 306sub 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 328sub 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 339sub 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 362sub cycle_ticker () { 363 $index++; 364 if ($index >= scalar(@ticker)) { 365 $index = 0 366 } 367 $slide++; 368 Irssi::statusbar_items_redraw('newsline_ticker'); 369} 370 371sub update_ticker () { 372 fork_get(); 373} 374 375sub 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 402sub 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 414sub 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 422sub 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 431Irssi::signal_add_first('complete word', \&sig_complete_word); 432Irssi::signal_add('setup saved', \&save_config); 433 434Irssi::command_bind('newsline', \&cmd_newsline); 435foreach 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 440Irssi::settings_add_int($IRSSI{'name'}, 'newsline_fetch_interval', 600); 441 442Irssi::settings_add_int($IRSSI{'name'}, 'newsline_ticker_max_width', 50); 443 444Irssi::settings_add_int($IRSSI{'name'}, 'newsline_ticker_cycle_delay', 3000); 445Irssi::settings_add_str($IRSSI{'name'}, 'newsline_sites_file', Irssi::get_irssi_dir()."/newsline_sites"); 446Irssi::settings_add_bool($IRSSI{'name'}, 'newsline_show_url', 1); 447Irssi::settings_add_bool($IRSSI{'name'}, 'newsline_ticker_scroll', 0); 448 449Irssi::statusbar_item_register('newsline_ticker', 0, 'newsline_ticker'); 450 451reload_config(); 452update_ticker(); 453print CLIENTCRAP '%B>>%n '.$IRSSI{name}.' '.$VERSION.' loaded: /newsline help for help'; 454