pbot/PBot/EventQueue.pm

379 lines
11 KiB
Perl
Raw Normal View History

# File: EventQueue.pm
#
# Purpose: Provides functionality to manage event subroutines which are invoked
2020-03-06 22:21:44 +01:00
# at a future time, optionally recurring.
#
# Note: PBot::EventQueue has no relation to PBot::EventDispatcher.
License project under MPL2 This patch adds the file LICENSE which is the verbatim copy of the Mozilla Public License Version 2.0 as retreived from https://www.mozilla.org/media/MPL/2.0/index.815ca599c9df.txt on 2017-03-05. This patch also places license headers for the MPL2 type A variant of the license header in the following files: PBot/AntiFlood.pm PBot/BanTracker.pm PBot/BlackList.pm PBot/BotAdminCommands.pm PBot/BotAdmins.pm PBot/ChanOpCommands.pm PBot/ChanOps.pm PBot/Channels.pm PBot/Commands.pm PBot/DualIndexHashObject.pm PBot/EventDispatcher.pm PBot/FactoidCommands.pm PBot/FactoidModuleLauncher.pm PBot/Factoids.pm PBot/HashObject.pm PBot/IRCHandlers.pm PBot/IgnoreList.pm PBot/IgnoreListCommands.pm PBot/Interpreter.pm PBot/LagChecker.pm PBot/Logger.pm PBot/MessageHistory.pm PBot/MessageHistory_SQLite.pm PBot/NickList.pm PBot/PBot.pm PBot/Plugins.pm PBot/Plugins/AntiAway.pm PBot/Plugins/AntiKickAutoRejoin.pm PBot/Plugins/AntiRepeat.pm PBot/Plugins/AntiTwitter.pm PBot/Plugins/AutoRejoin.pm PBot/Plugins/Counter.pm PBot/Plugins/Quotegrabs.pm PBot/Plugins/Quotegrabs/Quotegrabs_Hashtable.pm PBot/Plugins/Quotegrabs/Quotegrabs_SQLite.pm PBot/Plugins/UrlTitles.pm PBot/Plugins/_Example.pm PBot/Refresher.pm PBot/Registerable.pm PBot/Registry.pm PBot/RegistryCommands.pm PBot/SQLiteLogger.pm PBot/SQLiteLoggerLayer.pm PBot/SelectHandler.pm PBot/StdinReader.pm PBot/Timer.pm PBot/Utils/ParseDate.pm PBot/VERSION.pm build/update-version.pl modules/acronym.pl modules/ago.pl modules/c11std.pl modules/c2english.pl modules/c2english/CGrammar.pm modules/c2english/c2eng.pl modules/c99std.pl modules/cdecl.pl modules/cfaq.pl modules/cjeopardy/IRCColors.pm modules/cjeopardy/QStatskeeper.pm modules/cjeopardy/Scorekeeper.pm modules/cjeopardy/cjeopardy.pl modules/cjeopardy/cjeopardy_answer.pl modules/cjeopardy/cjeopardy_filter.pl modules/cjeopardy/cjeopardy_hint.pl modules/cjeopardy/cjeopardy_qstats.pl modules/cjeopardy/cjeopardy_scores.pl modules/cjeopardy/cjeopardy_show.pl modules/codepad.pl modules/compiler_block.pl modules/compiler_client.pl modules/compiler_vm/Diff.pm modules/compiler_vm/cc modules/compiler_vm/compiler_client.pl modules/compiler_vm/compiler_server.pl modules/compiler_vm/compiler_server_vbox_win32.pl modules/compiler_vm/compiler_server_watchdog.pl modules/compiler_vm/compiler_vm_client.pl modules/compiler_vm/compiler_vm_server.pl modules/compiler_vm/compiler_watchdog.pl modules/compiler_vm/languages/_c_base.pm modules/compiler_vm/languages/_default.pm modules/compiler_vm/languages/bash.pm modules/compiler_vm/languages/bc.pm modules/compiler_vm/languages/bf.pm modules/compiler_vm/languages/c11.pm modules/compiler_vm/languages/c89.pm modules/compiler_vm/languages/c99.pm modules/compiler_vm/languages/clang.pm modules/compiler_vm/languages/clang11.pm modules/compiler_vm/languages/clang89.pm modules/compiler_vm/languages/clang99.pm modules/compiler_vm/languages/clangpp.pm modules/compiler_vm/languages/clisp.pm modules/compiler_vm/languages/cpp.pm modules/compiler_vm/languages/freebasic.pm modules/compiler_vm/languages/go.pm modules/compiler_vm/languages/haskell.pm modules/compiler_vm/languages/java.pm modules/compiler_vm/languages/javascript.pm modules/compiler_vm/languages/ksh.pm modules/compiler_vm/languages/lua.pm modules/compiler_vm/languages/perl.pm modules/compiler_vm/languages/python.pm modules/compiler_vm/languages/python3.pm modules/compiler_vm/languages/qbasic.pm modules/compiler_vm/languages/scheme.pm modules/compiler_vm/languages/server/_c_base.pm modules/compiler_vm/languages/server/_default.pm modules/compiler_vm/languages/server/c11.pm modules/compiler_vm/languages/server/c89.pm modules/compiler_vm/languages/server/c99.pm modules/compiler_vm/languages/server/clang.pm modules/compiler_vm/languages/server/clang11.pm modules/compiler_vm/languages/server/clang89.pm modules/compiler_vm/languages/server/clang99.pm modules/compiler_vm/languages/server/cpp.pm modules/compiler_vm/languages/server/freebasic.pm modules/compiler_vm/languages/server/haskell.pm modules/compiler_vm/languages/server/java.pm modules/compiler_vm/languages/server/qbasic.pm modules/compiler_vm/languages/server/tendra.pm modules/compiler_vm/languages/sh.pm modules/compiler_vm/languages/tendra.pm modules/compliment modules/cstd.pl modules/define.pl modules/dice_roll.pl modules/excuse.sh modules/expand_macros.pl modules/fnord.pl modules/funnyish_quote.pl modules/g.pl modules/gdefine.pl modules/gen_cfacts.pl modules/gencstd.pl modules/get_title.pl modules/getcfact.pl modules/google.pl modules/gspy.pl modules/gtop10.pl modules/gtop15.pl modules/headlines.pl modules/horoscope modules/horrorscope modules/ideone.pl modules/insult.pl modules/love_quote.pl modules/man.pl modules/map.pl modules/math.pl modules/prototype.pl modules/qalc.pl modules/random_quote.pl modules/seen.pl modules/urban modules/weather.pl modules/wikipedia.pl pbot.pl pbot.sh It is highly recommended that this list of files is reviewed to ensure that all files are the copyright of the sole maintainer of the repository. If any files with license headers contain the intellectual property of anyone else, it is recommended that a request is made to revise this patch or that the explicit permission of the co-author is gained to allow for the license of the work to be changed. I (Tomasz Kramkowski), the contributor, take no responsibility for any legal action taken against the maintainer of this repository for incorrectly claiming copyright to any work not owned by the maintainer of this repository.
2017-03-05 22:33:31 +01:00
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
package PBot::EventQueue;
use parent 'PBot::Class';
2021-06-19 06:23:34 +02:00
use PBot::Imports;
2019-07-11 03:40:53 +02:00
use Time::HiRes qw/time/;
use Time::Duration;
sub initialize {
2020-02-15 23:38:32 +01:00
my ($self, %conf) = @_;
# array of pending events
2020-03-06 22:21:44 +01:00
$self->{event_queue} = [];
# register `eventqueue` bot command
$self->{pbot}->{commands}->register(sub { $self->cmd_eventqueue(@_) }, 'eventqueue', 1);
2020-03-06 22:21:44 +01:00
# add `can-eventqueue` capability to admin group
$self->{pbot}->{capabilities}->add('admin', 'can-eventqueue', 1);
2020-03-06 22:21:44 +01:00
}
# eventqueue bot command
sub cmd_eventqueue {
my ($self, $context) = @_;
2020-03-06 22:21:44 +01:00
my $usage = "Usage: eventqueue list [filter regex] | add <relative time> <command> [-repeat] | remove <regex>";
2020-03-06 22:21:44 +01:00
2020-05-02 05:59:51 +02:00
my $command = $self->{pbot}->{interpreter}->shift_arg($context->{arglist});
if (not defined $command) {
return $usage;
}
if ($command eq 'list') {
return "No events queued." if not @{$self->{event_queue}};
my $result = eval {
my $text = "Queued events:\n";
2020-05-02 05:59:51 +02:00
my ($regex) = $self->{pbot}->{interpreter}->shift_arg($context->{arglist});
my $i = 0;
2020-04-06 05:33:14 +02:00
my $events = 0;
foreach my $event (@{$self->{event_queue}}) {
$i++;
if ($regex) {
2020-04-06 05:33:14 +02:00
next unless $event->{id} =~ /$regex/i;
}
2020-04-06 05:33:14 +02:00
$events++;
my $duration = $event->{timeout} - time;
if ($duration < 0) {
# current time has passed an event's time but the
# event hasn't left the queue yet. we'll show these
# as, e.g., "pending 5s ago"
$duration = 'pending ' . concise ago -$duration;
} else {
$duration = 'in ' . concise duration $duration;
}
$text .= " $i) $duration: $event->{id}";
$text .= ' [R]' if $event->{repeating};
$text .= ";\n";
}
2020-03-06 22:21:44 +01:00
2020-04-06 05:33:14 +02:00
return "No events found." if $events == 0;
return $text . "$events events.\n";
};
if (my $error = $@) {
# strip source information to prettify error for non-developer consumption
$error =~ s/ at PBot.*//;
return "Bad regex: $error";
}
return $result;
}
if ($command eq 'add') {
2020-05-02 05:59:51 +02:00
my ($duration, $command) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 2);
if (not defined $duration or not defined $command) {
return "Usage: eventqueue add <relative time> <command> [-repeat]";
}
# convert text like "5 minutes" or "1 week" or "next tuesday" to seconds
my ($seconds, $error) = $self->{pbot}->{parsedate}->parsedate($duration);
return $error if defined $error;
# check for `-repeating` at front or end of command
my $repeating = $command =~ s/^-repeat\s+|\s+-repeat$//g;
my $cmd = {
nick => $context->{nick},
user => $context->{user},
host => $context->{host},
hostmask => $context->{hostmask},
command => $command,
};
$self->{pbot}->{interpreter}->add_to_command_queue($context->{from}, $cmd, $seconds, $repeating);
return "Command added to event queue.";
2020-03-06 22:21:44 +01:00
}
if ($command eq 'remove') {
2020-05-02 05:59:51 +02:00
my ($regex) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 1);
return "Usage: eventqueue remove <regex>" if not defined $regex;
$regex =~ s/(?<!\.)\*/.*?/g;
return $self->dequeue_event($regex);
}
return "Unknown command '$command'. $usage";
}
2020-03-06 22:21:44 +01:00
sub find_enqueue_position {
my ($self, $time) = @_;
2020-02-15 23:38:32 +01:00
# no events in queue yet, early-return first position
2020-03-06 22:21:44 +01:00
return 0 if not @{$self->{event_queue}};
2020-02-15 23:38:32 +01:00
# early-return first position if event's time is less
# than first position's
if ($time < $self->{event_queue}->[0]->{timeout}) {
2020-03-06 22:21:44 +01:00
return 0;
}
2020-02-15 23:38:32 +01:00
# early-return last position if event's time is greater
if ($time > $self->{event_queue}->[@{$self->{event_queue}} - 1]->{timeout}) {
2020-03-06 22:21:44 +01:00
return scalar @{$self->{event_queue}};
}
# binary search to find enqueue position
2020-03-06 22:21:44 +01:00
my $lo = 0;
my $hi = scalar @{$self->{event_queue}} - 1;
while ($lo <= $hi) {
my $mid = int (($hi + $lo) / 2);
if ($time < $self->{event_queue}->[$mid]->{timeout}) {
2020-03-06 22:21:44 +01:00
$hi = $mid - 1;
} elsif ($time > $self->{event_queue}->[$mid]->{timeout}) {
2020-03-06 22:21:44 +01:00
$lo = $mid + 1;
} else {
while ($mid < @{$self->{event_queue}} and $self->{event_queue}->[$mid]->{timeout} == $time) {
# found a slot with the same time. we "slide" down the array
# to append this event to the end of this region of same-times.
$mid++;
}
2020-03-06 22:21:44 +01:00
return $mid;
}
}
2020-03-06 22:21:44 +01:00
return $lo;
}
sub replace_subref_or_enqueue_event {
my ($self, $subref, $interval, $id, $repeating) = @_;
# find events matching id
my @events = grep { $_->{id} eq $id } @{$self->{event_queue}};
# no events found, enqueue new event
if (not @events) {
$self->enqueue_event($subref, $interval, $id, $repeating);
return;
}
# otherwise update existing events
foreach my $event (@events) {
$event->{subref} = $subref;
}
}
sub replace_or_enqueue_event {
my ($self, $subref, $interval, $id, $repeating) = @_;
# remove event if it exists
$self->dequeue_event($id) if $self->exists($id);
# enqueue new event
$self->enqueue_event($subref, $interval, $id, $repeating);
}
sub enqueue_event_unless_exists {
my ($self, $subref, $interval, $id, $repeating) = @_;
# event already exists, bail out
return if $self->exists($id);
# enqueue new event
$self->enqueue_event($subref, $interval, $id, $repeating);
}
# adds an event to the event queue
2020-03-06 22:21:44 +01:00
sub enqueue_event {
my ($self, $subref, $interval, $id, $repeating) = @_;
$id //= 'an event';
$repeating //= 0;
$interval //= 0;
2020-03-06 22:21:44 +01:00
my $event = {
id => $id,
subref => $subref,
2020-03-06 22:21:44 +01:00
interval => $interval,
timeout => time + $interval,
2020-03-06 22:21:44 +01:00
repeating => $repeating,
};
2020-03-06 22:21:44 +01:00
my $i = $self->find_enqueue_position($event->{timeout});
splice @{$self->{event_queue}}, $i, 0, $event;
my $debug = $self->{pbot}->{registry}->get_value('eventqueue', 'debug') // 0;
2020-03-06 22:21:44 +01:00
if ($debug > 1) {
$self->{pbot}->{logger}->log("Enqueued new event $id at position $i: timeout=$event->{timeout} interval=$interval repeating=$repeating\n");
2020-03-06 22:21:44 +01:00
}
}
# removes an event from the event queue, optionally invoking it.
# `id` can contain `.*` and `.*?` for wildcard-matching/globbing.
2020-03-06 22:21:44 +01:00
sub dequeue_event {
my ($self, $id, $execute) = @_;
2020-03-06 22:21:44 +01:00
my $result = eval {
2020-03-06 22:21:44 +01:00
$id = quotemeta $id;
$id =~ s/\\\.\\\*\\\?/.*?/g;
$id =~ s/\\\.\\\*/.*/g;
2020-04-06 05:33:14 +02:00
my $regex = qr/^$id$/i;
my $count = @{$self->{event_queue}};
my @removed = grep { $_->{id} =~ /$regex/i; } @{$self->{event_queue}};
2020-03-06 22:21:44 +01:00
@{$self->{event_queue}} = grep { $_->{id} !~ /$regex/i; } @{$self->{event_queue}};
$count -= @{$self->{event_queue}};
if ($execute) {
foreach my $event (@removed) {
$event->{subref}->($event);
}
}
return "No matching events." if not $count;
return "Removed $count event" . ($count == 1 ? '' : 's') . ': ' . join(', ', map { $_->{id} } @removed);
2020-03-06 22:21:44 +01:00
};
if ($@) {
my $error = $@;
$self->{pbot}->{logger}->log("Error in dequeue_event: $error\n");
$error =~ s/ at PBot.*//;
return "$error";
2020-03-06 22:21:44 +01:00
}
return $result;
2020-03-06 22:21:44 +01:00
}
# invoke and remove all events matching `id`, which can
# contain `.*` and `.*?` for wildcard-matching/globbing
sub execute_and_dequeue_event {
my ($self, $id) = @_;
return $self->dequeue_event($id, 1);
}
# adds an event, with repeating defaulted to enabled
sub enqueue {
my ($self, $subref, $interval, $id, $repeating) = @_;
$repeating //= 1;
$self->enqueue_event($subref, $interval, $id, $repeating);
}
# alias to dequeue_event, for consistency
sub dequeue {
2020-02-15 23:38:32 +01:00
my ($self, $id) = @_;
2020-03-06 22:21:44 +01:00
$self->dequeue_event($id);
}
sub exists {
my ($self, $id) = @_;
return scalar grep { $_->{id} eq $id } @{$self->{event_queue}};
}
sub update_repeating {
my ($self, $id, $repeating) = @_;
for (my $i = 0; $i < @{$self->{event_queue}}; $i++) {
if ($self->{event_queue}->[$i]->{id} eq $id) {
$self->{event_queue}->[$i]->{repeating} = $repeating;
last;
}
}
}
sub update_interval {
2020-03-06 22:21:44 +01:00
my ($self, $id, $interval, $dont_enqueue) = @_;
2020-03-06 22:21:44 +01:00
for (my $i = 0; $i < @{$self->{event_queue}}; $i++) {
if ($self->{event_queue}->[$i]->{id} eq $id) {
if ($dont_enqueue) {
# update interval in-place without moving event to new place in queue
# (allows event to fire at expected time, then updates to new timeout afterwards)
2020-03-06 22:21:44 +01:00
$self->{event_queue}->[$i]->{interval} = $interval;
} else {
# remove and add event in new position in queue
2020-03-06 22:21:44 +01:00
my $event = splice(@{$self->{event_queue}}, $i, 1);
$self->enqueue_event($event->{subref}, $interval, $id, $event->{repeating});
}
2020-02-15 23:38:32 +01:00
last;
}
}
}
sub duration_until_next_event {
my ($self) = @_;
return 0 if not @{$self->{event_queue}};
return $self->{event_queue}->[0]->{timeout} - time;
}
# invokes the current events and then returns seconds until next upcoming event
sub do_events {
2020-03-06 22:21:44 +01:00
my ($self) = @_;
# early-return if no events available
return 0 if not @{$self->{event_queue}};
2020-03-06 22:21:44 +01:00
my $debug = $self->{pbot}->{registry}->get_value('eventqueue', 'debug') // 0;
# repeating events to re-enqueue
my @enqueue;
for (my $i = 0; $i < @{$self->{event_queue}}; $i++) {
# we call time for a fresh time, instead of using a stale $now that
# could be well in the past depending on a previous event's duration
if (time >= $self->{event_queue}->[$i]->{timeout}) {
my $event = $self->{event_queue}->[$i];
$self->{pbot}->{logger}->log("Processing event $i: $event->{id}\n") if $debug > 1;
# call event's subref, passing event as argument
$event->{subref}->($event);
# remove event from queue
splice @{$self->{event_queue}}, $i--, 1;
# add event to re-enqueue queue if repeating
push @enqueue, $event if $event->{repeating};
} else {
# no more events ready at this time
if ($debug > 2) {
$self->{pbot}->{logger}->log("Event not ready yet: $self->{event_queue}->[$i]->{id} (timeout=$self->{event_queue}->[$i]->{timeout})\n");
}
last;
2020-03-06 22:21:44 +01:00
}
}
# re-enqueue repeating events
foreach my $event (@enqueue) {
$self->enqueue_event($event->{subref}, $event->{interval}, $event->{id}, 1);
2020-03-06 22:21:44 +01:00
}
return $self->duration_until_next_event;
2020-03-06 22:21:44 +01:00
}
1;