# by Stefan "tommie" Tomanek
#
use strict;
use vars qw($VERSION %IRSSI);
$VERSION = '2017040101';
%IRSSI = (
authors => 'Stefan \'tommie\' Tomanek',
contact => '[email protected] ',
name => 'Newsline',
description => 'brings various newstickers to Irssi (Slashdot, Freshmeat, Heise etc.)',
license => 'GPLv2',
changed => $VERSION,
modules => 'Data::Dumper XML::RSS LWP::UserAgent Unicode::String Text::Wrap',
depends => 'openurl',
sbitems => 'newsline_ticker',
commands => 'newsline'
);
use Irssi 20020324;
use Irssi::TextUI;
use Data::Dumper;
use XML::RSS;
use LWP::UserAgent;
use POSIX;
use Unicode::String qw(utf8 latin1);
use Text::Wrap;
use vars qw(@ticker $timestamp $slide $index $timer_cycle $timer_update %sites $forked);
$index = 0;
# Just to have some data for the first startup
%sites = ( Heise=>{page => 'http://www.heise.de/newsticker/heise.rdf', enable => 1, title=>'', description=>'', maxnews=>0},
'Freshmeat'=>{'page' => 'http://freshmeat.net/backend/fm.rdf', 'enable' => 1, title=>'', description=>'', maxnews=>0}
);
sub show_help() {
my $help = "newsline $VERSION
/newsline
List the downloaded headlines
/newsline
Open the entry indicated by via openurl.
Openurl.pl is available at http://irssi.org/scripts/.
/newsline description
Display a brief summary of the article if available
/newsline paste
Write the headline and link to the current channel or query,
add 'description' to a diplay the description as well
/newsline fetch
Retrieve new data from all enabled sources
/newsline reload
Reload configuration and sites
/newsline save
Save configration to ~/.irssi/newsline_sites
/newsline list
List all available sources
/newsline toggle
Enable or disable the source
/newsline add
Add a new source
";
my $text='';
foreach (split(/\n/, $help)) {
$_ =~ s/^\/(.*)$/%9\/$1%9/;
$text .= $_."\n";
}
print CLIENTCRAP &draw_box("Newsline", $text, "newsline help", 1);
}
sub fork_get() {
my ($rh, $wh);
pipe($rh, $wh);
return if $forked;
$forked = 1;
my $pid = fork();
if ($pid > 0) {
close $wh;
Irssi::pidwait_add($pid);
my $pipetag;
my @args = ($rh, \$pipetag);
$pipetag = Irssi::input_add(fileno($rh), INPUT_READ, \&pipe_input, \@args);
} else {
my (%siteinfo, @items);
eval {
foreach (sort keys %sites) {
eval {
my $site = $sites{$_};
next unless $site->{'enable'};
my $maxnews = -1;
$maxnews = $site->{maxnews} if defined $site->{maxnews};
my $url = $site->{'page'};
my $ua = LWP::UserAgent->new(env_proxy=>1, keep_alive=>1, timeout=>30);
my $request = HTTP::Request->new('GET', $url);
#$request->if_modified_since($timestamp) if $timestamp;
my $response = $ua->request($request);
if ($response->is_success) {
my $data = $response->content();
### FIXME I hate myself for this :)
$data =~ s/encoding="ISO-8859-15"/encoding="ISO-8859-1"/i;
my $rss = new XML::RSS();
$rss->parse($data);
my $title = $rss->{channel}->{title};
my $description = de_umlaut($rss->{channel}->{description});
my $link = de_umlaut($rss->{channel}->{link});
$siteinfo{$_} = {title=>$title, description=>$description, link=>$link};
foreach my $item (@{$rss->{items}}) {
next unless defined($item->{title}) && defined($item->{'link'});
my $title = de_umlaut($item->{title});
$title =~ s/\n/ /g;
my %story = ('title' => $title, 'link' => $item->{link}, 'source' => $_);
$story{description} = de_umlaut($item->{description}) if $item->{description};
push @items, \%story;
$maxnews--;
last if $maxnews == 0;
}
};
}
}
my %result = (news=>\@items, siteinfo=>\%siteinfo);
my $dumper = Data::Dumper->new([\%result]);
$dumper->Purity(1)->Deepcopy(1);
my $data = $dumper->Dump;
print($wh $data);
};
close($wh);
POSIX::_exit(1);
}
}
sub pipe_input {
my ($rh, $pipetag) = @{$_[0]};
my $text;
$text .= $_ foreach (<$rh>);
close($rh);
Irssi::input_remove($$pipetag);
return unless($text);
no strict;
my %result = %{ eval "$text" };
my @items = @{$result{news}};
my %siteinfo = %{$result{siteinfo}};
@ticker = @items;
foreach (sort keys %siteinfo) {
$sites{$_}->{title} = $siteinfo{$_}->{title};
$sites{$_}->{description} = $siteinfo{$_}->{description};
$sites{$_}->{link} = $siteinfo{$_}->{link};
}
$forked = 0;
}
sub draw_box ($$$$) {
my ($title, $text, $footer, $colour) = @_;
my $box = '';
$box .= '%R,--[%n%9%U'.$title.'%U%9%R]%n'."\n";
foreach (split(/\n/, $text)) {
$box .= '%R|%n '.$_."\n";
}
$box .= '%R`--<%n'.$footer.'%R>->%n';
$box =~ s/%.//g unless $colour;
return $box;
}
sub cmd_newsline ($$$) {
my ($args, $server, $witem) = @_;
$args =~ s/^\ +//;
my @arg = split(/\ +/, $args);
if (scalar(@arg) == 0) {
show_ticker(@ticker);
} elsif ($arg[0] eq 'paste') {
# paste tickernews
shift(@arg);
my $desc = 0;
if (defined $arg[0] && $arg[0] eq 'description') {
$desc = 1;
shift(@arg);
}
foreach (@arg) {
if (defined $ticker[$_-1]) {
my $message = $ticker[$_-1]->{'title'};
my $text = '['.$ticker[$_-1]->{'source'}.'] "'.$message.'" -> '.$ticker[$_-1]->{'link'};
$Text::Wrap::columns = 50;
my $article = wrap("","",$ticker[$_-1]->{description}) if ($desc && defined $ticker[$_-1]->{description});
my $text2 = draw_box($message, $article, $ticker[$_-1]->{source}, 0) if (defined $article);
if (($witem) and (($witem->{type} eq "CHANNEL") or ($witem->{type} eq "QUERY"))) {
$witem->command("MSG ".$witem->{name}." ".$text);
if (defined $text2) {
$witem->command("MSG ".$witem->{name}." ".$_) foreach (split /\n/, $text2);
}
}
}
}
} elsif ($arg[0] eq 'description') {
shift(@arg);
foreach (@arg) {
next unless defined $ticker[$_-1] and defined $ticker[$_-1]->{description};
$Text::Wrap::columns = 50;
my $filter = $ticker[$_-1]->{description};
$filter =~ s/<.*?>//g;
my $article = wrap("", "", $filter);
my $text = '';
print CLIENTCRAP draw_box($ticker[$_-1]->{title}, $article, $ticker[$_-1]->{source}, 1);
}
} elsif ($arg[0] eq 'help') {
show_help();
} elsif ($arg[0] eq 'fetch') {
fork_get()
} elsif ($arg[0] eq 'reload') {
reload_config();
} elsif ($arg[0] eq 'save') {
save_config();
} elsif ($arg[0] eq 'add') {
if (defined($arg[1]) && defined($arg[2])) {
my $source = $arg[1];
my $page = $arg[2];
$sites{$source} = {page => $page, enable => 1, maxnews=>0};
print CLIENTCRAP '%R>>%n Added new source "'.$arg[1].'"';
$timestamp = undef;
}
} elsif ($arg[0] eq 'delete') {
if (defined $arg[1] && defined $sites{$arg[1]}) {
delete $sites{$arg[1]};
print CLIENTCRAP "%R>>%n ".$arg[1]." deleted";
}
} elsif ($arg[0] eq 'toggle') {
# Toggle site
if (defined $arg[1] && defined $sites{$arg[1]}) {
if ($sites{$arg[1]}{'enable'} == 0) {
$sites{$arg[1]}{'enable'} = 1;
print CLIENTCRAP "%R>>%n ".$arg[1]." enabled";
} else {
$sites{$arg[1]}{'enable'} = 0;
print CLIENTCRAP "%R>>%n ".$arg[1]." disabled";
}
}
} elsif ($arg[0] eq 'limit') {
if (defined $arg[1] && defined $sites{$arg[1]}) {
if (defined $arg[2] && $arg[2] =~ /\d+/) {
$sites{$arg[1]}{'maxnews'} = $arg[2];
print CLIENTCRAP "%R>>%n ".$arg[1]." limited to ".$arg[2]." articles";
}
}
} elsif ($arg[0] eq 'list') {
my $text = "";
foreach (sort keys %sites) {
my %site = %{$sites{$_}};
$text .= "%9[".$_.']%9'."\n";
$text .= " %9|-[page ]->%9 ".$site{'page'}."\n";
#$text .= " %9|-[desc ]->%9 ".$site{'description'}."\n" if defined $site{'description'};
$Text::Wrap::columns = 60;
my $filter = $site{'description'};
$filter =~ s/<.*?>//;
my $desc = wrap(" %9|-[desc ]->%9 ",' %9|%9', $filter);
$desc =~ s// /g;
$text .= $desc."\n" if $site{'description'};
$text .= " %9|-[limit ]->%9 ".$site{'maxnews'}."\n";
$text .= " %9`-[enable]->%9 ".$site{'enable'}."\n";
}
print CLIENTCRAP draw_box("Newsline", $text, "newsline sources", 1);
} else {
foreach (@arg) {
if (defined $sites{$_}) {
call_openurl($sites{$_}->{'link'}) if defined $sites{$_}->{'link'};
} elsif (/\d+/ && defined $ticker[$_-1]) {
call_openurl($ticker[$_-1]->{'link'});
}
}
}
}
sub show_ticker (@) {
my (@ticker) = @_;
my $i = 1;
my $text = '';
foreach (@ticker) {
my $space = ' 'x(length(scalar(@ticker))-length($i));
my $newsitem = '%r'.$space.$i.'->%n['.$$_{source}.'] %9'.$$_{title}.'%9';
$newsitem .= ' %9[*]%9' if defined($$_{description});
$text .= $newsitem."\n";
$text .= " %B`->%n%U".$$_{link}."%U \n" if Irssi::settings_get_bool('newsline_show_url');
$i++;
}
print CLIENTCRAP draw_box("Newsline", $text, "headlines", 1);
}
sub call_openurl ($) {
my ($url) = @_;
no strict "refs";
# check for a loaded openurl
if (my $code = Irssi::Script::openurl::->can('launch_url')) {
$code->($url);
} else {
print CLIENTCRAP "%R>>%n Please install openurl.pl";
}
use strict "refs";
}
sub newsline_ticker ($$) {
my ($item, $get_size_only) = @_;
if (Irssi::settings_get_bool('newsline_ticker_scroll')) {
draw_tape($item, $get_size_only);
} else {
draw_ticker($item, $get_size_only);
}
}
sub draw_ticker ($$) {
my ($item, $get_size_only) = @_;
if ($index >= scalar(@ticker)) {
$index = 0
}
my $tape;
$tape .= '%F%Y%n' if $forked;
if (scalar(@ticker) > 0) {
my $title = $ticker[$index]->{'title'};
my $source = $ticker[$index]->{'source'};
$tape .= '>'.($index+1).': ['.$source.'] '.$title;
$tape .= ' [*]' if defined($ticker[$index]->{description});
$tape .= '<';
} else {
$tape .= '>Enter "/newsline fetch" to retrieve tickerdata>' unless $forked;
}
$tape = substr($tape, 0, Irssi::settings_get_int('newsline_ticker_max_width'));
my $format = "{sb ".$tape."}";
$item->{min_size} = $item->{max_size} = length($tape)+2;
$item->default_handler($get_size_only, $format, 0, 1);
}
sub rotate ($$) {
my ($text, $rot) = @_;
return($text) if length($text) < 1;
for (0..$rot) {
my $letter = substr($text, 0, 1);
$text = substr($text, 1);
$text = $text.$letter;
}
return($text);
}
sub draw_tape ($$) {
my ($item, $get_size_only) = @_;
my $tape;
if (scalar(@ticker) > 0) {
my $i=1;
foreach (@ticker) {
my $title = $_->{'title'};
my $source = $_->{'source'};
$tape .= '>'.($i).': ['.$source.'] '.$title.'|';
$i++;
}
$tape = $tape;
$slide = 0 if $slide >= length($tape);
$tape = rotate($tape, $slide);
$tape = substr($tape, 0, Irssi::settings_get_int('newsline_ticker_max_width'));
} else {
$tape .= 'Use "/newsline -f" to fetch tickerdata';
}
my $format = "{sb ".$tape."}";
$item->{min_size} = $item->{max_size} = length($tape)+2;
$item->default_handler($get_size_only, $format, 0, 1);
}
sub cycle_ticker () {
$index++;
if ($index >= scalar(@ticker)) {
$index = 0
}
$slide++;
Irssi::statusbar_items_redraw('newsline_ticker');
}
sub update_ticker () {
fork_get();
}
sub reload_config() {
my $filename = Irssi::settings_get_str('newsline_sites_file');
my $text;
if (-e $filename) {
local *F;
open F, "<",$filename;
$text .= $_ foreach ();
close F;
if ($text) {
no strict;
my %pages = %{ eval "$text" };
if (%pages) {
%sites = ();
foreach (keys %pages) {
$sites{$_} = $pages{$_};
}
}
}
}
Irssi::timeout_remove($timer_cycle) if defined $timer_cycle;
Irssi::timeout_remove($timer_update) if defined $timer_update;
$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;
$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;
Irssi::statusbar_items_redraw('newsline_ticker');
print CLIENTCRAP '%R>>%n Newsline sites loaded from '.$filename;
}
sub save_config() {
local *F;
my $filename = Irssi::settings_get_str('newsline_sites_file');
open(F, '>',$filename);
my $dumper = Data::Dumper->new([\%sites], ['sites']);
$dumper->Purity(1)->Deepcopy(1);
my $data = $dumper->Dump;
print (F $data);
close(F);
print CLIENTCRAP '%R>>%n Newsline sites saved to '.$filename;
}
sub de_umlaut ($) {
my ($data) = @_;
Unicode::String->stringify_as('utf8');
my $s = new Unicode::String($data);
my $result = $s->latin1();
return($result);
}
sub sig_complete_word ($$$$$) {
my ($list, $window, $word, $linestart, $want_space) = @_;
return unless $linestart =~ /^.newsline (toggle|delete|add|limit)/;
foreach (keys %sites) {
push @$list, $_ if /^(\Q$word\E.*)?$/;
}
Irssi::signal_stop();
}
Irssi::signal_add_first('complete word', \&sig_complete_word);
Irssi::signal_add('setup saved', \&save_config);
Irssi::command_bind('newsline', \&cmd_newsline);
foreach my $cmd ('description', 'paste', 'paste description', 'fetch', 'reload', 'save', 'list', 'toggle', 'add', 'delete', 'help', 'limit') {
Irssi::command_bind('newsline '.$cmd =>
sub { cmd_newsline("$cmd ".$_[0], $_[1], $_[2]); } );
}
Irssi::settings_add_int($IRSSI{'name'}, 'newsline_fetch_interval', 600);
Irssi::settings_add_int($IRSSI{'name'}, 'newsline_ticker_max_width', 50);
Irssi::settings_add_int($IRSSI{'name'}, 'newsline_ticker_cycle_delay', 3000);
Irssi::settings_add_str($IRSSI{'name'}, 'newsline_sites_file', Irssi::get_irssi_dir()."/newsline_sites");
Irssi::settings_add_bool($IRSSI{'name'}, 'newsline_show_url', 1);
Irssi::settings_add_bool($IRSSI{'name'}, 'newsline_ticker_scroll', 0);
Irssi::statusbar_item_register('newsline_ticker', 0, 'newsline_ticker');
reload_config();
update_ticker();
print CLIENTCRAP '%B>>%n '.$IRSSI{name}.' '.$VERSION.' loaded: /newsline help for help';