User:AnomieBOT/source/tasks/OEISUninterestingNumbers.pm

package tasks::OEISUninterestingNumbers;
use parent 'AnomieBOT::Task';

=pod

=begin metadata

Bot:     AnomieBOT
Task:    OEISUninterestingNumbers
BRFA:    N/A
Status:  Begun 2025-07-13
Created: 2025-07-13

Create a page listing some [[uninteresting number]]s:
* Smallest number that does not appear in any (truncated) sequence in [[OEIS]].

=end metadata

=cut

use utf8;
use strict;

use Date::Parse;
use HTTP::Response;
use IO::Uncompress::Gunzip qw/$GunzipError/;
use LWP::UserAgent;

sub new {
    my $class=shift;
    my $self=$class->SUPER::new;
    $self->{'ua'}=LWP::UserAgent->new(
        agent=>"AnomieBOT/1.0 (uninteresting number checker for en.wikipedia.org; see https://en.wikipedia.org/wiki/User:AnomieBOT)",
        keep_alive=>300,
    );
    bless $self, $class;
    return $self;
}

=pod

=for info
Per [[WP:BOTUSERSPACE]], any bot or automated editing process that affects only
the operator's or their own userspace, and which are not otherwise disruptive,
may be run without prior approval.

=cut

sub approved {
    return 999;
}

sub run {
    my ( $self, $api ) = @_;

    $api->task( 'OEISUninterestingNumbers', 0, 10, qw(d::IWNS) );

    my $debugmode = ( $api->DEBUG & 4 ) == 4;
    if ( $debugmode ) {
        $api->log( "!!! DEBUG MODE ACTIVE !!!" );
    }

    my $t = ( $api->store->{'nextrun'} // 0 ) - time();
    return $t if $t>0;

    my $title = 'User:AnomieBOT/OEIS-uninteresting numbers';
    my $tok = $api->edittoken( $title, EditRedir => 1 );
    if ( $tok->{'code'} eq 'shutoff' ) {
        $api->warn( "Task disabled: " . $tok->{'content'} . "\n" );
        return 300;
    }
    if ( $tok->{'code'} ne 'success' ) {
        $api->warn( "Failed to get edit token for $title: " . $tok->{'error'} . "\n" );
        return 300;
    }

    my $url = 'https://oeis.org/stripped.gz';
    my $r;
    if ( $debugmode ) {
        if ( -e "/tmp/oeis-stripped.gz" ) {
            my $content;
            $r = { is_success => 1 };
            open X, "<", "/tmp/oeis-stripped.gz" or die "Failed to open /tmp/oeis-stripped.gz: $!\n";
            { local $/=undef; $content = <X>; }
            close X;
            $r = new HTTP::Response( 200 );
            $r->content( $content );
            $api->log( "DEBUG: Loaded downloaded data from /tmp/oeis-stripped.gz" );
        } else {
            $api->log( "Downloading data" );
            $r = $self->{'ua'}->get( $url );
            open X, ">", "/tmp/oeis-stripped.gz" or die "Failed to write /tmp/oeis-stripped.gz: $!\n";
            { local $/=undef; print X $r->decoded_content; }
            close X;
            $api->log( "DEBUG: Saved downloaded data to /tmp/oeis-stripped.gz for reuse on re-runs" );
        }
    } else {
        $api->log( "Downloading data" );
        $r = $self->{'ua'}->get( $url );
    }
    if( $r->code =~ /^4\d\d$/ && $r->code !~ /^(400|408|409)$/ ) {
        $api->error( "Failed to fetch $url: " . $r->status_line . "\n" . $r->decoded_content );
        $api->error( "Will not try again (until bot restart anyway)" );
        return undef;
    } elsif ( ! $r->is_success ) {
        $api->error( "Failed to fetch $url: " . $r->status_line );
        $api->error( "Will try again later" );
        $api->store->{'nextrun'} = time + 3600;
        return 3600;
    } 

    $api->log( "Reading data" );
    my $z = IO::Uncompress::Gunzip->new( $r->decoded_content( 'ref' => 1 ) );
    if ( ! $z ) {
        $api->error( "Failed to uncompress data: $GunzipError" );
        return 300;
    }
    my $ts = undef;
    my %inFile = ();
    while ( <$z> ) {
        if ( /^# Last Modified: (.*)/ ) {
            $ts = str2time( $1, 'UTC' );
            $api->error( "Failed to parse last-modified timestamp $1" ) unless defined( $ts );
        }
        next if /^#/;
        for my $n ( split /,/ ) {
            $inFile{$1}++ if $n =~ /^\s*(\d+)\s*$/;
        }
    }
    close $z;
    $z = undef;

    if ( ! defined( $ts ) ) {
        $api->error( "Did not find 'Last Modified' header in file" );
        $api->store->{'nextrun'} = time + 3600;
        return 3600;
    }
    if ( ! %inFile ) {
        $api->error( "Did not find any integers in file" );
        $api->store->{'nextrun'} = time + 3600;
        return 3600;
    }

    $api->log( "Finding first unlisted number" );
    my $number = 0;
    while ( $inFile{$number} // 0 ) {
        $number++;
    }
    $api->log( "First unlisted number is $number as of " . gmtime( $ts ) );

    my $txt = qq(
        {{ $title/{{{1|display}}}
         | number     = $number
         | lastchange = $ts
         | arg = {{{2|}}}
        }}
    );
    $txt =~ s/^        //mg;
    $txt =~ s/^\s+//;
    $txt =~ s/\s*$/\n/;

    my $intxt = $tok->{'revisions'}[0]{'slots'}{'main'}{'*'} // '';
    $intxt =~ s/^\s+//;
    $intxt =~ s/\s*$/\n/;
    $intxt =~ s/\| lastchange = \d+/| lastchange = $ts/;

    if ( $txt ne $intxt ) {
        my $summary = "Updating OEIS-uninteresting number: $number";
        $api->log( "$summary in $title" );
        my $r = $api->edit( $tok, $txt, "$summary", 0, 1 );
        if ( $r->{'code'} ne 'success' ) {
            $api->warn( "Write error for $title: " . $r->{'error'} . "\n" );
            return 60;
        }
    }

    # OEIS seems to update just before 05:00 UTC. So aim for that.
    $t = 86400 - ( ( time - 5 * 3600 ) % 86400 );
    $api->store->{'nextrun'} = time + $t;
    return $t;
}

1;