diff --git a/app/Console/Commands/PublishToLemmyCommand.php b/app/Console/Commands/PublishToLemmyCommand.php index 8538a9c..9fd5ff4 100644 --- a/app/Console/Commands/PublishToLemmyCommand.php +++ b/app/Console/Commands/PublishToLemmyCommand.php @@ -3,7 +3,9 @@ namespace App\Console\Commands; use App\Models\Article; -use App\Services\Article\LemmyService; +use App\Modules\Lemmy\Services\LemmyPublisher; +use App\Services\Article\ArticleFetcher; +use Exception; use Illuminate\Console\Command; class PublishToLemmyCommand extends Command @@ -16,7 +18,13 @@ public function handle(): int { $article = Article::all()->firstOrFail(); - LemmyService::publish($article); + $this->info('Publishing article: ' . $article->url); + + try { + LemmyPublisher::fromConfig()->publish($article, ArticleFetcher::fetchArticle($article)); + } catch (Exception) { + return self::FAILURE; + } return self::SUCCESS; } diff --git a/app/Listeners/PublishArticle.php b/app/Listeners/PublishArticle.php index bba0651..d16c08a 100644 --- a/app/Listeners/PublishArticle.php +++ b/app/Listeners/PublishArticle.php @@ -3,13 +3,13 @@ namespace App\Listeners; use App\Events\ArticleReadyToPublish; -use App\Services\Article\LemmyService; +use App\Modules\Lemmy\Services\LemmyPublisher; +use App\Services\Article\ArticleFetcher; class PublishArticle { public function __construct() { - // } public function handle(ArticleReadyToPublish $event): void @@ -18,6 +18,6 @@ public function handle(ArticleReadyToPublish $event): void logger('Publishing article: ' . $article->id . ' : ' . $article->url); - LemmyService::publish($article); + LemmyPublisher::fromConfig()->publish($article, ArticleFetcher::fetchArticle($article)); } } diff --git a/app/Models/Article.php b/app/Models/Article.php index 480f219..48b3e79 100644 --- a/app/Models/Article.php +++ b/app/Models/Article.php @@ -24,13 +24,17 @@ class Article extends Model protected $fillable = [ 'url', + 'title', + 'description', 'is_valid', + 'fetched_at', 'validated_at', ]; public function casts(): array { return [ + 'fetched_at' => 'datetime', 'validated_at' => 'datetime', 'created_at' => 'datetime', 'updated_at' => 'datetime', diff --git a/app/Modules/Lemmy/LemmyRequest.php b/app/Modules/Lemmy/LemmyRequest.php new file mode 100644 index 0000000..1199a35 --- /dev/null +++ b/app/Modules/Lemmy/LemmyRequest.php @@ -0,0 +1,50 @@ +instance = $instance; + $this->token = $token; + } + + public function get(string $endpoint, array $params = []): Response + { + $url = "https://{$this->instance}/api/v3/{$endpoint}"; + + $request = Http::timeout(30); + + if ($this->token) { + $request = $request->withToken($this->token); + } + + return $request->get($url, $params); + } + + public function post(string $endpoint, array $data = []): Response + { + $url = "https://{$this->instance}/api/v3/{$endpoint}"; + + $request = Http::timeout(30); + + if ($this->token) { + $request = $request->withToken($this->token); + } + + return $request->post($url, $data); + } + + public function withToken(string $token): self + { + $this->token = $token; + return $this; + } +} diff --git a/app/Modules/Lemmy/Services/LemmyApiService.php b/app/Modules/Lemmy/Services/LemmyApiService.php new file mode 100644 index 0000000..5b7a5d3 --- /dev/null +++ b/app/Modules/Lemmy/Services/LemmyApiService.php @@ -0,0 +1,80 @@ +instance = $instance; + } + + public function login(string $username, string $password): ?string + { + try { + $request = new LemmyRequest($this->instance); + $response = $request->post('user/login', [ + 'username_or_email' => $username, + 'password' => $password, + ]); + + if (!$response->successful()) { + logger()->error('Lemmy login failed', [ + 'status' => $response->status(), + 'body' => $response->body() + ]); + return null; + } + + $data = $response->json(); + return $data['jwt'] ?? null; + } catch (Exception $e) { + logger()->error('Lemmy login exception', ['error' => $e->getMessage()]); + return null; + } + } + + public function getCommunityId(string $communityName): int + { + try { + $request = new LemmyRequest($this->instance); + $response = $request->get('community', ['name' => $communityName]); + + if (!$response->successful()) { + throw new Exception('Failed to fetch community: ' . $response->status()); + } + + $data = $response->json(); + return $data['community_view']['community']['id'] ?? throw new Exception('Community not found'); + } catch (Exception $e) { + logger()->error('Community lookup failed', ['error' => $e->getMessage()]); + throw $e; + } + } + + public function createPost(string $token, string $title, string $body, int $communityId): array + { + try { + $request = new LemmyRequest($this->instance, $token); + $response = $request->post('post', [ + 'name' => $title, + 'body' => $body, + 'community_id' => $communityId, + ]); + + if (!$response->successful()) { + throw new Exception('Failed to create post: ' . $response->status() . ' - ' . $response->body()); + } + + return $response->json(); + } catch (Exception $e) { + logger()->error('Post creation failed', ['error' => $e->getMessage()]); + throw $e; + } + } +} \ No newline at end of file diff --git a/app/Modules/Lemmy/Services/LemmyPublisher.php b/app/Modules/Lemmy/Services/LemmyPublisher.php new file mode 100644 index 0000000..bb15409 --- /dev/null +++ b/app/Modules/Lemmy/Services/LemmyPublisher.php @@ -0,0 +1,86 @@ +api = new LemmyApiService($instance); + $this->username = $username; + $this->community = $community; + } + + public static function fromConfig(): self + { + return new self( + config('lemmy.instance'), + config('lemmy.username'), + config('lemmy.community') + ); + } + + public function publish(Article $article, array $extractedData): ArticlePublication + { + $token = $this->getAuthToken(); + + if (!$token) { + throw new Exception('Failed to authenticate with Lemmy'); + } + + $communityId = $this->getCommunityId(); + + $postData = $this->api->createPost( + $token, + $extractedData['title'] ?? 'Untitled', + $extractedData['description'] ?? '', + $communityId + ); + + return $this->createPublicationRecord($article, $postData, $communityId); + } + + 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; + } + + return $this->api->login($username, $password); + }); + } + + private function getCommunityId(): int + { + return Cache::remember("lemmy_community_id_{$this->community}", 3600, function () { + return $this->api->getCommunityId($this->community); + }); + } + + private function createPublicationRecord(Article $article, array $postData, int $communityId): ArticlePublication + { + return ArticlePublication::create([ + 'article_id' => $article->id, + 'post_id' => $postData['post_view']['post']['id'], + 'community_id' => $communityId, + 'published_by' => $this->username, + 'published_at' => now(), + 'platform' => 'lemmy', + 'publication_data' => $postData, + ]); + } +} \ No newline at end of file diff --git a/app/Services/Article/ArticleDataExtractor.php b/app/Services/Article/ArticleDataExtractor.php new file mode 100644 index 0000000..aa8cb96 --- /dev/null +++ b/app/Services/Article/ArticleDataExtractor.php @@ -0,0 +1,75 @@ +]*>([^<]+)<\/h1>/i', $html, $matches)) { + return html_entity_decode(strip_tags($matches[1]), ENT_QUOTES, 'UTF-8'); + } + + // Try title tag + if (preg_match('/