diff --git a/app/Http/Controllers/CommunitiesController.php b/app/Http/Controllers/CommunitiesController.php new file mode 100644 index 0000000..3321035 --- /dev/null +++ b/app/Http/Controllers/CommunitiesController.php @@ -0,0 +1,80 @@ +orderBy('platform_instance_id') + ->orderBy('name') + ->get(); + + return view('pages.communities.index', compact('communities')); + } + + public function create(): View + { + $instances = PlatformInstance::where('is_active', true) + ->orderBy('name') + ->get(); + + return view('pages.communities.create', compact('instances')); + } + + public function store(Request $request): RedirectResponse + { + $validated = $request->validate([ + 'platform_instance_id' => 'required|exists:platform_instances,id', + 'name' => 'required|string|max:255', + 'display_name' => 'required|string|max:255', + 'community_id' => 'required|string|max:255', + 'description' => 'nullable|string', + ]); + + Community::create($validated); + + return redirect()->route('communities.index') + ->with('success', 'Community created successfully!'); + } + + public function edit(Community $community): View + { + $instances = PlatformInstance::where('is_active', true) + ->orderBy('name') + ->get(); + + return view('pages.communities.edit', compact('community', 'instances')); + } + + public function update(Request $request, Community $community): RedirectResponse + { + $validated = $request->validate([ + 'platform_instance_id' => 'required|exists:platform_instances,id', + 'name' => 'required|string|max:255', + 'display_name' => 'required|string|max:255', + 'community_id' => 'required|string|max:255', + 'description' => 'nullable|string', + ]); + + $community->update($validated); + + return redirect()->route('communities.index') + ->with('success', 'Community updated successfully!'); + } + + public function destroy(Community $community): RedirectResponse + { + $community->delete(); + + return redirect()->route('communities.index') + ->with('success', 'Community deleted successfully!'); + } +} \ No newline at end of file diff --git a/app/Jobs/PublishToLemmyJob.php b/app/Jobs/PublishToLemmyJob.php index 116919c..0fa49d9 100644 --- a/app/Jobs/PublishToLemmyJob.php +++ b/app/Jobs/PublishToLemmyJob.php @@ -38,10 +38,12 @@ public function handle(): void ]); try { - LemmyPublisher::fromActiveAccount()->publish($this->article, $extractedData); + $publications = LemmyPublisher::fromActiveAccount()->publish($this->article, $extractedData); logger()->info('Article published successfully', [ - 'article_id' => $this->article->id + 'article_id' => $this->article->id, + 'publications_count' => $publications->count(), + 'communities' => $publications->pluck('community_id')->toArray() ]); } catch (PublishException $e) { diff --git a/app/Models/Community.php b/app/Models/Community.php new file mode 100644 index 0000000..012ba4e --- /dev/null +++ b/app/Models/Community.php @@ -0,0 +1,44 @@ + 'boolean' + ]; + + public function platformInstance(): BelongsTo + { + return $this->belongsTo(PlatformInstance::class); + } + + public function platformAccounts(): BelongsToMany + { + return $this->belongsToMany(PlatformAccount::class, 'account_communities') + ->withPivot(['is_active', 'priority']) + ->withTimestamps(); + } + + public function getFullNameAttribute(): string + { + return $this->platformInstance->url . '/c/' . $this->name; + } +} diff --git a/app/Models/PlatformAccount.php b/app/Models/PlatformAccount.php index b4a5c4b..0ca039b 100644 --- a/app/Models/PlatformAccount.php +++ b/app/Models/PlatformAccount.php @@ -5,6 +5,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Crypt; use App\Enums\PlatformEnum; @@ -21,6 +22,7 @@ * @property string $status * @property Carbon $created_at * @property Carbon $updated_at + * @property Collection $activeCommunities * @method static where(string $string, PlatformEnum $platform) * @method static orderBy(string $string) * @method static create(array $validated) @@ -83,4 +85,18 @@ public function setAsActive(): void // Activate this account $this->update(['is_active' => true]); } + + public function communities(): BelongsToMany + { + return $this->belongsToMany(Community::class, 'account_communities') + ->withPivot(['is_active', 'priority']) + ->withTimestamps(); + } + + public function activeCommunities(): BelongsToMany + { + return $this->communities() + ->wherePivot('is_active', true) + ->orderByPivot('priority', 'desc'); + } } diff --git a/app/Models/PlatformInstance.php b/app/Models/PlatformInstance.php new file mode 100644 index 0000000..5acdd65 --- /dev/null +++ b/app/Models/PlatformInstance.php @@ -0,0 +1,44 @@ + PlatformEnum::class, + 'is_active' => 'boolean' + ]; + + public function communities(): HasMany + { + return $this->hasMany(Community::class); + } + + public static function findByUrl(PlatformEnum $platform, string $url): ?self + { + return static::where('platform', $platform) + ->where('url', $url) + ->first(); + } +} diff --git a/app/Modules/Lemmy/Services/LemmyPublisher.php b/app/Modules/Lemmy/Services/LemmyPublisher.php index a792a1f..8ba5d44 100644 --- a/app/Modules/Lemmy/Services/LemmyPublisher.php +++ b/app/Modules/Lemmy/Services/LemmyPublisher.php @@ -3,12 +3,14 @@ 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 App\Models\PlatformAccount; use App\Services\Auth\LemmyAuthService; use Exception; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use RuntimeException; @@ -28,7 +30,7 @@ public static function fromActiveAccount(): self $accounts = PlatformAccount::getActive(PlatformEnum::LEMMY); if ($accounts->isEmpty()) { - throw new RuntimeException('No active Lemmy accounts configured'); + throw new RuntimeException('No active Lemmy accounts configured'); // TODO Also make this into a PublishException } return new self($accounts->first()); @@ -37,52 +39,55 @@ public static function fromActiveAccount(): self /** * @throws PublishException */ - public function publish(Article $article, array $extractedData): ArticlePublication + public function publish(Article $article, array $extractedData): Collection { - try { - $token = LemmyAuthService::getToken($this->account); - $communityId = $this->getCommunityId(); + $publications = collect(); + $activeCommunities = $this->account->activeCommunities; - $languageId = $this->getLanguageIdForSource($article->url); - - $postData = $this->api->createPost( - $token, - $extractedData['title'] ?? 'Untitled', - $extractedData['description'] ?? '', - $communityId, - $article->url, - $extractedData['thumbnail'] ?? null, - $languageId - ); - - return $this->createPublicationRecord($article, $postData, $communityId); - } catch (Exception $e) { - throw new PublishException($article, PlatformEnum::LEMMY, $e); + if ($activeCommunities->isEmpty()) { + throw new PublishException($article, PlatformEnum::LEMMY, new RuntimeException('No active communities configured for account: ' . $this->account->username)); } + + $activeCommunities->each(function ($community) use ($article, $extractedData, $publications) { + try { + $publication = $this->publishToCommunity($article, $extractedData, $community); + $publications->push($publication); + } catch (Exception $e) { + logger()->warning('Failed to publish to community', [ + 'article_id' => $article->id, + 'community' => $community->name, + 'error' => $e->getMessage() + ]); + } + }); + + if ($publications->isEmpty()) { + throw new PublishException($article, PlatformEnum::LEMMY, new RuntimeException('Failed to publish to any community')); + } + + return $publications; } /** + * @throws PlatformAuthException * @throws Exception */ - private function getCommunityId(): int + private function publishToCommunity(Article $article, array $extractedData, $community): ArticlePublication { - $community = config('lemmy.community'); + $token = LemmyAuthService::getToken($this->account); + $languageId = $this->getLanguageIdForSource($article->url); - if (! $community) { - throw new RuntimeException('No community configured in config'); - } + $postData = $this->api->createPost( + $token, + $extractedData['title'] ?? 'Untitled', + $extractedData['description'] ?? '', + (int) $community->community_id, + $article->url, + $extractedData['thumbnail'] ?? null, + $languageId + ); - $cacheKey = "lemmy_community_id_$community"; - $cachedId = Cache::get($cacheKey); - - if ($cachedId) { - return $cachedId; - } - - $communityId = $this->api->getCommunityId($community); - Cache::put($cacheKey, $communityId, 3600); - - return $communityId; + return $this->createPublicationRecord($article, $postData, (int) $community->community_id); } private function createPublicationRecord(Article $article, array $postData, int $communityId): ArticlePublication diff --git a/database/migrations/2025_07_04_233000_create_platform_instances_table.php b/database/migrations/2025_07_04_233000_create_platform_instances_table.php new file mode 100644 index 0000000..23521db --- /dev/null +++ b/database/migrations/2025_07_04_233000_create_platform_instances_table.php @@ -0,0 +1,28 @@ +id(); + $table->enum('platform', ['lemmy']); + $table->string('url'); // lemmy.world, beehaw.org + $table->string('name'); // "Lemmy World", "Beehaw" + $table->text('description')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + + $table->unique(['platform', 'url']); + }); + } + + public function down(): void + { + Schema::dropIfExists('platform_instances'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_04_233100_create_communities_table.php b/database/migrations/2025_07_04_233100_create_communities_table.php new file mode 100644 index 0000000..48ae8a9 --- /dev/null +++ b/database/migrations/2025_07_04_233100_create_communities_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('platform_instance_id')->constrained()->onDelete('cascade'); + $table->string('name'); // "technology" + $table->string('display_name'); // "Technology" + $table->string('community_id'); // API ID from platform + $table->text('description')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + + $table->unique(['platform_instance_id', 'name']); + }); + } + + public function down(): void + { + Schema::dropIfExists('communities'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_04_233200_create_account_communities_table.php b/database/migrations/2025_07_04_233200_create_account_communities_table.php new file mode 100644 index 0000000..02bd15c --- /dev/null +++ b/database/migrations/2025_07_04_233200_create_account_communities_table.php @@ -0,0 +1,26 @@ +foreignId('platform_account_id')->constrained()->onDelete('cascade'); + $table->foreignId('community_id')->constrained()->onDelete('cascade'); + $table->boolean('is_active')->default(false); + $table->integer('priority')->default(0); // for ordering + $table->timestamps(); + + $table->primary(['platform_account_id', 'community_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('account_communities'); + } +}; \ No newline at end of file diff --git a/database/seeders/PlatformInstanceSeeder.php b/database/seeders/PlatformInstanceSeeder.php new file mode 100644 index 0000000..1b3e841 --- /dev/null +++ b/database/seeders/PlatformInstanceSeeder.php @@ -0,0 +1,30 @@ + PlatformEnum::LEMMY, + 'url' => 'belgae.social', + 'name' => 'Belgae Social', + 'description' => 'A Belgian Lemmy instance on the fediverse', + ], + ])->each (fn ($instanceData) => + PlatformInstance::updateOrCreate( + [ + 'platform' => $instanceData['platform'], + 'url' => $instanceData['url'], + ], + $instanceData + ) + ); + } +} \ No newline at end of file diff --git a/resources/views/partials/sidebar.blade.php b/resources/views/partials/sidebar.blade.php index bc504ed..c19df02 100644 --- a/resources/views/partials/sidebar.blade.php +++ b/resources/views/partials/sidebar.blade.php @@ -1,10 +1,9 @@ -
Admin Panel