Files
DatBrowser/app/Browser/Jobs/EldoradoRobuxPriceSentry/EldoradoRobuxPriceSentryJob.php
Matthias Guillitte 5e6164016c
All checks were successful
Test, build and push image to registry / phpunit-tests (push) Successful in 3m25s
Test, build and push image to registry / build-image (push) Successful in 2m59s
Eldorado : Added the minimum stock threshold
It now look at all of the offers of the first page and select the one matching the criterias
2025-12-20 16:20:07 +01:00

241 lines
8.7 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Browser\Jobs\EldoradoRobuxPriceSentry;
use App\Browser\BrowserJob;
use App\Models\Job;
use App\Models\JobArtifact;
use App\Models\JobRun;
use App\Notification\Providers\AllNotification;
use App\Browser\Jobs\EldoradoRobuxPriceSentry\EldoradoRobuxOffer;
use Facebook\WebDriver\WebDriverBy;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Laravel\Dusk\Browser;
class EldoradoRobuxPriceSentryJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
{
// === CONFIGURATION ===
public $timeout = 600; // 10 minutes
private const APPROXIMATIVE_RUNNING_MINUTES = 20;
private const LINK = "https://www.eldorado.gg/fr/buy-robux/g/70-0-0";
protected const int LOWEST_PRICE_INDEX = -1;
protected string $downloadFolder = "app/Browser/downloads/EldoradoRobuxPriceSentry/";
protected Collection $jobInfos;
protected JobRun $jobRun;
public function __construct($jobId = 5)
{
parent::__construct($jobId);
$this->downloadFolder = base_path($this->downloadFolder);
}
public function run(Browser $browser): ?JobRun
{
$startTime = microtime(true);
Log::info("Running EldoradoRobuxPriceSentryJob");
$this->jobInfos = Job::find($this->jobId)->jobInfosTable();
$this->jobRun = new JobRun([
"job_id" => $this->jobId,
"success" => false,
]);
$this->jobRun->save();
dump("visiting " . microtime(true) - $startTime);
$browser->visit(self::LINK);
sleep(5);
$this->sendPrices($browser);
$this->jobRun->success = true;
$this->jobRun->save();
Log::info("EldoradoRobuxPriceSentryJob run ended");
return $this->jobRun;
}
/**
* @inheritDoc
*/
public function runTest(Browser $browser): ?JobRun
{
$this->jobInfos = Job::find($this->jobId)->jobInfosTable();
try {
$browser->visit(self::LINK);
sleep(5);
return $this->makeSimpleJobRun(
true,
"Test réussi",
"Datboi a réussi à charger la page Eldorado."
);
} catch (\Exception $e) {
return $this->makeSimpleJobRun(
false,
"Test échoué",
"Datboi n'a pas réussi à charger la page Eldorado :\n" . $e->getMessage()
);
}
}
private function sendPrices(Browser $browser): void
{
$offers = $this->getOffers($browser);
$priceThreshold = $this->textToFloat($this->jobInfos->get("eldorado_robux_price_threshold"));
$stockThreshold = $this->jobInfos->get("eldorado_stock_quantity_threshold") == null ?
null :
$this->getIntFromText($this->jobInfos->get("eldorado_stock_quantity_threshold"));
foreach ($offers as $offer) {
Log::info("EldoradoRobuxPriceSentryJob: found offer - Price: " . $offer->robuxPrice . " €, Stock: " . $offer->robuxStock);
// Checks
if ($offer->robuxPrice > $priceThreshold || ($stockThreshold !== null && $offer->robuxStock < $stockThreshold)) {
continue; // Not matching the criteria
}
// Logging and artifact
Log::info("EldoradoRobuxPriceSentryJob: lowest price = $offer->robuxPrice €, threshold = $priceThreshold € - stock = $offer->robuxStock " . ($stockThreshold !== null ? "- stock threshold = $stockThreshold" : ""));
$this->jobRun->addArtifact(new JobArtifact([
"name" => "Trouvé le prix le plus bas",
"content" => "Prix le plus bas : $offer->robuxPrice €/Robux - Seuil défini : $priceThreshold €/Robux
Stock disponible : $offer->robuxStock " . ($stockThreshold !== null ? "- Seuil de stock défini : $stockThreshold" : ""),
]));
// get the link
$offerLink = $this->getLinkFromOfferIndex($browser, $offer->listingIndex);
// Send alert
$this->sendAlertNotification(
$offer->robuxPrice,
$priceThreshold,
$offer->robuxStock,
$stockThreshold,
$offerLink
);
Log::info("EldoradoRobuxPriceSentryJob: alert sent");
return; // Stops at first matching offer
}
Log::info("EldoradoRobuxPriceSentryJob: no alert sent");
}
private function sendAlertNotification(float $lowestPrice, float $priceThreshold, int $stock, int|null $stockThreshold, string $link): void
{
$options = [];
if ($this->jobInfos->get("eldorado_robux_price_discord_webhook") !== null) { // Custom discord webhook
$options["discord_webhook_url"] = $this->jobInfos->get("eldorado_robux_price_discord_webhook");
}
AllNotification::send(
new EldoradoRobuxPriceNotification(
$this->jobId,
$lowestPrice,
$priceThreshold,
$stock,
$stockThreshold,
$link
),
$options
);
$this->jobRun->addArtifact(new JobArtifact([
"name" => "Envoyé une alerte",
"content" => ""
]));
}
/**
* Retrieve the url for the given listing index.
* WILL NOT REDIRECT BACK TO THE OFFERS PAGE.
* @param Browser $browser
* @param int $listingIndex
* @return string
*/
private function getLinkFromOfferIndex(Browser $browser, int $listingIndex): string
{
if ($listingIndex == self::LOWEST_PRICE_INDEX) {
return self::LINK;
}
$offerCardElements = $this->getOffersCardElements($browser);
$offerCardElement = $offerCardElements[$listingIndex];
// Click the card element
$offerCardElement->click();
sleep(2);
// return the current url
return $browser->driver->getCurrentURL();
}
/**
* Get the list of offers on the page
* @return EldoradoRobuxOffer[]
*/
private function getOffers(Browser $browser): array
{
$offers = [];
// First offer (lowest price) (special case)
$firstOffer = $this->getFirstOffer($browser);
$offers[] = $firstOffer;
// Other offers
$offerCardElements = $this->getOffersCardElements($browser);
foreach ($offerCardElements as $index => $offerCardElement) {
$priceElement = $offerCardElement->findElement(WebDriverBy::xpath('.//eld-offer-price/strong'));
$priceText = $priceElement->getText(); // Ex: " 0,00520 € "
$price = $this->textToFloat($priceText);
// span.value next to a span.label with the text containing "stock"
$stockElement = $offerCardElement->findElement(WebDriverBy::xpath('.//span[@class="label" and contains(text(), "stock")]/following-sibling::*[@class="value"]'));
$stockText = $stockElement->getText(); // Ex: "En stock : 5000 unités"
$stock = $this->getIntFromText($stockText);
$offers[] = new EldoradoRobuxOffer(
robuxStock: $stock,
robuxPrice: $price,
listingIndex: $index
);
}
return $offers;
}
private function getFirstOffer(Browser $browser): EldoradoRobuxOffer
{
$lowestPriceElement = $browser->driver->findElement(WebDriverBy::xpath('(//eld-offer-price)[2]/strong'));
$lowestPriceText = $lowestPriceElement->getText(); // Ex: " 0,00478 € "
$lowestPrice = $this->textToFloat($lowestPriceText);
//$lowestPrice = $lowestPrice / 1000; // Price per Robux
// TODO : Look at the entire text to try to understand if it is per 1k or per single Robux
$lowestStockElement = $browser->driver->findElement(WebDriverBy::xpath('(//eld-quantity-details-wrapper)[1]//div[@class="quantity"]'));
$lowestStockText = $lowestStockElement->getText(); // Ex: "En stock : 9942199 unité"
$lowestStock = $this->getIntFromText($lowestStockText);
return new EldoradoRobuxOffer($lowestStock, $lowestPrice, self::LOWEST_PRICE_INDEX);
}
private function getOffersCardElements(Browser $browser): array
{
return $browser->driver->findElements(WebDriverBy::xpath('//div[contains(@class, "offer-seller-card")]'));
}
private function textToFloat(string $text): float
{
return floatval(str_replace(["", ","], ["", "."], trim($text)));
}
private function getIntFromText(string $text): int
{
$text = str_replace(["", " "], "", $text); // Remove spaces
preg_match('/\d+/', $text, $matches);
return intval($matches[0]);
}
}