Check community history for same urls

This commit is contained in:
myrmidex 2025-06-30 19:54:43 +02:00
parent 800df655bf
commit 18d7a2dbce
10 changed files with 312 additions and 3 deletions

View file

@ -0,0 +1,60 @@
<?php
namespace App\Console\Commands;
use App\Enums\PlatformEnum;
use App\Jobs\SyncChannelPostsJob;
use App\Modules\Lemmy\Services\LemmyApiService;
use Illuminate\Console\Command;
class SyncChannelPostsCommand extends Command
{
protected $signature = 'channel:sync {platform=lemmy}';
protected $description = 'Manually sync channel posts for a platform';
public function handle(): int
{
$platform = $this->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;
}
}
}

View file

@ -0,0 +1,76 @@
<?php
namespace App\Jobs;
use App\Enums\PlatformEnum;
use App\Exceptions\PlatformAuthException;
use App\Modules\Lemmy\Services\LemmyApiService;
use Exception;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Cache;
class SyncChannelPostsJob implements ShouldQueue
{
use Queueable;
public function __construct(
private readonly PlatformEnum $platform,
private readonly string $channelId,
private readonly string $channelName
) {
$this->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;
});
}
}

View file

@ -30,6 +30,7 @@ class Article extends Model
'title', 'title',
'description', 'description',
'is_valid', 'is_valid',
'is_duplicate',
'fetched_at', 'fetched_at',
'validated_at', 'validated_at',
]; ];
@ -37,6 +38,8 @@ class Article extends Model
public function casts(): array public function casts(): array
{ {
return [ return [
'is_valid' => 'boolean',
'is_duplicate' => 'boolean',
'fetched_at' => 'datetime', 'fetched_at' => 'datetime',
'validated_at' => 'datetime', 'validated_at' => 'datetime',
'created_at' => 'datetime', 'created_at' => 'datetime',

View file

@ -5,14 +5,21 @@
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; 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 class ArticlePublication extends Model
{ {
protected $fillable = [ protected $fillable = [
'article_id', 'article_id',
'published_at',
'community_id', 'community_id',
'published_by',
'post_id', 'post_id',
'published_at',
'published_by',
'platform', 'platform',
'publication_data', 'publication_data',
]; ];

View file

@ -0,0 +1,56 @@
<?php
namespace App\Models;
use App\Enums\PlatformEnum;
use Illuminate\Database\Eloquent\Model;
/**
* @method static where(string $string, PlatformEnum $platform)
* @method static updateOrCreate(array $array, array $array1)
*/
class PlatformChannelPost extends Model
{
protected $fillable = [
'platform',
'channel_id',
'channel_name',
'post_id',
'url',
'title',
'posted_at',
];
protected function casts(): array
{
return [
'posted_at' => '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(),
]
);
}
}

View file

@ -2,6 +2,8 @@
namespace App\Modules\Lemmy\Services; namespace App\Modules\Lemmy\Services;
use App\Enums\PlatformEnum;
use App\Models\PlatformChannelPost;
use App\Modules\Lemmy\LemmyRequest; use App\Modules\Lemmy\LemmyRequest;
use Exception; 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 public function createPost(string $token, string $title, string $body, int $communityId, ?string $url = null, ?string $thumbnail = null): array
{ {
try { try {

View file

@ -14,6 +14,7 @@ public function up(): void
$table->string('title')->nullable(); $table->string('title')->nullable();
$table->text('description')->nullable(); $table->text('description')->nullable();
$table->boolean('is_valid')->nullable(); $table->boolean('is_valid')->nullable();
$table->boolean('is_duplicate')->default(false);
$table->timestamp('validated_at')->nullable(); $table->timestamp('validated_at')->nullable();
$table->timestamps(); $table->timestamps();
}); });

View file

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('platform_channel_posts', function (Blueprint $table) {
$table->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');
}
};

View file

@ -1,6 +1,8 @@
<?php <?php
use App\Console\Commands\FetchNewArticlesCommand; use App\Console\Commands\FetchNewArticlesCommand;
use App\Enums\PlatformEnum;
use App\Jobs\SyncChannelPostsJob;
use App\Models\Article; use App\Models\Article;
use App\Modules\Lemmy\Services\LemmyPublisher; use App\Modules\Lemmy\Services\LemmyPublisher;
use App\Services\Article\ArticleFetcher; use App\Services\Article\ArticleFetcher;
@ -36,3 +38,24 @@
logger()->debug('No unpublished valid articles found for Lemmy publishing'); logger()->debug('No unpublished valid articles found for Lemmy publishing');
} }
})->everyFifteenMinutes()->name('publish-to-lemmy'); })->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');