4 Commits

Author SHA1 Message Date
e3713773b7 Save reposted reel id for future use
Some checks failed
Push image to registry / build-image (push) Failing after 52s
- Changed the managed account login from phone number, username or email to only username so it can be used for other things
2025-08-05 13:58:10 +02:00
aa936a2a11 Instagram jobs refactor + started InstagramNotifications 2025-08-05 13:25:25 +02:00
1f23b112d7 redo README 2025-08-05 13:11:49 +02:00
0aa34d170a Fix selenium user_profile folder ? 2025-08-03 12:46:00 +02:00
12 changed files with 530 additions and 157 deletions

View File

@ -1,66 +1,7 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p> # DatBrowser
<p align="center"> ## Development
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel ### Links
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: - [novnc](http://localhost:7900/?password=secret&autoconnect=true&scale=local)
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
### Premium Partners
- **[Vehikl](https://vehikl.com/)**
- **[Tighten Co.](https://tighten.co)**
- **[WebReinvent](https://webreinvent.com/)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
- **[Cyber-Duck](https://cyber-duck.co.uk)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Jump24](https://jump24.co.uk)**
- **[Redberry](https://redberry.international/laravel/)**
- **[Active Logic](https://activelogic.com)**
- **[byte5](https://byte5.de)**
- **[OP.GG](https://op.gg)**
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

View File

@ -161,7 +161,7 @@ abstract class BrowserJob implements ShouldQueue
'--disable-setuid-sandbox', '--disable-setuid-sandbox',
'--whitelisted-ips=""', '--whitelisted-ips=""',
'--disable-dev-shm-usage', '--disable-dev-shm-usage',
'--user-data-dir=/home/seluser/profile/nigga/', // seems that selenium doesn't like docker having a volume on the exact same folder ("session not created: probably user data directory is already in use") '--user-data-dir=/home/seluser/profile/nigga', // seems that selenium doesn't like docker having a volume on the exact same folder ("session not created: probably user data directory is already in use")
])->all()); ])->all());
return RemoteWebDriver::create( return RemoteWebDriver::create(
@ -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}');
}
");
}
} }

View 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_username"));
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
}
}
}
}

View File

@ -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
}
}
}

View File

@ -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(),
@ -217,8 +211,12 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
} }
} }
private function getLatestReelsFromAccount(Browser $browser, string $account): array private function getLatestReelsFromAccount(Browser $browser, string $account, int $maxReels = null, bool $save = true): array
{ {
if ($maxReels === null) {
$maxReels = config("jobs.instagramRepost.max_reposts_per_account");
}
$accountReels = []; // Indexed array to store new reels from the account $accountReels = []; // Indexed array to store new reels from the account
$browser->visit("https://instagram.com/{$account}/reels"); $browser->visit("https://instagram.com/{$account}/reels");
@ -258,12 +256,26 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
break; break;
} }
$reelModel = InstagramRepost::firstOrCreate( // Get the model for the reel
["reel_id" => $postId, "account_id" => $accountModel->id], $query = InstagramRepost::where("reel_id", $postId)
["reposted" => false, "repost_tries" => 0] ->where("account_id", $accountModel->id)
); ->first();
if (count($accountReels) < config("jobs.instagramRepost.max_reposts_per_account")) { if ($query) {
$reelModel = $query;
} else {
$reelModel = new InstagramRepost([
"reel_id" => $postId,
"account_id" => $accountModel->id,
"reposted" => false,
"repost_tries" => 0
]);
if ($save) {
$reelModel = $reelModel->save();
}
}
if (count($accountReels) < $maxReels) {
$accountReels[] = $reelModel; // Add it to the to be downloaded reels array $accountReels[] = $reelModel; // Add it to the to be downloaded reels array
} }
} }
@ -367,6 +379,10 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
$reel->reposted = true; $reel->reposted = true;
$reel->save(); $reel->save();
// set the repost ID to the lastest account reel ID
$reel->repost_reel_id = $this->getLatestPostId($browser, $reel);
$reel->save();
return true; return true;
} catch (\Exception $e) { } catch (\Exception $e) {
@ -390,6 +406,25 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
} }
} }
private function getLatestPostId(Browser $browser, InstagramRepost $reel): void
{
// Go to the profile page of the managed account
$newReel = $this->getLatestReelsFromAccount(
$browser,
$this->jobInfos->get('instagram_repost_account_username'),
1,
false // Don't save the reel, we don't want to repost our own reposts
)[0] ?? null;
if ($newReel === null) {
Log::error("No reels found for account: " . $this->jobInfos->get('instagram_repost_account_username'));
return;
}
// Return the reel ID
return $newReel->reel_id;
}
private function getReelCaption(InstagramRepost $reel, IInstagramVideo $videoInfo): string private function getReelCaption(InstagramRepost $reel, IInstagramVideo $videoInfo): string
{ {
if (isset($reel->instagram_caption)) { if (isset($reel->instagram_caption)) {
@ -437,80 +472,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
}
}
}
} }

View 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,
];
}

View 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
}

View 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;
}
}

View File

@ -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");
});
}
};

View File

@ -0,0 +1,33 @@
<?php
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
{
JobInfo::where('key', 'instagram_repost_account_email')
->update([
'key' => 'instagram_repost_account_username',
'description' => "Le nom d'utilisateur unique utilisé pour le compte Instagram de repost."
]);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
JobInfo::where('key', 'instagram_repost_account_username')
->update([
'key' => 'instagram_repost_account_email',
'description' => "L'adresse e-mail/nom d'utilisateur/N° de téléphone utilisée pour le compte Instagram de repost."
]);
}
};

View File

@ -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');

View File

@ -3,7 +3,7 @@
FROM selenium/standalone-chrome:latest AS final FROM selenium/standalone-chrome:latest AS final
COPY ./chromedriver /bin/chromedriver COPY ./chromedriver /bin/chromedriver
#RUN mkdir -p /home/seluser/profile/ RUN mkdir -p /home/seluser/profile/nigga
ENV TZ=Europe/Brussels ENV TZ=Europe/Brussels
# 30 minutes session timeout # 30 minutes session timeout