]> git.donarmstrong.com Git - infobot.git/blob - src/Modules/News.pl
- load file if we enabled option on the fly before re-run.
[infobot.git] / src / Modules / News.pl
1 #
2 # News.pl: Advanced news management
3 #   Author: dms
4 #  Version: v0.2 (20010326)
5 #  Created: 20010326
6 #    Notes: Testing done by greycat, kudos!
7 #
8 ### structure:
9 # news{ channel }{ string } { items }
10 # newsuser{ channel }{ user } = time()
11 ### where items 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 sub Parse {
21     my($what)   = @_;
22     $chan       = undef;
23
24     if (!keys %::news) {
25         if (!exists $cache{newsFirst}) {
26             &::DEBUG("looks like we enabled news option just then; loading up news file just in case.");
27             $cache{newsFirst} = 1;
28         }
29
30         &readNews();
31     }
32
33     if ($::msgType eq "private") {
34     } else {
35         $chan = $::chan;
36     }
37
38     if (defined $what and $what =~ s/^($::mask{chan})\s*//) {
39         # todo: check if the channel exists aswell.
40         $chan = $1;
41     }
42
43     if (!defined $chan) {
44         my @chans = &::GetNickInChans($::who);
45
46         if (scalar @chans > 1) {
47             &::msg($::who, "error: I dunno which channel you are referring to since you're on more than one.");
48             return;
49         }
50
51         if (scalar @chans == 0) {
52             &::msg($::who, "error: I couldn't find you on any chan. This must be a bug!");
53             return;
54         }
55
56         $chan = $chans[0];
57         &::DEBUG("Guessed $::who being on chan $chan");
58     }
59
60     if (!defined $what or $what =~ /^\s*$/) {
61         &list();
62         return;
63     }
64
65     if ($what =~ /^add(\s+(.*))?$/i) {
66         &add($2);
67     } elsif ($what =~ /^del(\s+(.*))?$/i) {
68         &del($2);
69     } elsif ($what =~ /^mod(\s+(.*))?$/i) {
70         &mod($2);
71     } elsif ($what =~ /^set(\s+(.*))?$/i) {
72         &set($2);
73     } elsif ($what =~ /^(\d)$/i) {
74         &::DEBUG("read shortcut called.");
75         &read($1);
76     } elsif ($what =~ /^read(\s+(.*))?$/i) {
77         &read($2);
78     } elsif ($what =~ /^list$/i) {
79         &::DEBUG("list longcut called.");
80         &list();
81     } elsif ($what =~ /^(expire|text|desc)(\s+(.*))?$/i) {
82         # shortcut/link.
83         # nice hack.
84         my($arg1,$arg2) = split(/\s+/, $3, 2);
85         &set("$arg1 $1 $arg2");
86     } elsif ($what =~ /^help(\s+(.*))?$/i) {
87         &::help("news$1");
88     } else {
89         &::DEBUG("could not parse '$what'.");
90         &::msg($::who, "unknown command: $what");
91     }
92 }
93
94 sub readNews {
95     my $file = "$::bot_base_dir/blootbot-news.txt";
96     if (! -f $file) {
97         return;
98     }
99
100     if (fileno NEWS) {
101         &::DEBUG("readNews: fileno exists, should never happen.");
102         return;
103     }
104
105     my($item,$chan);
106     my($ci,$cu) = (0,0);
107
108     open(NEWS, $file);
109     while (<NEWS>) {
110         chop;
111
112         # todo: allow commands.
113
114         if (/^[\s\t]+(\S+):[\s\t]+(.*)$/) {
115             if (!defined $item) {
116                 &::DEBUG("!defined item, never happen!");
117                 next;
118             }
119             $::news{$chan}{$item}{$1} = $2;
120             next;
121         }
122
123         # U <chan> <nick> <time>
124         if (/^U\s+(\S+)\s+(\S+)\s+(\d+)$/) {
125             $::newsuser{$1}{$2} = $3;
126             $cu++;
127             next;
128         }
129
130         if (/^(\S+)[\s\t]+(.*)$/) {
131             $chan = $1;
132             $item = $2;
133             $ci++;
134         }
135     }
136     close NEWS;
137
138     &::status("News: Read $ci items for ".scalar(keys %::news)
139                 ." chans, $cu users cache");
140 }
141
142 sub writeNews {
143     if (!scalar keys %::news) {
144         &::DEBUG("wN: nothing to write.");
145         return;
146     }
147
148     my $file = "$::bot_base_dir/blootbot-news.txt";
149
150     if (fileno NEWS) {
151         &::ERROR("fileno NEWS exists, should never happen.");
152         return;
153     }
154
155     # todo: add commands to output file.
156     my $c = 0;
157     my($cc,$ci,$cu) = (0,0,0);
158
159     open(NEWS, ">$file");
160     foreach $chan (sort keys %::news) {
161         $c = scalar keys %{ $::news{$chan} };
162         next unless ($c);
163         $cc++;
164
165         foreach $item (sort keys %{ $::news{$chan} }) {
166             $c = scalar keys %{ $::news{$chan}{$item} };
167             next unless ($c);
168             $ci++;
169
170             print NEWS "$chan $item\n";
171             foreach $what (sort keys %{ $::news{$chan}{$item} }) {
172                 print NEWS "    $what: $::news{$chan}{$item}{$what}\n";
173             }
174             print NEWS "\n";
175         }
176     }
177
178     # todo: show how many users we wrote down.
179     if (&::getChanConfList("newsKeepRead")) {
180         # old users are removed in newsFlush(), perhaps it should be
181         # done here.
182
183         foreach $chan (sort keys %::newsuser) {
184
185             foreach (sort keys %{ $::newsuser{$chan} }) {
186                 print NEWS "U $chan $_ $::newsuser{$chan}{$_}\n";
187                 $cu++;
188             }
189         }
190     }
191
192     close NEWS;
193
194     &::status("News: Wrote $ci items for $cc chans, $cu user cache.");
195 }
196
197 sub add {
198     my($str) = @_;
199
200     if (!defined $chan or !defined $str or $str =~ /^\s*$/) {
201         &::help("news add");
202         return;
203     }
204
205     if (length $str > 64) {
206         &::msg($::who, "That's not really an item (>64chars)");
207         return;
208     }
209
210     if (exists $::news{$chan}{$str}{Time}) {
211         &::msg($::who, "'$str' for $chan already exists!");
212         return;
213     }
214
215     $::news{$chan}{$str}{Time}  = time();
216     my $expire = &::getChanConfDefault("newsDefaultExpire",7);
217     $::news{$chan}{$str}{Expire}        = time() + $expire*60*60*24;
218     $::news{$chan}{$str}{Author}        = $::who;
219
220     my $agestr  = &::Time2String($::news{$chan}{$str}{Expire} - time() );
221     my $item    = &getNewsItem($str);
222     if ($item eq $str) {
223         &::DEBUG("item eq str ($item): should never happen.");
224     }
225     &::msg($::who, "Added '\037$str\037' at [".localtime(time).
226                 "] by \002$::who\002 for item #\002$item\002.");
227     &::msg($::who, "Now do 'news text $item <your_description>'");
228     &::msg($::who, "This item will expire at \002".
229         localtime($::news{$chan}{$str}{Expire})."\002 [$agestr from now] "
230     );
231 }
232
233 sub del {
234     my($what)   = @_;
235     my $item    = 0;
236
237     if (!defined $what) {
238         &::help("news del");
239         return;
240     }
241
242     if ($what =~ /^\d+$/) {
243         my $count = scalar keys %{ $::news{$chan} };
244         if (!$count) {
245             &::msg($::who, "No news for $chan.");
246             return;
247         }
248
249         if ($what > $count or $what < 0) {
250             &::msg($::who, "$what is out of range (max $count)");
251             return;
252         }
253
254         $item = &getNewsItem($what);
255         &::DEBUG("del: num: item => $item ($what)");
256         $what   = $item;        # hack hack hack.
257
258     } else {
259         $_      = &getNewsItem($what);  # hack hack hack.
260         $what   = $_ if (defined $_);
261
262         if (!exists $::news{$chan}{$what}) {
263             my @found;
264             foreach (keys %{ $::news{$chan} }) {
265                 next unless (/\Q$what\E/);
266                 push(@found, $_);
267             }
268
269             if (!scalar @found) {
270                 &::msg($::who, "could not find $what.");
271                 return;
272             }
273
274             if (scalar @found > 1) {
275                 &::msg($::who, "too many matches for $what.");
276                 return;
277             }
278
279             $what       = $found[0];
280             &::DEBUG("del: str: guessed what => $what");
281         }
282     }
283
284     if (exists $::news{$chan}{$what}) {
285         my $auth = 0;
286         $auth++ if ($::who eq $::news{$chan}{$what}{Author});
287         $auth++ if (&::IsFlag("o"));
288
289         if (!$auth) {
290             # todo: show when it'll expire.
291             &::msg($::who, "Sorry, you cannot remove items; just let them expire on their own.");
292             return;
293         }
294
295         &::msg($::who, "ok, deleted '$what' from \002$chan\002...");
296         delete $::news{$chan}{$what};
297     } else {
298         &::msg($::who, "error: not found $what in news for $chan.");
299     }
300 }
301
302 sub list {
303     if (!scalar keys %{ $::news{$chan} }) {
304         &::msg($::who, "No News for \002$chan\002.");
305         return;
306     }
307
308     if (&::IsChanConf("newsKeepRead")) {
309         $::newsuser{$chan}{$::who} = time();
310     }
311
312     &::msg($::who, "|==== News for \002$chan\002:");
313     my $newest  = 0;
314     foreach (keys %{ $::news{$chan} }) {
315         my $t   = $::news{$chan}{$_}{Time};
316         $newest = $t if ($t > $newest);
317     }
318     my $timestr = &::Time2String(time() - $newest);
319     &::msg($::who, "|= Last updated $timestr ago.");
320     &::msg($::who, " \037Num\037 \037Item ".(" "x40)." \037");
321
322     my $i = 1;
323     foreach ( &getNewsAll() ) {
324         my $subtopic    = $_;
325         my $setby       = $::news{$chan}{$subtopic}{Author};
326
327         if (!defined $subtopic) {
328             &::DEBUG("warn: subtopic == undef.");
329             next;
330         }
331
332         # todo: show request stats aswell.
333         &::msg($::who, sprintf("\002[\002%2d\002]\002 %s",
334                                 $i, $subtopic));
335         $i++;
336     }
337
338     &::msg($::who, "|= End of News.");
339     &::msg($::who, "use 'news read <#>' or 'news read <keyword>'");
340 }
341
342 sub read {
343     my($str) = @_;
344
345     if (!defined $chan or !defined $str or $str =~ /^\s*$/) {
346         &::help("news read");
347         return;
348     }
349
350     if (!scalar keys %{ $::news{$chan} }) {
351         &::msg($::who, "No News for \002$chan\002.");
352         return;
353     }
354
355 #    my $item   = (exists $::news{$chan}{$str}) ? $str : &getNewsItem($str);
356     my $item    = &getNewsItem($str);
357     if (!defined $item or !scalar keys %{ $::news{$chan}{$item} }) {
358         &::msg($::who, "No news item called '$str'");
359         return;
360     }
361
362     if (!exists $::news{$chan}{$item}{Text}) {
363         &::msg($::who, "Someone forgot to add info to this news item");
364         return;
365     }
366
367     # todo: show item number.
368     # todo: show ago-time aswell?
369     # todo: show request stats aswell.
370     my $t = localtime($::news{$chan}{$item}{Time});
371     my $a = $::news{$chan}{$item}{Author};
372     &::msg($::who, "+- News \002$chan\002 ##, item '\037$item\037':");
373     &::msg($::who, "| Added by $a at $t");
374     &::msg($::who, $::news{$chan}{$item}{Text});
375
376     $::news{$chan}{$item}{'Request_By'}   = $::who;
377     $::news{$chan}{$item}{'Request_Time'} = time();
378     $::news{$chan}{$item}{'Request_Count'}++;
379 }
380
381 sub mod {
382     my($item, $str) = split /\s+/, $_[0], 2;
383
384     if (!defined $item or $item eq "" or $str =~ /^\s*$/) {
385         &::help("news mod");
386         return;
387     }
388
389     my $news = &getNewsItem($item);
390
391     if (!defined $news) {
392         &::DEBUG("error: mod: news == undefined.");
393         return;
394     }
395     my $nnews = $::news{$chan}{$news}{Text};
396     my $mod_news  = $news;
397     my $mod_nnews = $nnews;
398
399     # SAR patch. mu++
400     if ($str =~ m|^\s*s([/,#\|])(.+?)\1(.*?)\1([a-z]*);?\s*$|) {
401         my ($delim, $op, $np, $flags) = ($1,$2,$3,$4);
402
403         if ($flags !~ /^(g)?$/) {
404             &::msg($::who, "error: Invalid flags to regex.");
405             return;
406         }
407
408         ### TODO: use m### to make code safe!
409         # todo: make code safer.
410         my $done = 0;
411         # todo: use eval to deal with flags easily.
412         if ($flags eq "") {
413             $done++ if (!$done and $mod_news  =~ s/\Q$op\E/$np/);
414             $done++ if (!$done and $mod_nnews =~ s/\Q$op\E/$np/);
415         } elsif ($flags eq "g") {
416             $done++ if ($mod_news  =~ s/\Q$op\E/$np/g);
417             $done++ if ($mod_nnews =~ s/\Q$op\E/$np/g);
418         }
419
420         if (!$done) {
421             &::msg($::who, "warning: regex not found in news.");
422             return;
423         }
424
425         if ($mod_news ne $news) { # news item.
426             if (exists $::news{$chan}{$mod_news}) {
427                 &::msg($::who, "item '$mod_news' already exists.");
428                 return;
429             }
430
431             &::msg($::who, "Moving item '$news' to '$mod_news' with SAR s/$op/$np/.");
432             foreach (keys %{ $::news{$chan}{$news} }) {
433                 $::news{$chan}{$mod_news}{$_} = $::news{$chan}{$news}{$_};
434                 delete $::news{$chan}{$news}{$_};
435             }
436             # needed?
437             delete $::news{$chan}{$news};
438         }
439
440         if ($mod_nnews ne $nnews) { # news Text/Description.
441             &::msg($::who, "Changing text for '$news' SAR s/$op/$np/.");
442             if ($mod_news ne $news) {
443                 $::news{$chan}{$mod_news}{Text} = $mod_nnews;
444             } else {
445                 $::news{$chan}{$news}{Text}     = $mod_nnews;
446             }
447         }
448
449         return;
450     } else {
451         &::msg($::who, "error: that regex failed ;(");
452         return;
453     }
454
455     &::msg($::who, "error: Invalid regex. Try s/1/2/, s#3#4#...");
456 }
457
458 sub set {
459     my($args) = @_;
460     $args =~ /^(\S+)\s+(\S+)\s+(.*)$/;
461     my($item, $what, $value) = ($1,$2,$3);
462
463     &::DEBUG("set called.");
464
465     if ($item eq "") {
466         &::help("news set");
467         return;
468     }
469
470     &::DEBUG("item => '$item'.");
471     my $news = &getNewsItem($item);
472     &::DEBUG("news => '$news'");
473
474     if (!defined $news) {
475         &::msg($::who, "Could not find item '$item' substring or # in news list.");
476         return;
477     }
478
479     # list all values for chan.
480     if (!defined $what) {
481         &::DEBUG("set: 1");
482         return;
483     }
484
485     my $ok = 0;
486     my @elements = ("Expire","Text");
487     foreach (@elements) {
488         next unless ($what =~ /^$_$/i);
489         $what = $_;
490         $ok++;
491         last;
492     }
493
494     if (!$ok) {
495         &::msg($::who, "Invalid set.  Try: @elements");
496         return;
497     }
498
499     # show (read) what.
500     if (!defined $value) {
501         &::DEBUG("set: 2");
502         return;
503     }
504
505     if (!exists $::news{$chan}{$news}) {
506         &::msg($::who, "news '$news' does not exist");
507         return;
508     }
509
510     if ($what eq "Expire") {
511         # todo: use do_set().
512
513         my $time = 0;
514         my $plus = ($value =~ s/^\+//g);
515         while ($value =~ s/^(\d+)(\S*)\s*//) {
516             my($int,$unit) = ($1,$2);
517             $time += $int       if ($unit =~ /^s(ecs?)?$/i);
518             $time += $int*60    if ($unit =~ /^m(in(utes?)?)?$/i);
519             $time += $int*60*60 if ($unit =~ /^h(ours?)?$/i);
520             $time += $int*60*60*24 if (!$unit or $unit =~ /^d(ays?)?$/i);
521             $time += $int*60*60*24*7 if ($unit =~ /^w(eeks?)?$/i);
522             $time += $int*60*60*24*30 if ($unit =~ /^mon(th)?$/i);
523         }
524
525         if ($value =~ s/^never$//i) {
526             # never.
527             $time = -1;
528         } elsif ($plus) {
529             # from now.
530             $time += time();
531         } else {
532             # from creation of item.
533             $time += $::news{$chan}{$news}{Time};
534         }
535
536         if (!$time or ($value and $value !~ /^never$/i)) {
537             &::DEBUG("set: Expire... need to parse.");
538             return;
539         }
540
541         if ($time == -1) {
542             &::msg($::who, "Set never expire for \002$item\002." );
543         } elsif ($time < -1) {
544             &::DEBUG("time should never be negative ($time).");
545             return;
546         } else {
547             &::msg($::who, "Set expire for \002$item\002, to ".
548                 localtime($time) ." [".&::Time2String($time - time())."]" );
549
550             if (time() > $time) {
551                 &::DEBUG("hrm... time() > $time, should expire.");
552             }
553         }
554
555
556         $::news{$chan}{$news}{Expire} = $time;
557
558         return;
559     }
560
561     my $auth = 0;
562     &::DEBUG("who => '$::who'");
563     my $author = $::news{$chan}{$news}{Author};
564     $auth++ if ($::who eq $author);
565     $auth++ if (&::IsFlag("o"));
566     if (!defined $author) {
567         &::DEBUG("news{$chan}{$news}{Author} is not defined! auth'd anyway");
568         $::news{$chan}{$news}{Author} = $::who;
569         $author = $::who;
570         $auth++;
571     }
572
573     if (!$auth) {
574         # todo: show when it'll expire.
575         &::msg($::who, "Sorry, you cannot set items. (author $author owns it)");
576         return;
577     }
578
579     my $old = $::news{$chan}{$news}{$what};
580     if (defined $old) {
581         &::DEBUG("old => $old.");
582     }
583     $::news{$chan}{$news}{$what} = $value;
584     &::msg($::who, "Setting [$chan]/{$news}/<$what> to '$value'.");
585 }
586
587 ###
588 ### helpers...
589 ###
590
591 sub getNewsAll {
592     my %time;
593     foreach (keys %{ $::news{$chan} }) {
594         $time{ $::news{$chan}{$_}{Time} } = $_;
595     }
596
597     my @items;
598     foreach (sort { $a <=> $b } keys %time) {
599         push(@items, $time{$_});
600     }
601
602     return @items;
603 }
604
605 sub getNewsItem {
606     my($what)   = @_;
607     my $item    = 0;
608
609     my %time;
610     foreach (keys %{ $::news{$chan} }) {
611         my $t = $::news{$chan}{$_}{Time};
612
613         if (!defined $t or $t !~ /^\d+$/) {
614             &::DEBUG("warn: t is undefined for news{$chan}{$_}{Time}; removing item.");
615             delete $::news{$chan}{$_};
616             next;
617         }
618
619         $time{$t} = $_;
620     }
621
622     # number to string resolution.
623     if ($what =~ /^\d+$/) {
624         foreach (sort { $a <=> $b } keys %time) {
625             $item++;
626             return $time{$_} if ($item == $what);
627         }
628
629     } else {
630         # partial string to full string resolution
631
632         my @items;
633         my $no;
634         foreach (sort { $a <=> $b } keys %time) {
635             $item++;
636             $no = $item if ($time{$_} eq $what);
637             push(@items, $time{$_}) if ($time{$_} =~ /\Q$what\E/i);
638         }
639
640         # since we have so much built into this function, there is so
641         # many guesses we can make.
642         # todo: split this command in the future into:
643         #       full_string->number and number->string
644         #       partial_string->full_string
645         if (defined $no and !@items) {
646             &::DEBUG("string->number resolution.");
647             return $no;
648         }
649
650         if (scalar @items > 1) {
651             &::DEBUG("Multiple matches, not guessing.");
652             &::msg($::who, "Multiple matches, not guessing.");
653             return;
654         }
655
656         if (@items) {
657             &::DEBUG("gNI: Guessed '$items[0]'.");
658             return $items[0];
659         } else {
660             &::DEBUG("gNI: No match.");
661             return;
662         }
663     }
664
665     &::ERROR("getNewsItem: Should not happen (what = $what)");
666     return;
667 }
668
669 sub do_set {
670     my($what,$value) = @_;
671
672     if (!defined $chan) {
673         &::DEBUG("do_set: chan not defined.");
674         return;
675     }
676
677     if (!defined $what or $what =~ /^\s*$/) {
678         &::DEBUG("what $what is not defined.");
679         return;
680     }
681     if (!defined $value or $value =~ /^\s*$/) {
682         &::DEBUG("value $value is not defined.");
683         return;
684     }
685
686     &::DEBUG("do_set: TODO...");
687 }
688
689 1;