]> git.donarmstrong.com Git - infobot.git/blob - src/Modules/News.pl
5e1200bcc5bea765a156c6aa3d3e3f1695cf9629
[infobot.git] / src / Modules / News.pl
1 #
2 # News.pl: Advanced news management
3 #   Author: dms
4 #  Version: v0.3 (20010412)
5 #  Created: 20010326
6 #    Notes: Testing done by greycat, kudos!
7 #
8 ### structure:
9 # news{ channel }{ string } { item }
10 # newsuser{ channel }{ user } = time()
11 ### where item is:
12 #       Time    - when it was added (used for sorting)
13 #       Author  - Who by.
14 #       Expire  - Time to expire.
15 #       Text    - Actual text.
16 ###
17
18 package News;
19
20 use strict;
21
22 use vars qw($who $chan);
23
24 sub Parse {
25     my($what)   = @_;
26     $chan       = undef;
27     $who        = lc $::who;
28
29     if (!keys %::news) {
30         if (!exists $::cache{newsFirst}) {
31             &::DEBUG("news: looks like we enabled news option just then; loading up news file just in case.");
32             $::cache{newsFirst} = 1;
33         }
34
35         &readNews();
36     }
37
38     if ($::msgType ne 'private') {
39         $chan = $::chan;
40     }
41
42     if (defined $what and $what =~ s/^($::mask{chan})\s*//) {
43         # TODO: check if the channel exists aswell.
44         $chan   = lc $1;
45
46         if (!&::IsNickInChan($who, $chan)) {
47             &::notice($who, "sorry but you're not on $chan.");
48             return;
49         }
50     }
51
52     if (!defined $chan) {
53         my @chans = &::getNickInChans($who);
54
55         if (scalar @chans > 1) {
56             &::notice($who, "error: I dunno which channel you are referring to since you're on more than one. Try 'news #chan ...' instead");
57             return;
58         }
59
60         if (scalar @chans == 0) {
61             &::notice($who, "error: I couldn't find you on any chan. This must be a bug!");
62             return;
63         }
64
65         $chan   = $chans[0];
66         &::VERB("Guessed $who being on chan $chan",2);
67         $::chan = $chan;        # hack for IsChanConf().
68     }
69
70     if (!defined $what or $what =~ /^\s*$/) {
71         &list();
72         return;
73     }
74
75     if ($what =~ /^add(\s+(.*))?$/i) {
76         &add($2);
77
78     } elsif ($what =~ /^del(\s+(.*))?$/i) {
79         &del($2);
80
81     } elsif ($what =~ /^mod(\s+(.*))?$/i) {
82         &mod($2);
83
84     } elsif ($what =~ /^set(\s+(.*))?$/i) {
85         &set($2);
86
87     } elsif ($what =~ /^(\d+)$/i) {
88         &::VERB("News: read shortcut called.",2);
89         &read($1);
90
91     } elsif ($what =~ /^read(\s+(.*))?$/i) {
92         &read($2);
93
94     } elsif ($what =~ /^(latest|new)(\s+(.*))?$/i) {
95         &latest($3 || $chan, 1);
96 #       $::cmdstats{'News latest'}++;
97
98     } elsif ($what =~ /^stats?$/i) {
99         &stats();
100
101     } elsif ($what =~ /^list$/i) {
102         &list();
103
104     } elsif ($what =~ /^(expire|text|desc)(\s+(.*))?$/i) {
105         # shortcut/link.
106         # nice hack.
107         my $cmd = $1;
108         my($arg1,$arg2) = split(/\s+/, $3, 2);
109         &set("$arg1 $cmd $arg2");
110
111     } elsif ($what =~ /^help(\s+(.*))?$/i) {
112         &::help("news $2");
113
114     } elsif ($what =~ /^newsflush$/i) {
115         &::msg($who, "newsflush called... check out the logs!");
116         &::newsFlush();
117
118     } elsif ($what =~ /^(un)?notify$/i) {
119         my $state = ($1) ? 0 : 1;
120
121         # TODO: don't notify even if 'News' is called.
122         if (&::IsChanConf('newsNotifyAll') <= 0) {
123             &::DEBUG("news: chan => $chan, ::chan => $::chan.");
124             &::notice($who, "not available for this channel or disabled altogether.");
125             return;
126         }
127
128         my $t = $::newsuser{$chan}{$who};
129         if ($state) {   # state = 1
130             if (defined $t and ($t == 0 or $t == -1)) {
131                 &::notice($who, "enabled notify.");
132                 delete $::newsuser{$chan}{$who};
133                 return;
134             }
135             &::notice($who, "already enabled.");
136
137         } else {                # state = 0
138             my $x = $::newsuser{$chan}{$who};
139             if (defined $x and ($x == 0 or $x == -1)) {
140                 &::notice($who, 'notify already disabled');
141                 return;
142             }
143             $::newsuser{$chan}{$who} = -1;
144             &::notice($who, "notify is now disabled.");
145         }
146
147     } else {
148         &::notice($who, "unknown command: $what");
149     }
150 }
151
152 sub readNews {
153     my $file = "$::bot_base_dir/blootbot-news.txt";
154     if (! -f $file or -z $file) {
155         return;
156     }
157
158     if (fileno NEWS) {
159         &::DEBUG("readNews: fileno exists, should never happen.");
160         return;
161     }
162
163     my($item,$chan);
164     my($ci,$cu) = (0,0);
165
166     open(NEWS, $file);
167     while (<NEWS>) {
168         chop;
169
170         # TODO: allow commands.
171
172         if (/^[\s\t]+(\S+):[\s\t]+(.*)$/) {
173             if (!defined $item) {
174                 &::DEBUG("news: !defined item, never happen!");
175                 next;
176             }
177
178             $::news{$chan}{$item}{$1} = $2;
179             next;
180         }
181
182         # U <chan> <nick> <time>
183         if (/^U\s+(\S+)\s+(\S+)\s+(\d+)$/) {
184             $::newsuser{$1}{$2} = $3;
185             $cu++;
186             next;
187         }
188
189         if (/^(\S+)[\s\t]+(.*)$/) {
190             $chan = $1;
191             $item = $2;
192             $ci++;
193         }
194     }
195     close NEWS;
196
197     my $cn = scalar(keys %::news);
198     return unless ($ci or $cn or $cu);
199
200     &::status("News: read ".
201         $ci. &::fixPlural(' item', $ci). ' for '.
202         $cn. &::fixPlural(' chan', $cn). ', '.
203         $cu. &::fixPlural(' user', $cu), ' cache'
204     );
205 }
206
207 sub writeNews {
208     if (!scalar keys %::news and !scalar keys %::newsuser) {
209         &::VERB("wN: nothing to write.",2);
210         return;
211     }
212
213     # should define this at the top of file.
214     my $file = "$::bot_base_dir/blootbot-news.txt";
215
216     if (fileno NEWS) {
217         &::ERROR("News: write: fileno NEWS exists, should never happen.");
218         return;
219     }
220
221     # TODO: add commands to output file.
222     my $c = 0;
223     my($cc,$ci,$cu) = (0,0,0);
224
225     open(NEWS, ">$file");
226     foreach $chan (sort keys %::news) {
227         $c = scalar keys %{ $::news{$chan} };
228         next unless ($c);
229         $cc++;
230         my $item;
231
232         foreach $item (sort keys %{ $::news{$chan} }) {
233             $c = scalar keys %{ $::news{$chan}{$item} };
234             next unless ($c);
235             $ci++;
236
237             print NEWS "$chan $item\n";
238             my $what;
239             foreach $what (sort keys %{ $::news{$chan}{$item} }) {
240                 print NEWS "    $what: $::news{$chan}{$item}{$what}\n";
241             }
242             print NEWS "\n";
243         }
244     }
245
246     # TODO: show how many users we wrote down.
247     if (&::getChanConfList('newsKeepRead')) {
248         # old users are removed in newsFlush(), perhaps it should be
249         # done here.
250
251         foreach $chan (sort keys %::newsuser) {
252
253             foreach (sort keys %{ $::newsuser{$chan} }) {
254                 print NEWS "U $chan $_ $::newsuser{$chan}{$_}\n";
255                 $cu++;
256             }
257         }
258     }
259
260     close NEWS;
261
262     &::status("News: Wrote $ci items for $cc chans, $cu user cache.");
263 }
264
265 sub add {
266     my($str) = @_;
267
268     if (!defined $chan or !defined $str or $str =~ /^\s*$/) {
269         &::help('news add');
270         return;
271     }
272
273     if (length $str > 64) {
274         &::notice($who, "That's not really an item (>64chars)");
275         return;
276     }
277
278     if (exists $::news{$chan}{$str}{Time}) {
279         &::notice($who, "'$str' for $chan already exists!");
280         return;
281     }
282
283     $::news{$chan}{$str}{Time}  = time();
284     my $expire = &::getChanConfDefault('newsDefaultExpire',7, $chan);
285     $::news{$chan}{$str}{Expire}        = time() + $expire*60*60*24;
286     $::news{$chan}{$str}{Author}        = $::who;       # case!
287
288     my $agestr  = &::Time2String($::news{$chan}{$str}{Expire} - time() );
289     my $item    = &newsS2N($str);
290     &::notice($who, "Added '\037$str\037' at [".gmtime(time).
291                 "] by \002$::who\002 for item #\002$item\002.");
292     &::notice($who, "Now do 'news text $item <your_description>'");
293     &::notice($who, "This item will expire at \002".
294         gmtime($::news{$chan}{$str}{Expire})."\002 [$agestr from now] "
295     );
296
297     &writeNews();
298 }
299
300 sub del {
301     my($what)   = @_;
302     my $item    = 0;
303
304     if (!defined $what) {
305         &::help('news del');
306         return;
307     }
308
309     if ($what =~ /^\d+$/) {
310         my $count = scalar keys %{ $::news{$chan} };
311         if (!$count) {
312             &::notice($who, "No news for $chan.");
313             return;
314         }
315
316         if ($what > $count or $what < 0) {
317             &::notice($who, "$what is out of range (max $count)");
318             return;
319         }
320
321         $item   = &getNewsItem($what);
322         $what   = $item;                # hack hack hack.
323
324     } else {
325         $_      = &getNewsItem($what);  # hack hack hack.
326         $what   = $_ if (defined $_);
327
328         if (!exists $::news{$chan}{$what}) {
329             my @found;
330             foreach (keys %{ $::news{$chan} }) {
331                 next unless (/\Q$what\E/);
332                 push(@found, $_);
333             }
334
335             if (!scalar @found) {
336                 &::notice($who, "could not find $what.");
337                 return;
338             }
339
340             if (scalar @found > 1) {
341                 &::notice($who, "too many matches for $what.");
342                 return;
343             }
344
345             $what       = $found[0];
346             &::DEBUG("news: del: str: guessed what => $what");
347         }
348     }
349
350     if (exists $::news{$chan}{$what}) {
351         my $auth = 0;
352         $auth++ if ($::who eq $::news{$chan}{$what}{Author});
353         $auth++ if (&::IsFlag('o'));
354
355         if (!$auth) {
356             # TODO: show when it'll expire.
357             &::notice($who, "Sorry, you cannot remove items; just let them expire on their own.");
358             return;
359         }
360
361         &::notice($who, "ok, deleted '$what' from \002$chan\002...");
362         delete $::news{$chan}{$what};
363     } else {
364         &::notice($who, "error: not found $what in news for $chan.");
365     }
366 }
367
368 sub list {
369     if (!scalar keys %{ $::news{$chan} }) {
370         &::notice($who, "No news for \002$chan\002.");
371         return;
372     }
373
374     if (&::IsChanConf('newsKeepRead') > 0) {
375         my $x = $::newsuser{$chan}{$who};
376
377         if (defined $x and ($x == 0 or $x == -1)) {
378             &::DEBUG("news: not updating time for $who.");
379         } else {
380             if (!scalar keys %{ $::news{$chan} }) {
381                 &::DEBUG("news: should not add $chan/$who to cache!");
382             }
383
384             $::newsuser{$chan}{$who} = time();
385         }
386     }
387
388     # &notice() breaks OPN :( - using msg() instead!
389     my $count = scalar keys %{ $::news{$chan} };
390     &::msg($who, "|==== News for \002$chan\002: ($count items)");
391     my $newest  = 0;
392     my $expire  = 0;
393     my $eno     = 0;
394     foreach (keys %{ $::news{$chan} }) {
395         my $t   = $::news{$chan}{$_}{Time};
396         my $e   = $::news{$chan}{$_}{Expire};
397         $newest = $t if ($t > $newest);
398         if ($e > 1 and $e < $expire) {
399             $expire     = $e;
400             &::DEBUG("before newsS2N($_)");
401             $eno        = &newsS2N($_);
402             &::DEBUG("after newsS2N($_) == $eno");
403         }
404     }
405     my $timestr = &::Time2String(time() - $newest);
406     &::msg($who, "|= Last updated $timestr ago.");
407     &::msg($who, " \037Num\037  \037Item ".(' 'x40)." \037");
408
409 #    &::DEBUG("news: list: expire = $expire");
410 #    &::DEBUG("news: list: eno    = $eno");
411
412     my $i = 1;
413     foreach ( &getNewsAll() ) {
414         my $subtopic    = $_;
415         my $setby       = $::news{$chan}{$subtopic}{Author};
416         my $chr         = (exists $::News{$chan}{$subtopic}{Text}) ? '' : '*';
417
418         if (!defined $subtopic) {
419             &::DEBUG("news: warn: subtopic == undef.");
420             next;
421         }
422
423         # TODO: show request stats aswell.
424         &::msg($who, sprintf("\002[\002%2d\002]\002%s %s",
425                                 $i, $chr, $subtopic));
426         $i++;
427     }
428
429     my $z = $::newsuser{$who};
430     if (defined $z) {
431         &::DEBUG("cache $who: $z");
432     } else {
433         &::DEBUG("cache: $who doesn't have newscache set.");
434     }
435
436     &::msg($who, "|= End of News.");
437     &::msg($who, "use 'news read <#>' or 'news read <keyword>'");
438 }
439
440 sub read {
441     my($str) = @_;
442
443     if (!defined $chan or !defined $str or $str =~ /^\s*$/) {
444         &::help('news read');
445         return;
446     }
447
448     if (!scalar keys %{ $::news{$chan} }) {
449         &::notice($who, "No news for \002$chan\002.");
450         return;
451     }
452
453     my $item    = &getNewsItem($str);
454     if (!defined $item or !scalar keys %{ $::news{$chan}{$item} }) {
455         # TODO: numerical check.
456         if ($str =~ /^(\d+)[-, ](\d+)$/ or
457             $str =~ /^-(\d+)$/ or
458             $str =~ /^(\d+)-$/ or 0
459         ) {
460             &::notice($who, "We don't support multiple requests of news items yet.  Sorry.");
461             return;
462         }
463
464         &::notice($who, "No news item called '$str'");
465         return;
466     }
467
468     if (!exists $::news{$chan}{$item}{Text}) {
469         &::notice($who, 'Someone forgot to add info to this news item');
470         return;
471     }
472
473     my $t       = gmtime( $::news{$chan}{$item}{Time} );
474     my $a       = $::news{$chan}{$item}{Author};
475     my $text    = $::news{$chan}{$item}{Text};
476     my $num     = &newsS2N($item);
477     my $rwho    = $::news{$chan}{$item}{Request_By} || $::who;
478     my $rcount  = $::news{$chan}{$item}{Request_Count} || 0;
479
480     if (length $text < $::param{maxKeySize}) {
481         &::VERB("NEWS: Possible news->factoid redirection.",2);
482         my $f   = &::getFactoid($text);
483
484         if (defined $f) {
485             &::VERB("NEWS: ok, $text is factoid redirection.",2);
486             $f =~ s/^<REPLY>\s*//i;     # anything else?
487             $text = $f;
488         }
489     }
490
491     $_ = $::news{$chan}{$item}{'Expire'};
492     my $e;
493     if ($_) {
494         $e = sprintf("\037%s\037  [%s from now]",
495                 scalar(gmtime($_)),
496                 &::Time2String($_ - time())
497         );
498     }
499
500     &::notice($who, "+- News \002$chan\002 #$num: $item");
501     &::notice($who, "| Added by $a at \037$t\037");
502     &::notice($who, "| Expire: $e") if (defined $e);
503     &::notice($who, $text);
504     &::notice($who, "| Requested \002$rcount\002 times, last by \002$rwho\002") if ($rcount and $rwho);
505
506     $::news{$chan}{$item}{'Request_By'}   = $::who;
507     $::news{$chan}{$item}{'Request_Time'} = time();
508     $::news{$chan}{$item}{'Request_Count'}++;
509 }
510
511 sub mod {
512     my($item, $str) = split /\s+/, $_[0], 2;
513
514     if (!defined $item or $item eq '' or $str =~ /^\s*$/) {
515         &::help('news mod');
516         return;
517     }
518
519     my $news = &getNewsItem($item);
520
521     if (!defined $news) {
522         &::DEBUG("news: error: mod: news == undefined.");
523         return;
524     }
525     my $nnews = $::news{$chan}{$news}{Text};
526     my $mod_news  = $news;
527     my $mod_nnews = $nnews;
528
529     # SAR patch. mu++
530     if ($str =~ m|^\s*s([/,#\|])(.+?)\1(.*?)\1([a-z]*);?\s*$|) {
531         my ($delim, $op, $np, $flags) = ($1,$2,$3,$4);
532
533         if ($flags !~ /^(g)?$/) {
534             &::notice($who, "error: Invalid flags to regex.");
535             return;
536         }
537
538         ### TODO: use m### to make code safe!
539         # TODO: make code safer.
540         my $done = 0;
541         # TODO: use eval to deal with flags easily.
542         if ($flags eq '') {
543             $done++ if (!$done and $mod_news  =~ s/\Q$op\E/$np/);
544             $done++ if (!$done and $mod_nnews =~ s/\Q$op\E/$np/);
545         } elsif ($flags eq 'g') {
546             $done++ if ($mod_news  =~ s/\Q$op\E/$np/g);
547             $done++ if ($mod_nnews =~ s/\Q$op\E/$np/g);
548         }
549
550         if (!$done) {
551             &::notice($who, "warning: regex not found in news.");
552             return;
553         }
554
555         if ($mod_news ne $news) { # news item.
556             if (exists $::news{$chan}{$mod_news}) {
557                 &::notice($who, "item '$mod_news' already exists.");
558                 return;
559             }
560
561             &::notice($who, "Moving item '$news' to '$mod_news' with SAR s/$op/$np/.");
562             foreach (keys %{ $::news{$chan}{$news} }) {
563                 $::news{$chan}{$mod_news}{$_} = $::news{$chan}{$news}{$_};
564                 delete $::news{$chan}{$news}{$_};
565             }
566             # needed?
567             delete $::news{$chan}{$news};
568         }
569
570         if ($mod_nnews ne $nnews) { # news Text/Description.
571             &::notice($who, "Changing text for '$news' SAR s/$op/$np/.");
572             if ($mod_news ne $news) {
573                 $::news{$chan}{$mod_news}{Text} = $mod_nnews;
574             } else {
575                 $::news{$chan}{$news}{Text}     = $mod_nnews;
576             }
577         }
578
579         return;
580     } else {
581         &::notice($who, "error: that regex failed ;(");
582         return;
583     }
584
585     &::notice($who, "error: Invalid regex. Try s/1/2/, s#3#4#...");
586 }
587
588 sub set {
589     my($args) = @_;
590     my($item, $what, $value);
591
592     if (!defined $args) {
593         &::DEBUG("news: set: args == NULL.");
594         return;
595     }
596
597     $item = $1 if ($args =~ s/^(\S+)\s*//);
598     $what = $1 if ($args =~ s/^(\S+)\s*//);
599     $value = $args;
600
601     if ($item eq '') {
602         &::help('news set');
603         return;
604     }
605
606     my $news = &getNewsItem($item);
607
608     if (!defined $news) {
609         &::notice($who, "Could not find item '$item' substring or # in news list.");
610         return;
611     }
612
613     # list all values for chan.
614     if (!defined $what or $what =~ /^\s*$/) {
615         &::msg($who, "set: you didn't fill me on the arguments! (what and values)");
616         return;
617     }
618
619     my $ok = 0;
620     my @elements = ('Expire','Text');
621     foreach (@elements) {
622         next unless ($what =~ /^$_$/i);
623         $what = $_;
624         $ok++;
625         last;
626     }
627
628     if (!$ok) {
629         &::notice($who, "Invalid set.  Try: @elements");
630         return;
631     }
632
633     # show (read) what.
634     if (!defined $value or $value =~ /^\s*$/) {
635         &::msg($who, "set: you didn't fill me on the arguments! (value)");
636         return;
637     }
638
639     if (!exists $::news{$chan}{$news}) {
640         &::notice($who, "news '$news' does not exist");
641         return;
642     }
643
644     if ($what eq 'Expire') {
645         # TODO: use do_set().
646
647         my $time = 0;
648         my $plus = ($value =~ s/^\+//g);
649         while ($value =~ s/^(\d+)(\S*)\s*//) {
650             my($int,$unit) = ($1,$2);
651             $time += $int       if ($unit =~ /^s(ecs?)?$/i);
652             $time += $int*60    if ($unit =~ /^m(in(utes?)?)?$/i);
653             $time += $int*60*60 if ($unit =~ /^h(ours?)?$/i);
654             $time += $int*60*60*24 if (!$unit or $unit =~ /^d(ays?)?$/i);
655             $time += $int*60*60*24*7 if ($unit =~ /^w(eeks?)?$/i);
656             $time += $int*60*60*24*30 if ($unit =~ /^mon(th)?$/i);
657         }
658
659         if ($value =~ s/^never$//i) {
660             # never.
661             $time = -1;
662         } elsif ($plus) {
663             # from now.
664             $time += time();
665         } else {
666             # from creation of item.
667             $time += $::news{$chan}{$news}{Time};
668         }
669
670         if (!$time or ($value and $value !~ /^never$/i)) {
671             &::DEBUG("news: set: Expire... need to parse.");
672             &::msg($who, "hrm... couldn't parse that.");
673             return;
674         }
675
676         if ($time == -1) {
677             &::notice($who, "Set never expire for \002$item\002." );
678         } elsif ($time < -1) {
679             &::DEBUG("news: time should never be negative ($time).");
680             return;
681         } else {
682             &::notice($who, "Set expire for \002$item\002, to ".
683                 gmtime($time) ." [".&::Time2String($time - time())."]" );
684
685             if (time() > $time) {
686                 &::DEBUG("news: hrm... time() > $time, should expire.");
687             }
688         }
689
690
691         $::news{$chan}{$news}{Expire} = $time;
692
693         return;
694     }
695
696     my $auth = 0;
697 #    &::DEBUG("news: who => '$who'");
698     my $author = $::news{$chan}{$news}{Author};
699     $auth++ if ($::who eq $author);
700     $auth++ if (&::IsFlag('o'));
701     if (!defined $author) {
702         &::DEBUG("news: news{$chan}{$news}{Author} is not defined! auth'd anyway");
703         $::news{$chan}{$news}{Author} = $::who;
704         $author = $::who;
705         $auth++;
706     }
707
708     if (!$auth) {
709         # TODO: show when it'll expire.
710         &::notice($who, "Sorry, you cannot set items. (author $author owns it)");
711         return;
712     }
713
714     # TODO: clean this up.
715     my $old = $::news{$chan}{$news}{$what};
716     if (defined $old) {
717         &::DEBUG("news: old => $old.");
718     }
719     $::news{$chan}{$news}{$what} = $value;
720     &::notice($who, "Setting [$chan]/{$news}/<$what> to '$value'.");
721 }
722
723 sub latest {
724     my ($tchan, $flag) = @_;
725
726     # hack hack hack.  fix later.
727     $chan = $tchan;
728     $who  = $::who;
729
730     # TODO: if chan = undefined, guess.
731 #    if (!exists $::news{$chan}) {
732     if (!exists $::channels{$chan}) {
733         &::notice($who, "invalid chan $chan") if ($flag);
734         return;
735     }
736
737     my $t = $::newsuser{$chan}{$who};
738 #    if (defined $t) {
739 #       &::DEBUG("newsuser: $chan/$who == $t");
740 #    } else {
741 #       &::DEBUG("newsuser: $chan/$who == undefined");
742 #    }
743
744     if (defined $t and ($t == 0 or $t == -1)) {
745         if ($flag) {
746             &::notice($who, "if you want to read news, try \002/msg $::ident news $chan\002 or \002/msg $::ident news $chan notify\002");
747         } else {
748             &::DEBUG("news: not displaying any new news for $who");
749             return;
750         }
751     }
752
753     $::chan     = $chan;
754     return if (&::IsChanConf('newsNotifyAll') <= 0);
755
756     # I don't understand this code ;)
757     $t = 1 if (!defined $t);
758
759     if (!defined $t) {
760 #       &::msg($who, "News is disabled for $chan");
761         &::DEBUG("news: $chan: something went really wrong.");
762         return;
763     }
764
765     my @new;
766     foreach (keys %{ $::news{$chan} }) {
767         next if (!defined $t);
768         next if ($t > $::news{$chan}{$_}{Time});
769
770         # don't list new items if they don't have Text.
771         if (!exists $::news{$chan}{$_}{Text}) {
772             if (time() - $::news{$chan}{$_}{Time} > 60*60*24*3) {
773                 &::DEBUG("deleting news{$chan}{$_} because it was too old and had no text info.");
774                 delete $::news{$chan}{$_};
775             }
776
777             next;
778         }
779
780         push(@new, $_);
781     }
782
783     # !scalar @new, $flag
784     if (!scalar @new and $flag) {
785         &::notice($who, "no new news for $chan for $who.");
786         # valid to set this?
787         $::newsuser{$chan}{$who} = time();
788         return;
789     }
790
791     # scalar @new, !$flag
792     my $unread  = scalar @new;
793     my $total   = scalar keys %{ $::news{$chan} };
794     if (!$flag && &::IsChanConf('newsTellUnread') <= 0) {
795         return;
796     }
797
798     if (!$flag) {
799         return unless ($unread);
800
801         # just a temporary measure not to flood ourself off the
802         # network with news until we get global notice() and msg()
803         # throttling.
804         if (time() - ($::cache{newsTime} || 0) < 5) {
805             &::status("news: not displaying latest notice to $who/$chan.");
806             return;
807         }
808
809         $::cache{newsTime} = time();
810         my $reply = "There are unread news in $chan ($unread unread, $total total). /msg $::ident news $::chan latest";
811         $reply   .= "  If you don't want further news notification, /msg $::ident news unnotify" if ($unread == $total);
812         &::notice($who, $reply);
813
814         return;
815     }
816
817     # scalar @new, $flag
818     if (scalar @new) {
819         &::notice($who, "+==== New news for \002$chan\002 ($unread new; $total total):");
820
821         my $t = $::newsuser{$chan}{$who};
822         if (defined $t and $t > 1) {
823             my $timestr = &::Time2String( time() - $t );
824             &::notice($who, "|= Last time read $timestr ago");
825         }
826
827         my $i;
828         my @sorted;
829         foreach (@new) {
830             $i   = &newsS2N($_);
831             $sorted[$i] = $_;
832         }
833
834         for ($i=0; $i<=scalar(@sorted); $i++) {
835             my $news = $sorted[$i];
836             next unless (defined $news);
837
838 #           my $age = time() - $::news{$chan}{$news}{Time};
839             my $msg = sprintf("\002[\002%2d\002]\002 %s", $i, $news);
840 ###                     $i, $_, &::Time2String($age)
841             $::conn->schedule(int((2+$i)/2), sub {
842                 &::notice($who, $msg);
843             } );
844         }
845
846         # TODO: implement throttling via schedule into &notice() / &msg().
847         $::conn->schedule(int((2+$i)/2), sub {
848             &::notice($who, "|= to read, do \002news $chan read <#>\002 or \002news $chan read <keyword>\002");
849         } );
850
851         # lame hack to prevent dupes if we just ignore it.
852         my $x = $::newsuser{$chan}{$who};
853         if (defined $x and ($x == 0 or $x == -1)) {
854             &::DEBUG("news: not updating time for $who. (2)");
855         } else {
856             $::newsuser{$chan}{$who} = time();
857         }
858     }
859 }
860
861 ###
862 ### helpers...
863 ###
864
865 sub getNewsAll {
866     my %time;
867     foreach (keys %{ $::news{$chan} }) {
868         $time{ $::news{$chan}{$_}{Time} } = $_;
869     }
870
871     my @items;
872     foreach (sort { $a <=> $b } keys %time) {
873         push(@items, $time{$_});
874     }
875
876     return @items;
877 }
878
879 sub newsS2N {
880     my($what)   = @_;
881     my $item    = 0;
882     my @items;
883     my $no;
884
885     my %time;
886     foreach (keys %{ $::news{$chan} }) {
887         my $t = $::news{$chan}{$_}{Time};
888
889         if (!defined $t or $t !~ /^\d+$/) {
890             &::DEBUG("news: warn: t is undefined for news{$chan}{$_}{Time}; removing item.");
891             delete $::news{$chan}{$_};
892             next;
893         }
894
895         $time{$t} = $_;
896     }
897
898     foreach (sort { $a <=> $b } keys %time) {
899         $item++;
900         return $item if ($time{$_} eq $what);
901     }
902
903     &::DEBUG("newsS2N($what): failed...");
904 }
905
906 sub getNewsItem {
907     my($what)   = @_;
908     my $item    = 0;
909
910     $what =~ s/^\#//;   # '#1' for example.
911
912     my %time;
913     foreach (keys %{ $::news{$chan} }) {
914         my $t = $::news{$chan}{$_}{Time};
915
916         if (!defined $t or $t !~ /^\d+$/) {
917             &::DEBUG("news: warn: t is undefined for news{$chan}{$_}{Time}; removing item.");
918             delete $::news{$chan}{$_};
919             next;
920         }
921
922         $time{$t} = $_;
923     }
924
925     # number to string resolution.
926     if ($what =~ /^\d+$/) {
927         foreach (sort { $a <=> $b } keys %time) {
928             $item++;
929             return $time{$_} if ($item == $what);
930         }
931
932     } else {
933         # partial string to full string resolution
934         # in some cases, string->number resolution.
935
936         my @items;
937         my $no;
938         foreach (sort { $a <=> $b } keys %time) {
939             $item++;
940 #           $no = $item if ($time{$_} eq $what);
941 ##          if ($time{$_} eq $what) {
942 ##              $no = $item;
943 ##              next;
944 ##          }
945
946             push(@items, $time{$_}) if ($time{$_} =~ /\Q$what\E/i);
947         }
948
949 ##      if (defined $no and !@items) {
950 ##          &::DEBUG("news: string->number resolution: $what->$no.");
951 ##          return $no;
952 ##      }
953
954         if (scalar @items > 1) {
955             &::DEBUG("news: Multiple matches, not guessing.");
956             &::notice($who, "Multiple matches, not guessing.");
957             return;
958         }
959
960         if (@items) {
961 #           &::DEBUG("news: gNI: part_string->full_string: $what->$items[0]");
962             return $items[0];
963         } else {
964             &::DEBUG("news: gNI: No match for '$what'");
965             return;
966         }
967     }
968
969     &::ERROR("news: gNI: should not happen (what = $what)");
970     return;
971 }
972
973 sub do_set {
974     my($what,$value) = @_;
975
976     if (!defined $chan) {
977         &::DEBUG("news: do_set: chan not defined.");
978         return;
979     }
980
981     if (!defined $what or $what =~ /^\s*$/) {
982         &::DEBUG("news: what $what is not defined.");
983         return;
984     }
985
986     if (!defined $value or $value =~ /^\s*$/) {
987         &::DEBUG("news: value $value is not defined.");
988         return;
989     }
990
991     &::TODO("news: do_set:");
992 }
993
994 sub stats {
995     &::DEBUG("News: stats called.");
996     &::msg($who, "check my logs/console.");
997     my($i,$j) = (0,0);
998
999     # total request count.
1000     foreach $chan (keys %::news) {
1001         foreach (keys %{ $::news{$chan} }) {
1002             $i += $::news{$chan}{$_}{Request_Count};
1003         }
1004     }
1005     &::DEBUG("news: stats: total request count => $i");
1006     $i = 0;
1007
1008     # total user cached.
1009     foreach $chan (keys %::newsuser) {
1010         $i += $::newsuser{$chan}{$_};
1011     }
1012     &::DEBUG("news: stats: total user cache => $i");
1013     $i = 0;
1014
1015     # average latest time read.
1016     my $t = time();
1017     foreach $chan (keys %::newsuser) {
1018         $i += $t - $::newsuser{$chan}{$_};
1019         &::DEBUG(" i = $i");
1020         $j++;
1021     }
1022     &::DEBUG("news: stats: average latest time read: total time: $i");
1023     &::DEBUG("news: ... count: $j");
1024     &::DEBUG("news:   average: ".sprintf("%.02f", $i/($j||1))." sec/user");
1025     $i = $j = 0;
1026 }
1027
1028 sub AUTOLOAD { &::AUTOLOAD(@_); }
1029
1030 1;