lemmyAuthService = $lemmyAuthService; } 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)', ]); $fullInstanceUrl = 'https://' . $this->instanceUrl; try { // Authenticate with Lemmy API first (before creating any records) $authResponse = $this->lemmyAuthService->authenticate( $fullInstanceUrl, $this->username, $this->password ); // Only create platform instance after successful authentication $platformInstance = PlatformInstance::firstOrCreate([ 'url' => $fullInstanceUrl, 'platform' => 'lemmy', ], [ 'name' => ucfirst($this->instanceUrl), 'is_active' => true, ]); // Create platform account $platformAccount = PlatformAccount::create([ 'platform' => 'lemmy', 'instance_url' => $fullInstanceUrl, 'username' => $this->username, 'password' => Crypt::encryptString($this->password), 'settings' => [ 'display_name' => $authResponse['person_view']['person']['display_name'] ?? null, 'description' => $authResponse['person_view']['person']['bio'] ?? null, 'person_id' => $authResponse['person_view']['person']['id'] ?? null, 'platform_instance_id' => $platformInstance->id, 'api_token' => $authResponse['jwt'] ?? null, ], 'is_active' => true, 'status' => 'active', ]); $this->existingAccount = [ 'id' => $platformAccount->id, 'username' => $platformAccount->username, 'instance_url' => $platformAccount->instance_url, ]; $this->nextStep(); } catch (\App\Exceptions\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' => $fullInstanceUrl, '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 { // Get language short code $language = Language::find($this->feedLanguageId); $langCode = $language->short_code; // Look up URL from config $url = config("feed.providers.{$this->feedProvider}.languages.{$langCode}.url"); if (!$url) { $this->formErrors['general'] = 'Invalid provider and language combination.'; $this->isLoading = false; return; } $providerConfig = config("feed.providers.{$this->feedProvider}"); Feed::firstOrCreate( ['url' => $url], [ 'name' => $this->feedName, 'type' => $providerConfig['type'] ?? 'website', 'provider' => $this->feedProvider, 'language_id' => $this->feedLanguageId, 'description' => $this->feedDescription ?: null, 'is_active' => true, ] ); $this->nextStep(); } 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 { $platformInstance = PlatformInstance::findOrFail($this->platformInstanceId); // Check for active platform accounts $activeAccounts = PlatformAccount::where('instance_url', $platformInstance->url) ->where('is_active', true) ->get(); if ($activeAccounts->isEmpty()) { $this->formErrors['general'] = 'No active platform accounts found for this instance. Please create a platform account first.'; $this->isLoading = false; return; } $channel = PlatformChannel::create([ 'platform_instance_id' => $this->platformInstanceId, 'channel_id' => $this->channelName, 'name' => $this->channelName, 'display_name' => ucfirst($this->channelName), 'description' => $this->channelDescription ?: null, 'language_id' => $this->channelLanguageId, 'is_active' => true, ]); // Attach first active account $channel->platformAccounts()->attach($activeAccounts->first()->id, [ 'is_active' => true, 'priority' => 1, 'created_at' => now(), 'updated_at' => now(), ]); // Sync existing posts from this channel for duplicate detection SyncChannelPostsJob::dispatch($channel); $this->nextStep(); } 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 { Route::create([ 'feed_id' => $this->routeFeedId, 'platform_channel_id' => $this->routeChannelId, 'priority' => $this->routePriority, 'is_active' => true, ]); // 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. */ 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[$code] = true; } } return array_keys($languageCodes); } /** * Get providers available for the current channel language. */ 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() { // 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'); } }