Split sources, refine publisher

This commit is contained in:
myrmidex 2025-06-30 18:18:30 +02:00
parent 83194cd64b
commit 800df655bf
13 changed files with 271 additions and 57 deletions

View file

@ -7,9 +7,9 @@
class FetchNewArticlesCommand extends Command
{
protected $signature = 'articles:fetch';
protected $signature = 'article:refresh';
protected $description = 'Fetches new articles';
protected $description = 'Fetches latest articles';
public function handle(): int
{

View file

@ -2,29 +2,27 @@
namespace App\Console\Commands;
use App\Jobs\PublishToLemmyJob;
use App\Models\Article;
use App\Modules\Lemmy\Services\LemmyPublisher;
use App\Services\Article\ArticleFetcher;
use Exception;
use Illuminate\Console\Command;
class PublishToLemmyCommand extends Command
{
protected $signature = 'article:publish-to-lemmy';
protected $description = 'Publish an article to Lemmy';
protected $description = 'Queue an article for publishing to Lemmy';
public function handle(): int
{
$article = Article::all()->firstOrFail();
$article = Article::all()
->filter(fn (Article $article) => $article->articlePublication === null)
->firstOrFail();
$this->info('Publishing article: ' . $article->url);
$this->info('Queuing article for publishing: ' . $article->url);
try {
LemmyPublisher::fromConfig()->publish($article, ArticleFetcher::fetchArticleData($article));
} catch (Exception) {
return self::FAILURE;
}
PublishToLemmyJob::dispatch($article);
$this->info('Article queued successfully');
return self::SUCCESS;
}

View file

@ -0,0 +1,8 @@
<?php
namespace App\Enums;
enum PlatformEnum: string
{
case LEMMY = 'lemmy';
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Exceptions;
use App\Enums\PlatformEnum;
use Exception;
class PlatformAuthException extends Exception
{
public function __construct(
private readonly PlatformEnum $platform,
string $reason = 'Authentication failed'
) {
$message = "Failed to authenticate with {$platform->value}: {$reason}";
parent::__construct($message);
}
public function getPlatform(): PlatformEnum
{
return $this->platform;
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace App\Exceptions;
use App\Enums\PlatformEnum;
use App\Models\Article;
use Exception;
use Throwable;
class PublishException extends Exception
{
public function __construct(
private readonly Article $article,
private readonly PlatformEnum $platform,
?Throwable $previous = null
) {
$message = "Failed to publish article #{$article->id} to {$platform->value}";
if ($previous) {
$message .= ": {$previous->getMessage()}";
}
parent::__construct($message, 0, $previous);
}
public function getArticle(): Article
{
return $this->article;
}
public function getPlatform(): PlatformEnum
{
return $this->platform;
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace App\Jobs;
use App\Exceptions\PublishException;
use App\Models\Article;
use App\Modules\Lemmy\Services\LemmyPublisher;
use App\Services\Article\ArticleFetcher;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class PublishToLemmyJob implements ShouldQueue
{
use Queueable;
public function __construct(
private readonly Article $article
) {
$this->onQueue('lemmy-posts');
}
public function handle(): void
{
if ($this->article->articlePublication !== null) {
logger()->info('Article already published, skipping', [
'article_id' => $this->article->id
]);
return;
}
$extractedData = ArticleFetcher::fetchArticleData($this->article);
logger()->info('Publishing article to Lemmy', [
'article_id' => $this->article->id,
'url' => $this->article->url
]);
try {
LemmyPublisher::fromConfig()->publish($this->article, $extractedData);
logger()->info('Article published successfully', [
'article_id' => $this->article->id
]);
} catch (PublishException $e) {
$this->fail($e);
}
}
}

View file

@ -3,8 +3,7 @@
namespace App\Listeners;
use App\Events\ArticleReadyToPublish;
use App\Modules\Lemmy\Services\LemmyPublisher;
use App\Services\Article\ArticleFetcher;
use App\Jobs\PublishToLemmyJob;
class PublishArticle
{
@ -16,8 +15,11 @@ public function handle(ArticleReadyToPublish $event): void
{
$article = $event->article;
logger('Publishing article: ' . $article->id . ' : ' . $article->url);
logger()->info('Article queued for publishing to Lemmy', [
'article_id' => $article->id,
'url' => $article->url
]);
LemmyPublisher::fromConfig()->publish($article, ArticleFetcher::fetchArticleData($article));
PublishToLemmyJob::dispatch($article);
}
}

View file

@ -6,6 +6,8 @@
use Database\Factories\ArticleFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Carbon;
/**
@ -16,6 +18,7 @@
* @property Carbon|null $validated_at
* @property Carbon $created_at
* @property Carbon $updated_at
* @property ArticlePublication $articlePublication
*/
class Article extends Model
{
@ -54,6 +57,16 @@ public function isValid(): bool
return $this->is_valid;
}
public function articlePublication(): HasOne
{
return $this->hasOne(ArticlePublication::class);
}
public function articlePublications(): HasMany
{
return $this->hasMany(ArticlePublication::class);
}
protected static function booted(): void
{
static::created(function ($article) {

View file

@ -57,15 +57,26 @@ public function getCommunityId(string $communityName): int
}
}
public function createPost(string $token, string $title, string $body, int $communityId): array
public function createPost(string $token, string $title, string $body, int $communityId, ?string $url = null, ?string $thumbnail = null): array
{
try {
$request = new LemmyRequest($this->instance, $token);
$response = $request->post('post', [
$postData = [
'name' => $title,
'body' => $body,
'community_id' => $communityId,
]);
];
if ($url) {
$postData['url'] = $url;
}
if ($thumbnail) {
$postData['custom_thumbnail'] = $thumbnail;
}
$response = $request->post('post', $postData);
if (!$response->successful()) {
throw new Exception('Failed to create post: ' . $response->status() . ' - ' . $response->body());

View file

@ -2,6 +2,9 @@
namespace App\Modules\Lemmy\Services;
use App\Enums\PlatformEnum;
use App\Exceptions\PlatformAuthException;
use App\Exceptions\PublishException;
use App\Models\Article;
use App\Models\ArticlePublication;
use Exception;
@ -29,38 +32,47 @@ public static function fromConfig(): self
);
}
/**
* @throws PublishException
*/
public function publish(Article $article, array $extractedData): ArticlePublication
{
$token = $this->getAuthToken();
try {
$token = $this->getAuthToken();
$communityId = $this->getCommunityId();
if (!$token) {
throw new Exception('Failed to authenticate with Lemmy');
$postData = $this->api->createPost(
$token,
$extractedData['title'] ?? 'Untitled',
$extractedData['description'] ?? '',
$communityId,
$article->url,
$extractedData['thumbnail'] ?? null
);
return $this->createPublicationRecord($article, $postData, $communityId);
} catch (Exception $e) {
throw new PublishException($article, PlatformEnum::LEMMY, $e);
}
$communityId = $this->getCommunityId();
$postData = $this->api->createPost(
$token,
$extractedData['title'] ?? 'Untitled',
$extractedData['description'] ?? '',
$communityId
);
return $this->createPublicationRecord($article, $postData, $communityId);
}
private function getAuthToken(): ?string
private function getAuthToken(): string
{
return Cache::remember('lemmy_jwt_token', 3600, function () {
$username = config('lemmy.username');
$password = config('lemmy.password');
if (!$username || !$password) {
logger()->error('Missing Lemmy credentials');
return null;
throw new PlatformAuthException(PlatformEnum::LEMMY, 'Missing credentials');
}
return $this->api->login($username, $password);
$token = $this->api->login($username, $password);
if (!$token) {
throw new PlatformAuthException(PlatformEnum::LEMMY, 'Login failed');
}
return $token;
});
}

View file

@ -80,12 +80,28 @@ public static function extractFullArticle(string $html): ?string
return null;
}
public static function extractThumbnail(string $html): ?string
{
// Try OpenGraph image first
if (preg_match('/<meta property="og:image" content="([^"]+)"/i', $html, $matches)) {
return $matches[1];
}
// Try first image in article content
if (preg_match('/<img[^>]+src="([^"]+)"/i', $html, $matches)) {
return $matches[1];
}
return null;
}
public static function extractData(string $html): array
{
return [
'title' => self::extractTitle($html),
'description' => self::extractDescription($html),
'full_article' => self::extractFullArticle($html),
'thumbnail' => self::extractThumbnail($html),
];
}
}

View file

@ -64,12 +64,26 @@ public static function extractFullArticle(string $html): ?string
return null;
}
public static function extractThumbnail(string $html): ?string
{
if (preg_match('/<meta property="og:image" content="([^"]+)"/i', $html, $matches)) {
return $matches[1];
}
if (preg_match('/<img[^>]+src="([^"]+)"/i', $html, $matches)) {
return $matches[1];
}
return null;
}
public static function extractData(string $html): array
{
return [
'title' => self::extractTitle($html),
'description' => self::extractDescription($html),
'full_article' => self::extractFullArticle($html),
'thumbnail' => self::extractThumbnail($html),
];
}
}

View file

@ -1,6 +1,38 @@
<?php
use App\Console\Commands\FetchNewArticlesCommand;
use App\Models\Article;
use App\Modules\Lemmy\Services\LemmyPublisher;
use App\Services\Article\ArticleFetcher;
use Illuminate\Support\Facades\Schedule;
Schedule::command(FetchNewArticlesCommand::class)->hourly();
Schedule::call(function () {
$article = Article::whereDoesntHave('articlePublications')
->where('is_valid', true)
->first();
if ($article) {
try {
logger()->info('Publishing article to Lemmy via scheduler', [
'article_id' => $article->id,
'url' => $article->url
]);
$extractedData = ArticleFetcher::fetchArticleData($article);
LemmyPublisher::fromConfig()->publish($article, $extractedData);
logger()->info('Successfully published article to Lemmy', [
'article_id' => $article->id
]);
} catch (Exception $e) {
logger()->error('Failed to publish article to Lemmy via scheduler', [
'article_id' => $article->id,
'error' => $e->getMessage()
]);
}
} else {
logger()->debug('No unpublished valid articles found for Lemmy publishing');
}
})->everyFifteenMinutes()->name('publish-to-lemmy');