Sort of working beta

This commit is contained in:
2025-02-06 17:30:45 +01:00
parent 5f42c707eb
commit 2ef114e154
97 changed files with 3093 additions and 106 deletions

272
app/Browser/BrowserJob.php Normal file
View File

@ -0,0 +1,272 @@
<?php
namespace App\Browser;
use App\Browser\JobArtifacts\JobRunArtifact;
use App\Exception\JobException;
use App\Models\JobArtifact;
use App\Models\JobRun;
use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\RemoteWebElement;
use Facebook\WebDriver\WebDriverBy;
use Illuminate\Support\Collection;
use Laravel\Dusk\Browser;
use Laravel\Dusk\TestCase as BaseTestCase;
use Closure;
use PHPUnit\Framework\Attributes\BeforeClass;
use Exception;
use Laravel\Dusk\Chrome\SupportsChrome;
use Laravel\Dusk\Concerns\ProvidesBrowser;
use Laravel\Dusk;
use Throwable;
abstract class BrowserJob
{
use SupportsChrome, ProvidesBrowser;
public int $jobId;
public function __construct(int $jobId)
{
$this->jobId = $jobId;
}
/**
* Execute the callback in a browser
* @param Closure $callback function with a Browser as parameter
* @return void
*/
private function executeInBrowser(Closure $callback): ?JobRun
{
$this->prepare();
$this->setUp();
$this->browse(function (Browser $browser) use ($callback, &$log) {
try {
$log = $callback($browser);
// } catch (Exception $e) {
// $browser->screenshot("failure-{$this->jobId}");
// dump($e);
// throw $e;
}
catch (Throwable $e) {
$browser->screenshot("failure-{$this->jobId}");
dump($e);
throw $e;
} finally {
$browser->quit();
return [];
}
});
return $log;
}
/**
* Execute the job
* @return void
*/
public function execute(): ?JobRun {
return $this->executeInBrowser(function (Browser $browser): ?JobRun {
return $this->run($browser);
});
}
/**
* Execute the job test
* @return void
*/
public function executeTest(): ?JobRun {
return $this->executeInBrowser(function (Browser $browser): ?JobRun {
return $this->runTest($browser);
});
}
/**
* Steps that run in the browser
* @param \Laravel\Dusk\Browser $browser
* @return void
*/
abstract public function run(Browser $browser): ?JobRun;
abstract public function runTest(Browser $browser): ?JobRun;
/**
* Prepare for Dusk test execution.
* @unused
*/
#[BeforeClass]
public static function prepare(): void
{
if (config("dusk.driver.url") == null && !(isset($_ENV['LARAVEL_SAIL']) && $_ENV['LARAVEL_SAIL'] == '1')) {
static::startChromeDriver(['--port=9515']);
}
}
/**
* Register the base URL with Dusk.
*
* @return void
*/
protected function setUp(): void
{
Browser::$baseUrl = "https://pdftools.matthiasg.dev/";
Browser::$storeScreenshotsAt = base_path('app/Browser/screenshots');
Browser::$storeConsoleLogAt = base_path('app/Browser/console');
Browser::$storeSourceAt = base_path('app/Browser/source');
/*Browser::$userResolver = function () {
return $this->user();
}; */
}
protected function makeSimpleJobRun(bool $success, string $name, string $content): JobRun {
$artifact = new JobRun([
"job_id" => $this->jobId,
"success" => $success
]);
$artifact->save();
$artifact->artifacts()->save(new JobArtifact([
"name" => $name,
"content" => $content,
]));
return $artifact;
}
/**
* Create the RemoteWebDriver instance.
*/
protected function driver(): RemoteWebDriver
{
$options = (new ChromeOptions)->addArguments(collect([
$this->shouldStartMaximized() ? '--start-maximized' : '--window-size=1360,1020',
'--disable-search-engine-choice-screen',
'--disable-gpu',
'--no-sandbox',
'--disable-setuid-sandbox',
'--whitelisted-ips=""',
'--disable-dev-shm-usage',
'--user-data-dir=/home/seluser/profile/',
])->all());
return RemoteWebDriver::create(
config("dusk.driver.url", 'http://localhost:9515'),
DesiredCapabilities::chrome()->setCapability(
ChromeOptions::CAPABILITY,
$options
)
);
}
public function terminate() {
$this->browse(function (Browser $browser) {
$browser->quit();
});
}
/**
* Determine whether the Dusk command has disabled headless mode.
*/
protected function hasHeadlessDisabled(): bool
{
return isset($_SERVER['DUSK_HEADLESS_DISABLED']) ||
isset($_ENV['DUSK_HEADLESS_DISABLED']);
}
/**
* Determine if the browser window should start maximized.
*/
protected function shouldStartMaximized(): bool
{
return isset($_SERVER['DUSK_START_MAXIMIZED']) ||
isset($_ENV['DUSK_START_MAXIMIZED']);
}
/**
* Register an "after class" tear down callback.
*
* @param \Closure $callback
* @return void
*/
public static function afterClass(Closure $callback)
{
static::$afterClassCallbacks[] = $callback;
}
public static function name()
{
return "test";
}
public static function dataName()
{
return "dataTest";
}
// BROWSER MACROS
protected function waitForAndClickText(Browser $browser, string $text, int $timeout = 30, bool $ignoreCase = true) {
$browser->waitForText($text, $timeout, $ignoreCase);
$this->findElementContainingText($browser, $text, $ignoreCase)?->click();
}
protected function waitForElementContainingTextAndGetIt(Browser $browser, string $text, int $timeout = 30, bool $ignoreCase = true): RemoteWebElement|null {
try {
$browser->waitForText($text, $timeout, $ignoreCase);
return $this->findElementContainingText($browser, $text, $ignoreCase);
} catch (Exception $e) {
return null;
}
}
protected function findElementContainingText(Browser $browser, string $text, bool $ignoreCase = true): RemoteWebElement|null {
try {
if ($ignoreCase) {
return $browser->driver->findElement(WebDriverBy::xpath("//*[{$this->xpathContainsIgnoreCase($text)}]"));
} else {
return $browser->driver->findElement(WebDriverBy::xpath("//*[contains(text(), \"{$text}\")]"));
}
} catch (Exception $e) {
return null;
}
}
private function xpathContainsIgnoreCase(string $needle, string $haystack = "text()") {
$needle = strtolower($needle);
return "contains(translate({$haystack}, \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\", \"abcdefghijklmnopqrstuvwxyz\"), \"{$needle}\")";
}
protected function waitForAndClickElementContainingText(Browser $browser, string $elementXpath, string $text, int $timeout = 30, bool $ignoreCase = true) {
$browser->waitForText($text, $timeout, $ignoreCase);
$this->findElementContainingElementWithText($browser, $elementXpath, $text, $ignoreCase)?->click();
}
protected function waitForElementContainingElementWithTextAndGetIt(Browser $browser, string $elementXpath, string $text, int $timeout = 30, bool $ignoreCase = true): RemoteWebElement|null {
try {
$browser->waitForText($text, $timeout, $ignoreCase);
sleep(2);
return $this->findElementContainingElementWithText($browser, $elementXpath, $text, $ignoreCase);
} catch (Exception $e) {
return null;
}
}
protected function findElementContainingElementWithText(Browser $browser, string $elementXpath, string $text, bool $ignoreCase = true): RemoteWebElement|null {
try {
if ($ignoreCase) {
dump("{$elementXpath}[.//*[{$this->xpathContainsIgnoreCase($text)}]]");
return $browser->driver->findElement(WebDriverBy::xpath("{$elementXpath}[.//*[{$this->xpathContainsIgnoreCase($text)}]]"));
} else {
return $browser->driver->findElement(WebDriverBy::xpath("{$elementXpath}[.//*[contains(text(), \"{$text}\")]]"));
}
} catch (Exception $e) {
return null;
}
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Browser\Components\Hellcase;
use Laravel\Dusk\Browser;
use Laravel\Dusk\Component as BaseComponent;
class MainNav extends BaseComponent
{
/**
* Get the root selector for the component.
*/
public function selector(): string
{
return 'header.header';
}
/**
* Assert that the browser page contains the component.
*/
public function assert(Browser $browser): void
{
$browser->assertVisible($this->selector());
}
/**
* Get the element shortcuts for the component.
*
* @return array<string, string>
*/
public function elements(): array
{
return [
'@logo' => 'a.header-logo',
'@daily-free-link' => 'a[href="/dailyfree"]',
];
}
public function goToHome(Browser $browser) {
$browser->scrollIntoView('@logo');
$browser->click('@logo');
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Browser\Jobs\Hellcase;
use Laravel\Dusk\Browser;
use function rtrim;
class HellcaseDailyFreeScreenshot {
public const IMG_FILE_NAME = "Hellcase-dailyFreeLoot";
/**
* QR code validity in seconds
* @var int
*/
public const QR_CODE_VALIDITY = 20;
public static function getImgFileAbsolutePath(): string {
return rtrim(Browser::$storeScreenshotsAt, '/') . "/Hellcase/" . static::IMG_FILE_NAME;
}
public static function getImgFileProjectPath(): string {
return app_path("Browser/screenshots/Hellcase/" . static::IMG_FILE_NAME);
}
public static function getImgFileExternalPath(): string {
return "screenshots/Hellcase/" . static::IMG_FILE_NAME;
}
}

View File

@ -0,0 +1,315 @@
<?php
namespace App\Browser\Jobs\Hellcase;
use App\Browser\BrowserJob;
use App\Browser\Components\Hellcase\MainNav;
use App\Models\JobArtifact;
use App\Browser\JobArtifacts\JobRunArtifact;
use App\Browser\Jobs\Hellcase\HellcaseLoginQrCode;
use App\Models\JobRun;
use App\Notification\NotificationBody\Hellcase\HellcaseNotificationDailyFreeBody;
use App\Notification\NotificationBody\Hellcase\HellcaseNotificationLoginBody;
use App\Notification\Notifications\Hellcase\HellcaseNotificationDailyFree;
use App\Notification\Notifications\Hellcase\HellcaseNotificationLogin;
use App\Notification\Providers\AllNotification;
use Dom\XPath;
use Facebook\WebDriver\WebDriverBy;
use Laravel\Dusk\Browser;
class HellcaseJob extends BrowserJob
{
private const STEAM_LOGIN_THRESHOLD = 5 * 60; // 5 minutes
private JobRun $jobRun;
public function __construct()
{
parent::__construct(2);
}
public function run(Browser $browser): ?JobRun
{
$this->jobRun = new JobRun([
"job_id" => $this->jobId,
"success" => false,
]);
$this->jobRun->save();
$browser->visit('https://hellcase.com');
sleep(5);
$this->removePopups($browser);
sleep(5);
$this->signin($browser);
$this->joinFreeGiveaways($browser);
$this->getDailyFree($browser);
$this->jobRun->success = true;
$this->jobRun->save();
return $this->jobRun;
}
/**
* @inheritDoc
*/
public function runTest(Browser $browser): ?JobRun
{
try {
$browser->visit('https://hellcase.com');
sleep(5);
$this->removePopups($browser);
$this->signin($browser);
return $this->makeSimpleJobRun(
true,
"Connexion réussie",
"Datboi a réussi à se connecter sur Hellcase"
);
} catch (\Exception $e) {
return $this->makeSimpleJobRun(
true,
"Connexion échouée",
"Datboi n'a pas réussi à se connecter sur Hellcase :\n" . $e->getMessage()
);
}
}
private function signin(Browser $browser)
{
try {
$browser->clickAtXPath('//button[.//span[text() = "Sign in"]]');
} catch (\Exception $e) {
return;
}
sleep(3);
$browser->clickAtXPath('//button[.//span[contains(text(), "Sign in through Steam")]]');
sleep(5);
// QR CODE SCANNING
try {
$browser->waitForTextIn("div", "Or sign in with QR", 30, true);
$qrCode = $browser->driver->findElement(WebDriverBy::xpath('//div[./*[contains(text(), "Or sign in with QR")]]'));
// Wait to be redirected to the Steam login page, while waiting take a new screenshot every 30 seconds
$isBackOnHellcase = false;
$secondsCounter = 0;
while (!$isBackOnHellcase && $secondsCounter < self::STEAM_LOGIN_THRESHOLD) {
// Take a screenshot of the QR code and send it
$qrCode->takeElementScreenshot(HellcaseLoginQrCode::getImgFileAbsolutePath());
AllNotification::send(
new HellcaseNotificationLogin(
$this->jobId,
new HellcaseNotificationLoginBody()
)
);
try {
$browser->waitForLocation("https://hellcase.com", HellcaseLoginQrCode::QR_CODE_VALIDITY); // The QR code is only valid for 20 seconds
} catch (\Exception $e) {
$secondsCounter += HellcaseLoginQrCode::QR_CODE_VALIDITY; // we've waited for QR_CODE_VALIDITY seconds
continue;
}
$isBackOnHellcase = true;
}
} catch (\Exception $e) {
// If the QR code is not found, we are not on the QR code page
$isBackOnHellcase = true;
} catch (\Throwable $e) {
// If the QR code is not found, we are not on the QR code page
$isBackOnHellcase = true;
}
if ($isBackOnHellcase) {
// Click a button tjat says "sign in"
$browser->waitForText("By signing into steam.loginhell.com through Steam", 30, true);
$browser->clickAtXPath('//input[@id = "imageLogin"]');
sleep(30);
}
}
private function joinFreeGiveaways(Browser $browser)
{
try {
$buttons = $browser->driver->findElements(WebDriverBy::xpath('//a[text() = "Join for free"]'));
} catch (\Exception $e) {
return;
}
if (sizeof($buttons) == 0) {
$this->jobRun->addArtifact(new JobArtifact([
"name" => "Pas de concours joignable",
"content" => ""
]));
}
foreach ($buttons as $button) {
$button->click();
sleep(5);
$this->joinGiveaway($browser);
$browser->within(new MainNav, function (Browser $browser) {
$browser->goToHome();
});
}
}
private function joinGiveaway(Browser $browser)
{
$joinButton = $browser->driver->findElement(WebDriverBy::xpath('//button[span[contains(text(), "Join for free")]]'));
$joinButton->click();
}
private function getDailyFree(Browser $browser)
{
$browser->visit('https://hellcase.com/dailyfree');
$browser->waitForText("Get Daily free loot", 30, true);
// Do we fill the conditions ?
if (sizeof(value: $browser->driver->findElements(WebDriverBy::xpath('//p[contains(text(), "Fulfill the conditions below")]'))) > 0) {
$this->fillDailyFreeConditions($browser);
}
// If we see "availible in 20 HR 49 MIN", parse the hours and minute and reschedule
$availibleInButton = $this->waitForElementContainingTextAndGetIt($browser, "available", 30);
if ($availibleInButton != null) {
$hours = $availibleInButton->getText();
$hours = explode(" ", $hours);
$minutes = $hours[2];
$hours = $hours[0];
// $this->reschedule($hours);
$this->jobRun->addArtifact(new JobArtifact([
"name" => "Cadeau gratuit pas encore disponible",
"content" => "Le cadeau gratuit journalier sera disponible dans $hours heures et $minutes minutes.\nDatboi se fera un plaisir d'aller le chercher pour vous."
]));
return;
}
$this->waitForAndClickText($browser, "Get Free Bonus", 30, true);
$lootElement = $browser->driver->findElement(WebDriverBy::xpath('//div[contains(@class, "daily-free-win-bonus")]'));
$lootElement->takeElementScreenshot(HellcaseDailyFreeScreenshot::getImgFileAbsolutePath());
AllNotification::send(
new HellcaseNotificationDailyFree($this->jobId, new HellcaseNotificationDailyFreeBody())
);
sleep(5000);
}
/**
* Must be on the dailyfree page
* @param \Laravel\Dusk\Browser $browser
* @throws \Exception
* @return void
*/
private function fillDailyFreeConditions(Browser $browser) {
// 1. See what conditions we need to fullfill
$conditions = [];
$conditionsDivs = $browser->driver->findElements(WebDriverBy::xpath('//*[@class = "daily-free-requirement__heading-left"]'));
for($i = 0; $i < sizeof($conditionsDivs); $i++) {
$conditionDiv = $conditionsDivs[$i];
// See if the element has the completed class
$conditions[$i] = [
"isFilled" => str_contains($conditionDiv->getAttribute("class"), "completed"),
"text" => $conditionDiv->getText()
];
}
if (sizeof($conditions) == 0) {
throw new \Exception("No dailyfree conditions found");
}
if (!$conditions[0]["isFilled"]) {
$this->changeSteamProfilePicture($browser);
}
if (!$conditions[1]["isFilled"]) {
$this->changeSteamProfileToPublic($browser);
}
}
/**
*
* Must be on the dailyfree page
* @param \Laravel\Dusk\Browser $browser
* @return void
*/
private function changeSteamProfilePicture(Browser $browser) {
// Get all of the availible image link
$images = $browser->driver->findElements(WebDriverBy::xpath('//a[@class = "daily-free-user-requirement-avatar-item"]'));
// Download the image from the second link in a special folder
$imageLink = $images[1]->getAttribute("href");
// Download the image in app/Browser/downloads/
$imagePath = base_path("app/Browser/downloads/Hellcase/pp.jpg");
file_put_contents($imagePath, file_get_contents($imageLink));
$this->goToSteamProfileSettings($browser);
// Wait for and click "Avatar"
$this->waitForAndClickText($browser, "Avatar");
// Wait for and click "Upload your avatar"
$browser->waitForText("Upload your avatar", 30, true);
// $browser->clickAtXPath('//*[contains(text(), "Upload your avatar")]');
// Upload the downloaded image
$browser->attach('input[type="file"]', $imagePath);
// Wait for and click save
$this->waitForAndClickText($browser, "Save");
// Go back to dailyfree
$browser->visit('https://hellcase.com/dailyfree');
sleep(10);
try {
// wait and click "Check the condition"
$this->waitForAndClickText($browser, "Check the condition");
$browser->waitForText("Your Steam profile avatar does not match any of the ones specified in the", 30, true);
} catch (\Exception $e) {
// If the text is not found, the condition is filled
return;
}
}
private function changeSteamProfileToPublic(Browser $browser) {
$this->goToSteamProfileSettings($browser);
// Wait for and click "Privacy Settings"
$this->waitForAndClickText($browser, "Privacy Settings");
$dropdownButton = $browser->driver->findElement(WebDriverBy::xpath('//div[text() = "My profile"]/div'));
$dropdownButton->click();
sleep(2);
// Div that contains the visible class ath the body root an element with public text
$publicOption = $browser->driver->findElement(WebDriverBy::xpath('/body/div[contains(@class, "visible")]/div[contains(text(), "Public")]'));
$publicOption->click();
// Go back to dailyfree
$browser->visit('https://hellcase.com/dailyfree');
}
/**
* From the dailyfree page
* @param \Laravel\Dusk\Browser $browser
* @return void
*/
private function goToSteamProfileSettings(Browser $browser) {
// Get the link that has text "Steam profile"
$steamProfileLink = $browser->driver->findElement(WebDriverBy::xpath('//a[contains(text(), "Steam profile")]'));
$browser->visit($steamProfileLink->getAttribute("href"));
$browser->waitForText("Level");
// Click "Edit Profile
$browser->clickAtXPath('//*[contains(text(), "Edit Profile")]');
}
private function removePopups(Browser $browser)
{
// $browser->script('document.querySelector("div.app-modal")[0].remove();');
// $browser->driver->executeScript('document.querySelector("div.app-modal")[0].remove();');
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Browser\Jobs\Hellcase;
use Laravel\Dusk\Browser;
use function rtrim;
class HellcaseLoginQrCode {
public const IMG_FILE_NAME = "SteamQRCode.png";
/**
* QR code validity in seconds
* @var int
*/
public const QR_CODE_VALIDITY = 20;
public static function getImgFileAbsolutePath(): string {
return rtrim(Browser::$storeScreenshotsAt, '/') . "/Hellcase/" . static::IMG_FILE_NAME;
}
public static function getImgFileProjectPath(): string {
return app_path("Browser/screenshots/Hellcase/" . static::IMG_FILE_NAME);
}
public static function getImgFileExternalPath(): string {
return "screenshots/Hellcase/" . static::IMG_FILE_NAME;
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Browser\Jobs\Parameters;
use App\Browser\BrowserJob;
use App\Models\JobArtifact;
use App\Models\JobRun;
use App\Notification\Notifications\SimpleNotification;
use App\Notification\Providers\AllNotification;
use Laravel\Dusk\Browser;
class ParametersJob extends BrowserJob
{
public function __construct()
{
parent::__construct(1);
}
/**
* @inheritDoc
*/
public function run(Browser $browser): ?JobRun
{
return null;
}
/**
* @inheritDoc
*/
public function runTest(Browser $browser): ?JobRun
{
try {
AllNotification::send(new SimpleNotification($this->jobId, "Test", "Test des notifications"));
AllNotification::send(new SimpleNotification($this->jobId, "Test", "Test des notifications d'erreur", true));
return $this->makeSimpleJobRun(true, "Envoi de notification réussi", "Datboi a réussi à envoyer des notifications");
} catch (\Throwable $e) {
return $this->makeSimpleJobRun(false, "Envoi de notification échoué", "Datboi n'a pas réussi à envoyer des notifications :\n" . $e->getMessage());
}
}
}

2
app/Browser/downloads/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore