It now look at all of the offers of the first page and select the one matching the criterias
241 lines
8.7 KiB
PHP
241 lines
8.7 KiB
PHP
<?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]);
|
||
}
|
||
}
|