Instagram jobs refactor + started InstagramNotifications
This commit is contained in:
@ -366,4 +366,16 @@ abstract class BrowserJob implements ShouldQueue
|
|||||||
AllNotification::send(new JobDebugNotification($this->jobId, $errorMessage));
|
AllNotification::send(new JobDebugNotification($this->jobId, $errorMessage));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function clickElementWithJavaScript(Browser $browser, string $xPathSelector): void
|
||||||
|
{
|
||||||
|
$browser->script("
|
||||||
|
var element = document.evaluate('{$xPathSelector}', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||||
|
if (element) {
|
||||||
|
element.click();
|
||||||
|
} else {
|
||||||
|
console.error('Element not found: {$xPathSelector}');
|
||||||
|
}
|
||||||
|
");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
112
app/Browser/Jobs/Instagram/InstagramAbstractJob.php
Normal file
112
app/Browser/Jobs/Instagram/InstagramAbstractJob.php
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Browser\Jobs\Instagram;
|
||||||
|
|
||||||
|
use App\Browser\BrowserJob;
|
||||||
|
use App\Browser\JobDebugScreenshot;
|
||||||
|
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;
|
||||||
|
use App\Services\AIPrompt\OpenAPIPrompt;
|
||||||
|
|
||||||
|
abstract class InstagramAbstractJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
|
||||||
|
{
|
||||||
|
// === CONFIGURATION ===
|
||||||
|
protected Collection $jobInfos;
|
||||||
|
protected JobRun $jobRun;
|
||||||
|
protected OpenAPIPrompt $openAPIPrompt;
|
||||||
|
|
||||||
|
public function __construct($jobId, OpenAPIPrompt $openAPIPrompt = null)
|
||||||
|
{
|
||||||
|
parent::__construct($jobId);
|
||||||
|
|
||||||
|
$this->openAPIPrompt = $openAPIPrompt ?? app(OpenAPIPrompt::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function clickNext(Browser $browser) {
|
||||||
|
$nextButton = $browser->driver->findElement(WebDriverBy::xpath('//div[contains(text(), "Next") or contains(text(), "Share")]'));
|
||||||
|
$nextButton->click();
|
||||||
|
sleep(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected 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)
|
||||||
|
{
|
||||||
|
if ($browser->assertSee("Search", true)) {
|
||||||
|
return; // Already signed in, skip some waiting for non existing text
|
||||||
|
}
|
||||||
|
|
||||||
|
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()));
|
||||||
|
// Stop the job run
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,183 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Browser\Jobs\InstagramRepost;
|
||||||
|
|
||||||
|
use App\Browser\JobDebugScreenshot;
|
||||||
|
use App\Browser\Jobs\Instagram\InstagramAbstractJob;
|
||||||
|
use App\Browser\Jobs\InstagramRepost\DescriptionPipeline\InstagramDescriptionPipeline;
|
||||||
|
use App\Models\InstagramNotification;
|
||||||
|
use App\Models\InstagramRepost;
|
||||||
|
use App\Models\Job;
|
||||||
|
use App\Models\JobRun;
|
||||||
|
use App\Notification\Notifications\JobDebugNotification;
|
||||||
|
use App\Notification\Providers\AllNotification;
|
||||||
|
use App\Services\Instagram\NotificationTypeDetector;
|
||||||
|
use Facebook\WebDriver\WebDriverBy;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Laravel\Dusk\Browser;
|
||||||
|
use App\Services\AIPrompt\OpenAPIPrompt;
|
||||||
|
|
||||||
|
class InstagramNotificationHandlingJob extends InstagramAbstractJob implements ShouldBeUniqueUntilProcessing
|
||||||
|
{
|
||||||
|
// === CONFIGURATION ===
|
||||||
|
|
||||||
|
public $timeout = 1800; // 30 minutes
|
||||||
|
|
||||||
|
private const APPROXIMATIVE_RUNNING_MINUTES = 2;
|
||||||
|
|
||||||
|
protected IInstagramVideoDownloader $videoDownloader;
|
||||||
|
|
||||||
|
protected ReelDescriptor $ReelDescriptor;
|
||||||
|
|
||||||
|
protected string $downloadFolder = "app/Browser/downloads/InstagramRepost/";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pipeline for processing Instagram post descriptions.
|
||||||
|
* This pipeline can be used to modify the description before reposting.
|
||||||
|
* For example, it can remove account references or add hashtags.
|
||||||
|
*
|
||||||
|
* @var InstagramDescriptionPipeline
|
||||||
|
*/
|
||||||
|
protected InstagramDescriptionPipeline $descriptionPipeline;
|
||||||
|
|
||||||
|
public function __construct($jobId = 4, ReelDescriptor $ReelDescriptor = null, OpenAPIPrompt $openAPIPrompt = null)
|
||||||
|
{
|
||||||
|
parent::__construct($jobId, $openAPIPrompt);
|
||||||
|
|
||||||
|
$this->downloadFolder = base_path($this->downloadFolder);
|
||||||
|
$this->videoDownloader = new YTDLPDownloader();
|
||||||
|
$this->ReelDescriptor = $ReelDescriptor ?? app(ReelDescriptor::class);
|
||||||
|
$this->descriptionPipeline = new InstagramDescriptionPipeline([
|
||||||
|
// Add steps to the pipeline here
|
||||||
|
new DescriptionPipeline\RemoveAccountsReferenceStep(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run(Browser $browser): ?JobRun
|
||||||
|
{
|
||||||
|
$startTime = microtime(true);
|
||||||
|
|
||||||
|
Log::info("Running InstagramNotificationHandlingJob");
|
||||||
|
$this->jobInfos = Job::find($this->jobId)->jobInfosTable();
|
||||||
|
$this->jobRun = new JobRun([
|
||||||
|
"job_id" => $this->jobId,
|
||||||
|
"success" => false,
|
||||||
|
]);
|
||||||
|
$this->jobRun->save();
|
||||||
|
|
||||||
|
dump("visiting " . microtime(true) - $startTime);
|
||||||
|
$browser->visit('https://instagram.com');
|
||||||
|
sleep(5);
|
||||||
|
// dump("removing popups " . microtime(true) - $startTime);
|
||||||
|
// $this->removePopups($browser); // TEMPORARY DISABLED
|
||||||
|
dump("signing in " . microtime(true) - $startTime);
|
||||||
|
$this->signin($browser);
|
||||||
|
sleep(2);
|
||||||
|
dump("Saving notifications " . microtime(true) - $startTime);
|
||||||
|
$this->saveNotifications($browser);
|
||||||
|
sleep(seconds: 5);
|
||||||
|
|
||||||
|
$this->jobRun->success = true;
|
||||||
|
$this->jobRun->save();
|
||||||
|
|
||||||
|
Log::info("InstagramNotificationHandlingJob 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);
|
||||||
|
$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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function saveNotifications(Browser $browser): void
|
||||||
|
{
|
||||||
|
$this->openNotificationsPanel($browser);
|
||||||
|
try {
|
||||||
|
$DOMnotifications = $browser->driver->findElements(WebDriverBy::xpath('//div[@class="x6s0dn4 x1q4h3jn x78zum5 x1y1aw1k x64bnmy xwib8y2 x13jy36j x87ps6o x1wq6e7o x1ffbijf x1h4gsww xnp2e5m x1ypdohk x1l895ks"]')); // TODO : not rely on class names
|
||||||
|
$notifications = [];
|
||||||
|
foreach ($DOMnotifications as $DOMnotification) {
|
||||||
|
// Process each notification
|
||||||
|
$DOMnotificationText = $DOMnotification->findElement(WebDriverBy::xpath('./div[2]/span'));
|
||||||
|
|
||||||
|
$notification = new InstagramNotification();
|
||||||
|
$notification->username = str_replace( "/", "", $DOMnotificationText->findElement(WebDriverBy::xpath('./a[1]'))->getAttribute('href'));
|
||||||
|
$notification->notification_type = NotificationTypeDetector::detectType($DOMnotificationText->getText());
|
||||||
|
// 05/08/2025 : Instagram removed the link to the post in the notification, the day I'm working on it :()
|
||||||
|
$postId = null;
|
||||||
|
try {
|
||||||
|
$postId = str_replace(["\/p/", "/"], "", $DOMnotification->findElement(WebDriverBy::xpath('./div[3]/a'))->getAttribute('href'));
|
||||||
|
dump("Post ID: " . $postId);
|
||||||
|
$repostId = InstagramRepost::where('repost_reel_id', $postId)->first()?->id;
|
||||||
|
dump("Repost ID: " . $repostId);
|
||||||
|
if ($repostId) {
|
||||||
|
$notification->instagram_repost_id = $repostId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (\Exception $e) {
|
||||||
|
Log::error("Failed to get post ID from notification : " . $e->getMessage());
|
||||||
|
}
|
||||||
|
$notification->message = $DOMnotificationText->getText();
|
||||||
|
|
||||||
|
// Save only if the notification is not already saved
|
||||||
|
try {
|
||||||
|
$notification->save();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error("Failed to save notification : " . $e->getMessage());
|
||||||
|
continue; // Skip to the next notification
|
||||||
|
}
|
||||||
|
dump("Notification saved : " . $notification);
|
||||||
|
$notifications[] = $notification;
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error("Failed to get notifications : " . $e->getMessage());
|
||||||
|
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
|
||||||
|
AllNotification::send(new JobDebugNotification($this->jobId, "Failed to open notifications panel: " . $e->getMessage()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function openNotificationsPanel(Browser $browser): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$notificationsButtonXpath = '//a[./div//span[contains(text(), "Notifications")]]';
|
||||||
|
$notificationsButton = $browser->driver->findElement(WebDriverBy::xpath($notificationsButtonXpath));
|
||||||
|
// $notificationsButton->click();
|
||||||
|
$this->clickElementWithJavaScript($browser, $notificationsButtonXpath);
|
||||||
|
sleep(1);
|
||||||
|
$browser->waitForText('Filter', 10, true);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error("Failed to open notifications panel : " . $e->getMessage());
|
||||||
|
$browser->screenshot(JobDebugScreenshot::getFileName($this->jobId));
|
||||||
|
AllNotification::send(new JobDebugNotification($this->jobId, "Failed to open notifications panel: " . $e->getMessage()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app()->environment('local')) {
|
||||||
|
Log::debug("Notifications panel opened successfully waiting 7 seconds for manual interaction");
|
||||||
|
sleep(7); // Allow time for manual interaction in local environment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Browser\Jobs\InstagramRepost;
|
namespace App\Browser\Jobs\InstagramRepost;
|
||||||
|
|
||||||
use App\Browser\BrowserJob;
|
|
||||||
use App\Browser\JobDebugScreenshot;
|
use App\Browser\JobDebugScreenshot;
|
||||||
|
use App\Browser\Jobs\Instagram\InstagramAbstractJob;
|
||||||
use App\Browser\Jobs\InstagramRepost\DescriptionPipeline\InstagramDescriptionPipeline;
|
use App\Browser\Jobs\InstagramRepost\DescriptionPipeline\InstagramDescriptionPipeline;
|
||||||
use App\Models\InstagramRepost;
|
use App\Models\InstagramRepost;
|
||||||
use App\Models\InstagramAccount;
|
use App\Models\InstagramAccount;
|
||||||
@ -19,7 +19,7 @@ use Illuminate\Support\Facades\Log;
|
|||||||
use Laravel\Dusk\Browser;
|
use Laravel\Dusk\Browser;
|
||||||
use App\Services\AIPrompt\OpenAPIPrompt;
|
use App\Services\AIPrompt\OpenAPIPrompt;
|
||||||
|
|
||||||
class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
|
class InstagramRepostJob extends InstagramAbstractJob implements ShouldBeUniqueUntilProcessing
|
||||||
{
|
{
|
||||||
// === CONFIGURATION ===
|
// === CONFIGURATION ===
|
||||||
|
|
||||||
@ -27,15 +27,10 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
|
|||||||
|
|
||||||
private const APPROXIMATIVE_RUNNING_MINUTES = 2;
|
private const APPROXIMATIVE_RUNNING_MINUTES = 2;
|
||||||
|
|
||||||
private Collection $jobInfos;
|
|
||||||
protected JobRun $jobRun;
|
|
||||||
|
|
||||||
protected IInstagramVideoDownloader $videoDownloader;
|
protected IInstagramVideoDownloader $videoDownloader;
|
||||||
|
|
||||||
protected ReelDescriptor $ReelDescriptor;
|
protected ReelDescriptor $ReelDescriptor;
|
||||||
|
|
||||||
protected OpenAPIPrompt $openAPIPrompt;
|
|
||||||
|
|
||||||
protected string $downloadFolder = "app/Browser/downloads/InstagramRepost/";
|
protected string $downloadFolder = "app/Browser/downloads/InstagramRepost/";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -49,12 +44,11 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
|
|||||||
|
|
||||||
public function __construct($jobId = 4, ReelDescriptor $ReelDescriptor = null, OpenAPIPrompt $openAPIPrompt = null)
|
public function __construct($jobId = 4, ReelDescriptor $ReelDescriptor = null, OpenAPIPrompt $openAPIPrompt = null)
|
||||||
{
|
{
|
||||||
parent::__construct($jobId);
|
parent::__construct($jobId, $openAPIPrompt);
|
||||||
|
|
||||||
$this->downloadFolder = base_path($this->downloadFolder);
|
$this->downloadFolder = base_path($this->downloadFolder);
|
||||||
$this->videoDownloader = new YTDLPDownloader();
|
$this->videoDownloader = new YTDLPDownloader();
|
||||||
$this->ReelDescriptor = $ReelDescriptor ?? app(ReelDescriptor::class);
|
$this->ReelDescriptor = $ReelDescriptor ?? app(ReelDescriptor::class);
|
||||||
$this->openAPIPrompt = $openAPIPrompt ?? app(OpenAPIPrompt::class);
|
|
||||||
$this->descriptionPipeline = new InstagramDescriptionPipeline([
|
$this->descriptionPipeline = new InstagramDescriptionPipeline([
|
||||||
// Add steps to the pipeline here
|
// Add steps to the pipeline here
|
||||||
new DescriptionPipeline\RemoveAccountsReferenceStep(),
|
new DescriptionPipeline\RemoveAccountsReferenceStep(),
|
||||||
@ -437,80 +431,4 @@ Your response format must strictly adhere to JSON with only one required field:
|
|||||||
}
|
}
|
||||||
return $llmAnswer;
|
return $llmAnswer;
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
30
app/Models/InstagramNotification.php
Normal file
30
app/Models/InstagramNotification.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class InstagramNotification extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'instagram_notifications';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'username',
|
||||||
|
'instagram_repost_id',
|
||||||
|
'notification_type',
|
||||||
|
'message',
|
||||||
|
'is_read',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'notification_type' => InstagramNotificationType::class,
|
||||||
|
'is_read' => 'boolean',
|
||||||
|
'created_at' => 'datetime',
|
||||||
|
'updated_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $attributes = [
|
||||||
|
'is_read' => false,
|
||||||
|
'is_processed' => false,
|
||||||
|
];
|
||||||
|
}
|
14
app/Models/InstagramNotificationType.php
Normal file
14
app/Models/InstagramNotificationType.php
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
enum InstagramNotificationType: string
|
||||||
|
{
|
||||||
|
case LIKE = 'LIKE';
|
||||||
|
case COMMENT = 'COMMENT';
|
||||||
|
case FOLLOW = 'FOLLOW';
|
||||||
|
case MESSAGE = 'MESSAGE';
|
||||||
|
case SYSTEM = 'SYSTEM'; // System message like congratulatory messages or updates
|
||||||
|
case MENTION = 'MENTION';
|
||||||
|
case OTHER = 'OTHER'; // Uncategorized or unclassified notifications
|
||||||
|
}
|
37
app/Services/Instagram/NotificationTypeDetector.php
Normal file
37
app/Services/Instagram/NotificationTypeDetector.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Instagram;
|
||||||
|
|
||||||
|
use App\Models\InstagramNotificationType;
|
||||||
|
use Facebook\WebDriver\Remote\RemoteWebElement;
|
||||||
|
|
||||||
|
class NotificationTypeDetector
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Detects the Instagram notification type from the DOM.
|
||||||
|
*
|
||||||
|
* @param string $descriptionText The text content of the notification description.
|
||||||
|
* @return InstagramNotificationType
|
||||||
|
*/
|
||||||
|
public static function detectType(string $descriptionText): InstagramNotificationType
|
||||||
|
{
|
||||||
|
$descriptionText = trim($descriptionText); // Remove leading/trailing whitespace
|
||||||
|
|
||||||
|
// Prioritize exact matches for reliability
|
||||||
|
if (strpos($descriptionText, 'liked') !== false) {
|
||||||
|
return InstagramNotificationType::LIKE;
|
||||||
|
} elseif (strpos($descriptionText, 'commented') !== false) {
|
||||||
|
return InstagramNotificationType::COMMENT;
|
||||||
|
} elseif (strpos($descriptionText, 'following you') !== false) {
|
||||||
|
return InstagramNotificationType::FOLLOW;
|
||||||
|
} elseif (strpos($descriptionText, 'message') !== false) {
|
||||||
|
return InstagramNotificationType::MESSAGE;
|
||||||
|
} elseif (strpos($descriptionText, 'congratulations') !== false || strpos($descriptionText, 'update') !== false) {
|
||||||
|
return InstagramNotificationType::SYSTEM;
|
||||||
|
} elseif (strpos($descriptionText, 'mention') !== false) {
|
||||||
|
return InstagramNotificationType::MENTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
return InstagramNotificationType::OTHER;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\InstagramNotificationType;
|
||||||
|
use App\Models\InstagramRepost;
|
||||||
|
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('instagram_notifications', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->string('username')->nullable();
|
||||||
|
$table->foreignIdFor(InstagramRepost::class)->nullable()
|
||||||
|
->constrained('instagram_reposts')
|
||||||
|
->noActionOnDelete()
|
||||||
|
->cascadeOnUpdate();
|
||||||
|
$table->enum('notification_type', array_column(InstagramNotificationType::cases(), 'value'));
|
||||||
|
$table->text('message')->nullable();
|
||||||
|
$table->boolean('is_read')->default(false);
|
||||||
|
$table->boolean('is_processed')->default(false);
|
||||||
|
|
||||||
|
$table->unique(['username', 'instagram_repost_id', 'notification_type', 'message'], 'unique_instagram_notification');
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('instagram_reposts', function (Blueprint $table) {
|
||||||
|
$table->string("repost_reel_id")->unique()->nullable()->comment('Reel ID of the reposted content');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('instagram_notifications');
|
||||||
|
|
||||||
|
Schema::table('instagram_reposts', function (Blueprint $table) {
|
||||||
|
$table->dropUnique(['repost_reel_id']);
|
||||||
|
$table->dropColumn("repost_reel_id");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -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\InstagramNotificationHandlingJob;
|
||||||
use App\Browser\Jobs\InstagramRepost\InstagramRepostJob;
|
use App\Browser\Jobs\InstagramRepost\InstagramRepostJob;
|
||||||
use App\Jobs\PruneOldJobRuns;
|
use App\Jobs\PruneOldJobRuns;
|
||||||
use App\Services\BrowserJobsInstances;
|
use App\Services\BrowserJobsInstances;
|
||||||
@ -24,4 +25,5 @@ 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');
|
Schedule::job(new InstagramRepostJob)->everyThreeHours()->onOneServer()->withoutOverlapping()->name('instagram_reposts')->description('Instagram reposts job');
|
||||||
|
Schedule::job(new InstagramNotificationHandlingJob)->hourly()->onOneServer()->withoutOverlapping()->name('instagram_reposts_notifications')->description('Instagram reposts notification handling job');
|
||||||
|
Reference in New Issue
Block a user