Compare commits
4 Commits
f0e52147e4
...
e3713773b7
Author | SHA1 | Date | |
---|---|---|---|
e3713773b7 | |||
aa936a2a11 | |||
1f23b112d7 | |||
0aa34d170a |
67
README.md
67
README.md
@ -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">
|
||||
<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>
|
||||
## Development
|
||||
|
||||
## 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:
|
||||
|
||||
- [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).
|
||||
- [novnc](http://localhost:7900/?password=secret&autoconnect=true&scale=local)
|
||||
|
@ -161,7 +161,7 @@ abstract class BrowserJob implements ShouldQueue
|
||||
'--disable-setuid-sandbox',
|
||||
'--whitelisted-ips=""',
|
||||
'--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());
|
||||
|
||||
return RemoteWebDriver::create(
|
||||
@ -366,4 +366,16 @@ abstract class BrowserJob implements ShouldQueue
|
||||
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_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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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(),
|
||||
@ -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
|
||||
|
||||
$browser->visit("https://instagram.com/{$account}/reels");
|
||||
@ -258,12 +256,26 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
|
||||
break;
|
||||
}
|
||||
|
||||
$reelModel = InstagramRepost::firstOrCreate(
|
||||
["reel_id" => $postId, "account_id" => $accountModel->id],
|
||||
["reposted" => false, "repost_tries" => 0]
|
||||
);
|
||||
// Get the model for the reel
|
||||
$query = InstagramRepost::where("reel_id", $postId)
|
||||
->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
|
||||
}
|
||||
}
|
||||
@ -367,6 +379,10 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
|
||||
$reel->reposted = true;
|
||||
$reel->save();
|
||||
|
||||
// set the repost ID to the lastest account reel ID
|
||||
$reel->repost_reel_id = $this->getLatestPostId($browser, $reel);
|
||||
$reel->save();
|
||||
|
||||
return true;
|
||||
|
||||
} 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
|
||||
{
|
||||
if (isset($reel->instagram_caption)) {
|
||||
@ -437,80 +472,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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");
|
||||
});
|
||||
}
|
||||
};
|
@ -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."
|
||||
]);
|
||||
}
|
||||
};
|
@ -2,6 +2,7 @@
|
||||
|
||||
use App\Browser\Jobs\Hellcase\HellcaseJob;
|
||||
use App\Browser\Jobs\HellcaseBattles\HellcaseBattlesJob;
|
||||
use App\Browser\Jobs\InstagramRepost\InstagramNotificationHandlingJob;
|
||||
use App\Browser\Jobs\InstagramRepost\InstagramRepostJob;
|
||||
use App\Jobs\PruneOldJobRuns;
|
||||
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)->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 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');
|
||||
|
@ -3,7 +3,7 @@
|
||||
FROM selenium/standalone-chrome:latest AS final
|
||||
|
||||
COPY ./chromedriver /bin/chromedriver
|
||||
#RUN mkdir -p /home/seluser/profile/
|
||||
RUN mkdir -p /home/seluser/profile/nigga
|
||||
|
||||
ENV TZ=Europe/Brussels
|
||||
# 30 minutes session timeout
|
||||
|
Reference in New Issue
Block a user