Sort of working beta
This commit is contained in:
69
.dockerignore
Normal file
69
.dockerignore
Normal file
@ -0,0 +1,69 @@
|
||||
# Include any files or directories that you don't want to be copied to your
|
||||
# container here (e.g., local build artifacts, temporary files, etc.).
|
||||
#
|
||||
# For more help, visit the .dockerignore file reference guide at
|
||||
# https://docs.docker.com/go/build-context-dockerignore/
|
||||
|
||||
# Database
|
||||
database/database.sqlite
|
||||
|
||||
**/.DS_Store
|
||||
**/__pycache__
|
||||
**/.venv
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
# **/compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
/.phpunit.cache
|
||||
/node_modules
|
||||
|
||||
# Is built later on
|
||||
/public/build
|
||||
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/vendor
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.env.example
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
/auth.json
|
||||
/.fleet
|
||||
/.idea
|
||||
/.nova
|
||||
/.vscode
|
||||
/.zed
|
||||
|
||||
# Browser
|
||||
app/Browser/console/**
|
||||
app/Browser/screenshots/**
|
||||
app/Browser/source/**
|
69
.env.docker
Normal file
69
.env.docker
Normal file
@ -0,0 +1,69 @@
|
||||
APP_NAME=DatBrowser
|
||||
APP_ENV=production
|
||||
APP_KEY=base64:O3G9GpqYI7ZkAvkffIiS+5oodFGe/yAQ/uIcSIqU3NU=
|
||||
APP_DEBUG=true
|
||||
APP_TIMEZONE=UTC
|
||||
APP_URL=http://127.0.0.1
|
||||
|
||||
APP_LOCALE=fr
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
# APP_MAINTENANCE_STORE=database
|
||||
|
||||
PHP_CLI_SERVER_WORKERS=4
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=db
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=datbrowser
|
||||
DB_USERNAME=datboi
|
||||
DB_PASSWORD=IMDATBOI
|
||||
|
||||
SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
# REDIS_CLIENT=phpredis
|
||||
# REDIS_HOST=127.0.0.1
|
||||
# REDIS_PASSWORD=null
|
||||
# REDIS_PORT=6379
|
||||
|
||||
# MAIL_MAILER=log
|
||||
# MAIL_SCHEME=null
|
||||
# MAIL_HOST=127.0.0.1
|
||||
# MAIL_PORT=2525
|
||||
# MAIL_USERNAME=null
|
||||
# MAIL_PASSWORD=null
|
||||
# MAIL_FROM_ADDRESS="hello@example.com"
|
||||
# MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
# AWS_ACCESS_KEY_ID=
|
||||
# AWS_SECRET_ACCESS_KEY=
|
||||
# AWS_DEFAULT_REGION=us-east-1
|
||||
# AWS_BUCKET=
|
||||
# AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
VITE_APP_URL="${APP_URL}"
|
||||
|
||||
DUSK_DRIVER_URL="http://undetected-chromedriver:4444"
|
40
.env.example
40
.env.example
@ -3,9 +3,9 @@ APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_TIMEZONE=UTC
|
||||
APP_URL=http://localhost:8000
|
||||
APP_URL=http://127.0.0.1:8000
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_LOCALE=fr
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
@ -43,24 +43,26 @@ CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
# REDIS_CLIENT=phpredis
|
||||
# REDIS_HOST=127.0.0.1
|
||||
# REDIS_PASSWORD=null
|
||||
# REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_SCHEME=null
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
# MAIL_MAILER=log
|
||||
# MAIL_SCHEME=null
|
||||
# MAIL_HOST=127.0.0.1
|
||||
# MAIL_PORT=2525
|
||||
# MAIL_USERNAME=null
|
||||
# MAIL_PASSWORD=null
|
||||
# MAIL_FROM_ADDRESS="hello@example.com"
|
||||
# MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
# AWS_ACCESS_KEY_ID=
|
||||
# AWS_SECRET_ACCESS_KEY=
|
||||
# AWS_DEFAULT_REGION=us-east-1
|
||||
# AWS_BUCKET=
|
||||
# AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
DUSK_DRIVER_URL="http://127.0.0.1:4444"
|
||||
|
23
.gitea/workflows/pushImage.yaml
Normal file
23
.gitea/workflows/pushImage.yaml
Normal file
@ -0,0 +1,23 @@
|
||||
name: Push Image to registry
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
|
||||
jobs:
|
||||
build-image:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${REGISTRY}
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
- name: Build and push
|
||||
run: |
|
||||
docker build -t ${REGISTRY}/${REPO_OWNER}/${IMAGE_NAME}:latest .
|
||||
docker push ${REGISTRY}/${REPO_OWNER}/${IMAGE_NAME}:latest
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -21,3 +21,7 @@ yarn-error.log
|
||||
/.nova
|
||||
/.vscode
|
||||
/.zed
|
||||
# Browser
|
||||
app/Browser/console
|
||||
app/Browser/screenshots
|
||||
app/Browser/source
|
||||
|
86
Dockerfile
Normal file
86
Dockerfile
Normal file
@ -0,0 +1,86 @@
|
||||
|
||||
# INSTALL PHP COMPOSER DEPENDENCIES
|
||||
FROM composer:lts AS composer-deps
|
||||
|
||||
WORKDIR /
|
||||
|
||||
# If your composer.json file defines scripts that run during dependency installation and
|
||||
# reference your application source files, uncomment the line below to copy all the files
|
||||
# into this layer.
|
||||
# COPY composer.json composer.lock ./
|
||||
COPY . .
|
||||
|
||||
# Download dependencies as a separate step to take advantage of Docker's caching.
|
||||
# Leverage a bind mounts to composer.json and composer.lock to avoid having to copy them
|
||||
# into this layer.
|
||||
# Leverage a cache mount to /tmp/cache so that subsequent builds don't have to re-download packages.
|
||||
RUN --mount=type=bind,source=composer.json,target=composer.json \
|
||||
--mount=type=bind,source=composer.lock,target=composer.lock \
|
||||
--mount=type=cache,target=/tmp/cache \
|
||||
composer install --no-interaction --prefer-dist
|
||||
|
||||
# ========================================
|
||||
|
||||
# BUILD VUE APP
|
||||
FROM node:20 AS build-vue
|
||||
|
||||
WORKDIR /usr/app
|
||||
|
||||
# COPY package.json tsconfig.json tailwind.config.js vite.config.js ./
|
||||
# COPY resources/ ./resources/
|
||||
COPY . .
|
||||
RUN mv .env.docker .env
|
||||
COPY --from=composer-deps /vendor/ ./vendor
|
||||
RUN mkdir -p public/build/ && npm i && npm run build
|
||||
|
||||
# ========================================
|
||||
|
||||
# RUN
|
||||
FROM php:8.2-alpine AS final
|
||||
|
||||
# Install system dependencies
|
||||
RUN apk update && apk add --no-cache \
|
||||
git \
|
||||
curl \
|
||||
libpng-dev \
|
||||
libxml2-dev \
|
||||
libzip-dev \
|
||||
zip \
|
||||
unzip \
|
||||
supervisor \
|
||||
nginx \
|
||||
openssl \
|
||||
linux-headers
|
||||
|
||||
RUN docker-php-ext-configure zip && docker-php-ext-install zip
|
||||
RUN docker-php-ext-install gd pdo pdo_mysql zip
|
||||
|
||||
|
||||
# Get latest Composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
WORKDIR /var/www
|
||||
|
||||
COPY . .
|
||||
COPY --from=build-vue /usr/app/public/build ./public/build
|
||||
RUN mv .env.docker .env
|
||||
|
||||
# DUSK
|
||||
# RUN php artisan dusk:install && php artisan dusk:chrome-driver && mv ./undetectedChromedriver/chromedriver-linux ./vendor/laravel/dusk/bin/chromedriver-linux
|
||||
# RUN php artisan dusk:install && php artisan dusk:chrome-driver
|
||||
# RUN php artisan dusk:install
|
||||
|
||||
RUN composer install --no-interaction --prefer-dist
|
||||
|
||||
# Link the storage directory to the public directory.
|
||||
RUN php artisan storage:link
|
||||
|
||||
# Laravel optimization commands
|
||||
# RUN php artisan cache:clear
|
||||
RUN php artisan config:cache && php artisan route:cache
|
||||
|
||||
RUN chmod +x ./dockerEntryPoint.sh
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["./dockerEntryPoint.sh"]
|
272
app/Browser/BrowserJob.php
Normal file
272
app/Browser/BrowserJob.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
43
app/Browser/Components/Hellcase/MainNav.php
Normal file
43
app/Browser/Components/Hellcase/MainNav.php
Normal 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');
|
||||
}
|
||||
}
|
29
app/Browser/Jobs/Hellcase/HellcaseDailyFreeScreenshot.php
Normal file
29
app/Browser/Jobs/Hellcase/HellcaseDailyFreeScreenshot.php
Normal 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;
|
||||
}
|
||||
}
|
315
app/Browser/Jobs/Hellcase/HellcaseJob.php
Normal file
315
app/Browser/Jobs/Hellcase/HellcaseJob.php
Normal 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();');
|
||||
}
|
||||
}
|
29
app/Browser/Jobs/Hellcase/HellcaseLoginQrCode.php
Normal file
29
app/Browser/Jobs/Hellcase/HellcaseLoginQrCode.php
Normal 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;
|
||||
}
|
||||
}
|
41
app/Browser/Jobs/Parameters/ParametersJob.php
Normal file
41
app/Browser/Jobs/Parameters/ParametersJob.php
Normal 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
2
app/Browser/downloads/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
69
app/Http/Controllers/JobController.php
Normal file
69
app/Http/Controllers/JobController.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Exception\JobException;
|
||||
use App\Models\Job;
|
||||
use App\Services\BrowserJobsInstances;
|
||||
use Cache;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use App\Exception\JobTestException;
|
||||
|
||||
class JobController extends Controller
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
protected BrowserJobsInstances $jobInstances,
|
||||
) {
|
||||
}
|
||||
|
||||
public function show($jobId, Request $request)
|
||||
{
|
||||
return Inertia::render('Job', [
|
||||
'job' => Job::where('id', $jobId)->with('jobInfos')->first(),
|
||||
'error' => $request->input('error'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update($jobId, Request $request)
|
||||
{
|
||||
$job = Job::where('id', $jobId)->first();
|
||||
$job->is_active = false; // Disable the job
|
||||
|
||||
$errors = [];
|
||||
|
||||
foreach ($job->jobInfos()->get() as $jobInfo) {
|
||||
$value = $request->input($jobInfo->id);
|
||||
if (!isset($value) && $jobInfo->is_required) {
|
||||
$errors[] = 'Le champ ' . $jobInfo->name . ' est requis.';
|
||||
continue;
|
||||
}
|
||||
|
||||
Cache::forget($jobInfo->key);
|
||||
|
||||
$jobInfo->value = $value;
|
||||
$jobInfo->save();
|
||||
}
|
||||
|
||||
$job->save();
|
||||
|
||||
if (count($errors) > 0) {
|
||||
return redirect()->route('jobs.show', ['job' => $job, 'error' => implode('<br />', $errors)]);
|
||||
}
|
||||
|
||||
$job->is_active = $request->input('is_active');
|
||||
$job->save();
|
||||
|
||||
return redirect()->route('jobs.show', ['job' => $job]);
|
||||
}
|
||||
|
||||
public function test($jobId, Request $request)
|
||||
{
|
||||
$log = $this->jobInstances->getJobInstance($jobId)->executeTest();
|
||||
if (!empty($log)) {
|
||||
return response()->json(['artifact' => $log->load('artifacts')]);
|
||||
}
|
||||
return response()->json([]);
|
||||
}
|
||||
}
|
@ -7,6 +7,22 @@ use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Job extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\JobFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
"is_active",
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
"is_active" => "boolean",
|
||||
];
|
||||
|
||||
public function jobInfos()
|
||||
{
|
||||
return $this->hasMany(JobInfo::class)->with("jobInfoType")->orderBy("created_at");
|
||||
}
|
||||
|
||||
public function jobRuns()
|
||||
{
|
||||
return $this->hasMany(JobRun::class)->orderBy("created_at");
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,17 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class JobArtifact extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\JobArtifactFactory> */
|
||||
use HasFactory;
|
||||
protected $fillable = [
|
||||
"name",
|
||||
"content",
|
||||
];
|
||||
|
||||
public function jobRun(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(JobRun::class);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,19 @@ use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class JobInfo extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\JobInfoFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
"value",
|
||||
];
|
||||
|
||||
public function job()
|
||||
{
|
||||
return $this->belongsTo(Job::class);
|
||||
}
|
||||
|
||||
public function jobInfoType()
|
||||
{
|
||||
return $this->belongsTo(JobInfoType::class)->select("id", "name");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,6 +7,12 @@ use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class JobInfoType extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\JobInfoTypeFactory> */
|
||||
use HasFactory;
|
||||
protected $fillable = [
|
||||
"name",
|
||||
];
|
||||
|
||||
public function jobInfos()
|
||||
{
|
||||
return $this->hasMany(JobInfo::class);
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,34 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class JobRun extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\JobRunFactory> */
|
||||
use HasFactory;
|
||||
protected $fillable = [
|
||||
"job_id",
|
||||
"success",
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
"success" => "boolean",
|
||||
];
|
||||
|
||||
protected $with = ['artifacts'];
|
||||
|
||||
public function job(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Job::class);
|
||||
}
|
||||
|
||||
public function artifacts(): HasMany
|
||||
{
|
||||
return $this->hasMany(JobArtifact::class);
|
||||
}
|
||||
|
||||
public function addArtifact(JobArtifact $artifact): void
|
||||
{
|
||||
$this->artifacts()->save($artifact);
|
||||
}
|
||||
}
|
||||
|
40
app/Notification/Notification.php
Normal file
40
app/Notification/Notification.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notification;
|
||||
|
||||
use App\Models\Job;
|
||||
use App\Notification\Stringifiable\StringifiableSimpleText;
|
||||
|
||||
abstract class Notification {
|
||||
|
||||
protected Job $job;
|
||||
private NotificationBody $body;
|
||||
|
||||
public bool $isError;
|
||||
|
||||
public function __construct(int $jobId, NotificationBody $body, bool $isError = false) {
|
||||
$this->job = Job::find($jobId);
|
||||
$this->body = $body;
|
||||
$this->isError = $isError;
|
||||
}
|
||||
|
||||
public function getTitle(): Stringifiable {
|
||||
return new StringifiableSimpleText($this->job->name);
|
||||
}
|
||||
|
||||
public function getBody(): Stringifiable {
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
abstract public function getLinkURL(): ?string;
|
||||
|
||||
public function getImageURL(): ?string {
|
||||
$imageProjectPath = $this->getImageProjectPath();
|
||||
if ($imageProjectPath === null) {
|
||||
return null;
|
||||
}
|
||||
return url($imageProjectPath);
|
||||
}
|
||||
|
||||
abstract public function getImageProjectPath(): ?string;
|
||||
}
|
8
app/Notification/NotificationBody.php
Normal file
8
app/Notification/NotificationBody.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notification;
|
||||
|
||||
use App\Notification\Stringifiable;
|
||||
|
||||
abstract class NotificationBody extends Stringifiable {
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notification\NotificationBody\Hellcase;
|
||||
|
||||
use App\Browser\Jobs\Hellcase\HellcaseLoginQrCode;
|
||||
use App\Notification\NotificationBody;
|
||||
|
||||
class HellcaseNotificationDailyFreeBody extends NotificationBody {
|
||||
|
||||
private string $content = "Vous avez remporté un cadeau gratuit sur Hellcase !";
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toMarkdownString(): string {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toString(): string {
|
||||
return $this->content;
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notification\NotificationBody\Hellcase;
|
||||
|
||||
use App\Browser\Jobs\Hellcase\HellcaseLoginQrCode;
|
||||
use App\Notification\NotificationBody;
|
||||
|
||||
class HellcaseNotificationLoginBody extends NotificationBody {
|
||||
|
||||
private string $content = "Veuillez utiliser steam guard pour vous connecter à Hellcase.\nLe QR code se rafrachira toutes les ". HellcaseLoginQrCode::QR_CODE_VALIDITY ." secondes.";
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toMarkdownString(): string {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toString(): string {
|
||||
return $this->content;
|
||||
}
|
||||
}
|
44
app/Notification/NotificationBody/ListNotificationBody.php
Normal file
44
app/Notification/NotificationBody/ListNotificationBody.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notification\NotificationBody;
|
||||
|
||||
use App\Notification\NotificationBody;
|
||||
use App\Notification\Stringifiable;
|
||||
|
||||
class ListNotificationBody extends NotificationBody {
|
||||
|
||||
private array $content;
|
||||
|
||||
public function __construct(array $content) {
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toMarkdownString(): string {
|
||||
$string = "";
|
||||
foreach ($this->content as $item) {
|
||||
$string .= "- ". $this->getTextFromContent($item) . "\n";
|
||||
}
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toString(): string {
|
||||
$string = "";
|
||||
foreach ($this->content as $item) {
|
||||
$string .= $this->getTextFromContent($item) . "\n";
|
||||
}
|
||||
return $string;
|
||||
}
|
||||
|
||||
public function getTextFromContent(string|Stringifiable $content): string {
|
||||
if ($content instanceof Stringifiable) {
|
||||
return $content->toString();
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
}
|
32
app/Notification/NotificationBody/SimpleNotificationBody.php
Normal file
32
app/Notification/NotificationBody/SimpleNotificationBody.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notification\NotificationBody;
|
||||
|
||||
use App\Notification\NotificationBody;
|
||||
use App\Notification\Stringifiable;
|
||||
|
||||
class SimpleNotificationBody extends NotificationBody {
|
||||
|
||||
private string $body;
|
||||
|
||||
public function __construct(string $body) {
|
||||
$this->body = $body;
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toMarkdownString(): string {
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
public function toHTMLString(): string {
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toString(): string {
|
||||
return $this->body;
|
||||
}
|
||||
}
|
7
app/Notification/NotificationProvider.php
Normal file
7
app/Notification/NotificationProvider.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notification;
|
||||
|
||||
abstract class NotificationProvider {
|
||||
abstract public static function send(Notification $notification): void;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notification\Notifications\Hellcase;
|
||||
|
||||
use App\Browser\Jobs\Hellcase\HellcaseDailyFreeScreenshot;
|
||||
use App\Browser\Jobs\Hellcase\HellcaseLoginQrCode;
|
||||
use App\Notification\Notification;
|
||||
use App\Notification\Notifications\NotificationLogin;
|
||||
use Laravel\Dusk\Browser;
|
||||
|
||||
class HellcaseNotificationDailyFree extends NotificationLogin {
|
||||
|
||||
public function __construct(int $jobId, \App\Notification\NotificationBody $body) {
|
||||
parent::__construct($jobId, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getImageProjectPath(): string|null {
|
||||
return HellcaseDailyFreeScreenshot::getImgFileProjectPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getLinkURL(): string|null {
|
||||
return route('jobs.show', ['job' => $this->job->id]);
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notification\Notifications\Hellcase;
|
||||
|
||||
use App\Browser\Jobs\Hellcase\HellcaseLoginQrCode;
|
||||
use App\Notification\Notification;
|
||||
use App\Notification\Notifications\NotificationLogin;
|
||||
use Laravel\Dusk\Browser;
|
||||
|
||||
class HellcaseNotificationLogin extends NotificationLogin {
|
||||
|
||||
public function __construct(int $jobId, \App\Notification\NotificationBody $body) {
|
||||
parent::__construct($jobId, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getImageProjectPath(): string|null {
|
||||
return HellcaseLoginQrCode::getImgFileProjectPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getLinkURL(): string|null {
|
||||
return route('jobs.show', ['job' => $this->job->id]);
|
||||
}
|
||||
}
|
11
app/Notification/Notifications/NotificationLogin.php
Normal file
11
app/Notification/Notifications/NotificationLogin.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notification\Notifications;
|
||||
|
||||
use App\Notification\Notification;
|
||||
|
||||
abstract class NotificationLogin extends Notification {
|
||||
public function __construct(int $jobId, \App\Notification\NotificationBody $body) {
|
||||
parent::__construct($jobId, $body, true);
|
||||
}
|
||||
}
|
36
app/Notification/Notifications/SimpleNotification.php
Normal file
36
app/Notification/Notifications/SimpleNotification.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notification\Notifications;
|
||||
|
||||
use App\Notification\Notification;
|
||||
use App\Notification\NotificationBody\SimpleNotificationBody;
|
||||
use App\Notification\Stringifiable;
|
||||
use App\Notification\Stringifiable\StringifiableSimpleText;
|
||||
|
||||
class SimpleNotification extends Notification {
|
||||
|
||||
private StringifiableSimpleText $title;
|
||||
|
||||
public function __construct(int $jobId, string $title, string $body, bool $isError = false) {
|
||||
$this->title = new StringifiableSimpleText($title);
|
||||
parent::__construct($jobId, new SimpleNotificationBody($body), $isError);
|
||||
}
|
||||
|
||||
public function getTitle(): Stringifiable {
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getImageProjectPath(): string|null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getLinkURL(): string|null {
|
||||
return route('home');
|
||||
}
|
||||
}
|
23
app/Notification/Providers/AllNotification.php
Normal file
23
app/Notification/Providers/AllNotification.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notification\Providers;
|
||||
|
||||
use App\Notification\NotificationProvider;
|
||||
use App\Notification\INotificationProvider;
|
||||
use App\Models\JobInfo;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class AllNotification extends NotificationProvider {
|
||||
private const NOTIFICATIONS_PROVIDERS = [
|
||||
DiscordWebHookNotification::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function send(\App\Notification\Notification $notification): void {
|
||||
foreach (self::NOTIFICATIONS_PROVIDERS as $provider) {
|
||||
$provider::send($notification);
|
||||
}
|
||||
}
|
||||
}
|
110
app/Notification/Providers/DiscordWebHookNotification.php
Normal file
110
app/Notification/Providers/DiscordWebHookNotification.php
Normal file
@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notification\Providers;
|
||||
|
||||
use App\Notification\NotificationProvider;
|
||||
use App\Notification\INotificationProvider;
|
||||
use App\Models\JobInfo;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class DiscordWebHookNotification extends NotificationProvider {
|
||||
|
||||
private const EMBED_COLOR = ["521254", "16058119"];
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function send(\App\Notification\Notification $notification): void {
|
||||
/*
|
||||
Test Json for a complete embed :
|
||||
{
|
||||
"content": "",
|
||||
"tts": false,
|
||||
"embeds": [
|
||||
{
|
||||
"id": 652627557,
|
||||
"title": "Title",
|
||||
"description": "This is the markdown body\n",
|
||||
"color": 521254,
|
||||
"fields": [
|
||||
{
|
||||
"id": 984079152,
|
||||
"name": "Field 1",
|
||||
"value": "test"
|
||||
}
|
||||
],
|
||||
"url": "https://localhost:8000",
|
||||
"image": {
|
||||
"url": "https://www.thoughtco.com/thmb/jzJO77P0K9zIbqxQOVOaHWFCfj4=/1732x1732/filters:fill(auto,1)/GettyImages-186451154-58c3965a3df78c353cf8cc7b.jpg"
|
||||
}
|
||||
}
|
||||
],
|
||||
"components": [],
|
||||
"actions": {},
|
||||
"username": "Datboi",
|
||||
"avatar_url": "https://www.fairytailrp.com/t40344-here-come-dat-boi"
|
||||
}
|
||||
*/
|
||||
$webHookUrl = static::getDiscordWebHookUrl($notification->isError);
|
||||
$body = [
|
||||
"content"=> "",
|
||||
"tts"=> false,
|
||||
"embeds" => [
|
||||
[
|
||||
"id" => 652627557,
|
||||
"title" => $notification->getTitle()->toString(),
|
||||
"description" => $notification->getBody()->toMarkdownString(),
|
||||
"color" => self::EMBED_COLOR[(int)$notification->isError],
|
||||
"url" => $notification->getLinkURL(),
|
||||
],
|
||||
],
|
||||
"username" => "Datboi",
|
||||
"avatar_url" => "https://media1.giphy.com/media/yDTWAecZcB2Jq/200w.gif?cid=6c09b952f68kz3wnkqsmyha8e7xrpe8n2kx0nkf2b8cir6am&rid=200w.gif&ct=g",
|
||||
];
|
||||
|
||||
if ($notification->getImageURL() !== null) {
|
||||
$body["embeds"][0]["image"] = [
|
||||
"url" => "attachment://image.png"
|
||||
];
|
||||
}
|
||||
|
||||
$payloadJson = json_encode($body);
|
||||
|
||||
$formData = [
|
||||
'payload_json' => $payloadJson,
|
||||
];
|
||||
|
||||
if ($notification->getImageURL() !== null) {
|
||||
$formData['file'] = curl_file_create($notification->getImageProjectPath(), 'image/png', 'image.png');
|
||||
}
|
||||
|
||||
$ch = curl_init($webHookUrl);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-type: multipart/form-data'));
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $formData);
|
||||
// curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
}
|
||||
|
||||
private static function getDiscordWebHookUrl(bool $isError): string {
|
||||
$generalWebHookUrlKey = 'discord_webhook_url';
|
||||
$generalWebHookUrl = Cache::rememberForever($generalWebHookUrlKey, function () use ($generalWebHookUrlKey) {
|
||||
return JobInfo::where('key', $generalWebHookUrlKey)->first()->value;
|
||||
});
|
||||
if ($generalWebHookUrl === null) {
|
||||
throw new \Exception("Le webhook discord n'a pas été configuré");
|
||||
}
|
||||
|
||||
if ($isError) {
|
||||
$errorWebHookUrlKey = 'discord_error_webhook_url';
|
||||
$errorWebHookUrl = Cache::rememberForever($errorWebHookUrlKey, function () use ($errorWebHookUrlKey) {
|
||||
return JobInfo::where('key', $errorWebHookUrlKey)->first()->value;
|
||||
});
|
||||
return $errorWebHookUrl ?? $generalWebHookUrl;
|
||||
}
|
||||
else {
|
||||
return $generalWebHookUrl;
|
||||
}
|
||||
}
|
||||
}
|
21
app/Notification/Stringifiable.php
Normal file
21
app/Notification/Stringifiable.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notification;
|
||||
|
||||
use Parsedown;
|
||||
|
||||
abstract class Stringifiable{
|
||||
private Parsedown $parsedown;
|
||||
|
||||
public function __construct() {
|
||||
$this->parsedown = new Parsedown();
|
||||
}
|
||||
|
||||
abstract public function toString(): string;
|
||||
|
||||
public function toHTMLString(): string {
|
||||
return $this->parsedown->text($this->toMarkdownString());
|
||||
}
|
||||
|
||||
abstract public function toMarkdownString(): string;
|
||||
}
|
34
app/Notification/Stringifiable/StringifiableSimpleText.php
Normal file
34
app/Notification/Stringifiable/StringifiableSimpleText.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notification\Stringifiable;
|
||||
|
||||
use App\Notification\Stringifiable;
|
||||
|
||||
class StringifiableSimpleText extends Stringifiable {
|
||||
private string $text;
|
||||
|
||||
public function __construct(string $text) {
|
||||
$this->text = $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toHTMLString(): string {
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toMarkdownString(): string {
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toString(): string {
|
||||
return $this->text;
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Browser\BrowserJob;
|
||||
use Illuminate\Support\Facades\Vite;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
|
37
app/Providers/BrowserJobsServiceProvider.php
Normal file
37
app/Providers/BrowserJobsServiceProvider.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Services\BrowserJobsInstances;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class BrowserJobsServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
||||
private BrowserJobsInstances $service;
|
||||
|
||||
public function __construct(Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
$this->service = new BrowserJobsInstances();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->instance(BrowserJobsInstances::class, $this->service);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->app->terminating(function (BrowserJobsInstances $instances) {
|
||||
$instances->terminateAll();
|
||||
});
|
||||
}
|
||||
}
|
92
app/Services/BrowserJobsInstances.php
Normal file
92
app/Services/BrowserJobsInstances.php
Normal file
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Browser\BrowserJob;
|
||||
use Cache;
|
||||
use Exception;
|
||||
|
||||
class BrowserJobsInstances {
|
||||
|
||||
/**
|
||||
* A dictionnary of all job instances by their jobId
|
||||
* @var array Dictionnary of BrowserJob by their jobId
|
||||
*/
|
||||
private $jobInstancesByJobId = [];
|
||||
|
||||
/**
|
||||
* Index all jobs in the app/Browser/Jobs directory
|
||||
* and store them in the cache
|
||||
* @return void
|
||||
*/
|
||||
private function indexJobsClassesById(): void
|
||||
{
|
||||
// Read all directories in app/Browser/jobs,
|
||||
// foreach directory, get the file named {directoryName}Job.php if it exists
|
||||
$folders = scandir(app_path('Browser/Jobs'));
|
||||
$files = [];
|
||||
foreach ($folders as $folder) {
|
||||
if ($folder == '.' || $folder == '..') {
|
||||
continue;
|
||||
}
|
||||
if (is_dir(app_path('Browser/Jobs/' . $folder))) {
|
||||
$potentialFileName = $folder . '/' . $folder . 'Job.php';
|
||||
if (file_exists(app_path('Browser/Jobs/' . $potentialFileName))) {
|
||||
$files[] = $potentialFileName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make a dictionnary of the id of the job as the key and the job class instance as value
|
||||
foreach ($files as $file) {
|
||||
$className = str_replace('.php', '', $file);
|
||||
$className = str_replace('/', '\\', $className);
|
||||
$fullClassName = 'App\Browser\Jobs\\' . $className;
|
||||
$jobInstance = new $fullClassName();
|
||||
$jobId = $jobInstance->jobId;
|
||||
Cache::put('jobClass' . $jobId, $fullClassName); // Met le nom de la classe en cache
|
||||
$this->jobInstancesByJobId[$jobId] = $jobInstance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of a job by it's jobId
|
||||
* @param mixed $jobId The ID of the job in the database
|
||||
* @return BrowserJob
|
||||
*/
|
||||
public function getJobInstance($jobId): BrowserJob
|
||||
{
|
||||
try {
|
||||
return $this->jobInstancesByJobId[$jobId];
|
||||
} catch (Exception $e) {
|
||||
return $this->getNewJobInstance($jobId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of a job class
|
||||
* @param int $jobId
|
||||
* @return object
|
||||
*/
|
||||
private function getNewJobInstance(int $jobId): BrowserJob {
|
||||
$jobClass = Cache::get('jobClass' . $jobId);
|
||||
if ($jobClass == null) { // If we don't have the class in cache, we put all of the job in cache
|
||||
$this->indexJobsClassesById();
|
||||
return $this->jobInstancesByJobId[$jobId]; // indexJobsClassesById() already created an instance of the job
|
||||
}
|
||||
// If we have the class in cache, we create a new instance of it
|
||||
$instance = new $jobClass();
|
||||
$this->jobInstancesByJobId[$jobId] = $instance;
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate all jobs
|
||||
* @return void
|
||||
*/
|
||||
public function terminateAll() {
|
||||
foreach ($this->jobInstancesByJobId as $jobInstance) {
|
||||
$jobInstance->terminate();
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ use Illuminate\Foundation\Configuration\Middleware;
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
api: __DIR__.'/../routes/api.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
|
@ -2,4 +2,5 @@
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\BrowserJobsServiceProvider::class,
|
||||
];
|
||||
|
54
compose.prod.yaml
Normal file
54
compose.prod.yaml
Normal file
@ -0,0 +1,54 @@
|
||||
# Comments are provided throughout this file to help you get started.
|
||||
# If you need more help, visit the Docker Compose reference guide at
|
||||
# https://docs.docker.com/go/compose-spec-reference/
|
||||
|
||||
# Here the instructions define your application as a service called "app".
|
||||
# This service is built from the Dockerfile in the current directory.
|
||||
# You can add other services your application may depend on here, such as a
|
||||
# database or a cache. For examples, see the Awesome Compose repository:
|
||||
# https://github.com/docker/awesome-compose
|
||||
services:
|
||||
app:
|
||||
image: git.matthiasg.dev/ninluc/datbrowser:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 80:80
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
undetected-chromedriver:
|
||||
condition: service_healthy
|
||||
|
||||
db:
|
||||
image: mysql:5.7
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", 'mysqladmin', 'ping', '-h', 'db', '-u', '${DB_USERNAME}', '-p${DB_PASSWORD}' ]
|
||||
interval: 2s
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
|
||||
MYSQL_DATABASE: ${DB_DATABASE}
|
||||
MYSQL_USER: ${DB_USERNAME}
|
||||
MYSQL_PASSWORD: ${DB_PASSWORD}
|
||||
volumes:
|
||||
- dbdata:/var/lib/mysql
|
||||
|
||||
undetected-chromedriver:
|
||||
image: git.matthiasg.dev/ninluc/selenium/standalone-uc:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /tmp:/tmp
|
||||
shm_size: 2gb
|
||||
ports:
|
||||
- "4444:4444"
|
||||
- "7900:7900"
|
||||
|
||||
watchtower:
|
||||
image: containrrr/watchtower
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
volumes:
|
||||
dbdata:
|
53
compose.yaml
Normal file
53
compose.yaml
Normal file
@ -0,0 +1,53 @@
|
||||
# Comments are provided throughout this file to help you get started.
|
||||
# If you need more help, visit the Docker Compose reference guide at
|
||||
# https://docs.docker.com/go/compose-spec-reference/
|
||||
|
||||
# Here the instructions define your application as a service called "app".
|
||||
# This service is built from the Dockerfile in the current directory.
|
||||
# You can add other services your application may depend on here, such as a
|
||||
# database or a cache. For examples, see the Awesome Compose repository:
|
||||
# https://github.com/docker/awesome-compose
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
target: final
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 80:80
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
undetected-chromedriver:
|
||||
condition: service_healthy
|
||||
|
||||
db:
|
||||
image: mysql:5.7
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", 'mysqladmin', 'ping', '-h', 'db', '-u', '${DB_USERNAME}', '-p${DB_PASSWORD}' ]
|
||||
interval: 2s
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
|
||||
MYSQL_DATABASE: ${DB_DATABASE}
|
||||
MYSQL_USER: ${DB_USERNAME}
|
||||
MYSQL_PASSWORD: ${DB_PASSWORD}
|
||||
volumes:
|
||||
- dbdata:/var/lib/mysql
|
||||
|
||||
undetected-chromedriver:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: undetectedChromedriver/seleniumChromedriverDockerfile
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /tmp:/tmp
|
||||
shm_size: 2gb
|
||||
ports:
|
||||
- "4444:4444"
|
||||
- "7900:7900"
|
||||
|
||||
volumes:
|
||||
dbdata:
|
@ -10,7 +10,9 @@
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"erusev/parsedown": "^1.7",
|
||||
"inertiajs/inertia-laravel": "^2.0",
|
||||
"laravel/dusk": "^8.2",
|
||||
"laravel/framework": "^11.31",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/tinker": "^2.9",
|
||||
|
216
composer.lock
generated
216
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "2f8fad98d414b6e76b3cad63a981e25e",
|
||||
"content-hash": "fe9d04d1c63e92ba62edb93168567747",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@ -510,6 +510,56 @@
|
||||
],
|
||||
"time": "2024-12-27T00:36:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "erusev/parsedown",
|
||||
"version": "1.7.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/erusev/parsedown.git",
|
||||
"reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
|
||||
"reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8.35"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"Parsedown": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Emanuil Rusev",
|
||||
"email": "hello@erusev.com",
|
||||
"homepage": "http://erusev.com"
|
||||
}
|
||||
],
|
||||
"description": "Parser for Markdown.",
|
||||
"homepage": "http://parsedown.org",
|
||||
"keywords": [
|
||||
"markdown",
|
||||
"parser"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/erusev/parsedown/issues",
|
||||
"source": "https://github.com/erusev/parsedown/tree/1.7.x"
|
||||
},
|
||||
"time": "2019-12-30T22:54:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "fruitcake/php-cors",
|
||||
"version": "v1.3.0",
|
||||
@ -970,16 +1020,16 @@
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/uri-template",
|
||||
"version": "v1.0.3",
|
||||
"version": "v1.0.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/guzzle/uri-template.git",
|
||||
"reference": "ecea8feef63bd4fef1f037ecb288386999ecc11c"
|
||||
"reference": "30e286560c137526eccd4ce21b2de477ab0676d2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/guzzle/uri-template/zipball/ecea8feef63bd4fef1f037ecb288386999ecc11c",
|
||||
"reference": "ecea8feef63bd4fef1f037ecb288386999ecc11c",
|
||||
"url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2",
|
||||
"reference": "30e286560c137526eccd4ce21b2de477ab0676d2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -1036,7 +1086,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/guzzle/uri-template/issues",
|
||||
"source": "https://github.com/guzzle/uri-template/tree/v1.0.3"
|
||||
"source": "https://github.com/guzzle/uri-template/tree/v1.0.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -1052,7 +1102,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-12-03T19:50:20+00:00"
|
||||
"time": "2025-02-03T10:55:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "inertiajs/inertia-laravel",
|
||||
@ -1129,17 +1179,89 @@
|
||||
"time": "2024-12-13T02:48:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/framework",
|
||||
"version": "v11.41.0",
|
||||
"name": "laravel/dusk",
|
||||
"version": "v8.2.14",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/framework.git",
|
||||
"reference": "42d6ae000c868c2abfa946da46702f2358493482"
|
||||
"url": "https://github.com/laravel/dusk.git",
|
||||
"reference": "28c9fce3900625522afc2468a9117cdf44f919c1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/42d6ae000c868c2abfa946da46702f2358493482",
|
||||
"reference": "42d6ae000c868c2abfa946da46702f2358493482",
|
||||
"url": "https://api.github.com/repos/laravel/dusk/zipball/28c9fce3900625522afc2468a9117cdf44f919c1",
|
||||
"reference": "28c9fce3900625522afc2468a9117cdf44f919c1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"ext-zip": "*",
|
||||
"guzzlehttp/guzzle": "^7.5",
|
||||
"illuminate/console": "^10.0|^11.0|^12.0",
|
||||
"illuminate/support": "^10.0|^11.0|^12.0",
|
||||
"php": "^8.1",
|
||||
"php-webdriver/webdriver": "^1.15.2",
|
||||
"symfony/console": "^6.2|^7.0",
|
||||
"symfony/finder": "^6.2|^7.0",
|
||||
"symfony/process": "^6.2|^7.0",
|
||||
"vlucas/phpdotenv": "^5.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.6",
|
||||
"orchestra/testbench": "^8.19|^9.0|^10.0",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpunit/phpunit": "^10.1|^11.0",
|
||||
"psy/psysh": "^0.11.12|^0.12"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-pcntl": "Used to gracefully terminate Dusk when tests are running."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Laravel\\Dusk\\DuskServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\Dusk\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "Laravel Dusk provides simple end-to-end testing and browser automation.",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"testing",
|
||||
"webdriver"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/dusk/issues",
|
||||
"source": "https://github.com/laravel/dusk/tree/v8.2.14"
|
||||
},
|
||||
"time": "2025-01-26T19:36:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/framework",
|
||||
"version": "v11.41.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/framework.git",
|
||||
"reference": "3ef433d5865f30a19b6b1be247586068399b59cc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/3ef433d5865f30a19b6b1be247586068399b59cc",
|
||||
"reference": "3ef433d5865f30a19b6b1be247586068399b59cc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -1341,7 +1463,7 @@
|
||||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2025-01-28T15:22:55+00:00"
|
||||
"time": "2025-01-30T13:25:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/prompts",
|
||||
@ -2646,6 +2768,72 @@
|
||||
],
|
||||
"time": "2024-11-21T10:39:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-webdriver/webdriver",
|
||||
"version": "1.15.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-webdriver/php-webdriver.git",
|
||||
"reference": "998e499b786805568deaf8cbf06f4044f05d91bf"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/998e499b786805568deaf8cbf06f4044f05d91bf",
|
||||
"reference": "998e499b786805568deaf8cbf06f4044f05d91bf",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-zip": "*",
|
||||
"php": "^7.3 || ^8.0",
|
||||
"symfony/polyfill-mbstring": "^1.12",
|
||||
"symfony/process": "^5.0 || ^6.0 || ^7.0"
|
||||
},
|
||||
"replace": {
|
||||
"facebook/webdriver": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"ergebnis/composer-normalize": "^2.20.0",
|
||||
"ondram/ci-detector": "^4.0",
|
||||
"php-coveralls/php-coveralls": "^2.4",
|
||||
"php-mock/php-mock-phpunit": "^2.0",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.2",
|
||||
"phpunit/phpunit": "^9.3",
|
||||
"squizlabs/php_codesniffer": "^3.5",
|
||||
"symfony/var-dumper": "^5.0 || ^6.0 || ^7.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-SimpleXML": "For Firefox profile creation"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"lib/Exception/TimeoutException.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Facebook\\WebDriver\\": "lib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.",
|
||||
"homepage": "https://github.com/php-webdriver/php-webdriver",
|
||||
"keywords": [
|
||||
"Chromedriver",
|
||||
"geckodriver",
|
||||
"php",
|
||||
"selenium",
|
||||
"webdriver"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-webdriver/php-webdriver/issues",
|
||||
"source": "https://github.com/php-webdriver/php-webdriver/tree/1.15.2"
|
||||
},
|
||||
"time": "2024-11-21T15:12:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoption/phpoption",
|
||||
"version": "1.9.3",
|
||||
|
8
config/dusk.php
Normal file
8
config/dusk.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
"driver" => [
|
||||
"url" => $_ENV['DUSK_DRIVER_URL'] ?? env('DUSK_DRIVER_URL') ?? null
|
||||
]
|
||||
];
|
83
config/sanctum.php
Normal file
83
config/sanctum.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Stateful Domains
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Requests from the following domains / hosts will receive stateful API
|
||||
| authentication cookies. Typically, these should include your local
|
||||
| and production domains which access your API via a frontend SPA.
|
||||
|
|
||||
*/
|
||||
|
||||
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
||||
'%s%s',
|
||||
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
|
||||
Sanctum::currentApplicationUrlWithPort()
|
||||
))),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sanctum Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This array contains the authentication guards that will be checked when
|
||||
| Sanctum is trying to authenticate a request. If none of these guards
|
||||
| are able to authenticate the request, Sanctum will use the bearer
|
||||
| token that's present on an incoming request for authentication.
|
||||
|
|
||||
*/
|
||||
|
||||
'guard' => ['web'],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Expiration Minutes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value controls the number of minutes until an issued token will be
|
||||
| considered expired. This will override any values set in the token's
|
||||
| "expires_at" attribute, but first-party sessions are not affected.
|
||||
|
|
||||
*/
|
||||
|
||||
'expiration' => null,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Token Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Sanctum can prefix new tokens in order to take advantage of numerous
|
||||
| security scanning initiatives maintained by open source platforms
|
||||
| that notify developers if they commit tokens into repositories.
|
||||
|
|
||||
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
||||
|
|
||||
*/
|
||||
|
||||
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sanctum Middleware
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When authenticating your first-party SPA with Sanctum you may need to
|
||||
| customize some of the middleware Sanctum uses while processing the
|
||||
| request. You may change the middleware listed below as required.
|
||||
|
|
||||
*/
|
||||
|
||||
'middleware' => [
|
||||
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
|
||||
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
|
||||
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
|
||||
],
|
||||
|
||||
];
|
35
database/migrations/2025_00_00_000000_create_cache_table.php
Normal file
35
database/migrations/2025_00_00_000000_create_cache_table.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
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
|
||||
{
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
@ -17,7 +17,7 @@ return new class extends Migration
|
||||
$table->string('name');
|
||||
$table->text('description')->nullable();
|
||||
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->boolean('is_active')->default(false);
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
@ -38,21 +38,24 @@ return new class extends Migration
|
||||
$jobs = [
|
||||
[
|
||||
'id' => 1,
|
||||
'name' => 'Hellcase',
|
||||
'description' => 'Prends le daily free et rentre dans les concours. Tourne toutes les 24h.',
|
||||
'is_active' => false,
|
||||
'name' => 'Paramètres généraux',
|
||||
'description' => "Les paramètres généraux de l'application.",
|
||||
'is_active' => true,
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'name' => 'Jeu gratuit Epic Games',
|
||||
'description' => 'Prends le jeu gratuit Epic games. Tourne tous les mois et tous les jours pendant la période de Noël.',
|
||||
'is_active' => false,
|
||||
'name' => 'Hellcase',
|
||||
'description' => 'Prends le daily free et rentre dans les concours. Tourne toutes les 24h.',
|
||||
],
|
||||
[
|
||||
'id' => 3,
|
||||
'name' => 'Jeu gratuit Epic Games',
|
||||
'description' => 'Prends le jeu gratuit Epic games. Tourne tous les mois et tous les jours pendant la période de Noël.',
|
||||
],
|
||||
[
|
||||
'id' => 4,
|
||||
'name' => 'Envoyer un post instagram',
|
||||
'description' => "Envoye un post instagram avec l'image et le texte fourni. Tourne tous les jours.",
|
||||
'is_active' => false,
|
||||
],
|
||||
];
|
||||
|
||||
|
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
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
|
||||
{
|
||||
Schema::create('job_info_types', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->string('name')->unique()->index(); // html input type
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
$this->seed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('job_info_types');
|
||||
}
|
||||
|
||||
public function seed(): void
|
||||
{
|
||||
$jobInfosTypes = [
|
||||
[
|
||||
"id" => 1,
|
||||
"name" => "text",
|
||||
],
|
||||
[
|
||||
"id" => 2,
|
||||
"name" => "email",
|
||||
],
|
||||
[
|
||||
"id" => 3,
|
||||
"name" => "password",
|
||||
],
|
||||
[
|
||||
"id" => 4,
|
||||
"name" => "url",
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($jobInfosTypes as $jobInfoType) {
|
||||
\App\Models\JobInfoType::forceCreate($jobInfoType);
|
||||
}
|
||||
}
|
||||
};
|
107
database/migrations/2025_01_23_190849_create_job_infos_table.php
Normal file
107
database/migrations/2025_01_23_190849_create_job_infos_table.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
use App\Models\JobInfoType;
|
||||
use App\Models\Job;
|
||||
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
|
||||
{
|
||||
Schema::create('job_infos', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->string('key')->unique();
|
||||
|
||||
$table->string('name');
|
||||
$table->text('description')->nullable();
|
||||
$table->string('placeholder')->nullable();
|
||||
|
||||
$table->text('value')->nullable();
|
||||
|
||||
$table->boolean('is_required')->default(true);
|
||||
|
||||
$table->foreignIdFor(JobInfoType::class, "job_info_type_id")->constrained()->onDelete('cascade');
|
||||
|
||||
$table->foreignIdFor(Job::class,'job_id')->constrained()->onDelete('cascade');
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
$this->seed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('job_infos');
|
||||
}
|
||||
|
||||
public function seed(): void
|
||||
{
|
||||
|
||||
$jobInfos = [
|
||||
/* GENERAL PARAMETERS */
|
||||
[
|
||||
"key" => "discord_webhook_url",
|
||||
"name" => "Messages - Webhook Discord",
|
||||
"description" => "Le lien discord webhook utilisé pour envoyer les messages des résultats des jobs.",
|
||||
"placeholder" => "https://discord.com/api/webhooks/...",
|
||||
"job_info_type_id" => 4,
|
||||
"job_id" => 1,
|
||||
],
|
||||
[
|
||||
"key" => "discord_error_webhook_url",
|
||||
"name" => "Alertes - Webhook Discord",
|
||||
"description" => "Le lien discord webhook utilisé pour envoyer les messages d'erreur et d'alerte. Laisser vide pour utiliser le même que les messages.",
|
||||
"placeholder" => "https://discord.com/api/webhooks/...",
|
||||
"is_required" => false,
|
||||
"job_info_type_id" => 4,
|
||||
"job_id" => 1,
|
||||
],
|
||||
|
||||
/* EPIC GAMES */
|
||||
[
|
||||
"key" => "epicgames_account_email",
|
||||
"name" => "E-mail",
|
||||
"description" => "L'adresse e-mail utilisée pour votre compte Epic Games.",
|
||||
"job_info_type_id" => 2,
|
||||
"job_id" => 3,
|
||||
],
|
||||
[
|
||||
"key" => "epicgames_account_password",
|
||||
"name" => "Mot de passe",
|
||||
"description" => "Le mot de passe utilisé pour votre compte Epic Games.",
|
||||
"job_info_type_id" => 3,
|
||||
"job_id" => 3,
|
||||
],
|
||||
|
||||
/* INSTAGRAM */
|
||||
[
|
||||
"key" => "instagram_account_email",
|
||||
"name" => "E-mail",
|
||||
"description" => "L'adresse e-mail utilisée pour votre compte Instagram.",
|
||||
"job_info_type_id" => 2,
|
||||
"job_id" => 4,
|
||||
],
|
||||
[
|
||||
"key" => "instagram_account_password",
|
||||
"name" => "Mot de passe",
|
||||
"description" => "Le mot de passe utilisé pour votre compte Instagram.",
|
||||
"job_info_type_id" => 3,
|
||||
"job_id" => 4,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($jobInfos as $jobInfo) {
|
||||
\App\Models\JobInfo::forceCreate($jobInfo);
|
||||
}
|
||||
}
|
||||
};
|
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
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
|
||||
{
|
||||
Schema::create('job_runs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->boolean('success')->default(true);
|
||||
|
||||
$table->foreignIdFor(\App\Models\Job::class)->constrained()->cascadeOnDelete();
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('job_runs');
|
||||
}
|
||||
};
|
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
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
|
||||
{
|
||||
Schema::create('job_artifacts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->string('name');
|
||||
$table->text('content')->nullable();
|
||||
|
||||
$table->foreignIdFor(\App\Models\JobRun::class)->constrained()->cascadeOnDelete();
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('job_artifacts');
|
||||
}
|
||||
};
|
7
dockerEntryPoint.sh
Normal file
7
dockerEntryPoint.sh
Normal file
@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Migrate the database
|
||||
php ./artisan migrate --force
|
||||
|
||||
# Start all of the server sumulataneously
|
||||
php ./artisan serve --no-interaction -vvv --port=80 --host=0.0.0.0
|
92
package-lock.json
generated
92
package-lock.json
generated
@ -5,6 +5,7 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^12.5.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.474.0",
|
||||
@ -1154,48 +1155,34 @@
|
||||
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ=="
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
"version": "10.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
|
||||
"integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
|
||||
"version": "12.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.5.0.tgz",
|
||||
"integrity": "sha512-GVyH1iYqNANwcahAx8JBm6awaNgvR/SwZ1fjr10b8l1HIgDp82ngNbfzJUgOgWEoxjL+URAggnlilAEXwCOZtg==",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.20",
|
||||
"@vueuse/metadata": "10.11.1",
|
||||
"@vueuse/shared": "10.11.1",
|
||||
"vue-demi": ">=0.14.8"
|
||||
"@vueuse/metadata": "12.5.0",
|
||||
"@vueuse/shared": "12.5.0",
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/core/node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node_modules/@vueuse/core/node_modules/@vueuse/shared": {
|
||||
"version": "12.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.5.0.tgz",
|
||||
"integrity": "sha512-vMpcL1lStUU6O+kdj6YdHDixh0odjPAUM15uJ9f7MY781jcYkIwFA4iv2EfoIPO6vBmvutI1HxxAwmf0cx5ISQ==",
|
||||
"dependencies": {
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "10.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz",
|
||||
"integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
|
||||
"version": "12.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.5.0.tgz",
|
||||
"integrity": "sha512-Ui7Lo2a7AxrMAXRF+fAp9QsXuwTeeZ8fIB9wsLHqzq9MQk+2gMYE2IGJW48VMJ8ecvCB3z3GsGLKLbSasQ5Qlg==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
@ -2768,6 +2755,53 @@
|
||||
"vue": ">= 3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/radix-vue/node_modules/@vueuse/core": {
|
||||
"version": "10.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
|
||||
"integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.20",
|
||||
"@vueuse/metadata": "10.11.1",
|
||||
"@vueuse/shared": "10.11.1",
|
||||
"vue-demi": ">=0.14.8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/radix-vue/node_modules/@vueuse/core/node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/radix-vue/node_modules/@vueuse/metadata": {
|
||||
"version": "10.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz",
|
||||
"integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/radix-vue/node_modules/nanoid": {
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
||||
|
@ -22,6 +22,7 @@
|
||||
"vue-tsc": "^2.0.24"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^12.5.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.474.0",
|
||||
|
BIN
public/Hellcase-joinGiveawayButton
Normal file
BIN
public/Hellcase-joinGiveawayButton
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.2 KiB |
4
pushSeleniumStandaloneUcImage.sh
Normal file
4
pushSeleniumStandaloneUcImage.sh
Normal file
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
sudo docker build -f undetectedChromedriver/seleniumChromedriverDockerfile -t git.matthiasg.dev/ninluc/selenium/standalone-uc:latest .
|
||||
sudo docker push git.matthiasg.dev/ninluc/selenium/standalone-uc:latest
|
@ -3,8 +3,8 @@ import { appName } from '@/app.ts'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-link class="flex justify-center items-center gap-3" to="/">
|
||||
<Link class="flex justify-center items-center gap-3" href="/">
|
||||
<img src="@/Assets/logo.png" alt="logo DatBrowser" class="h-20" />
|
||||
<h1 class="text-xl font-display select-none">{{ appName }}</h1>
|
||||
</router-link>
|
||||
</Link>
|
||||
</template>
|
||||
|
@ -1,4 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import Description from '@/Components/ui/text/Description.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 class="text-xl"><slot name="title" /></h2>
|
||||
<p class="text-dark-green"><slot name="description" /></p>
|
||||
<Description><slot name="description" /></Description>
|
||||
</template>
|
||||
|
139
resources/js/Components/Layout/Job/JobForm.vue
Normal file
139
resources/js/Components/Layout/Job/JobForm.vue
Normal file
@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import { Job, JobArtifact, JobInfo, JobInfoType, JobRunArtifact } from "@/types/Jobs/job";
|
||||
import { router } from "@inertiajs/vue3";
|
||||
import { onMounted, reactive, ref, watch } from "vue";
|
||||
import Button from "../../ui/button/Button.vue";
|
||||
import JobFormField from "./JobFormField.vue";
|
||||
import LoadingSpinner from "@/Components/ui/feedback/spinner/LoadingSpinner.vue";
|
||||
import { httpApi } from "@/lib/utils";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
job: Job;
|
||||
error?: string;
|
||||
}>(), {
|
||||
error: "",
|
||||
});
|
||||
|
||||
const isSaving = ref(false);
|
||||
const isTesting = ref(false);
|
||||
|
||||
const errorMessage = ref(props.error);
|
||||
|
||||
const jobSuccess = ref(true);
|
||||
|
||||
function submit() {
|
||||
isSaving.value = true;
|
||||
|
||||
if (!testForm()) {
|
||||
isTesting.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
router.patch("/jobs/" + props.job.id, {
|
||||
is_active: isActiveJobInfo.value.value,
|
||||
...props.job.job_infos.reduce((acc, jobInfo) => {
|
||||
acc[jobInfo.id] = jobInfo.value;
|
||||
return acc;
|
||||
}, {} as Record<number, string | boolean>),
|
||||
});
|
||||
setTimeout(() => {
|
||||
isSaving.value = false;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
const isActiveJobInfo = ref<JobInfo>({
|
||||
id: 0,
|
||||
name: "Activer",
|
||||
description: "Activer le job",
|
||||
value: props.job.is_active,
|
||||
is_required: false,
|
||||
job_info_type: { name: "checkbox" } as JobInfoType,
|
||||
} as JobInfo);
|
||||
|
||||
async function testJob() {
|
||||
isTesting.value = true;
|
||||
|
||||
submit();
|
||||
|
||||
console.log("Testing job", props.job.id);
|
||||
let response;
|
||||
try {
|
||||
response = await httpApi<{ artifact?: JobRunArtifact }>(
|
||||
`/jobs/${props.job.id}/test`
|
||||
);
|
||||
jobSuccess.value = response.artifact?.success ?? false;
|
||||
} catch (e) {
|
||||
console.error("Testing the job failed : ", e);
|
||||
jobSuccess.value = false;
|
||||
} finally {
|
||||
isTesting.value = false;
|
||||
}
|
||||
|
||||
if (response?.artifact) {
|
||||
alert(response.artifact.artifacts[0].name + " : " + response.artifact.artifacts[0].content);
|
||||
}
|
||||
}
|
||||
|
||||
function testForm(): boolean {
|
||||
console.log("Testing the form validity");
|
||||
const jobForm = document.querySelector('form[name="jobForm"]') as HTMLFormElement;
|
||||
if (jobForm.checkValidity() == false) {
|
||||
console.log("Form is not valid");
|
||||
if (jobForm.reportValidity) {
|
||||
jobForm.reportValidity()
|
||||
}
|
||||
else {
|
||||
errorMessage.value = "Le formulaire n'est pas valide";
|
||||
}
|
||||
isTesting.value = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<form @submit.prevent="submit" class="flex p-3 flex-col gap-4" name="jobForm">
|
||||
<JobFormField v-if="job.id != 1" :jobInfo="isActiveJobInfo" />
|
||||
|
||||
<JobFormField
|
||||
:jobInfo="jobInfo"
|
||||
v-for="jobInfo of job.job_infos"
|
||||
v-key="jobInfo.id"
|
||||
/>
|
||||
|
||||
<Transition>
|
||||
<p v-if="errorMessage" class="text-destructive">{{ errorMessage }}</p>
|
||||
</Transition>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="secondary"
|
||||
:disabled="isSaving || isTesting"
|
||||
>
|
||||
Enregistrer
|
||||
<LoadingSpinner v-if="isSaving" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
:disabled="isSaving || isTesting"
|
||||
@click.prevent="testJob"
|
||||
>
|
||||
Tester
|
||||
<LoadingSpinner v-if="isTesting" />
|
||||
</Button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* we will explain what these classes do next! */
|
||||
.v-enter-active,
|
||||
.v-leave-active {
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.v-enter-from,
|
||||
.v-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
26
resources/js/Components/Layout/Job/JobFormField.vue
Normal file
26
resources/js/Components/Layout/Job/JobFormField.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import Checkbox from '@/Components/ui/checkbox/Checkbox.vue';
|
||||
import VModelCheckbox from '@/Components/ui/checkbox/VModelCheckbox.vue';
|
||||
import Input from '@/Components/ui/input/Input.vue';
|
||||
import { Label } from '@/Components/ui/label';
|
||||
import Description from '@/Components/ui/text/Description.vue';
|
||||
import { JobInfo } from '@/types/Jobs/job';
|
||||
import { watch } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
jobInfo: JobInfo;
|
||||
}>();
|
||||
|
||||
const jobInfoType = props.jobInfo.job_info_type.name;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Label :for="'' + jobInfo.id" class="text">{{ jobInfo.name }}<span v-if="jobInfo.is_required" class="cursor-help" title="Requis" aria-label="Requis">*</span></Label>
|
||||
<Description>{{ jobInfo.description }}</Description>
|
||||
<Input v-if="jobInfoType != 'checkbox'" :type="jobInfoType" :id="'' + jobInfo.id" :name="'' + jobInfo.id" :placeholder="jobInfo.placeholder" v-model="jobInfo.value as string" :required="jobInfo.is_required" />
|
||||
<VModelCheckbox v-else :id="'' + jobInfo.id" :class="''" v-model="jobInfo.value as boolean" />
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
@ -8,7 +8,7 @@ defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MainNavItem :link="`/job/${job.id}`">
|
||||
<MainNavItem :link="`/jobs/${job.id}`">
|
||||
{{ job.name }}
|
||||
</MainNavItem>
|
||||
</template>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils.ts'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Primitive, type PrimitiveProps } from 'radix-vue'
|
||||
import { type ButtonVariants, buttonVariants } from '.'
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils.ts'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils.ts'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils.ts'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils.ts'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils.ts'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils.ts'
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
|
33
resources/js/Components/ui/checkbox/Checkbox.vue
Normal file
33
resources/js/Components/ui/checkbox/Checkbox.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import type { CheckboxRootEmits, CheckboxRootProps } from 'radix-vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Check } from 'lucide-vue-next'
|
||||
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<CheckboxRootEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CheckboxRoot
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn('peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||
props.class)"
|
||||
>
|
||||
<CheckboxIndicator class="flex h-full w-full items-center justify-center text-current">
|
||||
<slot>
|
||||
<Check class="h-4 w-4" />
|
||||
</slot>
|
||||
</CheckboxIndicator>
|
||||
</CheckboxRoot>
|
||||
</template>
|
15
resources/js/Components/ui/checkbox/VModelCheckbox.vue
Normal file
15
resources/js/Components/ui/checkbox/VModelCheckbox.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
class?: string;
|
||||
}>(), {
|
||||
class: '',
|
||||
});
|
||||
|
||||
const model = defineModel<boolean>({type: Boolean, default: false});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input type="checkbox" :checked="model" @click="() => (model = !model)" class="peer h-7 w-7 shrink-0 rounded-sm border border-gray ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 checked:bg-secondary checked:text-dark-green transition cursor-pointer">
|
||||
</template>
|
1
resources/js/Components/ui/checkbox/index.ts
Normal file
1
resources/js/Components/ui/checkbox/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as Checkbox } from './Checkbox.vue'
|
@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { Loader2 } from 'lucide-vue-next'
|
||||
import type { PrimitiveProps } from 'radix-vue'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { spinnerVariants, type SpinnerVariants } from '.'
|
||||
|
||||
interface Props extends PrimitiveProps {
|
||||
size?: SpinnerVariants['size']
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
:class="cn(spinnerVariants({ size }), props.class)"
|
||||
>
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
19
resources/js/Components/ui/feedback/spinner/index.ts
Normal file
19
resources/js/Components/ui/feedback/spinner/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
export const spinnerVariants = cva('animate-spin lucide lucide-loader-circle-icon', {
|
||||
variants: {
|
||||
size: {
|
||||
default: 'w-4 h-4 m-2',
|
||||
xs: 'w-1 h-1 m-1',
|
||||
sm: 'w-2 h-2 m-1',
|
||||
lg: 'w-7 h-7 m-3',
|
||||
xl: 'w-12 h-12 m-4',
|
||||
icon: 'w-10 h-10 m-4'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'default'
|
||||
}
|
||||
})
|
||||
|
||||
export type SpinnerVariants = VariantProps<typeof spinnerVariants>
|
24
resources/js/Components/ui/input/Input.vue
Normal file
24
resources/js/Components/ui/input/Input.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
|
||||
const props = defineProps<{
|
||||
defaultValue?: string | number
|
||||
modelValue?: string | number
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'update:modelValue', payload: string | number): void
|
||||
}>()
|
||||
|
||||
const modelValue = useVModel(props, 'modelValue', emits, {
|
||||
passive: true,
|
||||
defaultValue: props.defaultValue,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input v-model="modelValue" :class="cn('flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', props.class)">
|
||||
</template>
|
1
resources/js/Components/ui/input/index.ts
Normal file
1
resources/js/Components/ui/input/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as Input } from './Input.vue'
|
27
resources/js/Components/ui/label/Label.vue
Normal file
27
resources/js/Components/ui/label/Label.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Label, type LabelProps } from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<LabelProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Label
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</Label>
|
||||
</template>
|
1
resources/js/Components/ui/label/index.ts
Normal file
1
resources/js/Components/ui/label/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as Label } from './Label.vue'
|
3
resources/js/Components/ui/text/Description.vue
Normal file
3
resources/js/Components/ui/text/Description.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<p class="text-dark-green"><slot/></p>
|
||||
</template>
|
@ -4,12 +4,17 @@ import BaseLayout from "./BaseLayout.vue";
|
||||
import MainNavJobLink from "@/Components/Layout/MainNav/MainNavJobLink.vue";
|
||||
import { Job } from "@/types/Jobs/job";
|
||||
import MainNavItem from "@/Components/Layout/MainNav/MainNavItem.vue";
|
||||
import { httpApi } from "@/lib/utils";
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
let jobs = [
|
||||
{ id: 1, name: "Hellcase" },
|
||||
{ id: 2, name: "Jeu gratuit Epic Games" },
|
||||
{ id: 3, name: "Envoyer un post instagram" },
|
||||
];
|
||||
const jobs = ref<Job[]>([]);
|
||||
|
||||
async function fetchJobs() {
|
||||
let jobsRaw = await httpApi<Job[]>("/jobs");
|
||||
jobs.value = jobsRaw.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
||||
}
|
||||
|
||||
onMounted(fetchJobs);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -19,16 +24,16 @@ let jobs = [
|
||||
<ul class="flex flex-col gap-2">
|
||||
<MainNavItem link="/"> Accueil </MainNavItem>
|
||||
<MainNavJobLink
|
||||
:job="job as Job"
|
||||
v-for="job in jobs"
|
||||
:key="job.id"
|
||||
:job="job"
|
||||
/>
|
||||
</ul>
|
||||
</nav>
|
||||
</ScrollArea>
|
||||
|
||||
<ScrollArea class="flex-1 h-full overflow-auto">
|
||||
<main>
|
||||
<main class="p-3">
|
||||
<slot />
|
||||
</main>
|
||||
</ScrollArea>
|
||||
|
@ -1,10 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import JobCard from '../Components/ui/job/JobCard.vue'
|
||||
import JobForm from '../Components/Layout/Job/JobForm.vue'
|
||||
import JobCard from '../Components/Layout/Job/JobCard.vue'
|
||||
import { Job } from "@/types/Jobs/job";
|
||||
import { Head } from "@inertiajs/vue3";
|
||||
|
||||
defineProps<{
|
||||
job: Job;
|
||||
error?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
@ -12,4 +14,6 @@ defineProps<{
|
||||
<Head title="Welcome" />
|
||||
|
||||
<JobCard :job="job" />
|
||||
|
||||
<JobForm :job="job" :error="error" />
|
||||
</template>
|
||||
|
@ -4,3 +4,14 @@ import { twMerge } from "tailwind-merge"
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
|
||||
export async function httpApi<T>(route: string): Promise<T> {
|
||||
let response = await fetch(import.meta.env.VITE_APP_URL + "/api" + route);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
34
resources/js/types/Jobs/job.d.ts
vendored
34
resources/js/types/Jobs/job.d.ts
vendored
@ -3,5 +3,39 @@ export type Job = {
|
||||
name: string;
|
||||
description: string;
|
||||
is_active: boolean;
|
||||
|
||||
job_infos: JobInfo[];
|
||||
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
export type JobInfo = {
|
||||
id: number;
|
||||
|
||||
name: string;
|
||||
description: string;
|
||||
placeholder: string;
|
||||
|
||||
value: string | boolean;
|
||||
is_required: boolean;
|
||||
job_info_type: JobInfoType;
|
||||
|
||||
job_id: number;
|
||||
}
|
||||
|
||||
export type JobInfoType = {
|
||||
id: number;
|
||||
name: string;
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
export type JobRunArtifact = {
|
||||
jobId: number;
|
||||
artifacts: JobArtifact[];
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export type JobArtifact = {
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
@ -12,7 +12,11 @@
|
||||
|
||||
<!-- Scripts -->
|
||||
@routes
|
||||
@vite(['resources/js/app.ts', "resources/js/Pages/{$page['component']}.vue"])
|
||||
@if(config('app.env') === 'production')
|
||||
@vite(['resources/js/app.ts'])
|
||||
@else
|
||||
@vite(['resources/js/app.ts', "resources/js/Pages/{$page['component']}.vue"])
|
||||
@endif
|
||||
@inertiaHead
|
||||
</head>
|
||||
<body class="font-sans antialiased">
|
||||
|
19
routes/api.php
Normal file
19
routes/api.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\JobController;
|
||||
use App\Models\Job;
|
||||
use App\Services\BrowserJobsInstances;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/jobs', function (Request $request) {
|
||||
return response()->json(Job::all())->header('Cache-Control', 'public, max-age=30');
|
||||
});
|
||||
|
||||
|
||||
Route::get('/test/{id}', function (Request $request, $id, BrowserJobsInstances $BrowserJobsInstances) {
|
||||
$log = $BrowserJobsInstances->getJobInstance($id)->execute();
|
||||
return response()->json(['message' => 'Job ' . $id . ' ran : ' . $log]);
|
||||
});
|
||||
|
||||
Route::get('jobs/{job}/test', [JobController::class, 'test'])->name('jobs.test');
|
@ -1,15 +1,12 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\JobController;
|
||||
use App\Models\Job;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Inertia\Inertia;
|
||||
|
||||
Route::get('/', function () {
|
||||
return Inertia::render('Home');
|
||||
});
|
||||
})->name('home');
|
||||
|
||||
Route::get("/job/{jobId}", function ($jobId) {
|
||||
return Inertia::render('Job', [
|
||||
'job' => Job::find($jobId)
|
||||
]);
|
||||
});
|
||||
Route::resource('jobs', JobController::class)->only(['show', 'update']);
|
||||
|
21
tests/Browser/ExampleTest.php
Normal file
21
tests/Browser/ExampleTest.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Browser;
|
||||
|
||||
use Illuminate\Foundation\Testing\DatabaseMigrations;
|
||||
use Laravel\Dusk\Browser;
|
||||
use Tests\DuskTestCase;
|
||||
|
||||
class ExampleTest extends DuskTestCase
|
||||
{
|
||||
/**
|
||||
* A basic browser test example.
|
||||
*/
|
||||
public function testBasicExample(): void
|
||||
{
|
||||
$this->browse(function (Browser $browser) {
|
||||
$browser->visit('/')
|
||||
->assertSee('Laravel');
|
||||
});
|
||||
}
|
||||
}
|
36
tests/Browser/Pages/HomePage.php
Normal file
36
tests/Browser/Pages/HomePage.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Browser\Pages;
|
||||
|
||||
use Laravel\Dusk\Browser;
|
||||
|
||||
class HomePage extends Page
|
||||
{
|
||||
/**
|
||||
* Get the URL for the page.
|
||||
*/
|
||||
public function url(): string
|
||||
{
|
||||
return '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the browser is on the page.
|
||||
*/
|
||||
public function assert(Browser $browser): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the element shortcuts for the page.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function elements(): array
|
||||
{
|
||||
return [
|
||||
'@element' => '#selector',
|
||||
];
|
||||
}
|
||||
}
|
20
tests/Browser/Pages/Page.php
Normal file
20
tests/Browser/Pages/Page.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Browser\Pages;
|
||||
|
||||
use Laravel\Dusk\Page as BasePage;
|
||||
|
||||
abstract class Page extends BasePage
|
||||
{
|
||||
/**
|
||||
* Get the global element shortcuts for the site.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function siteElements(): array
|
||||
{
|
||||
return [
|
||||
'@element' => '#selector',
|
||||
];
|
||||
}
|
||||
}
|
2
tests/Browser/console/.gitignore
vendored
Normal file
2
tests/Browser/console/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
2
tests/Browser/screenshots/.gitignore
vendored
Normal file
2
tests/Browser/screenshots/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
2
tests/Browser/source/.gitignore
vendored
Normal file
2
tests/Browser/source/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
47
tests/DuskTestCase.php
Normal file
47
tests/DuskTestCase.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Facebook\WebDriver\Chrome\ChromeOptions;
|
||||
use Facebook\WebDriver\Remote\DesiredCapabilities;
|
||||
use Facebook\WebDriver\Remote\RemoteWebDriver;
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Dusk\TestCase as BaseTestCase;
|
||||
use PHPUnit\Framework\Attributes\BeforeClass;
|
||||
|
||||
abstract class DuskTestCase extends BaseTestCase
|
||||
{
|
||||
/**
|
||||
* Prepare for Dusk test execution.
|
||||
*/
|
||||
#[BeforeClass]
|
||||
public static function prepare(): void
|
||||
{
|
||||
if (! static::runningInSail()) {
|
||||
static::startChromeDriver(['--port=9515']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the RemoteWebDriver instance.
|
||||
*/
|
||||
protected function driver(): RemoteWebDriver
|
||||
{
|
||||
$options = (new ChromeOptions)->addArguments(collect([
|
||||
$this->shouldStartMaximized() ? '--start-maximized' : '--window-size=1920,1080',
|
||||
'--disable-search-engine-choice-screen',
|
||||
])->unless($this->hasHeadlessDisabled(), function (Collection $items) {
|
||||
return $items->merge([
|
||||
'--disable-gpu',
|
||||
'--headless=new',
|
||||
]);
|
||||
})->all());
|
||||
|
||||
return RemoteWebDriver::create(
|
||||
$_ENV['DUSK_DRIVER_URL'] ?? env('DUSK_DRIVER_URL') ?? 'http://localhost:9515',
|
||||
DesiredCapabilities::chrome()->setCapability(
|
||||
ChromeOptions::CAPABILITY, $options
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
BIN
undetectedChromedriver/chromedriver-linux
Executable file
BIN
undetectedChromedriver/chromedriver-linux
Executable file
Binary file not shown.
1
undetectedChromedriver/getChromeDriver.sh
Normal file
1
undetectedChromedriver/getChromeDriver.sh
Normal file
@ -0,0 +1 @@
|
||||
sudo docker run --rm -it -p 3389:3389 -v ./undetectedChromedriver:/root/.local/share/undetected_chromedriver/ ultrafunk/undetected-chromedriver:latest
|
10
undetectedChromedriver/seleniumChromedriverDockerfile
Normal file
10
undetectedChromedriver/seleniumChromedriverDockerfile
Normal file
@ -0,0 +1,10 @@
|
||||
FROM selenium/standalone-chrome:108.0 AS final
|
||||
|
||||
COPY undetectedChromedriver/chromedriver-linux /bin/chromedriver
|
||||
RUN mkdir -p /home/seluser/profile/
|
||||
|
||||
ENV TZ=Europe/Brussels
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --retries=3 CMD curl -s http://localhost:4444/wd/hub/status | jq -e '.value.ready == true' || exit 1
|
||||
|
||||
|
16
undetectedChromedriver/undetectedChromeDriver.yaml
Normal file
16
undetectedChromedriver/undetectedChromeDriver.yaml
Normal file
@ -0,0 +1,16 @@
|
||||
services:
|
||||
undetected-chromedriver:
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: undetectedChromedriver/seleniumChromedriverDockerfile
|
||||
volumes:
|
||||
- /tmp:/tmp
|
||||
- chromeProfile:/home/seluser/profile/
|
||||
shm_size: 2gb
|
||||
tty: true
|
||||
ports:
|
||||
- "4444:4444"
|
||||
- "7900:7900"
|
||||
|
||||
volumes:
|
||||
chromeProfile:
|
Reference in New Issue
Block a user