]> git.donarmstrong.com Git - infobot.git/blob - src/Modules/News.pl
More clean ups, forgotten what they were, 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
144     open(NEWS, ">$file");
145     foreach $chan (sort keys %::news) {
146         $c = scalar keys %{ $::news{$chan} };
147         next unless ($c);
148         $cc++;
149
150         foreach $item (sort keys %{ $::news{$chan} }) {
151             $c = scalar keys %{ $::news{$chan}{$item} };
152             next unless ($c);
153             $ci++;
154
155             print NEWS "$chan $item\n";
156             foreach $what (sort keys %{ $::news{$chan}{$item} }) {
157                 print NEWS "    $what: $::news{$chan}{$item}{$what}\n";
158             }
159             print NEWS "\n";
160         }
161     }
162
163     # todo: show how many users we wrote down.
164     if (&::getChanConfList("newsKeepRead")) {
165         # old users are removed in newsFlush(), perhaps it should be
166         # done here.
167
168         foreach $chan (sort keys %::newsuser) {
169
170             foreach (sort keys %{ $::newsuser{$chan} }) {
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     if ($item eq $str) {
208         &::DEBUG("item eq str ($item): should never happen.");
209     }
210     &::msg($::who, "Added '\037$str\037' at [".localtime(time).
211                 "] by \002$::who\002 for item #\002$item\002.");
212     &::msg($::who, "Now do 'news text $item <your_description>'");
213     &::msg($::who, "This item will expire at \002".
214         localtime($::news{$chan}{$str}{Expire})."\002 [$agestr from now] "
215     );
216 }
217
218 sub del {
219     my($what)   = @_;
220     my $item    = 0;
221
222     if (!defined $what) {
223         &::help("news del");
224         return;
225     }
226
227     if ($what =~ /^\d+$/) {
228         my $count = scalar keys %{ $::news{$chan} };
229         if (!$count) {
230             &::msg($::who, "No news for $chan.");
231             return;
232         }
233
234         if ($what > $count or $what < 0) {
235             &::msg($::who, "$what is out of range (max $count)");
236             return;
237         }
238
239         $item = &getNewsItem($what);
240         &::DEBUG("del: num: item => $item ($what)");
241         $what   = $item;        # hack hack hack.
242
243     } else {
244         $_      = &getNewsItem($what);  # hack hack hack.
245         $what   = $_ if (defined $_);
246
247         if (!exists $::news{$chan}{$what}) {
248             my @found;
249             foreach (keys %{ $::news{$chan} }) {
250                 next unless (/\Q$what\E/);
251                 push(@found, $_);
252             }
253
254             if (!scalar @found) {
255                 &::msg($::who, "could not find $what.");
256                 return;
257             }
258
259             if (scalar @found > 1) {
260                 &::msg($::who, "too many matches for $what.");
261                 return;
262             }
263
264             $what       = $found[0];
265             &::DEBUG("del: str: guessed what => $what");
266         }
267     }
268
269     if (exists $::news{$chan}{$what}) {
270         my $auth = 0;
271         $auth++ if ($::who eq $::news{$chan}{$what}{Author});
272         $auth++ if (&::IsFlag("o"));
273
274         if (!$auth) {
275             # todo: show when it'll expire.
276             &::msg($::who, "Sorry, you cannot remove items; just let them expire on their own.");
277             return;
278         }
279
280         &::msg($::who, "ok, deleted '$what' from \002$chan\002...");
281         delete $::news{$chan}{$what};
282     } else {
283         &::msg($::who, "error: not found $what in news for $chan.");
284     }
285 }
286
287 sub list {
288     if (!scalar keys %{ $::news{$chan} }) {
289         &::msg($::who, "No News for \002$chan\002.");
290         return;
291     }
292
293     if (&::IsChanConf("newsKeepRead")) {
294         $::newsuser{$chan}{$::who} = time();
295     }
296
297     &::msg($::who, "|==== News for \002$chan\002:");
298     my $newest  = 0;
299     foreach (keys %{ $::news{$chan} }) {
300         my $t   = $::news{$chan}{$_}{Time};
301         $newest = $t if ($t > $newest);
302     }
303     my $timestr = &::Time2String(time() - $newest);
304     &::msg($::who, "|= Last updated $timestr ago.");
305     &::msg($::who, " \037No\037  \037Item ".(" "x40)." \037");
306
307     my $i = 1;
308     foreach ( &getNewsAll() ) {
309         my $subtopic    = $_;
310         my $setby       = $::news{$chan}{$subtopic}{Author};
311
312         if (!defined $subtopic) {
313             &::DEBUG("warn: subtopic == undef.");
314             next;
315         }
316
317         # todo: show request stats aswell.
318         &::msg($::who, sprintf("\002[\002%2d\002]\002 %s",
319                                 $i, $subtopic));
320         $i++;
321     }
322
323     &::msg($::who, "|= End of News.");
324     &::msg($::who, "use 'news read <#>' or 'news read <keyword>'");
325 }
326
327 sub read {
328     my($str) = @_;
329
330     if (!defined $chan or !defined $str or $str =~ /^\s*$/) {
331         &::help("news read");
332         return;
333     }
334
335     if (!scalar keys %{ $::news{$chan} }) {
336         &::msg($::who, "No News for \002$chan\002.");
337         return;
338     }
339
340 #    my $item   = (exists $::news{$chan}{$str}) ? $str : &getNewsItem($str);
341     my $item    = &getNewsItem($str);
342     if (!defined $item or !scalar keys %{ $::news{$chan}{$item} }) {
343         &::msg($::who, "No news item called '$str'");
344         return;
345     }
346
347     if (!exists $::news{$chan}{$item}{Text}) {
348         &::msg($::who, "Someone forgot to add info to this news item");
349         return;
350     }
351
352     # todo: show item number.
353     # todo: show ago-time aswell?
354     # todo: show request stats aswell.
355     my $t = localtime($::news{$chan}{$item}{Time});
356     my $a = $::news{$chan}{$item}{Author};
357     &::msg($::who, "+- News \002$chan\002 ##, item '\037$item\037':");
358     &::msg($::who, "| Added by $a at $t");
359     &::msg($::who, $::news{$chan}{$item}{Text});
360
361     $::news{$chan}{$item}{'Request_By'}   = $::who;
362     $::news{$chan}{$item}{'Request_Time'} = time();
363     $::news{$chan}{$item}{'Request_Count'}++;
364 }
365
366 sub mod {
367     my($item, $str) = split /\s+/, $_[0], 2;
368
369     if (!defined $item or $item eq "" or $str =~ /^\s*$/) {
370         &::help("news mod");
371         return;
372     }
373
374     my $news = &getNewsItem($item);
375
376     if (!defined $news) {
377         &::DEBUG("error: mod: news == undefined.");
378         return;
379     }
380     my $nnews = $::news{$chan}{$news}{Text};
381     my $mod_news  = $news;
382     my $mod_nnews = $nnews;
383
384     # SAR patch. mu++
385     if ($str =~ m|^\s*s([/,#\|])(.+?)\1(.*?)\1([a-z]*);?\s*$|) {
386         my ($delim, $op, $np, $flags) = ($1,$2,$3,$4);
387
388         if ($flags !~ /^(g)?$/) {
389             &::msg($::who, "error: Invalid flags to regex.");
390             return;
391         }
392
393         ### TODO: use m### to make code safe!
394         # todo: make code safer.
395         my $done = 0;
396         # todo: use eval to deal with flags easily.
397         if ($flags eq "") {
398             $done++ if (!$done and $mod_news  =~ s/\Q$op\E/$np/);
399             $done++ if (!$done and $mod_nnews =~ s/\Q$op\E/$np/);
400         } elsif ($flags eq "g") {
401             $done++ if ($mod_news  =~ s/\Q$op\E/$np/g);
402             $done++ if ($mod_nnews =~ s/\Q$op\E/$np/g);
403         }
404
405         if (!$done) {
406             &::msg($::who, "warning: regex not found in news.");
407             return;
408         }
409
410         if ($mod_news ne $news) { # news item.
411             if (exists $::news{$chan}{$mod_news}) {
412                 &::msg($::who, "item '$mod_news' already exists.");
413                 return;
414             }
415
416             &::msg($::who, "Moving item '$news' to '$mod_news' with SAR s/$op/$np/.");
417             foreach (keys %{ $::news{$chan}{$news} }) {
418                 $::news{$chan}{$mod_news}{$_} = $::news{$chan}{$news}{$_};
419                 delete $::news{$chan}{$news}{$_};
420             }
421             # needed?
422             delete $::news{$chan}{$news};
423         }
424
425         if ($mod_nnews ne $nnews) { # news Text/Description.
426             &::msg($::who, "Changing text for '$news' SAR s/$op/$np/.");
427             if ($mod_news ne $news) {
428                 $::news{$chan}{$mod_news}{Text} = $mod_nnews;
429             } else {
430                 $::news{$chan}{$news}{Text}     = $mod_nnews;
431             }
432         }
433
434         return;
435     } else {
436         &::msg($::who, "error: that regex failed ;(");
437         return;
438     }
439
440     &::msg($::who, "error: Invalid regex. Try s/1/2/, s#3#4#...");
441 }
442
443 sub set {
444     my($args) = @_;
445     $args =~ /^(\S+)\s+(\S+)\s+(.*)$/;
446     my($item, $what, $value) = ($1,$2,$3);
447
448     &::DEBUG("set called.");
449
450     if ($item eq "") {
451         &::help("news set");
452         return;
453     }
454
455     &::DEBUG("item => '$item'.");
456     my $news = &getNewsItem($item);
457     &::DEBUG("news => '$news'");
458
459     if (!defined $news) {
460         &::msg($::who, "Could not find item '$item' substring or # in news list.");
461         return;
462     }
463
464     # list all values for chan.
465     if (!defined $what) {
466         &::DEBUG("set: 1");
467         return;
468     }
469
470     my $ok = 0;
471     my @elements = ("Expire","Text");
472     foreach (@elements) {
473         next unless ($what =~ /^$_$/i);
474         $what = $_;
475         $ok++;
476         last;
477     }
478
479     if (!$ok) {
480         &::msg($::who, "Invalid set.  Try: @elements");
481         return;
482     }
483
484     # show (read) what.
485     if (!defined $value) {
486         &::DEBUG("set: 2");
487         return;
488     }
489
490     if (!exists $::news{$chan}{$news}) {
491         &::msg($::who, "news '$news' does not exist");
492         return;
493     }
494
495     if ($what eq "Expire") {
496         # todo: use do_set().
497
498         my $time = 0;
499         my $plus = ($value =~ s/^\+//g);
500         while ($value =~ s/^(\d+)(\S*)\s*//) {
501             my($int,$unit) = ($1,$2);
502             $time += $int       if ($unit =~ /^s(ecs?)?$/i);
503             $time += $int*60    if ($unit =~ /^m(in(utes?)?)?$/i);
504             $time += $int*60*60 if ($unit =~ /^h(ours?)?$/i);
505             $time += $int*60*60*24 if (!$unit or $unit =~ /^d(ays?)?$/i);
506             $time += $int*60*60*24*7 if ($unit =~ /^w(eeks?)?$/i);
507             $time += $int*60*60*24*30 if ($unit =~ /^mon(th)?$/i);
508         }
509
510         if ($value =~ s/^never$//i) {
511             # never.
512             $time = -1;
513         } elsif ($plus) {
514             # from now.
515             $time += time();
516         } else {
517             # from creation of item.
518             $time += $::news{$chan}{$news}{Time};
519         }
520
521         if (!$time or ($value and $value !~ /^never$/i)) {
522             &::DEBUG("set: Expire... need to parse.");
523             return;
524         }
525
526         if ($time == -1) {
527             &::msg($::who, "Set never expire for \002$item\002." );
528         } elsif ($time < -1) {
529             &::DEBUG("time should never be negative ($time).");
530             return;
531         } else {
532             &::msg($::who, "Set expire for \002$item\002, to ".
533                 localtime($time) ." [".&::Time2String($time - time())."]" );
534
535             if (time() > $time) {
536                 &::DEBUG("hrm... time() > $time, should expire.");
537             }
538         }
539
540
541         $::news{$chan}{$news}{Expire} = $time;
542
543         return;
544     }
545
546     my $auth = 0;
547     &::DEBUG("who => '$::who'");
548     my $author = $::news{$chan}{$news}{Author};
549     $auth++ if ($::who eq $author);
550     $auth++ if (&::IsFlag("o"));
551     if (!defined $author) {
552         &::DEBUG("news{$chan}{$news}{Author} is not defined! auth'd anyway");
553         $::news{$chan}{$news}{Author} = $::who;
554         $author = $::who;
555         $auth++;
556     }
557
558     if (!$auth) {
559         # todo: show when it'll expire.
560         &::msg($::who, "Sorry, you cannot set items. (author $author owns it)");
561         return;
562     }
563
564     my $old = $::news{$chan}{$news}{$what};
565     if (defined $old) {
566         &::DEBUG("old => $old.");
567     }
568     $::news{$chan}{$news}{$what} = $value;
569     &::msg($::who, "Setting [$chan]/{$news}/<$what> to '$value'.");
570 }
571
572 ###
573 ### helpers...
574 ###
575
576 sub getNewsAll {
577     my %time;
578     foreach (keys %{ $::news{$chan} }) {
579         $time{ $::news{$chan}{$_}{Time} } = $_;
580     }
581
582     my @items;
583     foreach (sort { $a <=> $b } keys %time) {
584         push(@items, $time{$_});
585     }
586
587     return @items;
588 }
589
590 sub getNewsItem {
591     my($what)   = @_;
592     my $item    = 0;
593
594     my %time;
595     foreach (keys %{ $::news{$chan} }) {
596         my $t = $::news{$chan}{$_}{Time};
597
598         if (!defined $t or $t !~ /^\d+$/) {
599             &::DEBUG("warn: t is undefined for news{$chan}{$_}{Time}; removing item.");
600             delete $::news{$chan}{$_};
601             next;
602         }
603
604         $time{$t} = $_;
605     }
606
607     # number to string resolution.
608     if ($what =~ /^\d+$/) {
609         foreach (sort { $a <=> $b } keys %time) {
610             $item++;
611             return $time{$_} if ($item == $what);
612         }
613
614     } else {
615         # partial string to full string resolution
616
617         my @items;
618         my $no;
619         foreach (sort { $a <=> $b } keys %time) {
620             $item++;
621             $no = $item if ($time{$_} eq $what);
622             push(@items, $time{$_}) if ($time{$_} =~ /\Q$what\E/i);
623         }
624
625         # since we have so much built into this function, there is so
626         # many guesses we can make.
627         # todo: split this command in the future into:
628         #       full_string->number and number->string
629         #       partial_string->full_string
630         if (defined $no and !@items) {
631             &::DEBUG("string->number resolution.");
632             return $no;
633         }
634
635         if (scalar @items > 1) {
636             &::DEBUG("Multiple matches, not guessing.");
637             &::msg($::who, "Multiple matches, not guessing.");
638             return;
639         }
640
641         if (@items) {
642             &::DEBUG("gNI: Guessed '$items[0]'.");
643             return $items[0];
644         } else {
645             &::DEBUG("gNI: No match.");
646             return;
647         }
648     }
649
650     &::ERROR("getNewsItem: Should not happen (what = $what)");
651     return;
652 }
653
654 sub do_set {
655     my($what,$value) = @_;
656
657     if (!defined $chan) {
658         &::DEBUG("do_set: chan not defined.");
659         return;
660     }
661
662     if (!defined $what or $what =~ /^\s*$/) {
663         &::DEBUG("what $what is not defined.");
664         return;
665     }
666     if (!defined $value or $value =~ /^\s*$/) {
667         &::DEBUG("value $value is not defined.");
668         return;
669     }
670
671     &::DEBUG("do_set: TODO...");
672 }
673
674 1;