Instagram jobs refactor + started InstagramNotifications
This commit is contained in:
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;
|
||||
|
||||
use App\Browser\BrowserJob;
|
||||
use App\Browser\JobDebugScreenshot;
|
||||
use App\Browser\Jobs\Instagram\InstagramAbstractJob;
|
||||
use App\Browser\Jobs\InstagramRepost\DescriptionPipeline\InstagramDescriptionPipeline;
|
||||
use App\Models\InstagramRepost;
|
||||
use App\Models\InstagramAccount;
|
||||
@ -19,7 +19,7 @@ use Illuminate\Support\Facades\Log;
|
||||
use Laravel\Dusk\Browser;
|
||||
use App\Services\AIPrompt\OpenAPIPrompt;
|
||||
|
||||
class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
|
||||
class InstagramRepostJob extends InstagramAbstractJob implements ShouldBeUniqueUntilProcessing
|
||||
{
|
||||
// === CONFIGURATION ===
|
||||
|
||||
@ -27,15 +27,10 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
|
||||
|
||||
private const APPROXIMATIVE_RUNNING_MINUTES = 2;
|
||||
|
||||
private Collection $jobInfos;
|
||||
protected JobRun $jobRun;
|
||||
|
||||
protected IInstagramVideoDownloader $videoDownloader;
|
||||
|
||||
protected ReelDescriptor $ReelDescriptor;
|
||||
|
||||
protected OpenAPIPrompt $openAPIPrompt;
|
||||
|
||||
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)
|
||||
{
|
||||
parent::__construct($jobId);
|
||||
parent::__construct($jobId, $openAPIPrompt);
|
||||
|
||||
$this->downloadFolder = base_path($this->downloadFolder);
|
||||
$this->videoDownloader = new YTDLPDownloader();
|
||||
$this->ReelDescriptor = $ReelDescriptor ?? app(ReelDescriptor::class);
|
||||
$this->openAPIPrompt = $openAPIPrompt ?? app(OpenAPIPrompt::class);
|
||||
$this->descriptionPipeline = new InstagramDescriptionPipeline([
|
||||
// Add steps to the pipeline here
|
||||
new DescriptionPipeline\RemoveAccountsReferenceStep(),
|
||||
@ -437,80 +431,4 @@ Your response format must strictly adhere to JSON with only one required field:
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user