3
0
mirror of https://github.com/pragma-/pbot.git synced 2024-12-27 13:12:42 +01:00
pbot/lib/PBot/EventQueue.pm

411 lines
12 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.
2021-07-11 00:00:22 +02:00
# SPDX-FileCopyrightText: 2021 Pragmatic Software <pragma78@gmail.com>
# SPDX-License-Identifier: MIT
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
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";
}
# returns seconds until upcoming event.
sub duration_until_next_event {
my ($self) = @_;
return 0 if not @{$self->{event_queue}};
return $self->{event_queue}->[0]->{timeout} - time;
}
# invokes any current events and then returns seconds until upcoming event.
sub do_events {
my ($self) = @_;
# early-return if no events available
return 0 if not @{$self->{event_queue}};
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 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;
}
}
# re-enqueue repeating events
foreach my $event (@enqueue) {
$self->enqueue_event($event->{subref}, $event->{interval}, $event->{id}, 1);
}
return $self->duration_until_next_event;
}
# check if an event is in the event queue.
sub exists {
my ($self, $id) = @_;
return scalar grep { $_->{id} eq $id } @{$self->{event_queue}};
}
# quickly and efficiently find the best position in the event
# queue array for a given time value
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;
}
# adds an event to the event queue, optionally repeating
2020-03-06 22:21:44 +01:00
sub enqueue_event {
my ($self, $subref, $interval, $id, $repeating) = @_;
# default values
$id //= "unnamed (${interval}s $subref)";
$repeating //= 0;
$interval //= 0;
# create event structure
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,
};
# find position to add event
2020-03-06 22:21:44 +01:00
my $i = $self->find_enqueue_position($event->{timeout});
# add the event to the event queue array
2020-03-06 22:21:44 +01:00
splice @{$self->{event_queue}}, $i, 0, $event;
# debugging noise
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
}
}
# convenient alias to add an event with repeating defaulted to enabled.
sub enqueue {
my ($self, $subref, $interval, $id, $repeating) = @_;
$self->enqueue_event($subref, $interval, $id, $repeating // 1);
}
# 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 {
# escape special characters
2020-03-06 22:21:44 +01:00
$id = quotemeta $id;
# unescape .* and .*?
2020-03-06 22:21:44 +01:00
$id =~ s/\\\.\\\*\\\?/.*?/g;
$id =~ s/\\\.\\\*/.*/g;
# compile regex
2020-04-06 05:33:14 +02:00
my $regex = qr/^$id$/i;
# count total events before removal
my $count = @{$self->{event_queue}};
# collect events to be removed
my @removed = grep { $_->{id} =~ /$regex/i; } @{$self->{event_queue}};
# remove events from event queue
2020-03-06 22:21:44 +01:00
@{$self->{event_queue}} = grep { $_->{id} !~ /$regex/i; } @{$self->{event_queue}};
# set count to total events removed
$count -= @{$self->{event_queue}};
# invoke removed events, if requested
if ($execute) {
foreach my $event (@removed) {
$event->{subref}->($event);
}
}
# nothing removed
return "No matching events." if not $count;
# list all removed events
my $removed = "Removed $count event" . ($count == 1 ? '' : 's') . ': ' . join(', ', map { $_->{id} } @removed);
$self->{pbot}->{logger}->log("EventQueue: dequeued $removed\n");
return $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
}
# alias to dequeue_event, for consistency.
sub dequeue {
my ($self, $id) = @_;
$self->dequeue_event($id);
}
# 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);
}
# replace code subrefs for matching events. if no events
# were found, then add the event to the event queue.
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;
}
}
# remove existing events of this id then enqueue new event.
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);
}
# add event unless it already had been added.
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);
}
# update the `repeating` flag for all events matching `id`.
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;
}
}
}
# update the `interval` value for all events matching `id`.
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
}
}
}
1;