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';