HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/be.naniguide.com/app/Library/InMemoryRateTracker.php
<?php

namespace Acelle\Library;

use Carbon\Carbon;
use Exception;
use Acelle\Library\Exception\RateLimitExceeded;
use Closure;
use Illuminate\Support\Facades\Cache;

/*
 * Log every credit used to a log file, group by $mode, which could be 'minute' (default), hour, day, month...
 * For example, a log file grouped by minute may look like this ( lines of YYYYMMDDHHMM:COUNT format)
 *
 * 202307230913:1200
 * 202307230914:1250
 * 202307230914:1052
 * 202307230930:1178
 *
 * Group by 'minute' supports minute granularity limits (emails per 1 minute)
 * Group by 'hour' supports hour granularity limits (emails per 1 hour), but does not support minute (lower than 1 hour) granularity limits
 *
 */

class InMemoryRateTracker
{
    protected $resourceKey;
    protected $mode = 'minute'; // hour, day, month, year
    protected $seperator = ':';
    protected $blockFormat = [
        'minute' => 'YmdHi',
        'hour' => 'YmdH00',
        'day' => 'Ymd0000',
        'month' => 'Ym000000',
        'year' => 'Y00000000',
    ];

    protected $limits;

    public function __construct(string $resourceKey, $limits = []) // RateLimit class
    {
        $this->resourceKey = $resourceKey;
        $this->limits = $limits;
    }

    public function test(Carbon $now = null, $failedCallback = null)
    {
        with_cache_lock($this->resourceKey, function () use ($now, $failedCallback) {
            $now = $now ?: Carbon::now();

            // Throw an exception if test fails (quota exceeded)
            $this->doTest($now, $failedCallback);

        }, $timeout = 15);
    }

    public function count(Carbon $now = null, $failedCallback = null)
    {
        with_cache_lock($this->resourceKey, function () use ($now, $failedCallback) {
            $now = $now ?: Carbon::now();

            // Throw an exception if test fails (quota exceeded)
            $this->doTest($now, $failedCallback);

            // Record credits use
            $this->record($now);

        }, $timeout = 15);
    }

    private function doTest($now, $failedCallback)
    {
        foreach ($this->limits as $limit) {
            $period = sprintf("%s %s", $limit->getPeriodValue(), $limit->getPeriodUnit());
            $fromDatetime = $now->copy()->subtract($period);

            $creditsUsed = $this->getCreditsUsed($fromDatetime, $now);

            if ($creditsUsed >= $limit->getAmount()) {
                $msg = sprintf("%s exceeded! %s/%s used", $limit->getDescription(), $creditsUsed, $limit->getAmount());

                if ($failedCallback) {
                    $failedCallback($msg);
                }

                throw new RateLimitExceeded($msg);
            }
        }
    }

    // Reverse of count()
    // @deprecated: rollback is not needed as even a failed operation is also counted in rate limits
    public function rollback()
    {
        with_cache_lock($this->resourceKey, function () {
            list($lastBlock, $count) = $this->getLastRecord();

            if (is_null($lastBlock)) {
                throw new Exception('Cannot rollback! There is no previous count!');
            }

            if ($count == 1) {
                $this->removeRecord($lastBlock);
            } else {
                $this->updateRecord($lastBlock, $count - 1);
            }
        }, $timeout = 15);
    }

    private function record(Carbon $now)
    {
        // Make something like: 202307231527
        $currentBlock = $this->makeBlock($now); // create block for the current date/time
        list($lastBlock, $count) = $this->getLastRecord();

        if ($currentBlock == $lastBlock) {
            $this->updateRecord($lastBlock, $count + 1);
        } else {
            $this->updateRecord($currentBlock, $count = 1);
        }
    }

    public function updateRecord($block, $count)
    {
        $store = Cache::get($this->resourceKey);
        $store[$block] = $count;

        Cache::put($this->resourceKey, $store);
    }

    public function removeRecord($block)
    {
        $store = Cache::get($this->resourceKey);
        unset($store[$block]);

        Cache::put($this->resourceKey, $store);
    }

    public function getRecords(Carbon $fromDatetime = null, Carbon $toDatetime = null)
    {
        $fromDatetime = $fromDatetime ?: Carbon::createFromTimestamp(0); // Create the earliest date of 1970-01-01
        $toDatetime = $toDatetime ?: Carbon::now(); // Current date

        $fromDatetimeStr = $this->makeBlock($fromDatetime);
        $toDatetimeStr = $this->makeBlock($toDatetime);

        $records = [];

        $store = Cache::get($this->resourceKey);

        if (is_null($store)) {
            return $records;
        }

        foreach ($store as $block => $count) {
            if ($block >= $fromDatetimeStr && $block <= $toDatetimeStr) {
                $records[] = [$block, $count];
            }
        }

        // Return
        return $records;
    }

    public function getCreditsUsed(Carbon $fromDatetime = null, Carbon $toDatetime = null)
    {
        $records = $this->getRecords($fromDatetime, $toDatetime);
        $counts = array_map(function ($record) {
            list($block, $count) = $record;
            return $count;
        }, $records);
        $total = array_sum($counts);
        return $total;
    }

    public function getRateLimits()
    {
        return $this->limits;
    }

    // Convert the provided datetime $now to a string
    public function makeBlock($now)
    {
        $now = $now ?: Carbon::now();
        $format = $this->blockFormat[$this->mode];
        return $now->format($format);
    }

    private function getLastRecord()
    {
        $store = Cache::get($this->resourceKey);
        if (empty($store)) { // in case of an empty array
            return [ null, null ];
        }

        $lastKey = array_key_last($store);
        $lastValue = $store[$lastKey];

        return [ $lastKey, $lastValue ];
    }

    // Example of $period: "24 hours", "1 week"
    // i.e. Clean up credit tracking logs that are older than "24 hours", "1 week"
    public function cleanup(string $period = null)
    {
        with_cache_lock($this->resourceKey, function () use ($period) {
            if (is_null($period)) {
                Cache::forget($this->resourceKey);
                return;
            }

            $fromDatetime = now()->subtract($period); // Current date
            $fromDatetimeStr = $this->makeBlock($fromDatetime);
            $store = Cache::get($this->resourceKey);

            if (is_null($store)) {
                return;
            }

            $newStore = [];
            foreach ($store as $block => $count) {
                if ($block >= $fromDatetimeStr) {
                    $newStore[$block] = $count;
                }
            }

            Cache::put($this->resourceKey, $newStore);
        });
    }

    public function getLimitsDescription()
    {
        $str = [];
        foreach ($this->getRateLimits() as $limit) {
            $str[] = $limit->getDescription();
        }

        return implode(', ', $str);
    }
}