diff --git a/app/Console/Commands/SyncChannelPostsCommand.php b/app/Console/Commands/SyncChannelPostsCommand.php new file mode 100644 index 0000000..f41e8d9 --- /dev/null +++ b/app/Console/Commands/SyncChannelPostsCommand.php @@ -0,0 +1,60 @@ +argument('platform'); + + if ($platform === 'lemmy') { + return $this->syncLemmy(); + } + + $this->error("Unsupported platform: {$platform}"); + return self::FAILURE; + } + + private function syncLemmy(): int + { + $communityName = config('lemmy.community'); + + if (!$communityName) { + $this->error('Missing Lemmy community configuration (lemmy.community)'); + return self::FAILURE; + } + + try { + $this->info("Getting community ID for: {$communityName}"); + + $api = new LemmyApiService(config('lemmy.instance')); + $communityId = $api->getCommunityId($communityName); + + $this->info("Running sync job for Lemmy community: {$communityName} (ID: {$communityId})"); + + SyncChannelPostsJob::dispatchSync( + PlatformEnum::LEMMY, + (string) $communityId, + $communityName + ); + + $this->info('Channel posts synced successfully'); + + return self::SUCCESS; + + } catch (\Exception $e) { + $this->error("Failed to sync channel posts: {$e->getMessage()}"); + return self::FAILURE; + } + } +} diff --git a/app/Jobs/SyncChannelPostsJob.php b/app/Jobs/SyncChannelPostsJob.php new file mode 100644 index 0000000..66d1316 --- /dev/null +++ b/app/Jobs/SyncChannelPostsJob.php @@ -0,0 +1,76 @@ +onQueue('lemmy-posts'); + } + + public function handle(): void + { + if ($this->platform === PlatformEnum::LEMMY) { + $this->syncLemmyChannelPosts(); + } + } + + private function syncLemmyChannelPosts(): void + { + try { + $api = new LemmyApiService(config('lemmy.instance')); + $token = $this->getAuthToken($api); + + $api->syncChannelPosts($token, (int) $this->channelId, $this->channelName); + + logger()->info('Channel posts synced successfully', [ + 'platform' => $this->platform->value, + 'channel_id' => $this->channelId, + 'channel_name' => $this->channelName + ]); + + } catch (Exception $e) { + logger()->error('Failed to sync channel posts', [ + 'platform' => $this->platform->value, + 'channel_id' => $this->channelId, + 'error' => $e->getMessage() + ]); + + throw $e; + } + } + + private function getAuthToken(LemmyApiService $api): string + { + return Cache::remember('lemmy_jwt_token', 3600, function () use ($api) { + $username = config('lemmy.username'); + $password = config('lemmy.password'); + + if (!$username || !$password) { + throw new PlatformAuthException(PlatformEnum::LEMMY, 'Missing credentials'); + } + + $token = $api->login($username, $password); + + if (!$token) { + throw new PlatformAuthException(PlatformEnum::LEMMY, 'Login failed'); + } + + return $token; + }); + } +} diff --git a/app/Models/Article.php b/app/Models/Article.php index cdd6511..6596ac9 100644 --- a/app/Models/Article.php +++ b/app/Models/Article.php @@ -30,6 +30,7 @@ class Article extends Model 'title', 'description', 'is_valid', + 'is_duplicate', 'fetched_at', 'validated_at', ]; @@ -37,6 +38,8 @@ class Article extends Model public function casts(): array { return [ + 'is_valid' => 'boolean', + 'is_duplicate' => 'boolean', 'fetched_at' => 'datetime', 'validated_at' => 'datetime', 'created_at' => 'datetime', diff --git a/app/Models/ArticlePublication.php b/app/Models/ArticlePublication.php index f72f312..52b87cc 100644 --- a/app/Models/ArticlePublication.php +++ b/app/Models/ArticlePublication.php @@ -5,14 +5,21 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +/** + * @property integer $article_id + * @property integer $community_id + * @property integer $post_id + * + * @method static create(array $array) + */ class ArticlePublication extends Model { protected $fillable = [ 'article_id', - 'published_at', 'community_id', - 'published_by', 'post_id', + 'published_at', + 'published_by', 'platform', 'publication_data', ]; diff --git a/app/Models/PlatformChannelPost.php b/app/Models/PlatformChannelPost.php new file mode 100644 index 0000000..700e3ae --- /dev/null +++ b/app/Models/PlatformChannelPost.php @@ -0,0 +1,56 @@ + 'datetime', + 'platform' => PlatformEnum::class, + ]; + } + + public static function urlExists(PlatformEnum $platform, string $channelId, string $url): bool + { + return self::where('platform', $platform) + ->where('channel_id', $channelId) + ->where('url', $url) + ->exists(); + } + + public static function storePost(PlatformEnum $platform, string $channelId, ?string $channelName, string $postId, ?string $url, ?string $title, ?\DateTime $postedAt = null): self + { + return self::updateOrCreate( + [ + 'platform' => $platform, + 'channel_id' => $channelId, + 'post_id' => $postId, + ], + [ + 'channel_name' => $channelName, + 'url' => $url, + 'title' => $title, + 'posted_at' => $postedAt ?? now(), + ] + ); + } +} diff --git a/app/Modules/Lemmy/Services/LemmyApiService.php b/app/Modules/Lemmy/Services/LemmyApiService.php index 0e369ff..5178eab 100644 --- a/app/Modules/Lemmy/Services/LemmyApiService.php +++ b/app/Modules/Lemmy/Services/LemmyApiService.php @@ -2,6 +2,8 @@ namespace App\Modules\Lemmy\Services; +use App\Enums\PlatformEnum; +use App\Models\PlatformChannelPost; use App\Modules\Lemmy\LemmyRequest; use Exception; @@ -57,6 +59,54 @@ public function getCommunityId(string $communityName): int } } + public function syncChannelPosts(string $token, int $communityId, string $communityName): void + { + try { + $request = new LemmyRequest($this->instance, $token); + $response = $request->get('post/list', [ + 'community_id' => $communityId, + 'limit' => 50, + 'sort' => 'New' + ]); + + if (!$response->successful()) { + logger()->warning('Failed to sync channel posts', [ + 'status' => $response->status(), + 'community_id' => $communityId + ]); + return; + } + + $data = $response->json(); + $posts = $data['posts'] ?? []; + + foreach ($posts as $postData) { + $post = $postData['post']; + + PlatformChannelPost::storePost( + PlatformEnum::LEMMY, + (string) $communityId, + $communityName, + (string) $post['id'], + $post['url'] ?? null, + $post['name'] ?? null, + isset($post['published']) ? new \DateTime($post['published']) : null + ); + } + + logger()->info('Synced channel posts', [ + 'community_id' => $communityId, + 'posts_count' => count($posts) + ]); + + } catch (Exception $e) { + logger()->error('Exception while syncing channel posts', [ + 'error' => $e->getMessage(), + 'community_id' => $communityId + ]); + } + } + public function createPost(string $token, string $title, string $body, int $communityId, ?string $url = null, ?string $thumbnail = null): array { try { diff --git a/app/Modules/Lemmy/Services/LemmyPublisher.php b/app/Modules/Lemmy/Services/LemmyPublisher.php index d4a36f1..a8fd87d 100644 --- a/app/Modules/Lemmy/Services/LemmyPublisher.php +++ b/app/Modules/Lemmy/Services/LemmyPublisher.php @@ -67,7 +67,7 @@ private function getAuthToken(): string } $token = $this->api->login($username, $password); - + if (!$token) { throw new PlatformAuthException(PlatformEnum::LEMMY, 'Login failed'); } diff --git a/database/migrations/2025_06_29_072202_create_articles_table.php b/database/migrations/2025_06_29_072202_create_articles_table.php index 8821f4d..889bd74 100644 --- a/database/migrations/2025_06_29_072202_create_articles_table.php +++ b/database/migrations/2025_06_29_072202_create_articles_table.php @@ -14,6 +14,7 @@ public function up(): void $table->string('title')->nullable(); $table->text('description')->nullable(); $table->boolean('is_valid')->nullable(); + $table->boolean('is_duplicate')->default(false); $table->timestamp('validated_at')->nullable(); $table->timestamps(); }); diff --git a/database/migrations/2025_06_30_163438_create_platform_channel_posts_table.php b/database/migrations/2025_06_30_163438_create_platform_channel_posts_table.php new file mode 100644 index 0000000..d8c1914 --- /dev/null +++ b/database/migrations/2025_06_30_163438_create_platform_channel_posts_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('platform'); + $table->string('channel_id'); + $table->string('channel_name')->nullable(); + $table->string('post_id'); + $table->longText('url')->nullable(); + $table->string('title')->nullable(); + $table->timestamp('posted_at'); + $table->timestamps(); + + $table->index(['platform', 'channel_id']); + $table->index(['platform', 'channel_id', 'posted_at']); + // Will add URL index with prefix after table creation + $table->unique(['platform', 'channel_id', 'post_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('platform_channel_posts'); + } +}; diff --git a/routes/console.php b/routes/console.php index 3aeb350..bc6a0cc 100644 --- a/routes/console.php +++ b/routes/console.php @@ -1,6 +1,8 @@ debug('No unpublished valid articles found for Lemmy publishing'); } })->everyFifteenMinutes()->name('publish-to-lemmy'); + +Schedule::call(function () { + $communityId = config('lemmy.community_id'); + $communityName = config('lemmy.community'); + + if ($communityId && $communityName) { + SyncChannelPostsJob::dispatch( + PlatformEnum::LEMMY, + $communityId, + $communityName + ); + + logger()->info('Dispatched channel posts sync job', [ + 'platform' => 'lemmy', + 'community_id' => $communityId, + 'community_name' => $communityName + ]); + } else { + logger()->warning('Missing Lemmy community configuration for sync job'); + } +})->everyTenMinutes()->name('sync-lemmy-channel-posts');