Added Scheduling
All checks were successful
Push image to registry / build-image (push) Successful in 4m20s

This commit is contained in:
2025-02-07 14:02:42 +01:00
parent 8d1e0474c4
commit 63490c136e
22 changed files with 126 additions and 437 deletions

View File

@ -67,3 +67,4 @@ VITE_APP_NAME="${APP_NAME}"
VITE_APP_URL="${APP_URL}" VITE_APP_URL="${APP_URL}"
DUSK_DRIVER_URL="http://undetected-chromedriver:4444" DUSK_DRIVER_URL="http://undetected-chromedriver:4444"
DUSK_START_MAXIMIZED=true

View File

@ -64,5 +64,7 @@ MEMCACHED_HOST=127.0.0.1
# AWS_USE_PATH_STYLE_ENDPOINT=false # AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}" VITE_APP_NAME="${APP_NAME}"
VITE_APP_URL="${APP_URL}"
DUSK_DRIVER_URL="http://127.0.0.1:4444" DUSK_DRIVER_URL="http://127.0.0.1:4444"
DUSK_START_MAXIMIZED=true

View File

@ -72,6 +72,9 @@ RUN mv .env.docker .env
RUN composer install --no-interaction --prefer-dist RUN composer install --no-interaction --prefer-dist
# CRON
RUN echo "* * * * * cd /usr/app && php artisan schedule:run >> /dev/null 2>&1" > /etc/crontabs/root
# Link the storage directory to the public directory. # Link the storage directory to the public directory.
RUN php artisan storage:link RUN php artisan storage:link

View File

@ -2,29 +2,29 @@
namespace App\Browser; namespace App\Browser;
use App\Browser\JobArtifacts\JobRunArtifact;
use App\Exception\JobException;
use App\Models\JobArtifact; use App\Models\JobArtifact;
use App\Models\JobRun; use App\Models\JobRun;
use Closure;
use Exception;
use Facebook\WebDriver\Chrome\ChromeOptions; use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\DesiredCapabilities; use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver; use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\RemoteWebElement; use Facebook\WebDriver\Remote\RemoteWebElement;
use Facebook\WebDriver\WebDriverBy; use Facebook\WebDriver\WebDriverBy;
use Illuminate\Support\Collection; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Laravel\Dusk\Browser; 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\Chrome\SupportsChrome;
use Laravel\Dusk\Concerns\ProvidesBrowser; use Laravel\Dusk\Concerns\ProvidesBrowser;
use Laravel\Dusk; use PHPUnit\Framework\Attributes\BeforeClass;
use Throwable; use Throwable;
abstract class BrowserJob abstract class BrowserJob implements ShouldQueue
{ {
use SupportsChrome, ProvidesBrowser; use SupportsChrome, ProvidesBrowser, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $jobId; public int $jobId;
@ -209,8 +209,16 @@ abstract class BrowserJob
return "dataTest"; return "dataTest";
} }
/**
* Execute the job.
*/
public function handle(): void
{
$this->execute();
}
// BROWSER MACROS
// === BROWSER MACROS ===
protected function waitForAndClickText(Browser $browser, string $text, int $timeout = 30, bool $ignoreCase = true) { protected function waitForAndClickText(Browser $browser, string $text, int $timeout = 30, bool $ignoreCase = true) {
$browser->waitForText($text, $timeout, $ignoreCase); $browser->waitForText($text, $timeout, $ignoreCase);
$this->findElementContainingText($browser, $text, $ignoreCase)?->click(); $this->findElementContainingText($browser, $text, $ignoreCase)?->click();

View File

@ -15,6 +15,7 @@ use App\Notification\Notifications\Hellcase\HellcaseNotificationLogin;
use App\Notification\Providers\AllNotification; use App\Notification\Providers\AllNotification;
use Dom\XPath; use Dom\XPath;
use Facebook\WebDriver\WebDriverBy; use Facebook\WebDriver\WebDriverBy;
use Illuminate\Support\Facades\Schedule;
use Laravel\Dusk\Browser; use Laravel\Dusk\Browser;
class HellcaseJob extends BrowserJob class HellcaseJob extends BrowserJob
@ -169,23 +170,24 @@ class HellcaseJob extends BrowserJob
} }
// If we see "availible in 20 HR 49 MIN", parse the hours and minute and reschedule // If we see "availible in 20 HR 49 MIN", parse the hours and minute and reschedule
$availibleInButton = $this->waitForElementContainingTextAndGetIt($browser, "available", 30); $availibleInButton = $browser->driver->findElement(WebDriverBy::xpath('//button[contains(@class, "daily-free-banner__button")]'));
if ($availibleInButton != null) { if ($availibleInButton->getAttribute("disabled") == "true") {
$hours = $availibleInButton->getText(); $hours = $availibleInButton->getText();
$hours = explode(" ", $hours); $hours = explode(" ", $hours);
$minutes = $hours[2]; $minutes = $hours[4];
$hours = $hours[0]; $hours = $hours[2];
// $this->reschedule($hours); // $this->reschedule($hours);
$this->jobRun->addArtifact(new JobArtifact([ $this->jobRun->addArtifact(new JobArtifact([
"name" => "Cadeau gratuit pas encore disponible", "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." "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; return;
} }
$this->waitForAndClickText($browser, "Get Free Bonus", 30, true); // Click the dailyfree button
$availibleInButton->click();
$lootElement = $browser->driver->findElement(WebDriverBy::xpath('//div[contains(@class, "daily-free-win-bonus")]')); $lootElement = $browser->driver->findElement(WebDriverBy::xpath('//div[contains(@class, "daily-free-win-bonus")]'));
$lootElement->takeElementScreenshot(HellcaseDailyFreeScreenshot::getImgFileAbsolutePath()); $lootElement->takeElementScreenshot(HellcaseDailyFreeScreenshot::getImgFileAbsolutePath());
@ -193,8 +195,6 @@ class HellcaseJob extends BrowserJob
AllNotification::send( AllNotification::send(
new HellcaseNotificationDailyFree($this->jobId, new HellcaseNotificationDailyFreeBody()) new HellcaseNotificationDailyFree($this->jobId, new HellcaseNotificationDailyFreeBody())
); );
sleep(5000);
} }
/** /**

View File

@ -1,52 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use Inertia\Response;
class AuthenticatedSessionController extends Controller
{
/**
* Display the login view.
*/
public function create(): Response
{
return Inertia::render('Auth/Login', [
'canResetPassword' => Route::has('password.request'),
'status' => session('status'),
]);
}
/**
* Handle an incoming authentication request.
*/
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(route('dashboard', absolute: false));
}
/**
* Destroy an authenticated session.
*/
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}

View File

@ -1,41 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class ConfirmablePasswordController extends Controller
{
/**
* Show the confirm password view.
*/
public function show(): Response
{
return Inertia::render('Auth/ConfirmPassword');
}
/**
* Confirm the user's password.
*/
public function store(Request $request): RedirectResponse
{
if (! Auth::guard('web')->validate([
'email' => $request->user()->email,
'password' => $request->password,
])) {
throw ValidationException::withMessages([
'password' => __('auth.password'),
]);
}
$request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended(route('dashboard', absolute: false));
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class EmailVerificationNotificationController extends Controller
{
/**
* Send a new email verification notification.
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false));
}
$request->user()->sendEmailVerificationNotification();
return back()->with('status', 'verification-link-sent');
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class EmailVerificationPromptController extends Controller
{
/**
* Display the email verification prompt.
*/
public function __invoke(Request $request): RedirectResponse|Response
{
return $request->user()->hasVerifiedEmail()
? redirect()->intended(route('dashboard', absolute: false))
: Inertia::render('Auth/VerifyEmail', ['status' => session('status')]);
}
}

View File

@ -1,69 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class NewPasswordController extends Controller
{
/**
* Display the password reset view.
*/
public function create(Request $request): Response
{
return Inertia::render('Auth/ResetPassword', [
'email' => $request->email,
'token' => $request->route('token'),
]);
}
/**
* Handle an incoming new password request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'token' => 'required',
'email' => 'required|email',
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user) use ($request) {
$user->forceFill([
'password' => Hash::make($request->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
if ($status == Password::PASSWORD_RESET) {
return redirect()->route('login')->with('status', __($status));
}
throw ValidationException::withMessages([
'email' => [trans($status)],
]);
}
}

View File

@ -1,29 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class PasswordController extends Controller
{
/**
* Update the user's password.
*/
public function update(Request $request): RedirectResponse
{
$validated = $request->validate([
'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back();
}
}

View File

@ -1,51 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class PasswordResetLinkController extends Controller
{
/**
* Display the password reset link request view.
*/
public function create(): Response
{
return Inertia::render('Auth/ForgotPassword', [
'status' => session('status'),
]);
}
/**
* Handle an incoming password reset link request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => 'required|email',
]);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$status = Password::sendResetLink(
$request->only('email')
);
if ($status == Password::RESET_LINK_SENT) {
return back()->with('status', __($status));
}
throw ValidationException::withMessages([
'email' => [trans($status)],
]);
}
}

View File

@ -1,51 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Inertia\Inertia;
use Inertia\Response;
class RegisteredUserController extends Controller
{
/**
* Display the registration view.
*/
public function create(): Response
{
return Inertia::render('Auth/Register');
}
/**
* Handle an incoming registration request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|lowercase|email|max:255|unique:'.User::class,
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::login($user);
return redirect(route('dashboard', absolute: false));
}
}

View File

@ -1,27 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
}

View File

@ -1,18 +1,15 @@
# 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: services:
app: app:
image: git.matthiasg.dev/ninluc/datbrowser:latest image: git.matthiasg.dev/ninluc/datbrowser:latest
restart: unless-stopped restart: unless-stopped
ports: ports:
- 80:80 - 80:80
environment:
APP_URL: datbrowser.matthiasg.dev
volumes:
- browserDownloads:/app/Browser/downloads
- browserScreenshots:/app/Browser/screenshots
- browserSource:/app/Browser/source
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
@ -52,3 +49,6 @@ services:
volumes: volumes:
dbdata: dbdata:
browserDownloads:
browserScreenshots:
browserSource:

View File

@ -37,7 +37,7 @@ return [
'database' => [ 'database' => [
'driver' => 'database', 'driver' => 'database',
'connection' => env('DB_QUEUE_CONNECTION'), 'connection' => env('DB_QUEUE_CONNECTION'),
'table' => env('DB_QUEUE_TABLE', 'jobs'), 'table' => env('DB_QUEUE_TABLE', 'queue_jobs'),
'queue' => env('DB_QUEUE', 'default'), 'queue' => env('DB_QUEUE', 'default'),
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
'after_commit' => false, 'after_commit' => false,

View File

@ -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('queue_jobs', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('queue_jobs');
}
};

View File

@ -47,16 +47,16 @@ return new class extends Migration
'name' => 'Hellcase', 'name' => 'Hellcase',
'description' => 'Prends le daily free et rentre dans les concours. Tourne toutes les 24h.', 'description' => 'Prends le daily free et rentre dans les concours. Tourne toutes les 24h.',
], ],
[ // [
'id' => 3, // 'id' => 3,
'name' => 'Jeu gratuit Epic Games', // '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.', // 'description' => 'Prends le jeu gratuit Epic games. Tourne tous les mois et tous les jours pendant la période de Noël.',
], // ],
[ // [
'id' => 4, // 'id' => 4,
'name' => 'Envoyer un post instagram', // 'name' => 'Envoyer un post instagram',
'description' => "Envoye un post instagram avec l'image et le texte fourni. Tourne tous les jours.", // 'description' => "Envoye un post instagram avec l'image et le texte fourni. Tourne tous les jours.",
], // ],
]; ];
foreach ($jobs as $job) { foreach ($jobs as $job) {

View File

@ -67,37 +67,37 @@ return new class extends Migration
"job_id" => 1, "job_id" => 1,
], ],
/* EPIC GAMES */ // /* EPIC GAMES */
[ // [
"key" => "epicgames_account_email", // "key" => "epicgames_account_email",
"name" => "E-mail", // "name" => "E-mail",
"description" => "L'adresse e-mail utilisée pour votre compte Epic Games.", // "description" => "L'adresse e-mail utilisée pour votre compte Epic Games.",
"job_info_type_id" => 2, // "job_info_type_id" => 2,
"job_id" => 3, // "job_id" => 3,
], // ],
[ // [
"key" => "epicgames_account_password", // "key" => "epicgames_account_password",
"name" => "Mot de passe", // "name" => "Mot de passe",
"description" => "Le mot de passe utilisé pour votre compte Epic Games.", // "description" => "Le mot de passe utilisé pour votre compte Epic Games.",
"job_info_type_id" => 3, // "job_info_type_id" => 3,
"job_id" => 3, // "job_id" => 3,
], // ],
/* INSTAGRAM */ // /* INSTAGRAM */
[ // [
"key" => "instagram_account_email", // "key" => "instagram_account_email",
"name" => "E-mail", // "name" => "E-mail",
"description" => "L'adresse e-mail utilisée pour votre compte Instagram.", // "description" => "L'adresse e-mail utilisée pour votre compte Instagram.",
"job_info_type_id" => 2, // "job_info_type_id" => 2,
"job_id" => 4, // "job_id" => 4,
], // ],
[ // [
"key" => "instagram_account_password", // "key" => "instagram_account_password",
"name" => "Mot de passe", // "name" => "Mot de passe",
"description" => "Le mot de passe utilisé pour votre compte Instagram.", // "description" => "Le mot de passe utilisé pour votre compte Instagram.",
"job_info_type_id" => 3, // "job_info_type_id" => 3,
"job_id" => 4, // "job_id" => 4,
], // ],
]; ];
foreach ($jobInfos as $jobInfo) { foreach ($jobInfos as $jobInfo) {

View File

@ -3,5 +3,9 @@
# Migrate the database # Migrate the database
php ./artisan migrate --force php ./artisan migrate --force
# Queue worker
php ./artisan queue:work --queue=high,default --tries=3 --timeout=90 --sleep=3 --daemon --no-interaction &
# Start all of the server sumulataneously # Start all of the server sumulataneously
php ./artisan serve --no-interaction -vvv --port=80 --host=0.0.0.0 php ./artisan serve --no-interaction -vvv --port=80 --host=0.0.0.0

View File

@ -10,10 +10,9 @@ Route::get('/jobs', function (Request $request) {
return response()->json(Job::all())->header('Cache-Control', 'public, max-age=30'); return response()->json(Job::all())->header('Cache-Control', 'public, max-age=30');
}); });
Route::get('/test/{id}', function (Request $request, $id, BrowserJobsInstances $BrowserJobsInstances) { Route::get('/test/{id}', function (Request $request, $id, BrowserJobsInstances $BrowserJobsInstances) {
$log = $BrowserJobsInstances->getJobInstance($id)->execute(); $log = $BrowserJobsInstances->getJobInstance($id)->execute();
return response()->json(['message' => 'Job ' . $id . ' ran : ' . $log]); return response()->json(['message' => 'Job ' . $id . ' ran', 'jobRun' => $log->load('artifacts')]);
}); });
Route::get('jobs/{job}/test', [JobController::class, 'test'])->name('jobs.test'); Route::get('jobs/{job}/test', [JobController::class, 'test'])->name('jobs.test');

View File

@ -1,8 +1,14 @@
<?php <?php
use App\Browser\Jobs\Hellcase\HellcaseJob;
use App\Models\Job;
use App\Services\BrowserJobsInstances;
use Illuminate\Foundation\Inspiring; use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
Artisan::command('inspire', function () { Artisan::command('inspire', function () {
$this->comment(Inspiring::quote()); $this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote'); })->purpose('Display an inspiring quote');
Schedule::job(new HellcaseJob)->daily()->onOneServer()->withoutOverlapping()->name('hellcase')->description('Hellcase job');
// Schedule::job(new HellcaseJob)->everyMinute()->onOneServer()->withoutOverlapping()->name('hellcase')->description('Hellcase job');