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