fedi-feed-router/app/Livewire/Channels.php

301 lines
9.6 KiB
PHP

<?php
namespace App\Livewire;
use App\Actions\CreateChannelAction;
use App\Enums\LogLevelEnum;
use App\Events\ActionPerformed;
use App\Models\ArticlePublication;
use App\Models\Language;
use App\Models\PlatformAccount;
use App\Models\PlatformChannel;
use App\Models\PlatformInstance;
use App\Models\RouteArticle;
use App\Services\Platform\CommunityDirectory;
use Exception;
use Illuminate\Contracts\View\View;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Validation\Rule;
use Livewire\Component;
use RuntimeException;
class Channels extends Component
{
public ?int $managingChannelId = null;
public bool $showCreateModal = false;
public ?int $newCommunityId = null;
public ?int $newPlatformInstanceId = null;
/** @var array<int, array{id: int, name: string, title: string}> */
public array $availableCommunities = [];
public ?string $communityLoadError = null;
public ?int $newLanguageId = null;
public string $newDescription = '';
public ?int $editingChannelId = null;
public string $editDisplayName = '';
public ?int $editLanguageId = null;
public string $editDescription = '';
public function toggle(int $channelId): void
{
$channel = PlatformChannel::findOrFail($channelId);
$channel->is_active = ! $channel->is_active;
$channel->save();
}
public function deleteChannel(int $channelId): void
{
$channel = PlatformChannel::find($channelId);
if (! $channel instanceof PlatformChannel) {
return;
}
$name = $channel->display_name;
// Routes, keywords, route articles, publications, account links and synced posts
// all cascade at the database level.
$channel->delete();
if ($this->managingChannelId === $channelId) {
$this->managingChannelId = null;
}
if ($this->editingChannelId === $channelId) {
$this->editingChannelId = null;
}
ActionPerformed::dispatch('Deleted platform channel', LogLevelEnum::WARNING, [
'platform_channel_id' => $channelId,
'display_name' => $name,
]);
}
public function openCreateModal(): void
{
$this->reset(['newCommunityId', 'newPlatformInstanceId', 'newLanguageId', 'newDescription', 'availableCommunities', 'communityLoadError']);
$this->resetErrorBag();
$this->showCreateModal = true;
}
public function updatedNewPlatformInstanceId(?int $value): void
{
$this->reset(['newCommunityId', 'availableCommunities', 'communityLoadError']);
if (! $value) {
return;
}
$instance = PlatformInstance::find($value);
if (! $instance) {
return;
}
try {
$this->availableCommunities = app(CommunityDirectory::class)->forInstance($instance);
} catch (Exception $e) {
$this->communityLoadError = 'Could not reach this instance to list its communities: '.$e->getMessage();
}
}
public function refreshCommunities(): void
{
$instance = $this->newPlatformInstanceId ? PlatformInstance::find($this->newPlatformInstanceId) : null;
if (! $instance) {
return;
}
app(CommunityDirectory::class)->forget($instance);
$this->updatedNewPlatformInstanceId($this->newPlatformInstanceId);
}
public function closeCreateModal(): void
{
$this->showCreateModal = false;
}
public function createChannel(CreateChannelAction $action): void
{
$this->validate([
'newCommunityId' => [
'required',
'integer',
Rule::in(collect($this->availableCommunities)->pluck('id')->all()),
Rule::unique('platform_channels', 'channel_id')
->where('platform_instance_id', $this->newPlatformInstanceId),
],
'newPlatformInstanceId' => 'required|integer|exists:platform_instances,id',
'newLanguageId' => 'nullable|integer|exists:languages,id',
], [
'newCommunityId.in' => 'Select a community from this instance.',
'newCommunityId.unique' => 'A channel for this community already exists.',
]);
$name = collect($this->availableCommunities)->firstWhere('id', $this->newCommunityId)['name'] ?? null;
try {
$action->execute(
$name,
$this->newCommunityId,
$this->newPlatformInstanceId,
$this->newLanguageId,
// Blade textarea binds an empty string when blank; the action expects null for "no description".
$this->newDescription !== '' ? $this->newDescription : null,
);
} catch (UniqueConstraintViolationException $e) {
$this->addError('newCommunityId', 'A channel for this community already exists.');
return;
} catch (RuntimeException $e) {
$this->addError('newPlatformInstanceId', $e->getMessage());
return;
}
$this->closeCreateModal();
}
public function openEditModal(int $channelId): void
{
$channel = PlatformChannel::findOrFail($channelId);
$this->resetErrorBag();
$this->editingChannelId = $channelId;
$this->editDisplayName = $channel->display_name;
$this->editLanguageId = $channel->language_id;
$this->editDescription = $channel->description ?? '';
}
public function closeEditModal(): void
{
$this->editingChannelId = null;
}
// The community pairing (name, channel_id, platform_instance_id) is deliberately immutable:
// it is the channel's remote identity, unique per instance, and re-pointing it would change
// the meaning of every route already attached.
public function updateChannel(): void
{
if ($this->editingChannelId === null) {
return;
}
$this->validate([
'editDisplayName' => 'required|string|max:255',
'editLanguageId' => 'nullable|integer|exists:languages,id',
]);
PlatformChannel::findOrFail($this->editingChannelId)->update([
'display_name' => $this->editDisplayName,
'language_id' => $this->editLanguageId,
'description' => $this->editDescription !== '' ? $this->editDescription : null,
]);
$this->closeEditModal();
}
public function openAccountModal(int $channelId): void
{
$this->managingChannelId = $channelId;
}
public function closeAccountModal(): void
{
$this->managingChannelId = null;
}
public function attachAccount(int $accountId): void
{
if (! $this->managingChannelId) {
return;
}
$channel = PlatformChannel::findOrFail($this->managingChannelId);
if (! $channel->platformAccounts()->where('platform_account_id', $accountId)->exists()) {
$channel->platformAccounts()->attach($accountId, [
'is_active' => true,
'priority' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
public function detachAccount(int $channelId, int $accountId): void
{
$channel = PlatformChannel::findOrFail($channelId);
$channel->platformAccounts()->detach($accountId);
}
/**
* Row counts per channel, so the delete confirmation can say what is about to go.
*
* @return array<int, array{articles: int, publications: int}>
*/
private function deletionImpact(): array
{
/** @var array<int, int> $articles */
$articles = RouteArticle::query()
->selectRaw('platform_channel_id, COUNT(*) as aggregate')
->groupBy('platform_channel_id')
->pluck('aggregate', 'platform_channel_id')
->all();
/** @var array<int, int> $publications */
$publications = ArticlePublication::query()
->selectRaw('platform_channel_id, COUNT(*) as aggregate')
->groupBy('platform_channel_id')
->pluck('aggregate', 'platform_channel_id')
->all();
$impact = [];
foreach (array_keys($articles + $publications) as $channelId) {
$impact[$channelId] = [
'articles' => (int) ($articles[$channelId] ?? 0),
'publications' => (int) ($publications[$channelId] ?? 0),
];
}
return $impact;
}
public function render(): View
{
$channels = PlatformChannel::with(['platformInstance', 'platformAccounts'])->orderBy('name')->get();
$allAccounts = PlatformAccount::where('is_active', true)->get();
$managingChannel = $this->managingChannelId
? PlatformChannel::with('platformAccounts')->find($this->managingChannelId)
: null;
$availableAccounts = $managingChannel
? $allAccounts->filter(fn ($account) => ! $managingChannel->platformAccounts->contains('id', $account->id))
: collect();
return view('livewire.channels', [
'channels' => $channels,
'managingChannel' => $managingChannel,
'editingChannel' => $this->editingChannelId !== null
? PlatformChannel::with('platformInstance')->find($this->editingChannelId)
: null,
'availableAccounts' => $availableAccounts,
'deletionImpact' => $this->deletionImpact(),
'platformInstances' => PlatformInstance::where('is_active', true)->orderBy('name')->get(),
'languages' => Language::where('is_active', true)->orderBy('name')->get(),
])->layout('layouts.app');
}
}