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