#! /usr/bin/perl

##  Delay lines for a certain number of seconds.
##
##  Primarily meant to be used to generate a delayed feed with innfeed.
##  See the delayer(8) manual page for more details, and how to set it.
##
##  Initial version written by Christian Mock <cm@tahina.priv.at> in July 1998,
##  and put into the public domain.
##
##  Various bug fixes, code and documentation improvements since then
##  in 2001, 2003, 2022, 2024, 2025.

use strict;
use warnings;

use Getopt::Long;
use Storable;

(@ARGV >= 2) || die "usage: $0 delay prog-n-args\n";

my $delay = 60;
my $buffered = 1;
my $store;

if ($ARGV[0] =~ /^[0-9]+$/) {
    # Legacy calling convention
    $delay = shift;
} else {
    GetOptions(
        'delay=i'   => \$delay,
        'buffered!' => \$buffered,
        'store=s'   => \$store,
    ) or die('Usage!');
}

my $timeout = $delay;

open(OUT, "|-", @ARGV) || die "open |prog-n-args: $!\n";
if (!$buffered) {
    select(OUT);
    $| = 1;
    select(STDOUT);
}

my @queue;
if ($store) {
    eval { @queue = @{ retrieve($store) } };
    unlink($store);
}

my $rin = '';
my $rout;
vec($rin, fileno(STDIN), 1) = 1;

while (1) {
    select($rout = $rin, undef, undef, $timeout);
    my $now = time();
    my $exp = $now + $delay;

    if (vec($rout, fileno(STDIN), 1)) {
        my $line = <STDIN>;
        if (!defined $line) {    # exit NOW!
            if ($store) {
                eval { store(\@queue, $store); };
            } else {
                foreach (@queue) {
                    s/^[^:]+://g;
                    print OUT;
                }
                close(OUT);
                sleep(1);
            }
            exit;
        }
        push(@queue, "$exp:$line");
    }

    undef $timeout;
    while (@queue) {
        my ($first, $line) = split(/:/, $queue[0], 2);
        if ($first > $now) {
            $timeout = $first - $now;
            last;
        }
        print OUT $line;
        shift(@queue);
    }
}

__END__

=head1 NAME

delayer - A pipe to delay line-based input by a given time

=head1 SYNOPSIS

I<some-program> | B<delayer> [B<-->[B<no->]B<buffered>] [B<--delay>
I<seconds>] [B<--store> I<filename>] B<--> I<some-other-program> [I<args> ...]

Legacy calling convention:

I<some-program> | B<delayer> I<seconds> I<some-other-program> [I<args> ...]

=head1 DESCRIPTION

The B<delayer> program implements a delaying pipe.  Lines sent to the standard
input of the process are spooled, and only printed to the standard input of
I<some-other-program> after a certain delay time has passed.

The main use case is for a news feed that deliberately should not distribute
articles as soon as possible.  One reason is giving cancel control articles
and NoCeM messages time to arrive so that B<innd> remembers the Message-IDs of
those cancelled articles before they actually arrive.  It permits cancelling
articles before they are locally stored and spread to other peers.  The delay
can be set up for outgoing feeds wanting that or, even better for not slowing
the propagation of articles, internally between a frontend instance of B<innd>
receiving the articles from all your peers and another local instance of
B<innd> fed by your frontend with a delay except for cancels and NoCeM
articles.

Another use case is using a link only as a backup.

=head1 CONFIGURATION

The steps to set up a delayed feed using B<delayer> and B<innfeed> are:

=over 4

=item *

Choose a name for that feed, e.g. C<innfeed-delayed>.

=item *

In I<pathetc>, copy F<innfeed.conf> to F<innfeed-delayed.conf>.

=item *

Edit F<innfeed-delayed.conf> in I<pathetc>, and change the occurrences of
C<innfeed> to C<innfeed-delayed>, typically in the I<log-file>, I<pid-file>
and I<status-file> parameters.  If these parameters are not set, you should
explicitly set them in F<innfeed-delayed.conf> so that their default values
do not conflict with a running instance in parallel of a real-time feed using
B<innfeed>.  For instance:

    log-file:    innfeed-delayed.log
    pid-file:    innfeed-delayed.pid
    status-file: innfeed-delayed.status

Using the same value for I<backlog-directory> in both F<innfeed.conf> and
F<innfeed-delayed.conf> is fine because the site names in F<newsfeeds> are
unique (see below with C<news.uu.net> and C<news.uu.net-delayed>).

=item *

Possibly limit I<max-connections> to C<1>.

=item *

Only keep in F<innfeed-delayed.conf> the configuration of the peers which
should receive a delayed feed.

=item *

Add a new entry to F<newsfeeds> in I<pathetc> like:

    innfeed-delayed!\
        :!*\
        :Tc,Wnm*,S16384:<pathbin>/delayer --delay 300 -- \
            <pathbin>/innfeed -c innfeed-delayed.conf

This will delay articles via that feed for 300 seconds.  If the intent is to
give NoCeM messages time to arrive, the delay should match the frequency at
which they are sent in the news.lists.filters newsgroup.

=item *

Use C<innfeed-delayed!> instead of C<innfeed!> in the F<newsfeeds> entries for
peers which should receive a delayed feed.  If you wish, you can also set up
two entries for each peer, keeping a real-time feed through C<innfeed!> for
control articles and NoCeM messages, and delaying the feed of other articles
through C<innfeed-delayed!>.

    news.uu.net/uunet\
        :!*,control,control.*,news.lists.filters\
        :Tm:innfeed!

    news.uu.net-delayed/uunet\
        :*,!control,!control.*,@news.lists.filters\
        :Tm:innfeed-delayed!

In that case, be sure to use the same C<news.uu.net-delayed> peer name in
F<innfeed-delayed.conf>.  Note that you should only configure a delayed feed
for a remote peer if its news administrator agrees with that (he may want a
real-time feed, or already have locally implemented a delay on his incoming
feeds).

=item *

Reload the F<newsfeeds> configuration file:

    ctlinnd reload newsfeeds 'setting delayed feeds'

=back

=head1 OPTIONS

=over 4

=item B<--buffered>, B<--no-buffered>

By default, output is buffered.  This increases the time until an article is
actually sent if the number of articles is small.  Disable buffering to have
a more accurate delay, at a price of a (possibly neglectable) performance
overhead.

=item B<--delay> I<seconds>

Delay articles by the given amount of seconds.  Default is C<60>.

=item B<--store> I<filename>

By default, all buffered lines are written out if the input is closed, even if
the configured delay has not been reached yet.

When this flag is used, these lines will be written to the given file instead,
to be used upon next startup.  Using an absolute path name is recommended.

=item B<--> I<some-other-program> [I<args> ...]

B<--> separates the options to B<delayer> from the program the output is
written to: I<some-other-program> is the full path to the program, optionally
followed with some I<args> parameters.

=back

=head1 LEGACY OPTIONS

This interface is considered legacy and will be removed some day.

=over 4

=item I<delay>

Delay articles by the given amount of seconds.

=item I<some-other-program> [I<args> ...]

The full path to the program the output is written to, optionally followed
with some I<args> parameters.

=back

=head1 BUGS

If the standard input is closed (when for instance the feed is closed or
restarted), all lines in the store are printed immediately, breaking the
contract of delaying them, unless the B<--store> option is used.

If the number of articles in that feed is rather low (just a few articles per
delay time or less), some effects of buffering will delay the transmission
even further.  See the B<--no-buffering> option to alleviate this.

=head1 HISTORY

Initial version written in July 1998 by Christian Mock <cm@tahina.priv.at>.

Improved and documented by Christoph Biedl in January 2024.

=head1 SEE ALSO

delay(1), innfeed.conf(5), newsfeeds(5).

=cut
