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
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Browser\Jobs\EldoradoRobuxPriceSentry;
|
||||||
|
|
||||||
|
class EldoradoRobuxOffer
|
||||||
|
{
|
||||||
|
public int $robuxStock;
|
||||||
|
public float $robuxPrice;
|
||||||
|
|
||||||
|
public int $listingIndex;
|
||||||
|
|
||||||
|
public function __construct(int $robuxStock, float $robuxPrice, int $listingIndex)
|
||||||
|
{
|
||||||
|
$this->robuxStock = $robuxStock;
|
||||||
|
$this->robuxPrice = $robuxPrice;
|
||||||
|
$this->listingIndex = $listingIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
@@ -10,21 +10,25 @@ use App\Notification\Stringifiable\StringifiableSimpleText;
|
|||||||
class EldoradoRobuxPriceNotification extends Notification {
|
class EldoradoRobuxPriceNotification extends Notification {
|
||||||
|
|
||||||
private float $price;
|
private float $price;
|
||||||
private float $threshold;
|
private float $priceThreshold;
|
||||||
private string $link;
|
private string $link;
|
||||||
|
private int $stock;
|
||||||
|
private int|null $stockThreshold;
|
||||||
|
|
||||||
public function __construct(int $jobId, float $price, float $threshold, string $link) {
|
public function __construct(int $jobId, float $price, float $priceThreshold, int $stock, int|null $stockThreshold, string $link) {
|
||||||
parent::__construct($jobId);
|
parent::__construct($jobId);
|
||||||
|
|
||||||
$this->price = $price;
|
$this->price = $price;
|
||||||
$this->threshold = $threshold;
|
$this->priceThreshold = $priceThreshold;
|
||||||
|
$this->stock = $stock;
|
||||||
|
$this->stockThreshold = $stockThreshold;
|
||||||
$this->link = $link;
|
$this->link = $link;
|
||||||
|
|
||||||
$this->setBody($this->generateBody());
|
$this->setBody($this->generateBody());
|
||||||
}
|
}
|
||||||
|
|
||||||
private function generateBody() {
|
private function generateBody() {
|
||||||
return new EldoradoRobuxPriceNotificationBody($this->price, $this->threshold, $this->link);
|
return new EldoradoRobuxPriceNotificationBody($this->price, $this->priceThreshold, $this->stock, $this->stockThreshold, $this->link);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getTitle(): Stringifiable {
|
public function getTitle(): Stringifiable {
|
||||||
|
|||||||
@@ -7,14 +7,18 @@ use App\Notification\NotificationBody;
|
|||||||
class EldoradoRobuxPriceNotificationBody extends NotificationBody {
|
class EldoradoRobuxPriceNotificationBody extends NotificationBody {
|
||||||
|
|
||||||
private float $price;
|
private float $price;
|
||||||
private float $threshold;
|
private float $priceThreshold;
|
||||||
|
private int $stock;
|
||||||
|
private int|null $stockThreshold;
|
||||||
private string $link;
|
private string $link;
|
||||||
|
|
||||||
public function __construct(float $price, float $threshold, string $link) {
|
public function __construct(float $price, float $priceThreshold, int $stock, int|null $stockThreshold, string $link) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
|
||||||
$this->price = $price;
|
$this->price = $price;
|
||||||
$this->threshold = $threshold;
|
$this->priceThreshold = $priceThreshold;
|
||||||
|
$this->stock = $stock;
|
||||||
|
$this->stockThreshold = $stockThreshold;
|
||||||
$this->link = $link;
|
$this->link = $link;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,13 +26,21 @@ class EldoradoRobuxPriceNotificationBody extends NotificationBody {
|
|||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
public function toMarkdownString(): string {
|
public function toMarkdownString(): string {
|
||||||
return "Le prix des Robux sur Eldorado est actuellement de **" . number_format($this->price, 5, ",", " ") . " €**/Robux, ce qui est inférieur ou égal au seuil de **" . number_format($this->threshold, 5, ",", " ") . " €**.\n\n[Voir l'offre sur Eldorado]( " . $this->link . " )";
|
return "Le prix des Robux sur Eldorado est actuellement de **" . $this->floatFormat($this->price) . " €**/Robux, ce qui est inférieur ou égal au seuil de **" . $this->floatFormat($this->priceThreshold) . " €**.\nLe stock est de **" . $this->intFormat($this->stock) . "**" . ($this->stockThreshold !== null ? " (seuil : **" . $this->intFormat($this->stockThreshold) . "**)" : "") . "\n[Voir l'offre sur Eldorado]( " . $this->link . " )";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
public function toString(): string {
|
public function toString(): string {
|
||||||
return "Le prix des Robux sur Eldorado est actuellement de " . number_format($this->price, 5, ",", " ") . " €/Robux, ce qui est inférieur ou égal au seuil de " . number_format($this->threshold, 5, ",", " ") . " €.\n\nVoir l'offre sur Eldorado : " . $this->link;
|
return "Le prix des Robux sur Eldorado est actuellement de " . $this->floatFormat($this->price) . " €/Robux, ce qui est inférieur ou égal au seuil de " . $this->floatFormat($this->priceThreshold) . " €.\nLe stock est de " . $this->intFormat($this->stock) . ($this->stockThreshold !== null ? " (seuil : " . $this->intFormat($this->stockThreshold) . ")" : "") . "\nVoir l'offre sur Eldorado : " . $this->link;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function floatFormat(float $number): string {
|
||||||
|
return number_format($number, 5, ",", " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function intFormat(int $number): string {
|
||||||
|
return number_format($number, 0, ",", " ");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,23 +3,16 @@
|
|||||||
namespace App\Browser\Jobs\EldoradoRobuxPriceSentry;
|
namespace App\Browser\Jobs\EldoradoRobuxPriceSentry;
|
||||||
|
|
||||||
use App\Browser\BrowserJob;
|
use App\Browser\BrowserJob;
|
||||||
use App\Browser\JobDebugScreenshot;
|
|
||||||
use App\Browser\Jobs\InstagramRepost\DescriptionPipeline\InstagramDescriptionPipeline;
|
|
||||||
use App\Models\InstagramNotification;
|
|
||||||
use App\Models\InstagramRepost;
|
|
||||||
use App\Models\Job;
|
use App\Models\Job;
|
||||||
use App\Models\JobArtifact;
|
use App\Models\JobArtifact;
|
||||||
use App\Models\JobRun;
|
use App\Models\JobRun;
|
||||||
use App\Notification\Notifications\JobDebugNotification;
|
|
||||||
use App\Notification\Notifications\SimpleNotification;
|
|
||||||
use App\Notification\Providers\AllNotification;
|
use App\Notification\Providers\AllNotification;
|
||||||
use App\Services\Instagram\NotificationTypeDetector;
|
use App\Browser\Jobs\EldoradoRobuxPriceSentry\EldoradoRobuxOffer;
|
||||||
use Facebook\WebDriver\WebDriverBy;
|
use Facebook\WebDriver\WebDriverBy;
|
||||||
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
|
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Laravel\Dusk\Browser;
|
use Laravel\Dusk\Browser;
|
||||||
use App\Services\AIPrompt\OpenAPIPrompt;
|
|
||||||
|
|
||||||
class EldoradoRobuxPriceSentryJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
|
class EldoradoRobuxPriceSentryJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
|
||||||
{
|
{
|
||||||
@@ -28,8 +21,10 @@ class EldoradoRobuxPriceSentryJob extends BrowserJob implements ShouldBeUniqueUn
|
|||||||
public $timeout = 600; // 10 minutes
|
public $timeout = 600; // 10 minutes
|
||||||
private const APPROXIMATIVE_RUNNING_MINUTES = 20;
|
private const APPROXIMATIVE_RUNNING_MINUTES = 20;
|
||||||
private const LINK = "https://www.eldorado.gg/fr/buy-robux/g/70-0-0";
|
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 string $downloadFolder = "app/Browser/downloads/EldoradoRobuxPriceSentry/";
|
||||||
|
|
||||||
|
|
||||||
protected Collection $jobInfos;
|
protected Collection $jobInfos;
|
||||||
protected JobRun $jobRun;
|
protected JobRun $jobRun;
|
||||||
|
|
||||||
@@ -91,21 +86,47 @@ class EldoradoRobuxPriceSentryJob extends BrowserJob implements ShouldBeUniqueUn
|
|||||||
|
|
||||||
private function sendPrices(Browser $browser): void
|
private function sendPrices(Browser $browser): void
|
||||||
{
|
{
|
||||||
$lowestPriceElement = $browser->driver->findElement(WebDriverBy::xpath('(//eld-offer-price)[2]/strong'));
|
$offers = $this->getOffers($browser);
|
||||||
$lowestPriceText = $lowestPriceElement->getText(); // Ex: " 0,00478 € "
|
$priceThreshold = $this->textToFloat($this->jobInfos->get("eldorado_robux_price_threshold"));
|
||||||
$lowestPrice = $this->textToFloat($lowestPriceText);
|
$stockThreshold = $this->jobInfos->get("eldorado_stock_quantity_threshold") == null ?
|
||||||
//$lowestPrice = $lowestPrice / 1000; // Price per Robux
|
null :
|
||||||
// TODO : Look at the entire text to try to understand if it is per 1k or per single Robux
|
$this->getIntFromText($this->jobInfos->get("eldorado_stock_quantity_threshold"));
|
||||||
|
|
||||||
$threshold = $this->textToFloat($this->jobInfos->get("eldorado_robux_price_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
|
||||||
|
}
|
||||||
|
|
||||||
Log::info("EldoradoRobuxPriceSentryJob: lowest price = $lowestPrice €, threshold = $threshold €");
|
// 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([
|
$this->jobRun->addArtifact(new JobArtifact([
|
||||||
"name" => "Trouvé le prix le plus bas",
|
"name" => "Trouvé le prix le plus bas",
|
||||||
"content" => "Prix le plus bas : $lowestPrice €/Robux - Seuil défini : $threshold €/Robux"
|
"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" : ""),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
if ($lowestPrice <= $threshold) {
|
|
||||||
|
// 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 = [];
|
$options = [];
|
||||||
if ($this->jobInfos->get("eldorado_robux_price_discord_webhook") !== null) { // Custom discord webhook
|
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");
|
$options["discord_webhook_url"] = $this->jobInfos->get("eldorado_robux_price_discord_webhook");
|
||||||
@@ -115,8 +136,10 @@ class EldoradoRobuxPriceSentryJob extends BrowserJob implements ShouldBeUniqueUn
|
|||||||
new EldoradoRobuxPriceNotification(
|
new EldoradoRobuxPriceNotification(
|
||||||
$this->jobId,
|
$this->jobId,
|
||||||
$lowestPrice,
|
$lowestPrice,
|
||||||
$threshold,
|
$priceThreshold,
|
||||||
self::LINK
|
$stock,
|
||||||
|
$stockThreshold,
|
||||||
|
$link
|
||||||
),
|
),
|
||||||
$options
|
$options
|
||||||
);
|
);
|
||||||
@@ -125,15 +148,93 @@ class EldoradoRobuxPriceSentryJob extends BrowserJob implements ShouldBeUniqueUn
|
|||||||
"name" => "Envoyé une alerte",
|
"name" => "Envoyé une alerte",
|
||||||
"content" => ""
|
"content" => ""
|
||||||
]));
|
]));
|
||||||
|
|
||||||
Log::info("EldoradoRobuxPriceSentryJob: alert sent");
|
|
||||||
} else {
|
|
||||||
Log::info("EldoradoRobuxPriceSentryJob: no alert sent");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
private function textToFloat(string $text): float
|
||||||
{
|
{
|
||||||
return floatval(str_replace(["€", ","], ["", "."], trim($text)));
|
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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Job;
|
||||||
|
use App\Models\JobInfo;
|
||||||
|
use App\Models\JobInfoType;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$eldoradoJob = Job::where("name", "Eldorado Robux Price Sentry")->first();
|
||||||
|
|
||||||
|
JobInfo::forceCreate([
|
||||||
|
"key" => "eldorado_stock_quantity_threshold",
|
||||||
|
"name" => "Seuil de quantité de robux en stock",
|
||||||
|
"description" => "Le seuil de quantité de robux en stock pour déclencher une alerte.",
|
||||||
|
"placeholder" => "10000",
|
||||||
|
"is_required" => false,
|
||||||
|
"job_info_type_id" => JobInfoType::where("name", "number")->first()->id,
|
||||||
|
"job_id" => $eldoradoJob->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
JobInfo::where("key", "eldorado_stock_quantity_threshold")->delete();
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user