File Coverage

blib/lib/Bot/Cobalt/Plugin/Info3.pm
Criterion Covered Total %
statement 12 14 85.7
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 17 19 89.4


line stmt bran cond sub pod time code
1             package Bot::Cobalt::Plugin::Info3;
2             $Bot::Cobalt::Plugin::Info3::VERSION = '0.021003';
3 1     1   901 use strictures 2;
  1         4  
  1         30  
4 1     1   133 use v5.10;
  1         2  
5              
6             ## Handles glob-style "info" response topics
7             ## Modelled on darkbot/cobalt1 behavior
8             ## Commands:
9             ## add
10             ## del(ete)
11             ## replace
12             ## (d)search
13             ##
14             ## Also handles darkbot-style variable replacement
15              
16 1     1   3 use Bot::Cobalt;
  1         2  
  1         5  
17 1     1   595 use Bot::Cobalt::Common;
  1         2  
  1         7  
18 1     1   52 use Bot::Cobalt::DB;
  0            
  0            
19              
20             use Bot::Cobalt::Plugin::RDB::SearchCache;
21              
22             use File::Spec;
23              
24             use POSIX ();
25              
26              
27             sub new { bless {}, shift }
28              
29             sub Cobalt_register {
30             my ($self, $core) = splice @_, 0, 2;
31              
32             $self->{Cache} = Bot::Cobalt::Plugin::RDB::SearchCache->new(
33             MaxKeys => 20,
34             );
35              
36             $self->{NegCache} = Bot::Cobalt::Plugin::RDB::SearchCache->new(
37             MaxKeys => 8,
38             );
39              
40             my $pcfg = plugin_cfg( $self );
41             my $var = core->var;
42              
43             my $relative_to_var = $pcfg->{Opts}->{InfoDB} //
44             File::Spec->catfile( 'db', 'info3.db' );
45              
46             my $dbpath = File::Spec->catfile(
47             $var,
48             File::Spec->splitpath( $relative_to_var )
49             );
50              
51             $self->{DB_PATH} = $dbpath;
52             $self->{DB} = Bot::Cobalt::DB->new(
53             File => $dbpath,
54             );
55              
56             $self->{MAX_TRIGGERED} = $pcfg->{Opts}->{MaxTriggered} || 3;
57              
58             ## hash mapping contexts/channels to previously-triggered topics
59             ## used for MaxTriggered
60             $self->{LastTriggered} = { };
61              
62             ## glob-to-re mapping:
63             $self->{Globs} = { };
64             ## reverse of above:
65             $self->{Regexes} = { };
66              
67             ## build our initial hashes (this is slow, ~1s on spork's huge db)
68             $self->{DB}->dbopen(ro => 1) || croak 'DB open failure';
69             while (my ($glob, $ref) = each %{ $self->{DB}->Tied }) {
70             ++$core->Provided->{info_topics};
71             my $regex = $ref->{Regex};
72             $self->{Globs}->{$glob} = my $compiled_re = qr/$regex/i;
73             $self->{Regexes}->{$compiled_re} = $glob;
74             }
75             $self->{DB}->dbclose;
76              
77             register($self, 'SERVER',
78             [
79             'public_msg',
80             'ctcp_action',
81             'info3_relay_string',
82             'info3_expire_maxtriggered',
83             ],
84             );
85              
86             logger->info("Loaded, topics: ".($core->Provided->{info_topics}||=0));
87              
88             PLUGIN_EAT_NONE
89             }
90              
91             sub Cobalt_unregister {
92             my ($self, $core) = splice @_, 0, 2;
93              
94             logger->info("Unregistering Info plugin");
95              
96             delete $core->Provided->{info_topics};
97              
98             PLUGIN_EAT_NONE
99             }
100              
101             sub Bot_ctcp_action {
102             my ($self, $core) = splice @_, 0, 2;
103             my $msg = ${$_[0]};
104             my $context = $msg->context;
105             ## similar to _public_msg handler
106             ## pre-pend ~action+ and run a match
107              
108             my @message = @{ $msg->message_array };
109             return PLUGIN_EAT_NONE unless @message;
110              
111             my $str = join ' ', '~action', @message;
112              
113             my $nick = $msg->src_nick;
114             my $channel = $msg->target;
115              
116             ## is this a channel? ctcp_action doesn't differentiate on its own
117             my $first = substr($channel, 0, 1);
118             return PLUGIN_EAT_NONE
119             unless grep { $_ eq $first } ( '#', '&', '+' );
120              
121             ## should we be sending info3 responses anyway?
122             my $chcfg = $core->get_channels_cfg($context);
123             return PLUGIN_EAT_NONE
124             if defined $chcfg->{$channel}->{info3_response}
125             and $chcfg->{$channel}->{info3_response} == 0;
126              
127             return PLUGIN_EAT_NONE
128             if $self->_over_max_triggered($context, $channel, $str);
129              
130             my $match = $self->_info_match($str, 'ACTION') || return PLUGIN_EAT_NONE;
131              
132             if ( index($match, '~') == 0) {
133              
134             my $rdb = substr( (split ' ', $match)[0], 1);
135              
136             if ($rdb) {
137             broadcast( 'rdb_triggered',
138             $context,
139             $channel,
140             $nick,
141             lc($rdb),
142             $match,
143             ## orig question str for Q~ etc replacement:
144             join(' ', @message)
145             );
146             return PLUGIN_EAT_NONE
147             }
148             }
149              
150             logger->debug("issuing info3_relay_string in response to action");
151             broadcast( 'info3_relay_string',
152             $context, $channel, $nick, $match, join(' ', @message)
153             );
154              
155             PLUGIN_EAT_NONE
156             }
157              
158             sub Bot_public_msg {
159             my ($self, $core) = splice @_, 0, 2;
160             my $msg = ${$_[0]};
161             my $context = $msg->context;
162              
163             my @message = @{ $msg->message_array };
164             return PLUGIN_EAT_NONE unless @message;
165              
166             my $with_highlight;
167             if ($msg->highlight) {
168             ## we were highlighted -- might be an info3 cmd
169             my %handlers = (
170             'add' => '_info_add',
171             'del' => '_info_del',
172             'delete' => '_info_del',
173             'replace' => '_info_replace',
174             'search' => '_info_search',
175             'dsearch' => '_info_dsearch',
176             'display' => '_info_display',
177             'about' => '_info_about',
178             'tell' => '_info_tell',
179             'infovars' => '_info_varhelp',
180             );
181              
182             $message[1] = lc($message[1]) if $message[1];
183             if ($message[1] && grep { $_ eq $message[1] } keys %handlers) {
184             ## this is apparently a valid command
185             my @args = @message[2 .. $#message];
186             my $method = $handlers{ $message[1] };
187             if ( $self->can($method) ) {
188             ## pass handlers $msg ref as first arg
189             ## the rest is the remainder of the string
190             ## (without highlight or command)
191             ## ...which may be nothing, up to the handler to send syntax RPL
192             my $resp = $self->$method($msg, @args);
193             broadcast( 'message',
194             $context, $msg->channel, $resp ) if $resp;
195             return PLUGIN_EAT_NONE
196             } else {
197             logger->warn($message[1]." is a valid cmd but method missing");
198             return PLUGIN_EAT_NONE
199             }
200              
201             } else {
202             ## not an info3 cmd
203             ## shift the highlight off and see if it's a match, below
204             ## save the highlighted version, it might still be a valid match
205             $with_highlight = join ' ', @message;
206             shift @message;
207             }
208              
209             }
210              
211             ## rejoin message
212             my $str = join ' ', @message;
213              
214             my $nick = $msg->src_nick;
215             my $channel = $msg->channel;
216              
217             my $chcfg = $core->get_channels_cfg($context) || {};
218             return PLUGIN_EAT_NONE
219             if defined $chcfg->{$channel}->{info3_response}
220             and $chcfg->{$channel}->{info3_response} == 0;
221              
222             return PLUGIN_EAT_NONE
223             if $self->_over_max_triggered($context, $channel, $str);
224              
225             ## check for matches
226             my $match = $self->_info_match($str);
227             if ($with_highlight && ! defined $match) {
228             $match = $self->_info_match($with_highlight);
229             }
230             return PLUGIN_EAT_NONE unless $match;
231              
232             ## ~rdb, maybe? hand off to RDB.pm
233             if ( index($match, '~') == 0) {
234             my $rdb = (split ' ', $match)[0];
235             $rdb = substr($rdb, 1);
236             if ($rdb) {
237             logger->debug("issuing rdb_triggered");
238             broadcast( 'rdb_triggered',
239             $context,
240             $channel,
241             $nick,
242             lc($rdb),
243             $match,
244             $str
245             );
246             return PLUGIN_EAT_NONE
247             }
248             }
249              
250             logger->debug("issuing info3_relay_string");
251              
252             broadcast( 'info3_relay_string',
253             $context, $channel, $nick, $match, $str
254             );
255              
256             PLUGIN_EAT_NONE
257             }
258              
259             sub Bot_info3_relay_string {
260             my ($self, $core) = splice @_, 0, 2;
261             my $context = ${$_[0]};
262             my $channel = ${$_[1]};
263             my $nick = ${$_[2]};
264             my $string = ${$_[3]};
265             my $orig = ${$_[4]};
266              
267             ## format and send info3 response
268             ## also received from RDB when handing off ~rdb responses
269              
270             logger->debug("info3_relay_string received; calling _info_format");
271              
272             my $resp = $self->_info_format($context, $nick, $channel, $string, $orig);
273              
274             ## if $resp is a +action, send ctcp action
275             if ( index($resp, '+') == 0 ) {
276             $resp = substr($resp, 1);
277             logger->debug("Dispatching action -> $channel");
278             broadcast('action', $context, $channel, $resp);
279             } else {
280             logger->debug("Dispatching msg -> $channel");
281             broadcast('message', $context, $channel, $resp);
282             }
283              
284             return PLUGIN_EAT_NONE
285             }
286              
287             sub Bot_info3_expire_maxtriggered {
288             my ($self, $core) = splice @_, 0, 2;
289             my $context = ${ $_[0] };
290             my $channel = ${ $_[1] };
291              
292             unless ($context && $channel) {
293             logger->debug(
294             "missing context and channel pair in expire_maxtriggered"
295             );
296             }
297             delete $self->{LastTriggered}->{$context}->{$channel};
298              
299             logger->debug("cleared maxtriggered for $channel on $context");
300              
301             return PLUGIN_EAT_ALL
302             }
303              
304             ### Internal methods
305              
306             sub _over_max_triggered {
307             my ($self, $context, $channel, $str) = @_;
308              
309             if ($self->{LastTriggered}->{$context}->{$channel}) {
310             my $lasttrig = $self->{LastTriggered}->{$context}->{$channel};
311             my ($last_match, $tries) = @$lasttrig;
312             if ($str eq $last_match) {
313             ++$tries;
314             if ($tries > $self->{MAX_TRIGGERED}) {
315             ## we've hit this topic too many times in a row
316             ## plugin should EAT_NONE
317             logger->debug("Over trigger limit for $str");
318              
319             ## set a timer to expire this LastTriggered
320             core->timer_set( 90,
321             {
322             Alias => plugin_alias($self),
323             Event => 'info3_expire_maxtriggered',
324             Args => [ $context, $channel ],
325             },
326             );
327              
328             return 1
329             } else {
330             ## haven't hit MAX_TRIGGERED yet.
331             $self->{LastTriggered}->{$context}->{$channel} = [$str, $tries];
332             }
333             } else {
334             ## not the previously-returned topic
335             ## reset
336             delete $self->{LastTriggered}->{$context}->{$channel};
337             }
338             } else {
339             $self->{LastTriggered}->{$context}->{$channel} = [ $str, 1 ];
340             }
341              
342             return 0
343             }
344              
345              
346             sub _info_add {
347             my ($self, $msg, $glob, @args) = @_;
348             my $string = join ' ', @args;
349              
350             my $context = $msg->context;
351             my $nick = $msg->src_nick;
352              
353             my $auth_user = core->auth->username($context, $nick);
354             my $auth_level = core->auth->level($context, $nick);
355              
356             my $pcfg = plugin_cfg( $self );
357             my $required = $pcfg->{RequiredLevels}->{AddTopic} // 2;
358             unless ($auth_level >= $required) {
359             return core->rpl( q{RPL_NO_ACCESS},
360             nick => $nick,
361             );
362             }
363              
364             unless ($glob && $string) {
365             return core->rpl( q{INFO_BADSYNTAX_ADD} );
366             }
367              
368             ## lowercase
369             $glob = decode_irc(lc $glob);
370              
371             if (exists $self->{Globs}->{$glob}) {
372             ## topic already exists, use replace instead!
373             return core->rpl( q{INFO_ERR_EXISTS},
374             topic => $glob,
375             nick => $nick,
376             );
377             }
378              
379             ## set up a re
380             my $re = glob_to_re_str($glob);
381             ## anchored:
382             $re = '^'.$re.'$' ;
383              
384             ## add to db, keyed on glob:
385             unless ($self->{DB}->dbopen) {
386             logger->warn("DB open failure");
387             return 'DB open failure'
388             }
389             $self->{DB}->put( $glob,
390             {
391             AddedAt => time(),
392             AddedBy => $auth_user,
393             Regex => $re,
394             Response => decode_irc($string),
395             }
396             );
397             $self->{DB}->dbclose;
398              
399             ## invalidate info3 cache:
400             $self->{Cache}->invalidate('info3');
401             $self->{NegCache}->invalidate('info3_neg');
402              
403             ## add to internal hashes:
404             my $compiled_re = qr/$re/i;
405             $self->{Regexes}->{$compiled_re} = $glob;
406             $self->{Globs}->{$glob} = $compiled_re;
407              
408             core->Provided->{info_topics} += 1;
409              
410             logger->debug("topic add: $glob ($re)");
411              
412             ## return RPL
413             return core->rpl( q{INFO_ADD},
414             topic => $glob,
415             nick => $nick,
416             )
417             }
418              
419             sub _info_del {
420             my ($self, $msg, @args) = @_;
421             my ($glob) = @args;
422              
423             my $context = $msg->context;
424             my $nick = $msg->src_nick;
425              
426             my $auth_user = core->auth->username($context, $nick);
427             my $auth_level = core->auth->level($context, $nick);
428              
429             my $pcfg = plugin_cfg( $self );
430             my $required = $pcfg->{RequiredLevels}->{DelTopic} // 2;
431             unless ($auth_level >= $required) {
432             return core->rpl( q{RPL_NO_ACCESS},
433             nick => $nick,
434             )
435             }
436              
437             unless ($glob) {
438             return core->rpl( q{INFO_BADSYNTAX_DEL} )
439             }
440              
441              
442             unless (exists $self->{Globs}->{$glob}) {
443             return core->rpl( q{INFO_ERR_NOSUCH},
444             topic => $glob,
445             nick => $nick,
446             );
447             }
448              
449             ## delete from db
450             unless ($self->{DB}->dbopen) {
451             logger->warn("DB open failure");
452             return 'DB open failure'
453             }
454             $self->{DB}->del($glob);
455             $self->{DB}->dbclose;
456              
457             $self->{Cache}->invalidate('info3');
458             $self->{NegCache}->invalidate('info3_neg');
459              
460             ## delete from internal hashes
461             my $regex = delete $self->{Globs}->{$glob};
462             delete $self->{Regexes}->{$regex};
463              
464             core->Provided->{info_topics} -= 1;
465              
466             logger->debug("topic del: $glob ($regex)");
467              
468             return core->rpl( q{INFO_DEL},
469             topic => $glob,
470             nick => $nick,
471             )
472             }
473              
474             sub _info_replace {
475             my ($self, $msg, @args) = @_;
476             my ($glob, @splstring) = @args;
477             my $string = join ' ', @splstring;
478             $glob = lc $glob;
479              
480             my $context = $msg->context;
481             my $nick = $msg->src_nick;
482              
483             my $auth_user = core->auth->username($context, $nick);
484             my $auth_level = core->auth->level($context, $nick);
485              
486             my $pcfg = plugin_cfg( $self );
487             my $req_del = $pcfg->{RequiredLevels}->{DelTopic} // 2;
488             my $req_add = $pcfg->{RequiredLevels}->{AddTopic} // 2;
489             ## auth check for BOTH add and del reqlevels:
490             unless ($auth_level >= $req_add && $auth_level >= $req_del) {
491             return core->rpl( q{RPL_NO_ACCESS},
492             nick => $nick,
493             );
494             }
495              
496             unless ($glob && $string) {
497             return core->rpl( q{INFO_BADSYNTAX_REPL} );
498             }
499              
500             unless (exists $self->{Globs}->{$glob}) {
501             return core->rpl( q{INFO_ERR_NOSUCH},
502             topic => $glob,
503             nick => $nick,
504             )
505             }
506              
507             logger->debug("replace called for $glob by $nick ($auth_user)");
508              
509             $self->{Cache}->invalidate('info3');
510             $self->{NegCache}->invalidate('info3_neg');
511              
512             unless ($self->{DB}->dbopen) {
513             logger->warn("DB open failure");
514             return 'DB open failure'
515             }
516             $self->{DB}->del($glob);
517             $self->{DB}->dbclose;
518             core->Provided->{info_topics} -= 1;
519              
520             logger->debug("topic del (replace): $glob");
521              
522             my $regex = delete $self->{Globs}->{$glob};
523             delete $self->{Regexes}->{$regex};
524              
525             my $re = glob_to_re_str($glob);
526             $re = '^'.$re.'$' ;
527              
528             unless ($self->{DB}->dbopen) {
529             logger->warn("DB open failure");
530             return 'DB open failure'
531             }
532             $self->{DB}->put( $glob,
533             {
534             AddedAt => time(),
535             AddedBy => $auth_user,
536             Regex => $re,
537             Response => $string,
538             }
539             );
540             $self->{DB}->dbclose;
541             core->Provided->{info_topics} += 1;
542              
543             my $compiled_re = qr/$re/i;
544             $self->{Regexes}->{$compiled_re} = $glob;
545             $self->{Globs}->{$glob} = $compiled_re;
546              
547             logger->debug("topic add (replace): $glob ($re)");
548              
549             return core->rpl( q{INFO_REPLACE},
550             topic => $glob,
551             nick => $nick,
552             )
553             }
554              
555             sub _info_tell {
556             ## 'tell X about Y' syntax
557             my ($self, $msg, @args) = @_;
558             my $target = shift @args;
559              
560             unless ($target) {
561             return core->rpl( q{INFO_TELL_WHO},
562             nick => $msg->src_nick,
563             )
564             }
565              
566             unless (@args) {
567             return core->rpl( q{INFO_TELL_WHAT},
568             nick => $msg->src_nick,
569             target => $target
570             )
571             }
572              
573             my $str_to_match;
574             ## might be 'tell X Y':
575             if (lc $args[0] eq 'about') {
576             ## 'tell X about Y' syntax
577             $str_to_match = join ' ', @args[1 .. $#args];
578             } else {
579             ## 'tell X Y' syntax
580             $str_to_match = join ' ', @args;
581             }
582              
583             ## find info match
584             my $match = $self->_info_match($str_to_match);
585             unless ($match) {
586             return core->rpl( q{INFO_DONTKNOW},
587             nick => $msg->src_nick,
588             topic => $str_to_match
589             );
590             }
591              
592             ## if $match is a RDB, send rdb_triggered and bail
593             if ( index($match, '~') == 0) {
594             my $rdb = (split ' ', $match)[0];
595             $rdb = substr($rdb, 1);
596             if ($rdb) {
597             ## rdb_triggered will take it from here
598             broadcast( 'rdb_triggered',
599             $msg->context,
600             $msg->channel,
601             $target,
602             lc($rdb),
603             $match,
604             $str_to_match
605             );
606             return
607             }
608             }
609              
610             my $channel = $msg->channel;
611              
612             logger->debug("issuing info3_relay_string for tell");
613              
614             broadcast( 'info3_relay_string',
615             $msg->context, $channel, $target, $match, $str_to_match
616             );
617              
618             return
619             }
620              
621             sub _info_about {
622             my ($self, $msg, @args) = @_;
623             my ($glob) = @args;
624              
625             unless ($glob) {
626             my $count = core->Provided->{info_topics};
627             return "$count info topics in database."
628             }
629              
630             unless (exists $self->{Globs}->{$glob}) {
631             return core->rpl( q{INFO_ERR_NOSUCH},
632             topic => $glob,
633             nick => $msg->src_nick,
634             )
635             }
636              
637             ## parse and display addedat/addedby info
638             $self->{DB}->dbopen(ro => 1) || return 'DB open failure';
639             my $ref = $self->{DB}->get($glob);
640             $self->{DB}->dbclose;
641              
642             my $addedby = $ref->{AddedBy} || '(undef)';
643              
644             my $addedat = POSIX::strftime(
645             "%H:%M:%S (%Z) %Y-%m-%d", localtime( $ref->{AddedAt} )
646             );
647              
648             my $str_len = length( $ref->{Response} );
649              
650             return core->rpl( q{INFO_ABOUT},
651             nick => $msg->src_nick,
652             topic => $glob,
653             author => $addedby,
654             date => $addedat,
655             length => $str_len,
656             )
657             }
658              
659             sub _info_display {
660             ## return raw topic
661             my ($self, $msg, @args) = @_;
662             my ($glob) = @args;
663             return "No topic specified" unless $glob; # FIXME rpl?
664              
665             ## check if glob exists
666             unless (exists $self->{Globs}->{$glob}) {
667             return core->rpl( q{INFO_ERR_NOSUCH},
668             topic => $glob,
669             nick => $msg->src_nick,
670             )
671             }
672              
673             ## if so, show unparsed Response
674             $self->{DB}->dbopen(ro => 1) || return 'DB open failure';
675             my $ref = $self->{DB}->get($glob);
676             $self->{DB}->dbclose;
677             my $response = $ref->{Response};
678              
679             return $response
680             }
681              
682             sub _info_search {
683             my ($self, $msg, @args) = @_;
684             my ($str) = @args;
685              
686             my @matches = $self->_info_exec_search($str);
687             return 'No matches' unless @matches;
688              
689             my $resp = "Matches: ";
690             while ( length($resp) < 350 && @matches) {
691             $resp .= ' '.shift(@matches);
692             }
693              
694             return $resp
695             }
696              
697             sub _info_exec_search {
698             my ($self, $str) = @_;
699             return 'Nothing to search' unless $str;
700              
701             my @matches;
702              
703             for my $glob (keys %{ $self->{Globs} }) {
704             push(@matches, $glob) unless index($glob, $str) == -1;
705             }
706              
707             return @matches
708             }
709              
710             sub _info_dsearch {
711             my ($self, $msg, @args) = @_;
712             my $str = join ' ', @args;
713              
714             my $pcfg = plugin_cfg( $self );
715             my $req_lev = $pcfg->{RequiredLevels}->{DeepSearch} // 0;
716             my $usr_lev = core->auth->level($msg->context, $msg->src_nick);
717             unless ($usr_lev >= $req_lev) {
718             return core->rpl( q{RPL_NO_ACCESS},
719             nick => $msg->src_nick
720             )
721             }
722              
723             my @matches = $self->_info_exec_dsearch($str);
724             return 'No matches' unless @matches;
725              
726             my $resp = "Matches: ";
727             while ( length($resp) < 350 && @matches) {
728             $resp .= ' '.shift(@matches);
729             }
730              
731             return $resp
732             }
733              
734             sub _info_exec_dsearch {
735             my ($self, $str) = @_;
736              
737             my $cache = $self->{Cache};
738             my @matches = $cache->fetch('info3', $str) || ();
739             ## matches found in searchcache
740             return @matches if @matches;
741              
742             $self->{DB}->dbopen(ro => 1) || return 'DB open failure';
743              
744             for my $glob (keys %{ $self->{Globs} }) {
745             my $ref = $self->{DB}->get($glob);
746             unless (ref $ref eq 'HASH') {
747             logger->error(
748             "Inconsistent Info3? $glob appears to have no value.",
749             "This could indicate database corruption."
750             );
751             next
752             }
753              
754             my $resp_str = $ref->{Response};
755             push(@matches, $glob) unless index($resp_str, $str) == -1;
756             }
757              
758             $self->{DB}->dbclose;
759              
760             $cache->cache('info3', $str, [ @matches ]);
761              
762             return @matches;
763             }
764              
765             sub _info_match {
766             my ($self, $txt, $isaction) = @_;
767             ## see if text matches a glob in hash
768             ## if so retrieve string from db and return it
769              
770             my $str;
771              
772             return if $self->{NegCache}->fetch('info3_neg', $txt);
773              
774             for my $re (keys %{ $self->{Regexes} }) {
775             if ($txt =~ $re) {
776             my $glob = $self->{Regexes}->{$re};
777             ## is this glob an action response?
778             if ( index($glob, '~action') == 0 ) {
779             ## action topic, are we matching a ctcp_action?
780             next unless $isaction;
781             } else {
782             ## not an action topic
783             next if $isaction;
784             }
785              
786             $self->{DB}->dbopen(ro => 1) || return 'DB open failure';
787             my $ref = $self->{DB}->get($glob) || { };
788             $self->{DB}->dbclose;
789              
790             $str = $ref->{Response} // 'Error retrieving Response string';
791              
792             last
793             }
794             }
795              
796             return $str if $str;
797              
798             ## negative searchcache if there's no match
799             ## really only helps in case of flood ...
800             $self->{NegCache}->cache('info3_neg', $txt, [1]);
801             return
802             }
803              
804              
805             sub _info_varhelp {
806             my ($self, $msg) = @_;
807              
808             my $help =
809             ' !~ = CmdChar, B~ = BotNick, C~ = Channel, H~ = UserHost, N~ = Nick,'
810             .' P~ = Port, Q~ = Question, R~ = RandomNick, S~ = Server'
811             .' t~ = unixtime, T~ = localtime, V~ = Version, W~ = Website'
812             ;
813              
814             broadcast( 'notice',
815             $msg->context,
816             $msg->src_nick,
817             $help
818             );
819              
820             return ''
821             }
822              
823             # Variable replacement / format
824             sub _info_format {
825             my ($self, $context, $nick, $channel, $str, $orig) = @_;
826             ## variable replacement for responses
827             ## some of these need to pull info from context
828             ## maintains oldschool darkbot6 variable format
829              
830             logger->debug("formatting text response ($context)");
831              
832             my $irc_obj = irc_object($context);
833             return $str unless ref $irc_obj;
834              
835             my $ccfg = core->get_core_cfg;
836             my $cmdchar = $ccfg->opts->{CmdChar};
837             my @users = $irc_obj->channel_list($channel) if $channel;
838             my $random = $users[ rand @users ] if @users;
839             my $website = core->url;
840              
841             my $vars = {
842             '!' => $cmdchar, ## CmdChar
843             B => $irc_obj->nick_name, ## bot's nick for this context
844             C => $channel, ## channel
845             H => $irc_obj->nick_long_form($irc_obj->nick_name) || '',
846             N => $nick, ## nickname
847             P => $irc_obj->port, ## remote port
848             Q => $orig, ## question string
849             R => $random, ## random nickname
850             S => $irc_obj->server, ## current server
851             t => time, ## unixtime
852             T => scalar localtime, ## parsed time
853             V => 'cobalt-'.core->version, ## version
854             W => core->url, ## website
855             };
856              
857             ## 1~ 2~ .. etc
858             my $x = 0;
859             for my $item (split ' ', $orig) {
860             ++$x;
861             $vars->{$x} = $item;
862             }
863              
864             ## var replace kinda like rplprintf
865             ## call _info3repl()
866             my $re = qr/((\S)~)/;
867             $str =~ s/$re/__info3repl($1, $2, $vars)/ge;
868             return $str
869             }
870             sub __info3repl {
871             my ($orig, $match, $vars) = @_;
872             return $orig unless defined $vars->{$match};
873             return $vars->{$match}
874             }
875              
876              
877             1;
878             __END__