437 lines
14 KiB
PHP
437 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire;
|
|
|
|
use App\Actions\CreateChannelAction;
|
|
use App\Actions\CreateFeedAction;
|
|
use App\Actions\CreatePlatformAccountAction;
|
|
use App\Actions\CreateRouteAction;
|
|
use App\Exceptions\PlatformAuthException;
|
|
use App\Jobs\ArticleDiscoveryJob;
|
|
use App\Jobs\SyncChannelPostsJob;
|
|
use App\Models\Feed;
|
|
use App\Models\Language;
|
|
use App\Models\PlatformAccount;
|
|
use App\Models\PlatformChannel;
|
|
use App\Models\PlatformInstance;
|
|
use App\Models\Route;
|
|
use App\Models\Setting;
|
|
use App\Services\OnboardingService;
|
|
use Exception;
|
|
use InvalidArgumentException;
|
|
use Livewire\Component;
|
|
use RuntimeException;
|
|
|
|
class Onboarding extends Component
|
|
{
|
|
// Step tracking (1-6: welcome, platform, channel, feed, route, complete)
|
|
public int $step = 1;
|
|
|
|
// Platform form
|
|
public string $instanceUrl = '';
|
|
|
|
public string $username = '';
|
|
|
|
public string $password = '';
|
|
|
|
/** @var array<string, mixed>|null */
|
|
public ?array $existingAccount = null;
|
|
|
|
// Feed form
|
|
public string $feedName = '';
|
|
|
|
public string $feedProvider = 'vrt';
|
|
|
|
public ?int $feedLanguageId = null;
|
|
|
|
public string $feedDescription = '';
|
|
|
|
// Channel form
|
|
public string $channelName = '';
|
|
|
|
public ?int $platformInstanceId = null;
|
|
|
|
public ?int $channelLanguageId = null;
|
|
|
|
public string $channelDescription = '';
|
|
|
|
// Route form
|
|
public ?int $routeFeedId = null;
|
|
|
|
public ?int $routeChannelId = null;
|
|
|
|
public int $routePriority = 50;
|
|
|
|
// State
|
|
/** @var array<string, string> */
|
|
public array $formErrors = [];
|
|
|
|
public bool $isLoading = false;
|
|
|
|
#[\Livewire\Attributes\Locked]
|
|
public ?int $previousChannelLanguageId = null;
|
|
|
|
protected CreatePlatformAccountAction $createPlatformAccountAction;
|
|
|
|
protected CreateFeedAction $createFeedAction;
|
|
|
|
protected CreateChannelAction $createChannelAction;
|
|
|
|
protected CreateRouteAction $createRouteAction;
|
|
|
|
public function boot(
|
|
CreatePlatformAccountAction $createPlatformAccountAction,
|
|
CreateFeedAction $createFeedAction,
|
|
CreateChannelAction $createChannelAction,
|
|
CreateRouteAction $createRouteAction,
|
|
): void {
|
|
$this->createPlatformAccountAction = $createPlatformAccountAction;
|
|
$this->createFeedAction = $createFeedAction;
|
|
$this->createChannelAction = $createChannelAction;
|
|
$this->createRouteAction = $createRouteAction;
|
|
}
|
|
|
|
public function mount(): void
|
|
{
|
|
// Check for existing platform account
|
|
$account = PlatformAccount::where('is_active', true)->first();
|
|
if ($account) {
|
|
$this->existingAccount = [
|
|
'id' => $account->id,
|
|
'username' => $account->username,
|
|
'instance_url' => $account->instance_url,
|
|
];
|
|
}
|
|
|
|
// Pre-fill feed form if exists
|
|
$feed = Feed::where('is_active', true)->first();
|
|
if ($feed) {
|
|
$this->feedName = $feed->name;
|
|
$this->feedProvider = $feed->provider ?? 'vrt';
|
|
$this->feedLanguageId = $feed->language_id;
|
|
$this->feedDescription = $feed->description ?? '';
|
|
}
|
|
|
|
// Pre-fill channel form if exists
|
|
$channel = PlatformChannel::where('is_active', true)->first();
|
|
if ($channel) {
|
|
$this->channelName = $channel->name;
|
|
$this->platformInstanceId = $channel->platform_instance_id;
|
|
$this->channelLanguageId = $channel->language_id;
|
|
$this->channelDescription = $channel->description ?? '';
|
|
}
|
|
|
|
// Pre-fill route form if exists
|
|
$route = Route::where('is_active', true)->first();
|
|
if ($route) {
|
|
$this->routeFeedId = $route->feed_id;
|
|
$this->routeChannelId = $route->platform_channel_id;
|
|
$this->routePriority = $route->priority;
|
|
}
|
|
}
|
|
|
|
public function goToStep(int $step): void
|
|
{
|
|
$this->step = $step;
|
|
$this->formErrors = [];
|
|
}
|
|
|
|
public function nextStep(): void
|
|
{
|
|
$this->step++;
|
|
$this->formErrors = [];
|
|
|
|
// When entering feed step, inherit language from channel
|
|
if ($this->step === 4 && $this->channelLanguageId) {
|
|
$this->feedLanguageId = $this->channelLanguageId;
|
|
}
|
|
}
|
|
|
|
public function previousStep(): void
|
|
{
|
|
if ($this->step > 1) {
|
|
$this->step--;
|
|
$this->formErrors = [];
|
|
}
|
|
}
|
|
|
|
public function continueWithExistingAccount(): void
|
|
{
|
|
$this->nextStep();
|
|
}
|
|
|
|
public function deleteAccount(): void
|
|
{
|
|
if ($this->existingAccount) {
|
|
PlatformAccount::destroy($this->existingAccount['id']);
|
|
$this->existingAccount = null;
|
|
}
|
|
}
|
|
|
|
public function createPlatformAccount(): void
|
|
{
|
|
$this->formErrors = [];
|
|
$this->isLoading = true;
|
|
|
|
$this->validate([
|
|
'instanceUrl' => 'required|string|max:255|regex:/^[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$/',
|
|
'username' => 'required|string|max:255',
|
|
'password' => 'required|string|min:6',
|
|
], [
|
|
'instanceUrl.regex' => 'Please enter a valid domain name (e.g., lemmy.world, belgae.social)',
|
|
]);
|
|
|
|
try {
|
|
$platformAccount = $this->createPlatformAccountAction->execute(
|
|
$this->instanceUrl,
|
|
$this->username,
|
|
$this->password,
|
|
);
|
|
|
|
$this->existingAccount = [
|
|
'id' => $platformAccount->id,
|
|
'username' => $platformAccount->username,
|
|
'instance_url' => $platformAccount->instance_url,
|
|
];
|
|
|
|
$this->nextStep();
|
|
} catch (PlatformAuthException $e) {
|
|
$message = $e->getMessage();
|
|
if (str_contains($message, 'Rate limited by')) {
|
|
$this->formErrors['general'] = $message;
|
|
} elseif (str_contains($message, 'Connection failed')) {
|
|
$this->formErrors['general'] = 'Unable to connect to the Lemmy instance. Please check the URL and try again.';
|
|
} else {
|
|
$this->formErrors['general'] = 'Invalid username or password. Please check your credentials and try again.';
|
|
}
|
|
} catch (Exception $e) {
|
|
logger()->error('Lemmy platform account creation failed', [
|
|
'instance_url' => 'https://'.$this->instanceUrl,
|
|
'username' => $this->username,
|
|
'error' => $e->getMessage(),
|
|
'class' => get_class($e),
|
|
]);
|
|
$this->formErrors['general'] = 'An error occurred while setting up your account. Please try again.';
|
|
} finally {
|
|
$this->isLoading = false;
|
|
}
|
|
}
|
|
|
|
public function createFeed(): void
|
|
{
|
|
$this->formErrors = [];
|
|
$this->isLoading = true;
|
|
|
|
// Get available provider codes for validation
|
|
$availableProviders = collect($this->getProvidersForLanguage())->pluck('code')->implode(',');
|
|
|
|
$this->validate([
|
|
'feedName' => 'required|string|max:255',
|
|
'feedProvider' => "required|in:{$availableProviders}",
|
|
'feedLanguageId' => 'required|exists:languages,id',
|
|
'feedDescription' => 'nullable|string|max:1000',
|
|
]);
|
|
|
|
try {
|
|
$this->createFeedAction->execute(
|
|
$this->feedName,
|
|
$this->feedProvider,
|
|
$this->feedLanguageId,
|
|
$this->feedDescription ?: null,
|
|
);
|
|
|
|
$this->nextStep();
|
|
} catch (InvalidArgumentException $e) {
|
|
$this->formErrors['general'] = 'Invalid provider and language combination.';
|
|
} catch (Exception $e) {
|
|
$this->formErrors['general'] = 'Failed to create feed. Please try again.';
|
|
} finally {
|
|
$this->isLoading = false;
|
|
}
|
|
}
|
|
|
|
public function createChannel(): void
|
|
{
|
|
$this->formErrors = [];
|
|
$this->isLoading = true;
|
|
|
|
$this->validate([
|
|
'channelName' => 'required|string|max:255',
|
|
'platformInstanceId' => 'required|exists:platform_instances,id',
|
|
'channelLanguageId' => 'required|exists:languages,id',
|
|
'channelDescription' => 'nullable|string|max:1000',
|
|
]);
|
|
|
|
// If language changed, reset feed form
|
|
if ($this->previousChannelLanguageId !== null && $this->previousChannelLanguageId !== $this->channelLanguageId) {
|
|
$this->feedName = '';
|
|
$this->feedProvider = '';
|
|
$this->feedDescription = '';
|
|
$this->routeFeedId = null;
|
|
$this->routeChannelId = null;
|
|
}
|
|
$this->previousChannelLanguageId = $this->channelLanguageId;
|
|
|
|
try {
|
|
$channel = $this->createChannelAction->execute(
|
|
$this->channelName,
|
|
$this->platformInstanceId,
|
|
$this->channelLanguageId,
|
|
$this->channelDescription ?: null,
|
|
);
|
|
|
|
// Sync existing posts from this channel for duplicate detection
|
|
SyncChannelPostsJob::dispatch($channel);
|
|
|
|
$this->nextStep();
|
|
} catch (RuntimeException $e) {
|
|
$this->formErrors['general'] = $e->getMessage();
|
|
} catch (Exception $e) {
|
|
$this->formErrors['general'] = 'Failed to create channel. Please try again.';
|
|
} finally {
|
|
$this->isLoading = false;
|
|
}
|
|
}
|
|
|
|
public function createRoute(): void
|
|
{
|
|
$this->formErrors = [];
|
|
$this->isLoading = true;
|
|
|
|
$this->validate([
|
|
'routeFeedId' => 'required|exists:feeds,id',
|
|
'routeChannelId' => 'required|exists:platform_channels,id',
|
|
'routePriority' => 'nullable|integer|min:1|max:100',
|
|
]);
|
|
|
|
try {
|
|
$this->createRouteAction->execute(
|
|
$this->routeFeedId,
|
|
$this->routeChannelId,
|
|
$this->routePriority,
|
|
);
|
|
|
|
// Trigger article discovery
|
|
ArticleDiscoveryJob::dispatch();
|
|
|
|
$this->nextStep();
|
|
} catch (Exception $e) {
|
|
$this->formErrors['general'] = 'Failed to create route. Please try again.';
|
|
} finally {
|
|
$this->isLoading = false;
|
|
}
|
|
}
|
|
|
|
public function completeOnboarding(): void
|
|
{
|
|
Setting::updateOrCreate(
|
|
['key' => 'onboarding_completed'],
|
|
['value' => now()->toIso8601String()]
|
|
);
|
|
|
|
app(OnboardingService::class)->clearCache();
|
|
|
|
$this->redirect(route('dashboard'));
|
|
}
|
|
|
|
/**
|
|
* Get language codes that have at least one active provider.
|
|
*/
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
public function getAvailableLanguageCodes(): array
|
|
{
|
|
$providers = config('feed.providers', []);
|
|
$languageCodes = [];
|
|
|
|
foreach ($providers as $provider) {
|
|
if (! ($provider['is_active'] ?? false)) {
|
|
continue;
|
|
}
|
|
foreach (array_keys($provider['languages'] ?? []) as $code) {
|
|
$languageCodes[(string) $code] = true;
|
|
}
|
|
}
|
|
|
|
return array_keys($languageCodes);
|
|
}
|
|
|
|
/**
|
|
* Get providers available for the current channel language.
|
|
*/
|
|
/**
|
|
* @return array<int, array<string, string>>
|
|
*/
|
|
public function getProvidersForLanguage(): array
|
|
{
|
|
if (! $this->channelLanguageId) {
|
|
return [];
|
|
}
|
|
|
|
$language = Language::find($this->channelLanguageId);
|
|
if (! $language) {
|
|
return [];
|
|
}
|
|
|
|
$langCode = $language->short_code;
|
|
$providers = config('feed.providers', []);
|
|
$available = [];
|
|
|
|
foreach ($providers as $key => $provider) {
|
|
if (! ($provider['is_active'] ?? false)) {
|
|
continue;
|
|
}
|
|
if (isset($provider['languages'][$langCode])) {
|
|
$available[] = [
|
|
'code' => $provider['code'],
|
|
'name' => $provider['name'],
|
|
'description' => $provider['description'] ?? '',
|
|
];
|
|
}
|
|
}
|
|
|
|
return $available;
|
|
}
|
|
|
|
/**
|
|
* Get the current channel language model.
|
|
*/
|
|
public function getChannelLanguage(): ?Language
|
|
{
|
|
if (! $this->channelLanguageId) {
|
|
return null;
|
|
}
|
|
|
|
return Language::find($this->channelLanguageId);
|
|
}
|
|
|
|
public function render(): \Illuminate\Contracts\View\View
|
|
{
|
|
// For channel step: only show languages that have providers
|
|
$availableCodes = $this->getAvailableLanguageCodes();
|
|
$wizardLanguages = Language::where('is_active', true)
|
|
->whereIn('short_code', $availableCodes)
|
|
->orderBy('name')
|
|
->get();
|
|
|
|
$platformInstances = PlatformInstance::where('is_active', true)->orderBy('name')->get();
|
|
$feeds = Feed::with('language')->where('is_active', true)->orderBy('name')->get();
|
|
$channels = PlatformChannel::with('language')->where('is_active', true)->orderBy('name')->get();
|
|
|
|
// For feed step: only show providers for the channel's language
|
|
$feedProviders = collect($this->getProvidersForLanguage());
|
|
|
|
// Get channel language for display
|
|
$channelLanguage = $this->getChannelLanguage();
|
|
|
|
return view('livewire.onboarding', [
|
|
'wizardLanguages' => $wizardLanguages,
|
|
'platformInstances' => $platformInstances,
|
|
'feeds' => $feeds,
|
|
'channels' => $channels,
|
|
'feedProviders' => $feedProviders,
|
|
'channelLanguage' => $channelLanguage,
|
|
])->layout('layouts.onboarding');
|
|
}
|
|
}
|