Compare commits
25 Commits
jobs/hellc
...
70cd2a41b1
Author | SHA1 | Date | |
---|---|---|---|
70cd2a41b1 | |||
d498203147 | |||
69fdad8f4e | |||
982f4ca7a7 | |||
7e37ea86d0 | |||
31c293e4a6 | |||
55a7b16d9e | |||
4008724169 | |||
235440078d | |||
5599e170a7 | |||
034a2c8754 | |||
e6ff59ff18 | |||
02bd6eae82 | |||
e6c6ab98fd | |||
5522195438 | |||
7f50822692 | |||
25a9063169 | |||
aa7d107c03 | |||
faf408582f | |||
b49af7e56e | |||
85aa0c7a04 | |||
c26a09701a | |||
fadb4d2748 | |||
c5f5d94912 | |||
af96f996ee |
@ -36,7 +36,7 @@ RUN mkdir -p public/build/ && npm i && npm run build
|
|||||||
# ========================================
|
# ========================================
|
||||||
|
|
||||||
# RUN
|
# RUN
|
||||||
FROM php:8.2-alpine AS final
|
FROM php:8.3-alpine AS final
|
||||||
|
|
||||||
ARG APP_ENV_FILE=.env.docker
|
ARG APP_ENV_FILE=.env.docker
|
||||||
|
|
||||||
@ -59,6 +59,10 @@ RUN apk update && apk add --no-cache \
|
|||||||
RUN docker-php-ext-configure zip && docker-php-ext-install zip
|
RUN docker-php-ext-configure zip && docker-php-ext-install zip
|
||||||
RUN docker-php-ext-install gd pdo pdo_mysql zip
|
RUN docker-php-ext-install gd pdo pdo_mysql zip
|
||||||
|
|
||||||
|
# Install latest version of the linux binary of yt-dlp into /bin/yt-dlp
|
||||||
|
# Get the file from https://github.com/yt-dlp/yt-dlp-master-builds/releases/latest/download/yt-dlp
|
||||||
|
RUN curl -L https://github.com/yt-dlp/yt-dlp-master-builds/releases/latest/download/yt-dlp -o /bin/yt-dlp \
|
||||||
|
&& chmod +x /bin/yt-dlp
|
||||||
|
|
||||||
# Get latest Composer
|
# Get latest Composer
|
||||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||||
|
@ -9,19 +9,19 @@ use function rtrim;
|
|||||||
class JobDebugScreenshot {
|
class JobDebugScreenshot {
|
||||||
public const IMG_FILE_NAME = "debug-";
|
public const IMG_FILE_NAME = "debug-";
|
||||||
|
|
||||||
public static function getFileName(int $jobId): string {
|
public static function getFileName(int $jobId, bool $withExtension = false): string {
|
||||||
return static::IMG_FILE_NAME . $jobId . ".png";
|
return static::IMG_FILE_NAME . $jobId . ($withExtension ? ".png": "");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getImgFileAbsolutePath(int $jobId): string {
|
public static function getImgFileAbsolutePath(int $jobId): string {
|
||||||
return rtrim(Browser::$storeScreenshotsAt, '/') . "/" . static::getFileName($jobId);
|
return rtrim(Browser::$storeScreenshotsAt, '/') . "/" . static::getFileName($jobId, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getImgFileProjectPath(int $jobId): string {
|
public static function getImgFileProjectPath(int $jobId): string {
|
||||||
return app_path("Browser/screenshots/" . static::getFileName($jobId));
|
return app_path("Browser/screenshots/" . static::getFileName($jobId, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getImgFileExternalPath(int $jobId): string {
|
public static function getImgFileExternalPath(int $jobId): string {
|
||||||
return "screenshots/" . static::getFileName($jobId);
|
return "screenshots/" . static::getFileName($jobId, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,19 +9,19 @@ use function rtrim;
|
|||||||
class JobErrorScreenshot {
|
class JobErrorScreenshot {
|
||||||
public const IMG_FILE_NAME = "failure-";
|
public const IMG_FILE_NAME = "failure-";
|
||||||
|
|
||||||
public static function getFileName(int $jobId): string {
|
public static function getFileName(int $jobId, bool $withExtension = false): string {
|
||||||
return static::IMG_FILE_NAME . $jobId . ".png";
|
return static::IMG_FILE_NAME . $jobId . ($withExtension ? ".png": "");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getImgFileAbsolutePath(int $jobId): string {
|
public static function getImgFileAbsolutePath(int $jobId): string {
|
||||||
return rtrim(Browser::$storeScreenshotsAt, '/') . "/" . static::getFileName($jobId);
|
return rtrim(Browser::$storeScreenshotsAt, '/') . "/" . static::getFileName($jobId, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getImgFileProjectPath(int $jobId): string {
|
public static function getImgFileProjectPath(int $jobId): string {
|
||||||
return app_path("Browser/screenshots/" . static::getFileName($jobId));
|
return app_path("Browser/screenshots/" . static::getFileName($jobId, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getImgFileExternalPath(int $jobId): string {
|
public static function getImgFileExternalPath(int $jobId): string {
|
||||||
return "screenshots/" . static::getFileName($jobId);
|
return "screenshots/" . static::getFileName($jobId, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,6 @@ class HellcaseJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
|
|||||||
|
|
||||||
public function __construct($jobId = 2)
|
public function __construct($jobId = 2)
|
||||||
{
|
{
|
||||||
Log::info("Constructing HellcaseJob");
|
|
||||||
parent::__construct($jobId);
|
parent::__construct($jobId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +81,7 @@ class HellcaseJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
|
|||||||
);
|
);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return $this->makeSimpleJobRun(
|
return $this->makeSimpleJobRun(
|
||||||
true,
|
false,
|
||||||
"Connexion échouée",
|
"Connexion échouée",
|
||||||
"Datboi n'a pas réussi à se connecter sur Hellcase :\n" . $e->getMessage()
|
"Datboi n'a pas réussi à se connecter sur Hellcase :\n" . $e->getMessage()
|
||||||
);
|
);
|
||||||
@ -171,7 +170,8 @@ class HellcaseJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
|
|||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
$nextSlideButton = $browser->driver->findElement(WebDriverBy::xpath('//button[@class="_button_1ygbm_7 _next_1ygbm_24"]'));
|
// $nextSlideButton = $browser->driver->findElement(WebDriverBy::xpath('//button[@class="_button_1ygbm_7 _next_1ygbm_24"]'));
|
||||||
|
$nextSlideButton = "Button next slide is deactivated";
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
|
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
|
||||||
AllNotification::send(new JobDebugNotification($this->jobId, "No next slide button found"));
|
AllNotification::send(new JobDebugNotification($this->jobId, "No next slide button found"));
|
||||||
@ -186,7 +186,7 @@ class HellcaseJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
|
|||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$clickedFailsCounter++;
|
$clickedFailsCounter++;
|
||||||
try {
|
try {
|
||||||
$nextSlideButton->click();
|
// $nextSlideButton->click();
|
||||||
} catch (\Exception $_) {}
|
} catch (\Exception $_) {}
|
||||||
sleep(3);
|
sleep(3);
|
||||||
continue;
|
continue;
|
||||||
@ -226,7 +226,11 @@ class HellcaseJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
|
|||||||
private function getDailyFree(Browser $browser)
|
private function getDailyFree(Browser $browser)
|
||||||
{
|
{
|
||||||
$browser->visit('https://hellcase.com/dailyfree');
|
$browser->visit('https://hellcase.com/dailyfree');
|
||||||
|
try {
|
||||||
$browser->waitForText("Get Daily free loot", 30, true);
|
$browser->waitForText("Get Daily free loot", 30, true);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return; // Pobably means we do not have access to a daily free loot
|
||||||
|
}
|
||||||
|
|
||||||
// Do we fill the conditions ?
|
// Do we fill the conditions ?
|
||||||
if (sizeof(value: $browser->driver->findElements(WebDriverBy::xpath('//p[contains(text(), "Fulfill the conditions below")]'))) > 0) {
|
if (sizeof(value: $browser->driver->findElements(WebDriverBy::xpath('//p[contains(text(), "Fulfill the conditions below")]'))) > 0) {
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Browser\Jobs\HellcaseBattles;
|
||||||
|
|
||||||
|
use Laravel\Dusk\Browser;
|
||||||
|
use function rtrim;
|
||||||
|
|
||||||
|
|
||||||
|
class HellcaseBattleScreenshot {
|
||||||
|
public const IMG_FILE_NAME = "Hellcase-battle";
|
||||||
|
|
||||||
|
public static function getImgFileAbsolutePath(): string {
|
||||||
|
return rtrim(Browser::$storeScreenshotsAt, '/') . "/HellcaseBattles/" . static::IMG_FILE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getImgFileProjectPath(): string {
|
||||||
|
return app_path("Browser/screenshots/HellcaseBattles/" . static::IMG_FILE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getImgFileExternalPath(): string {
|
||||||
|
return "screenshots/HellcaseBattles/" . static::IMG_FILE_NAME;
|
||||||
|
}
|
||||||
|
}
|
@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
namespace App\Browser\Jobs\HellcaseBattles;
|
namespace App\Browser\Jobs\HellcaseBattles;
|
||||||
|
|
||||||
|
use App\Browser\JobDebugScreenshot;
|
||||||
use App\Browser\Jobs\Hellcase\HellcaseJob;
|
use App\Browser\Jobs\Hellcase\HellcaseJob;
|
||||||
use App\Models\HellcaseBattle;
|
use App\Models\HellcaseBattle;
|
||||||
use App\Models\Job;
|
use App\Models\Job;
|
||||||
|
use App\Models\JobArtifact;
|
||||||
use App\Models\JobRun;
|
use App\Models\JobRun;
|
||||||
use App\Notification\Notifications\JobDebugNotification;
|
use App\Notification\Notifications\JobDebugNotification;
|
||||||
use App\Notification\Providers\AllNotification;
|
use App\Notification\Providers\AllNotification;
|
||||||
@ -20,10 +22,10 @@ class HellcaseBattlesJob extends HellcaseJob implements ShouldBeUniqueUntilProce
|
|||||||
{
|
{
|
||||||
private Collection $jobInfos;
|
private Collection $jobInfos;
|
||||||
private array $battlesToAdd = [];
|
private array $battlesToAdd = [];
|
||||||
|
private array $battlesSent = [];
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
Log::info("Constructing HellcaseBattlesJob");
|
|
||||||
parent::__construct(3);
|
parent::__construct(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,8 +49,16 @@ class HellcaseBattlesJob extends HellcaseJob implements ShouldBeUniqueUntilProce
|
|||||||
|
|
||||||
$this->sendFinishedBattles($browser);
|
$this->sendFinishedBattles($browser);
|
||||||
|
|
||||||
|
$this->jobRun->addArtifact(new JobArtifact([
|
||||||
|
"name" => count($this->battlesSent) . " battailles envoyées",
|
||||||
|
]));
|
||||||
|
|
||||||
$this->createNewBattles();
|
$this->createNewBattles();
|
||||||
|
|
||||||
|
$this->jobRun->addArtifact(new JobArtifact([
|
||||||
|
"name" => count($this->battlesToAdd) . " nouvelles battailles ajoutées pour surveillage",
|
||||||
|
]));
|
||||||
|
|
||||||
$this->jobRun->success = true;
|
$this->jobRun->success = true;
|
||||||
$this->jobRun->save();
|
$this->jobRun->save();
|
||||||
|
|
||||||
@ -70,13 +80,12 @@ class HellcaseBattlesJob extends HellcaseJob implements ShouldBeUniqueUntilProce
|
|||||||
$browser->visit('https://hellcase.com/casebattle');
|
$browser->visit('https://hellcase.com/casebattle');
|
||||||
$browser->waitForText("CASES", 30, true);
|
$browser->waitForText("CASES", 30, true);
|
||||||
|
|
||||||
AllNotification::send(new JobDebugNotification($this->jobId, "I hate niggers"));
|
|
||||||
|
|
||||||
// Sort by price
|
// Sort by price
|
||||||
try {
|
try {
|
||||||
$sortByPriceDiv = $browser->driver->findElement(WebDriverBy::xpath("//*[span[contains(text(), 'Value')]]"));
|
$sortByPriceDiv = $browser->driver->findElement(WebDriverBy::xpath("//*[span[contains(text(), 'Value')]]"));
|
||||||
$sortByPriceDiv->click();
|
$sortByPriceDiv->click();
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
|
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
|
||||||
AllNotification::send(new JobDebugNotification($this->jobId, "Failed to sort by price"));
|
AllNotification::send(new JobDebugNotification($this->jobId, "Failed to sort by price"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -103,25 +112,28 @@ class HellcaseBattlesJob extends HellcaseJob implements ShouldBeUniqueUntilProce
|
|||||||
$battleLinkButton = $battle->findElement(WebDriverBy::xpath('./div//button[text() = "watch"]'));
|
$battleLinkButton = $battle->findElement(WebDriverBy::xpath('./div//button[text() = "watch"]'));
|
||||||
$battleLinkButton->sendKeys("\n");
|
$battleLinkButton->sendKeys("\n");
|
||||||
sleep(3);
|
sleep(3);
|
||||||
|
try { // If we still are on the casebattle page, it means the battle was cancelled or something else
|
||||||
|
$browser->waitForLocation("https://hellcase.com/casebattle/", 3);
|
||||||
|
} catch (Exception $e) {
|
||||||
$battleLink = $browser->driver->getCurrentURL();
|
$battleLink = $browser->driver->getCurrentURL();
|
||||||
|
|
||||||
$this->battlesToAdd[$battleLink] = $battleValue;
|
$this->battlesToAdd[$battleLink] = $battleValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function sendFinishedBattles(Browser $browser) {
|
private function sendFinishedBattles(Browser $browser) {
|
||||||
// foreach battle that we didn"t already planned to add with $this->battlesToAdd
|
// foreach battle that we didn"t already planned to add with $this->battlesToAdd
|
||||||
foreach (HellcaseBattle::all() as $battle) {
|
foreach (HellcaseBattle::all() as $battle) {
|
||||||
dump($battle);
|
|
||||||
if (!array_key_exists($battle->getUrl(), $this->battlesToAdd)) {
|
if (!array_key_exists($battle->getUrl(), $this->battlesToAdd)) {
|
||||||
dump("finished");
|
|
||||||
$browser->visit($battle->getUrl());
|
$browser->visit($battle->getUrl());
|
||||||
|
|
||||||
try {
|
sleep(2);
|
||||||
$browser->waitForText("Started at");
|
$browser->waitForText("Case Battle");
|
||||||
|
|
||||||
|
if ($this->findElementContainingText($browser, "Started at:") != null) { // battle is finished
|
||||||
// Send the battle
|
// Send the battle
|
||||||
$this->sendBattle($browser, $battle);
|
$this->sendBattle($browser, $battle);
|
||||||
} catch (Exception $e) { // Battle is not finished or error (like battle cancelled)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$battle->delete();
|
$battle->delete();
|
||||||
@ -130,16 +142,35 @@ class HellcaseBattlesJob extends HellcaseJob implements ShouldBeUniqueUntilProce
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function sendBattle(Browser $browser, HellcaseBattle $battle) {
|
private function sendBattle(Browser $browser, HellcaseBattle $battle) {
|
||||||
AllNotification::send(new JobDebugNotification($this->jobId, "Battle sent" . $battle->getUrl()));
|
try {
|
||||||
|
$battleHeader = $browser->driver->findElement(WebDriverBy::xpath("//*[contains(@class, 'case-battle-game__header')]"));
|
||||||
|
sleep(2); // Wait for the animations to finish
|
||||||
|
$battleHeader->takeElementScreenshot(HellcaseBattleScreenshot::getImgFileAbsolutePath());
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
|
||||||
|
AllNotification::send(new JobDebugNotification($this->jobId, "Failed to screenshot battle"));
|
||||||
|
}
|
||||||
|
$this->battlesSent[$battle->getUrl()] = $battle->value;
|
||||||
|
|
||||||
|
$options = [];
|
||||||
|
|
||||||
|
if ($this->jobInfos->get("hellcase_battles_discord_webhook_url") !== null) { // Custom discord webhook
|
||||||
|
$options["discord_webhook_url"] = $this->jobInfos->get("hellcase_battles_discord_webhook_url");
|
||||||
|
}
|
||||||
|
|
||||||
|
AllNotification::send(new HellcaseBattlesNofication($this->jobId, $battle), $options);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createNewBattles() {
|
private function createNewBattles() {
|
||||||
foreach ($this->battlesToAdd as $battleLink => $battleValue) {
|
foreach ($this->battlesToAdd as $battleLink => $battleValue) {
|
||||||
$battleLink = explode("/", $battleLink);
|
$battleLink = explode("/", $battleLink);
|
||||||
HellcaseBattle::firstOrCreate([
|
try {
|
||||||
|
HellcaseBattle::create([
|
||||||
"battle_id" => $battleLink[count($battleLink) - 1],
|
"battle_id" => $battleLink[count($battleLink) - 1],
|
||||||
"value" => $battleValue,
|
"value" => $battleValue,
|
||||||
]);
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Browser\Jobs\HellcaseBattles;
|
||||||
|
|
||||||
|
use App\Models\HellcaseBattle;
|
||||||
|
use App\Notification\Notification;
|
||||||
|
use App\Notification\Stringifiable;
|
||||||
|
use App\Notification\Stringifiable\StringifiableSimpleText;
|
||||||
|
|
||||||
|
class HellcaseBattlesNofication extends Notification {
|
||||||
|
|
||||||
|
private HellcaseBattle $battle;
|
||||||
|
|
||||||
|
public function __construct(int $jobId, HellcaseBattle $battle) {
|
||||||
|
parent::__construct($jobId);
|
||||||
|
|
||||||
|
$this->battle = $battle;
|
||||||
|
|
||||||
|
$this->setBody($this->generateBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateBody() {
|
||||||
|
return new HellcaseBattlesNotificationBody($this->battle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): Stringifiable {
|
||||||
|
return new StringifiableSimpleText("Nouvelle bataille de caisses Hellcase");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function getImageProjectPath(): string|null {
|
||||||
|
return HellcaseBattleScreenshot::getImgFileProjectPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function getLinkURL(): string|null {
|
||||||
|
return $this->battle->getUrl();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Browser\Jobs\HellcaseBattles;
|
||||||
|
|
||||||
|
use App\Models\HellcaseBattle;
|
||||||
|
use App\Notification\NotificationBody;
|
||||||
|
|
||||||
|
class HellcaseBattlesNotificationBody extends NotificationBody {
|
||||||
|
|
||||||
|
private HellcaseBattle $battle;
|
||||||
|
|
||||||
|
public function __construct(HellcaseBattle $battle) {
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
$this->battle = $battle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function toMarkdownString(): string {
|
||||||
|
return "
|
||||||
|
- Valeur : **{$this->battle->value} €**
|
||||||
|
- Lien : **{$this->battle->getUrl()}**
|
||||||
|
";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function toString(): string {
|
||||||
|
return "
|
||||||
|
- Valeur : {$this->battle->value} €
|
||||||
|
- Lien : {$this->battle->getUrl()}
|
||||||
|
";
|
||||||
|
}
|
||||||
|
}
|
54
app/Browser/Jobs/InstagramRepost/IInstagramVideo.php
Normal file
54
app/Browser/Jobs/InstagramRepost/IInstagramVideo.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Browser\Jobs\InstagramRepost;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for Instagram video handling.
|
||||||
|
* Holds the data of a video from Instagram.
|
||||||
|
*/
|
||||||
|
interface IInstagramVideo
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the URL of the video.
|
||||||
|
*
|
||||||
|
* @return string The URL of the video.
|
||||||
|
*/
|
||||||
|
public function getUrl(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the title of the video.
|
||||||
|
*
|
||||||
|
* @return string The title of the video.
|
||||||
|
*/
|
||||||
|
public function getTitle(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the caption of the video.
|
||||||
|
*
|
||||||
|
* @return string The caption of the video.
|
||||||
|
*/
|
||||||
|
public function getDescription(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the date when the video was posted.
|
||||||
|
*
|
||||||
|
* @return ?DateTimeImmutable The date of the post.
|
||||||
|
*/
|
||||||
|
public function getPostDate(): ?DateTimeImmutable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filename of the video.
|
||||||
|
*
|
||||||
|
* @return string The filename of the video.
|
||||||
|
*/
|
||||||
|
public function getFilename(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the filename of the video.
|
||||||
|
*
|
||||||
|
* @param string $filename The filename to set.
|
||||||
|
*/
|
||||||
|
public function setFilename(string $filename): void;
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Browser\Jobs\InstagramRepost;
|
||||||
|
|
||||||
|
use App\Models\JobRun;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
interface IInstagramVideoDownloader
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Download the video from the given Instagram post URL.
|
||||||
|
*
|
||||||
|
* @param int $jobId The ID of the job for which the video is being downloaded.
|
||||||
|
* @param JobRun $jobRun The job run instance for logging and tracking.
|
||||||
|
* @param string $postUrl The URL of the Instagram post.
|
||||||
|
* @param Collection $jobInfos The job information collection.
|
||||||
|
* @param string $downloadFolder The folder where the video should be downloaded.
|
||||||
|
* @param string $accountEmail The email or username of the Instagram account.
|
||||||
|
* @param string $accountPassword The password for the Instagram account.
|
||||||
|
* @return IInstagramVideo|null The path to the downloaded video file, or null if the download failed.
|
||||||
|
*/
|
||||||
|
public function downloadVideo(int $jobId, JobRun $jobRun, string $postUrl, string $downloadFolder, string $accountEmail, string $accountPassword): ?IInstagramVideo;
|
||||||
|
}
|
439
app/Browser/Jobs/InstagramRepost/InstagramRepostJob.php
Normal file
439
app/Browser/Jobs/InstagramRepost/InstagramRepostJob.php
Normal file
@ -0,0 +1,439 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Browser\Jobs\InstagramRepost;
|
||||||
|
|
||||||
|
use App\Browser\BrowserJob;
|
||||||
|
use App\Browser\JobDebugScreenshot;
|
||||||
|
use App\Models\InstagramRepost;
|
||||||
|
use App\Models\InstagramAccount;
|
||||||
|
use App\Models\Job;
|
||||||
|
use App\Models\JobArtifact;
|
||||||
|
use App\Models\JobRun;
|
||||||
|
use App\Notification\Notifications\JobDebugNotification;
|
||||||
|
use App\Notification\Providers\AllNotification;
|
||||||
|
use Facebook\WebDriver\WebDriverBy;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Laravel\Dusk\Browser;
|
||||||
|
|
||||||
|
class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
|
||||||
|
{
|
||||||
|
// === CONFIGURATION ===
|
||||||
|
// TODO : put that in laravel config file
|
||||||
|
/**
|
||||||
|
* Maximum number of posts to repost per account
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private const MAX_REPOSTS_PER_ACCOUNT = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max number of reposts per job
|
||||||
|
* This is the maximum number of posts that will be reposted in a single job run.
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private const MAX_REPOSTS_PER_JOB = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of tries to repost a reel
|
||||||
|
* If a reel fails to be reposted, it will be retried up to this number of times.
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private const MAX_REPOST_TRIES = 3;
|
||||||
|
|
||||||
|
private const APPROXIMATIVE_RUNNING_MINUTES = 2;
|
||||||
|
|
||||||
|
private Collection $jobInfos;
|
||||||
|
protected JobRun $jobRun;
|
||||||
|
|
||||||
|
protected IInstagramVideoDownloader $videoDownloader;
|
||||||
|
|
||||||
|
protected string $downloadFolder = "app/Browser/downloads/InstagramRepost/";
|
||||||
|
|
||||||
|
public function __construct($jobId = 4)
|
||||||
|
{
|
||||||
|
parent::__construct($jobId);
|
||||||
|
|
||||||
|
$this->downloadFolder = base_path($this->downloadFolder);
|
||||||
|
$this->videoDownloader = new YTDLPDownloader();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run(Browser $browser): ?JobRun
|
||||||
|
{
|
||||||
|
Log::info("Running InstagramRepostJob");
|
||||||
|
$this->jobInfos = Job::find($this->jobId)->jobInfosTable();
|
||||||
|
$this->jobRun = new JobRun([
|
||||||
|
"job_id" => $this->jobId,
|
||||||
|
"success" => false,
|
||||||
|
]);
|
||||||
|
$this->jobRun->save();
|
||||||
|
|
||||||
|
$browser->visit('https://instagram.com');
|
||||||
|
sleep(5);
|
||||||
|
$this->removePopups($browser);
|
||||||
|
sleep(2);
|
||||||
|
$this->signin($browser);
|
||||||
|
sleep(2);
|
||||||
|
$this->repostLatestPosts($browser);
|
||||||
|
sleep(5);
|
||||||
|
|
||||||
|
$this->jobRun->success = true;
|
||||||
|
$this->jobRun->save();
|
||||||
|
|
||||||
|
Log::info("InstagramRepostJob run ended");
|
||||||
|
|
||||||
|
return $this->jobRun;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function runTest(Browser $browser): ?JobRun
|
||||||
|
{
|
||||||
|
$this->jobInfos = Job::find($this->jobId)->jobInfosTable();
|
||||||
|
try {
|
||||||
|
$browser->visit('https://instagram.com');
|
||||||
|
sleep(2);
|
||||||
|
$this->removePopups($browser);
|
||||||
|
sleep(2);
|
||||||
|
$this->signin($browser);
|
||||||
|
sleep(3);
|
||||||
|
return $this->makeSimpleJobRun(
|
||||||
|
true,
|
||||||
|
"Connexion réussie",
|
||||||
|
"Datboi a réussi à se connecter sur Instagram"
|
||||||
|
);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->makeSimpleJobRun(
|
||||||
|
false,
|
||||||
|
"Connexion échouée",
|
||||||
|
"Datboi n'a pas réussi à se connecter sur Instagram :\n" . $e->getMessage()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function repostLatestPosts(Browser $browser) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Download the latest reels from the accounts specified in the job infos
|
||||||
|
$toDownloadReels = []; // Array to store to download reels to post later
|
||||||
|
|
||||||
|
$accounts = explode(",", $this->jobInfos->get("instagram_repost_accounts"));
|
||||||
|
foreach ($accounts as $account) {
|
||||||
|
$account = trim($account);
|
||||||
|
|
||||||
|
$toDownloadReels = array_merge($toDownloadReels, $this->getLatestReelsFromAccount($browser, $account));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add unreposted reels to the job run if not enough reels were downloaded
|
||||||
|
if (count($toDownloadReels) < self::MAX_REPOSTS_PER_JOB) {
|
||||||
|
$toDownloadReelsIds = array_map(function ($reel) {
|
||||||
|
return $reel->reel_id;
|
||||||
|
}, $toDownloadReels);
|
||||||
|
|
||||||
|
$unrepostedReels = InstagramRepost::where("reposted", false)
|
||||||
|
->where("repost_tries", "<", self::MAX_REPOST_TRIES) // Limit to 3 tries
|
||||||
|
->whereNotIn("reel_id", $toDownloadReelsIds) // Avoid already downloaded reels
|
||||||
|
->whereIn("account_id", InstagramAccount::whereIn("username", $accounts)->pluck("id"))
|
||||||
|
->take(self::MAX_REPOSTS_PER_JOB - count($toDownloadReels))
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($unrepostedReels as $reel) {
|
||||||
|
$toDownloadReels[] = $reel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shuffling and keeping only the x first posts
|
||||||
|
shuffle($toDownloadReels);
|
||||||
|
$toDownloadReels = array_slice($toDownloadReels, 0, self::MAX_REPOSTS_PER_JOB);
|
||||||
|
|
||||||
|
$this->jobRun->addArtifact(new JobArtifact([
|
||||||
|
"name" => count($toDownloadReels) . " reels sélectionnés pour être repost",
|
||||||
|
"content" => ""
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Download the reels
|
||||||
|
$downloadedReels = [];
|
||||||
|
foreach ($toDownloadReels as $repost) {
|
||||||
|
$downloadedReels[] = [
|
||||||
|
$repost,
|
||||||
|
$this->downloadReel(
|
||||||
|
$browser,
|
||||||
|
$repost
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->jobRun->addArtifact(new JobArtifact([
|
||||||
|
"name" => count($downloadedReels) . " reels sélectionnés qui ont été téléchargés",
|
||||||
|
"content" => ""
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Now repost all downloaded reels
|
||||||
|
$repostedReelsCounter = 0;
|
||||||
|
foreach ($downloadedReels as $infos) {
|
||||||
|
$reel = $infos[0];
|
||||||
|
$videoInfo = $infos[1];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO : Avoid getting the reel from the db again, store it in the downloadedReels array
|
||||||
|
$repostedReelsCounter += $this->repostReel($browser, InstagramRepost::where('reel_id', $reel->reel_id)->first(), $videoInfo);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error("Failed to repost reel: {$videoInfo->getTitle()} - " . $e->getMessage());
|
||||||
|
AllNotification::send(new JobDebugNotification($this->jobId, "Failed to repost reel: {$videoInfo->getTitle()} - " . $e->getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->jobRun->addArtifact(new JobArtifact([
|
||||||
|
"name" => $repostedReelsCounter . " reels sélectionnés qui ont été repostés",
|
||||||
|
"content" => ""
|
||||||
|
]));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
|
||||||
|
AllNotification::send(new JobDebugNotification($this->jobId, "Unknown error when trying to repost reels : " . $e->getMessage()));
|
||||||
|
} finally {
|
||||||
|
// Removes all videos in the download folder
|
||||||
|
$files = glob($this->downloadFolder . '*'); // Get all files in the download folder
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (is_file($file)) {
|
||||||
|
unlink($file); // Delete the file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getLatestReelsFromAccount(Browser $browser, string $account): array
|
||||||
|
{
|
||||||
|
$accountReels = []; // Indexed array to store new reels from the account
|
||||||
|
|
||||||
|
$browser->visit("https://instagram.com/{$account}/reels");
|
||||||
|
sleep(3);
|
||||||
|
$browser->waitForText("followers", 10, true);
|
||||||
|
|
||||||
|
// If we are here, the account exists
|
||||||
|
$accountModel = InstagramAccount::where("username", $account)->first();
|
||||||
|
if ($accountModel == null) { // Does not exist in the database yet
|
||||||
|
$accountModel = new InstagramAccount([
|
||||||
|
"username" => $account,
|
||||||
|
]);
|
||||||
|
$accountModel->save();
|
||||||
|
Log::info("New Instagram account added: {$account}");
|
||||||
|
} else {
|
||||||
|
Log::debug("Instagram account already exists: {$account}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$repostedPosts = $accountModel->reposts()->where("reposted", true)->pluck("reel_id");
|
||||||
|
|
||||||
|
// Posts must be sorted by latest post date first
|
||||||
|
// TODO : Scroll when not enough post are shown
|
||||||
|
$posts = $browser->driver->findElements(WebDriverBy::xpath('//a[contains(@href, "'.$account.'/reel/")][not(.//*[local-name() = "svg"][@aria-label="Pinned post icon"])]'));
|
||||||
|
|
||||||
|
foreach ($posts as $post) {
|
||||||
|
$postUrl = $post->getAttribute('href');
|
||||||
|
$postId = explode("/", $postUrl)[3] ?? null;
|
||||||
|
|
||||||
|
if ($postId === null) {
|
||||||
|
AllNotification::send(new JobDebugNotification($this->jobId, "Can't get Instagram post ID from url"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Break the loop if the post has already been reposted
|
||||||
|
if ($repostedPosts->contains($postId)) {
|
||||||
|
Log::debug("Post already reposted: {$postUrl}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reelModel = InstagramRepost::firstOrCreate(
|
||||||
|
["reel_id" => $postId, "account_id" => $accountModel->id],
|
||||||
|
["reposted" => false, "repost_tries" => 0]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (count($accountReels) < self::MAX_REPOSTS_PER_ACCOUNT) {
|
||||||
|
$accountReels[] = $reelModel; // Add it to the to be downloaded reels array
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $accountReels;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function downloadReel(Browser $browser, InstagramRepost $reel): ?IInstagramVideo
|
||||||
|
{
|
||||||
|
$videoInfo = $this->videoDownloader->downloadVideo(
|
||||||
|
$this->jobId,
|
||||||
|
$this->jobRun,
|
||||||
|
$reel->getUrl(),
|
||||||
|
$this->downloadFolder,
|
||||||
|
$this->jobInfos->get("instagram_repost_account_email"),
|
||||||
|
$this->jobInfos->get("instagram_repost_account_password")
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($videoInfo === null) {
|
||||||
|
Log::error("Failed to download video for post: {$reel->reel_id}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Set the filename to the post ID
|
||||||
|
$newFilename = $this->downloadFolder . $reel->reel_id . ".mp4";
|
||||||
|
rename($videoInfo->getFilename(), $newFilename);
|
||||||
|
$videoInfo->setFilename($newFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info("Downloaded video: {$videoInfo->getTitle()} : {$videoInfo->getDescription()}");
|
||||||
|
|
||||||
|
return $videoInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function repostReel(Browser $browser, InstagramRepost $reel, IInstagramVideo $videoInfo): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
Log::info("Reposting reel: {$reel->reel_id} - {$videoInfo->getTitle()}");
|
||||||
|
|
||||||
|
// Increment the repost tries
|
||||||
|
$reel->repost_tries++;
|
||||||
|
$reel->save();
|
||||||
|
|
||||||
|
// TODO Reset if a problem occurs and try again with a limit of 3 attempts
|
||||||
|
$browser->visit('https://instagram.com');
|
||||||
|
sleep(2);
|
||||||
|
|
||||||
|
// Navigate to the reel upload page
|
||||||
|
$createButton = $browser->driver->findElement(WebDriverBy::xpath('//a[./div//span[contains(text(), "Create")]]'));
|
||||||
|
$createButton->click();
|
||||||
|
sleep(2);
|
||||||
|
$newPostButton = $browser->driver->findElement(WebDriverBy::xpath('//a[./div//span[contains(text(), "Post")]][@href="#"]'));
|
||||||
|
$newPostButton->click();
|
||||||
|
sleep(3);
|
||||||
|
|
||||||
|
// Upload the video file
|
||||||
|
$selectFileButton = $browser->driver->findElement(WebDriverBy::xpath('//button[contains(text(), "Select from computer")]'));
|
||||||
|
$selectFileButton->click();
|
||||||
|
sleep(2);
|
||||||
|
$browser->attach('input[type="file"]._ac69', $this->downloadFolder . $reel->reel_id . ".mp4");
|
||||||
|
|
||||||
|
sleep(10); // TODO : Wait for the file to be uploaded
|
||||||
|
|
||||||
|
$this->removePopups($browser);
|
||||||
|
sleep(2);
|
||||||
|
|
||||||
|
// Put original resolution
|
||||||
|
$this->putOriginalResolution($browser);
|
||||||
|
|
||||||
|
$this->clickNext($browser);
|
||||||
|
$this->clickNext($browser); // Skip cover photo and trim
|
||||||
|
|
||||||
|
// Add a caption
|
||||||
|
$captionInput = $browser->driver->findElement(WebDriverBy::xpath('//div[@contenteditable]'));
|
||||||
|
$captionInput->sendKeys($videoInfo->getDescription());
|
||||||
|
|
||||||
|
sleep(2); // Wait for the caption to be added
|
||||||
|
|
||||||
|
$this->clickNext($browser); // Share the post
|
||||||
|
|
||||||
|
sleep(5); // Wait for the post to be completed
|
||||||
|
|
||||||
|
// Check if the post was successful
|
||||||
|
try {
|
||||||
|
$browser->waitForText("Your reel has been shared.", 60, true);
|
||||||
|
Log::info("Reel reposted successfully: {$reel->reel_id}");
|
||||||
|
|
||||||
|
// Mark the reel as reposted in the database
|
||||||
|
$reel->reposted = true;
|
||||||
|
$reel->save();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
try {
|
||||||
|
$browser->waitForText("Your post was shared", 60, true);
|
||||||
|
$closeButton = $browser->driver->findElement(WebDriverBy::xpath('//div[./div/*[local-name() = "svg"][@aria-label="Close"]]'));
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error("Failed to repost reel: {$reel->reel_id} - " . $e->getMessage());
|
||||||
|
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
|
||||||
|
AllNotification::send(new JobDebugNotification($this->jobId, "Failed to repost reel: {$reel->reel_id} - " . $e->getMessage()));
|
||||||
|
$closeButton->click();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function clickNext(Browser $browser) {
|
||||||
|
$nextButton = $browser->driver->findElement(WebDriverBy::xpath('//div[contains(text(), "Next") or contains(text(), "Share")]'));
|
||||||
|
$nextButton->click();
|
||||||
|
sleep(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function putOriginalResolution(Browser $browser)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$chooseResolutionButton = $browser->driver->findElement(WebDriverBy::xpath('//button[./div/*[local-name() = "svg"][@aria-label="Select crop"]]'));
|
||||||
|
$chooseResolutionButton->click();
|
||||||
|
sleep(2);
|
||||||
|
// Choos "original" resolution
|
||||||
|
$originalResolutionButton = $browser->driver->findElement(WebDriverBy::xpath('//div[./div/div/span[contains(text(), "Original")]]'));
|
||||||
|
$originalResolutionButton->click();
|
||||||
|
sleep(2);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error("Failed to set original resolution: " . $e->getMessage());
|
||||||
|
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
|
||||||
|
AllNotification::send(new JobDebugNotification($this->jobId, "Failed to set \"original\" resolution: " . $e->getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function signin(Browser $browser)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$browser->waitForText("Log in", 10, true);
|
||||||
|
sleep(3);
|
||||||
|
$emailButton = $browser->driver->findElement(WebDriverBy::xpath('//input[contains(@aria-label, "email")]'));
|
||||||
|
$emailButton->click();
|
||||||
|
$emailButton->sendKeys($this->jobInfos->get("instagram_repost_account_email"));
|
||||||
|
sleep(3);
|
||||||
|
$passwordButton = $browser->driver->findElement(WebDriverBy::xpath('//input[contains(@aria-label, "Password")]'));
|
||||||
|
$passwordButton->click();
|
||||||
|
$passwordButton->sendKeys($this->jobInfos->get("instagram_repost_account_password") . "\n");
|
||||||
|
sleep(5);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Probably no need to signin
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$browser->waitForText("Search", 15, true);
|
||||||
|
$this->removePopups($browser);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error("Failed to sign in: " . $e->getMessage());
|
||||||
|
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
|
||||||
|
AllNotification::send(new JobDebugNotification($this->jobId, "Failed to sign in: " . $e->getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function removePopups(Browser $browser)
|
||||||
|
{
|
||||||
|
$popupsTypes = [
|
||||||
|
['//button[contains(text(), "Allow all cookies")]'], // Allow all cookies
|
||||||
|
['//button[contains(text(), "Not Now")]', ["Popup Not Now clicked"]], // Not now
|
||||||
|
['//button[contains(text(), "OK")]', ["Popup Ok clicked"]], // OK
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($popupsTypes as $popup) {
|
||||||
|
try {
|
||||||
|
$button = $browser->driver->findElement(WebDriverBy::xpath($popup[0]));
|
||||||
|
if ($button === null) {
|
||||||
|
continue; // No button found, continue to the next popup
|
||||||
|
}
|
||||||
|
if (isset($popup[1])) {
|
||||||
|
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
|
||||||
|
AllNotification::send(new JobDebugNotification($this->jobId, $popup[1][0]));
|
||||||
|
}
|
||||||
|
$button->click();
|
||||||
|
sleep(2);
|
||||||
|
return; // Exit after clicking the first popup found
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Porbably no popup found, continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
app/Browser/Jobs/InstagramRepost/YTDLPDownloader.php
Normal file
56
app/Browser/Jobs/InstagramRepost/YTDLPDownloader.php
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Browser\Jobs\InstagramRepost;
|
||||||
|
|
||||||
|
use App\Models\JobArtifact;
|
||||||
|
use App\Models\JobRun;
|
||||||
|
use App\Notification\Notifications\JobDebugNotification;
|
||||||
|
use App\Notification\Providers\AllNotification;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use YoutubeDl\Options;
|
||||||
|
use YoutubeDl\YoutubeDl;
|
||||||
|
|
||||||
|
class YTDLPDownloader implements IInstagramVideoDownloader
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function downloadVideo(int $jobId, JobRun $jobRun, string $postUrl, string $downloadFolder, string $accountEmail, string $accountPassword): ?YTDLPVideo
|
||||||
|
{
|
||||||
|
$dl = new YoutubeDl();
|
||||||
|
$options = Options::create()
|
||||||
|
->downloadPath($downloadFolder)
|
||||||
|
->apLogin($accountEmail, $accountPassword)
|
||||||
|
->checkAllFormats(true)
|
||||||
|
//->format('bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]') // TODO allow downloading audio and video separately and then merging them
|
||||||
|
->format('best[ext=mp4]')
|
||||||
|
->url($postUrl);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$videosCollection = $dl->download($options);
|
||||||
|
foreach ($videosCollection->getVideos() as $video) {
|
||||||
|
if ($video->getError() !== null) {
|
||||||
|
$jobRun->addArtifact(new JobArtifact([
|
||||||
|
"name" => "Erreur lors du téléchargement de la vidéo \"{$video->getTitle()}\"",
|
||||||
|
"content" => $video->getError(),
|
||||||
|
]));
|
||||||
|
Log::error("Error downloading video: " . $video->getError());
|
||||||
|
return null; // Return null if there was an error downloading the video
|
||||||
|
} else {
|
||||||
|
$IVideo = new YTDLPVideo(
|
||||||
|
$video->getWebpageUrl(),
|
||||||
|
$video->getTitle(),
|
||||||
|
$video->getDescription() ?? "",
|
||||||
|
$video->getUploadDate(),
|
||||||
|
$video->getFilename(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $IVideo; // Return the video object if download was successful
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
AllNotification::send(new JobDebugNotification($jobId, "Error while downloading video: " . $e->getMessage()));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
71
app/Browser/Jobs/InstagramRepost/YTDLPVideo.php
Normal file
71
app/Browser/Jobs/InstagramRepost/YTDLPVideo.php
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Browser\Jobs\InstagramRepost;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
|
||||||
|
class YTDLPVideo implements IInstagramVideo
|
||||||
|
{
|
||||||
|
private string $url;
|
||||||
|
private string $title;
|
||||||
|
private string $description;
|
||||||
|
private ?DateTimeImmutable $postDate;
|
||||||
|
private string $fileName;
|
||||||
|
|
||||||
|
public function __construct(string $url, string $title, string $description, ?DateTimeImmutable $postDate, string $filename)
|
||||||
|
{
|
||||||
|
$this->url = $url;
|
||||||
|
$this->title = $title;
|
||||||
|
$this->description = $description;
|
||||||
|
$this->postDate = $postDate;
|
||||||
|
$this->fileName = $filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function getUrl(): string
|
||||||
|
{
|
||||||
|
return $this->url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return $this->title;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function getPostDate(): ?DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->postDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function getFilename(): string
|
||||||
|
{
|
||||||
|
return $this->fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function setFilename(string $filename): void
|
||||||
|
{
|
||||||
|
$this->fileName = $filename;
|
||||||
|
}
|
||||||
|
}
|
19
app/Models/InstagramAccount.php
Normal file
19
app/Models/InstagramAccount.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class InstagramAccount extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'instagram_repost_accounts';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'username',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function reposts()
|
||||||
|
{
|
||||||
|
return $this->hasMany(InstagramRepost::class, 'account_id');
|
||||||
|
}
|
||||||
|
}
|
26
app/Models/InstagramRepost.php
Normal file
26
app/Models/InstagramRepost.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class InstagramRepost extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'instagram_reposts';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'reel_id',
|
||||||
|
'reposted',
|
||||||
|
'account_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function account()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(InstagramAccount::class, 'account_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUrl(): string
|
||||||
|
{
|
||||||
|
return "https://www.instagram.com/reel/{$this->reel_id}/";
|
||||||
|
}
|
||||||
|
}
|
@ -3,5 +3,5 @@
|
|||||||
namespace App\Notification;
|
namespace App\Notification;
|
||||||
|
|
||||||
abstract class NotificationProvider {
|
abstract class NotificationProvider {
|
||||||
abstract public static function send(Notification $notification): void;
|
abstract public static function send(Notification $notification, array $options): void;
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ class JobDebugNotification extends Notification {
|
|||||||
private string|null $title;
|
private string|null $title;
|
||||||
private string|null $screenShotProjectPath;
|
private string|null $screenShotProjectPath;
|
||||||
|
|
||||||
public function __construct(int $jobId, string $body, string $title = null, string $error = null, ?string $screenshotProjectPath = "", bool $isError = false) {
|
public function __construct(int $jobId, string $body, string $title = null, string $error = null, ?string $screenshotProjectPath = "", bool $isError = true) {
|
||||||
parent::__construct($jobId, isError:$isError);
|
parent::__construct($jobId, isError:$isError);
|
||||||
$this->title = $title;
|
$this->title = $title;
|
||||||
if ($screenshotProjectPath === "") {
|
if ($screenshotProjectPath === "") {
|
||||||
|
@ -12,9 +12,9 @@ class AllNotification extends NotificationProvider {
|
|||||||
/**
|
/**
|
||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
public static function send(\App\Notification\Notification $notification): void {
|
public static function send(\App\Notification\Notification $notification, array $options = []): void {
|
||||||
foreach (self::NOTIFICATIONS_PROVIDERS as $provider) {
|
foreach (self::NOTIFICATIONS_PROVIDERS as $provider) {
|
||||||
$provider::send($notification);
|
$provider::send($notification, $options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ class DiscordWebHookNotification extends NotificationProvider {
|
|||||||
/**
|
/**
|
||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
public static function send(\App\Notification\Notification $notification): void {
|
public static function send(\App\Notification\Notification $notification, array $options): void {
|
||||||
/*
|
/*
|
||||||
Test Json for a complete embed :
|
Test Json for a complete embed :
|
||||||
{
|
{
|
||||||
@ -44,7 +44,7 @@ class DiscordWebHookNotification extends NotificationProvider {
|
|||||||
"avatar_url": "https://www.fairytailrp.com/t40344-here-come-dat-boi"
|
"avatar_url": "https://www.fairytailrp.com/t40344-here-come-dat-boi"
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
$webHookUrl = static::getDiscordWebHookUrl($notification->isError);
|
$webHookUrl = static::getDiscordWebHookUrl($notification->isError, $options);
|
||||||
$body = [
|
$body = [
|
||||||
"content"=> "",
|
"content"=> "",
|
||||||
"tts"=> false,
|
"tts"=> false,
|
||||||
@ -73,20 +73,28 @@ class DiscordWebHookNotification extends NotificationProvider {
|
|||||||
'payload_json' => $payloadJson,
|
'payload_json' => $payloadJson,
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($notification->getImageURL() !== null) {
|
if ($notification->getImageURL() !== null && is_file($notification->getImageProjectPath())) {
|
||||||
$formData['file'] = curl_file_create($notification->getImageProjectPath(), 'image/png', 'image.png');
|
|
||||||
}
|
}
|
||||||
|
$formData['file'] = curl_file_create($notification->getImageProjectPath(), 'image/png', 'image.png');
|
||||||
|
|
||||||
$ch = curl_init($webHookUrl);
|
$ch = curl_init($webHookUrl);
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-type: multipart/form-data'));
|
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-type: multipart/form-data'));
|
||||||
curl_setopt($ch, CURLOPT_POST, 1);
|
curl_setopt($ch, CURLOPT_POST, 1);
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $formData);
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $formData);
|
||||||
// curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
// curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||||
$response = curl_exec($ch);
|
if (!curl_exec($ch)) {
|
||||||
|
$error = curl_error($ch);
|
||||||
|
\Log::error("Discord WebHook Notification failed: {$error}");
|
||||||
|
throw new \Exception("Discord WebHook Notification failed: {$error}");
|
||||||
|
}
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function getDiscordWebHookUrl(bool $isError): string {
|
private static function getDiscordWebHookUrl(bool $isError, array $options): string {
|
||||||
|
if (isset($options["discord_webhook_url"]) && $options["discord_webhook_url"] !== null) {
|
||||||
|
return $options["discord_webhook_url"];
|
||||||
|
}
|
||||||
|
|
||||||
$generalWebHookUrlKey = 'discord_webhook_url';
|
$generalWebHookUrlKey = 'discord_webhook_url';
|
||||||
$generalWebHookUrl = Cache::rememberForever($generalWebHookUrlKey, function () use ($generalWebHookUrlKey) {
|
$generalWebHookUrl = Cache::rememberForever($generalWebHookUrlKey, function () use ($generalWebHookUrlKey) {
|
||||||
return JobInfo::where('key', $generalWebHookUrlKey)->first()->value;
|
return JobInfo::where('key', $generalWebHookUrlKey)->first()->value;
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.3",
|
||||||
"erusev/parsedown": "^1.7",
|
"erusev/parsedown": "^1.7",
|
||||||
"inertiajs/inertia-laravel": "^2.0",
|
"inertiajs/inertia-laravel": "^2.0",
|
||||||
"laravel/dusk": "^8.2",
|
"laravel/dusk": "^8.2",
|
||||||
@ -18,6 +18,7 @@
|
|||||||
"laravel/sanctum": "^4.0",
|
"laravel/sanctum": "^4.0",
|
||||||
"laravel/telescope": "^5.5",
|
"laravel/telescope": "^5.5",
|
||||||
"laravel/tinker": "^2.9",
|
"laravel/tinker": "^2.9",
|
||||||
|
"norkunas/youtube-dl-php": "dev-master",
|
||||||
"tightenco/ziggy": "^2.0"
|
"tightenco/ziggy": "^2.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
137
composer.lock
generated
137
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "6008577001548e6e63c074be98000d97",
|
"content-hash": "9a964008040d9ce219547515fe65dd86",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
@ -3011,6 +3011,69 @@
|
|||||||
},
|
},
|
||||||
"time": "2024-12-30T11:07:19+00:00"
|
"time": "2024-12-30T11:07:19+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "norkunas/youtube-dl-php",
|
||||||
|
"version": "dev-master",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/norkunas/youtube-dl-php.git",
|
||||||
|
"reference": "4c954b3b8c6b30d0c0135ec758b61a91f0ac3b6a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/norkunas/youtube-dl-php/zipball/4c954b3b8c6b30d0c0135ec758b61a91f0ac3b6a",
|
||||||
|
"reference": "4c954b3b8c6b30d0c0135ec758b61a91f0ac3b6a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-json": "*",
|
||||||
|
"php": ">=7.4.0",
|
||||||
|
"symfony/filesystem": "^5.1|^6.0|^7.0",
|
||||||
|
"symfony/polyfill-php80": "^1.28",
|
||||||
|
"symfony/process": "^5.1|^6.0|^7.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"mikey179/vfsstream": "^1.6.11",
|
||||||
|
"php-cs-fixer/shim": "^3.60",
|
||||||
|
"phpstan/phpstan": "^1.11.8",
|
||||||
|
"phpstan/phpstan-phpunit": "^1.4.0",
|
||||||
|
"phpstan/phpstan-strict-rules": "^1.6.0",
|
||||||
|
"symfony/phpunit-bridge": "^6.4.10"
|
||||||
|
},
|
||||||
|
"default-branch": true,
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.0-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"YoutubeDl\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Tomas Norkūnas",
|
||||||
|
"email": "norkunas.tom@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "youtube-dl / yt-dlp wrapper for php",
|
||||||
|
"keywords": [
|
||||||
|
"youtube",
|
||||||
|
"youtube-dl",
|
||||||
|
"yt-dlp"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/norkunas/youtube-dl-php/issues",
|
||||||
|
"source": "https://github.com/norkunas/youtube-dl-php/tree/v2.10.0"
|
||||||
|
},
|
||||||
|
"time": "2025-02-20T17:32:37+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "nunomaduro/termwind",
|
"name": "nunomaduro/termwind",
|
||||||
"version": "v2.3.0",
|
"version": "v2.3.0",
|
||||||
@ -5211,6 +5274,72 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-09-25T14:20:29+00:00"
|
"time": "2024-09-25T14:20:29+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/filesystem",
|
||||||
|
"version": "v7.3.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/filesystem.git",
|
||||||
|
"reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb",
|
||||||
|
"reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.2",
|
||||||
|
"symfony/polyfill-ctype": "~1.8",
|
||||||
|
"symfony/polyfill-mbstring": "~1.8"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/process": "^6.4|^7.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\Filesystem\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Provides basic utilities for the filesystem",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/filesystem/tree/v7.3.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2024-10-25T15:15:23+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/finder",
|
"name": "symfony/finder",
|
||||||
"version": "v7.2.2",
|
"version": "v7.2.2",
|
||||||
@ -9591,11 +9720,13 @@
|
|||||||
],
|
],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"stability-flags": [],
|
"stability-flags": {
|
||||||
|
"norkunas/youtube-dl-php": 20
|
||||||
|
},
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": {
|
"platform": {
|
||||||
"php": "^8.2"
|
"php": "^8.3"
|
||||||
},
|
},
|
||||||
"platform-dev": [],
|
"platform-dev": [],
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.6.0"
|
||||||
|
@ -0,0 +1,114 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\InstagramAccount;
|
||||||
|
use App\Models\InstagramRepost;
|
||||||
|
use App\Models\Job;
|
||||||
|
use App\Models\JobInfo;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
$newJobId = 4;
|
||||||
|
Job::forceCreate([
|
||||||
|
"id" => $newJobId,
|
||||||
|
"name" => "Instagram Repost",
|
||||||
|
"description" => "Reposte les publications Instagram des comptes donnés.",
|
||||||
|
]);
|
||||||
|
|
||||||
|
JobInfo::forceCreate([
|
||||||
|
"key" => "instagram_repost_accounts",
|
||||||
|
"name" => "Comptes Instagram à reposter",
|
||||||
|
"description" => "Liste des noms des comptes Instagram à partir desquels les publications seront repostées.\nSéparez les comptes par des virgules.",
|
||||||
|
"placeholder" => "is.it.ninluc, freddiedredd",
|
||||||
|
"is_required" => true,
|
||||||
|
"job_info_type_id" => 1,
|
||||||
|
"job_id" => $newJobId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
JobInfo::forceCreate([
|
||||||
|
"key" => "instagram_repost_account_email",
|
||||||
|
"name" => "Identifiant",
|
||||||
|
"description" => "L'adresse e-mail/nom d'utilisateur/N° de téléphone utilisée pour le compte Instagram de repost.",
|
||||||
|
"is_required" => true,
|
||||||
|
"job_info_type_id" => 1,
|
||||||
|
"job_id" => $newJobId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
JobInfo::forceCreate([
|
||||||
|
"key" => "instagram_repost_account_password",
|
||||||
|
"name" => "Mot de passe",
|
||||||
|
"description" => "Le mot de passe utilisée pour le compte Instagram de repost.",
|
||||||
|
"is_required" => true,
|
||||||
|
"job_info_type_id" => 3,
|
||||||
|
"job_id" => $newJobId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Schema::create('instagram_repost_accounts', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->string("username")->unique();
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('instagram_reposts', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->string("reel_id")->unique();
|
||||||
|
$table->boolean("reposted")->default(false);
|
||||||
|
$table->integer("repost_tries")->default(0);
|
||||||
|
$table->foreignIdFor(InstagramAccount::class, "account_id")
|
||||||
|
->constrained('instagram_repost_accounts')
|
||||||
|
->cascadeOnDelete();
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Already reposted posts
|
||||||
|
$notDeadLmaoAccount = InstagramAccount::forceCreate([
|
||||||
|
"username" => "notdeadlmao69",
|
||||||
|
]);
|
||||||
|
$negusflexAccount = InstagramAccount::forceCreate([
|
||||||
|
"username" => "negusflex",
|
||||||
|
]);
|
||||||
|
InstagramRepost::forceCreate([
|
||||||
|
"reel_id" => "DKbW7M_RWV7",
|
||||||
|
"reposted" => true,
|
||||||
|
"account_id" => $notDeadLmaoAccount->id,
|
||||||
|
]);
|
||||||
|
InstagramRepost::forceCreate([
|
||||||
|
"reel_id" => "DKccuTMTmP_",
|
||||||
|
"reposted" => true,
|
||||||
|
"account_id" => $negusflexAccount->id,
|
||||||
|
]);
|
||||||
|
InstagramRepost::forceCreate([
|
||||||
|
"reel_id" => "DJmUjhWSnqm",
|
||||||
|
"reposted" => true,
|
||||||
|
"account_id" => $negusflexAccount->id,
|
||||||
|
]);
|
||||||
|
InstagramRepost::forceCreate([
|
||||||
|
"reel_id" => "DKcdSGnv6uq",
|
||||||
|
"reposted" => true,
|
||||||
|
"account_id" => $negusflexAccount->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Job::where("id", 4)->delete();
|
||||||
|
JobInfo::where("job_id", 4)->delete();
|
||||||
|
|
||||||
|
Schema::dropIfExists('instagram_repost_accounts');
|
||||||
|
Schema::dropIfExists('instagram_reposts');
|
||||||
|
}
|
||||||
|
};
|
@ -13,7 +13,7 @@ Route::get('/jobs', function (Request $request) {
|
|||||||
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();
|
||||||
dump($log);
|
dump($log);
|
||||||
return response()->json(['message' => 'Job ' . $id . ' ran', 'jobRun' => $log->load('artifacts')]);
|
dump(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');
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use App\Browser\Jobs\Hellcase\HellcaseJob;
|
use App\Browser\Jobs\Hellcase\HellcaseJob;
|
||||||
use App\Browser\Jobs\HellcaseBattles\HellcaseBattlesJob;
|
use App\Browser\Jobs\HellcaseBattles\HellcaseBattlesJob;
|
||||||
|
use App\Browser\Jobs\InstagramRepost\InstagramRepostJob;
|
||||||
use App\Jobs\PruneOldJobRuns;
|
use App\Jobs\PruneOldJobRuns;
|
||||||
use App\Services\BrowserJobsInstances;
|
use App\Services\BrowserJobsInstances;
|
||||||
use Illuminate\Foundation\Inspiring;
|
use Illuminate\Foundation\Inspiring;
|
||||||
@ -23,3 +24,4 @@ Schedule::job(new PruneOldJobRuns)->monthly()->onOneServer()->withoutOverlapping
|
|||||||
Schedule::job(new HellcaseJob)->daily()->onOneServer()->withoutOverlapping()->name('hellcase')->description('Hellcase job');
|
Schedule::job(new HellcaseJob)->daily()->onOneServer()->withoutOverlapping()->name('hellcase')->description('Hellcase job');
|
||||||
// Schedule::job(new HellcaseJob)->everyMinute()->onOneServer()->withoutOverlapping()->name('hellcase')->description('Hellcase job');
|
// Schedule::job(new HellcaseJob)->everyMinute()->onOneServer()->withoutOverlapping()->name('hellcase')->description('Hellcase job');
|
||||||
Schedule::job(new HellcaseBattlesJob)->hourly()->onOneServer()->withoutOverlapping()->name('hellcase_battles')->description('Hellcase battles job');
|
Schedule::job(new HellcaseBattlesJob)->hourly()->onOneServer()->withoutOverlapping()->name('hellcase_battles')->description('Hellcase battles job');
|
||||||
|
Schedule::job(new InstagramRepostJob())->everyThreeHours()->onOneServer()->withoutOverlapping()->name('instagram_reposts')->description('Intagrame reposts job');
|
||||||
|
@ -1,54 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Auth;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class AuthenticationTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_login_screen_can_be_rendered(): void
|
|
||||||
{
|
|
||||||
$response = $this->get('/login');
|
|
||||||
|
|
||||||
$response->assertStatus(200);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_users_can_authenticate_using_the_login_screen(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this->post('/login', [
|
|
||||||
'email' => $user->email,
|
|
||||||
'password' => 'password',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertAuthenticated();
|
|
||||||
$response->assertRedirect(route('dashboard', absolute: false));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_users_can_not_authenticate_with_invalid_password(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$this->post('/login', [
|
|
||||||
'email' => $user->email,
|
|
||||||
'password' => 'wrong-password',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertGuest();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_users_can_logout(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->post('/logout');
|
|
||||||
|
|
||||||
$this->assertGuest();
|
|
||||||
$response->assertRedirect('/');
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Auth;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Auth\Events\Verified;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Illuminate\Support\Facades\Event;
|
|
||||||
use Illuminate\Support\Facades\URL;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class EmailVerificationTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_email_verification_screen_can_be_rendered(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->unverified()->create();
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->get('/verify-email');
|
|
||||||
|
|
||||||
$response->assertStatus(200);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_email_can_be_verified(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->unverified()->create();
|
|
||||||
|
|
||||||
Event::fake();
|
|
||||||
|
|
||||||
$verificationUrl = URL::temporarySignedRoute(
|
|
||||||
'verification.verify',
|
|
||||||
now()->addMinutes(60),
|
|
||||||
['id' => $user->id, 'hash' => sha1($user->email)]
|
|
||||||
);
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->get($verificationUrl);
|
|
||||||
|
|
||||||
Event::assertDispatched(Verified::class);
|
|
||||||
$this->assertTrue($user->fresh()->hasVerifiedEmail());
|
|
||||||
$response->assertRedirect(route('dashboard', absolute: false).'?verified=1');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_email_is_not_verified_with_invalid_hash(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->unverified()->create();
|
|
||||||
|
|
||||||
$verificationUrl = URL::temporarySignedRoute(
|
|
||||||
'verification.verify',
|
|
||||||
now()->addMinutes(60),
|
|
||||||
['id' => $user->id, 'hash' => sha1('wrong-email')]
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->actingAs($user)->get($verificationUrl);
|
|
||||||
|
|
||||||
$this->assertFalse($user->fresh()->hasVerifiedEmail());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Auth;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class PasswordConfirmationTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_confirm_password_screen_can_be_rendered(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->get('/confirm-password');
|
|
||||||
|
|
||||||
$response->assertStatus(200);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_password_can_be_confirmed(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->post('/confirm-password', [
|
|
||||||
'password' => 'password',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response->assertRedirect();
|
|
||||||
$response->assertSessionHasNoErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_password_is_not_confirmed_with_invalid_password(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this->actingAs($user)->post('/confirm-password', [
|
|
||||||
'password' => 'wrong-password',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response->assertSessionHasErrors();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Auth;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Auth\Notifications\ResetPassword;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Illuminate\Support\Facades\Notification;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class PasswordResetTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_reset_password_link_screen_can_be_rendered(): void
|
|
||||||
{
|
|
||||||
$response = $this->get('/forgot-password');
|
|
||||||
|
|
||||||
$response->assertStatus(200);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_reset_password_link_can_be_requested(): void
|
|
||||||
{
|
|
||||||
Notification::fake();
|
|
||||||
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$this->post('/forgot-password', ['email' => $user->email]);
|
|
||||||
|
|
||||||
Notification::assertSentTo($user, ResetPassword::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_reset_password_screen_can_be_rendered(): void
|
|
||||||
{
|
|
||||||
Notification::fake();
|
|
||||||
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$this->post('/forgot-password', ['email' => $user->email]);
|
|
||||||
|
|
||||||
Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
|
|
||||||
$response = $this->get('/reset-password/'.$notification->token);
|
|
||||||
|
|
||||||
$response->assertStatus(200);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_password_can_be_reset_with_valid_token(): void
|
|
||||||
{
|
|
||||||
Notification::fake();
|
|
||||||
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$this->post('/forgot-password', ['email' => $user->email]);
|
|
||||||
|
|
||||||
Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
|
|
||||||
$response = $this->post('/reset-password', [
|
|
||||||
'token' => $notification->token,
|
|
||||||
'email' => $user->email,
|
|
||||||
'password' => 'password',
|
|
||||||
'password_confirmation' => 'password',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response
|
|
||||||
->assertSessionHasNoErrors()
|
|
||||||
->assertRedirect(route('login'));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Auth;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Illuminate\Support\Facades\Hash;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class PasswordUpdateTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_password_can_be_updated(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this
|
|
||||||
->actingAs($user)
|
|
||||||
->from('/profile')
|
|
||||||
->put('/password', [
|
|
||||||
'current_password' => 'password',
|
|
||||||
'password' => 'new-password',
|
|
||||||
'password_confirmation' => 'new-password',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response
|
|
||||||
->assertSessionHasNoErrors()
|
|
||||||
->assertRedirect('/profile');
|
|
||||||
|
|
||||||
$this->assertTrue(Hash::check('new-password', $user->refresh()->password));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_correct_password_must_be_provided_to_update_password(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this
|
|
||||||
->actingAs($user)
|
|
||||||
->from('/profile')
|
|
||||||
->put('/password', [
|
|
||||||
'current_password' => 'wrong-password',
|
|
||||||
'password' => 'new-password',
|
|
||||||
'password_confirmation' => 'new-password',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response
|
|
||||||
->assertSessionHasErrors('current_password')
|
|
||||||
->assertRedirect('/profile');
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Auth;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class RegistrationTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_registration_screen_can_be_rendered(): void
|
|
||||||
{
|
|
||||||
$response = $this->get('/register');
|
|
||||||
|
|
||||||
$response->assertStatus(200);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_new_users_can_register(): void
|
|
||||||
{
|
|
||||||
$response = $this->post('/register', [
|
|
||||||
'name' => 'Test User',
|
|
||||||
'email' => 'test@example.com',
|
|
||||||
'password' => 'password',
|
|
||||||
'password_confirmation' => 'password',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertAuthenticated();
|
|
||||||
$response->assertRedirect(route('dashboard', absolute: false));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature;
|
|
||||||
|
|
||||||
// use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class ExampleTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* A basic test example.
|
|
||||||
*/
|
|
||||||
public function test_the_application_returns_a_successful_response(): void
|
|
||||||
{
|
|
||||||
$response = $this->get('/');
|
|
||||||
|
|
||||||
$response->assertStatus(200);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class ProfileTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_profile_page_is_displayed(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this
|
|
||||||
->actingAs($user)
|
|
||||||
->get('/profile');
|
|
||||||
|
|
||||||
$response->assertOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_profile_information_can_be_updated(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this
|
|
||||||
->actingAs($user)
|
|
||||||
->patch('/profile', [
|
|
||||||
'name' => 'Test User',
|
|
||||||
'email' => 'test@example.com',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response
|
|
||||||
->assertSessionHasNoErrors()
|
|
||||||
->assertRedirect('/profile');
|
|
||||||
|
|
||||||
$user->refresh();
|
|
||||||
|
|
||||||
$this->assertSame('Test User', $user->name);
|
|
||||||
$this->assertSame('test@example.com', $user->email);
|
|
||||||
$this->assertNull($user->email_verified_at);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this
|
|
||||||
->actingAs($user)
|
|
||||||
->patch('/profile', [
|
|
||||||
'name' => 'Test User',
|
|
||||||
'email' => $user->email,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response
|
|
||||||
->assertSessionHasNoErrors()
|
|
||||||
->assertRedirect('/profile');
|
|
||||||
|
|
||||||
$this->assertNotNull($user->refresh()->email_verified_at);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_user_can_delete_their_account(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this
|
|
||||||
->actingAs($user)
|
|
||||||
->delete('/profile', [
|
|
||||||
'password' => 'password',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response
|
|
||||||
->assertSessionHasNoErrors()
|
|
||||||
->assertRedirect('/');
|
|
||||||
|
|
||||||
$this->assertGuest();
|
|
||||||
$this->assertNull($user->fresh());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_correct_password_must_be_provided_to_delete_account(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$response = $this
|
|
||||||
->actingAs($user)
|
|
||||||
->from('/profile')
|
|
||||||
->delete('/profile', [
|
|
||||||
'password' => 'wrong-password',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response
|
|
||||||
->assertSessionHasErrors('password')
|
|
||||||
->assertRedirect('/profile');
|
|
||||||
|
|
||||||
$this->assertNotNull($user->fresh());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Unit;
|
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
class ExampleTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* A basic test example.
|
|
||||||
*/
|
|
||||||
public function test_that_true_is_true(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue(true);
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user