diff --git a/app-modules/activity/src/Message/Actions/NewMessage.php b/app-modules/activity/src/Message/Actions/NewMessage.php index 301b7bf99..81c4f88c0 100644 --- a/app-modules/activity/src/Message/Actions/NewMessage.php +++ b/app-modules/activity/src/Message/Actions/NewMessage.php @@ -28,6 +28,7 @@ public function persist(NewMessageDTO $messageDTO): void 'external_account_id' => $messageDTO->externalAccountId, 'model_type' => (new User)->getMorphClass(), 'username' => $messageDTO->providerUsername, + 'avatar' => $messageDTO->avatar, ]); $userContext = resolve(ResolveUserContext::class)->handle($userDto); diff --git a/app-modules/activity/src/Message/DTOs/NewMessageDTO.php b/app-modules/activity/src/Message/DTOs/NewMessageDTO.php index 3fd773e40..2852cc21f 100644 --- a/app-modules/activity/src/Message/DTOs/NewMessageDTO.php +++ b/app-modules/activity/src/Message/DTOs/NewMessageDTO.php @@ -18,6 +18,7 @@ public function __construct( public string $channelId, public string $content, public DateTimeImmutable $sentAt, + public ?string $avatar = null, ) {} /** @@ -33,7 +34,8 @@ public static function make(array $payload): self providerMessageId: $payload['provider_message_id'], channelId: $payload['channel_id'], content: $payload['content'], - sentAt: new DateTimeImmutable($payload['sent_at']) + sentAt: new DateTimeImmutable($payload['sent_at']), + avatar: $payload['avatar'] ?? null ); } } diff --git a/app-modules/bot-discord/src/Events/MessageReceivedEvent.php b/app-modules/bot-discord/src/Events/MessageReceivedEvent.php index 211f12934..5146afcff 100644 --- a/app-modules/bot-discord/src/Events/MessageReceivedEvent.php +++ b/app-modules/bot-discord/src/Events/MessageReceivedEvent.php @@ -39,12 +39,13 @@ public function handle(Message $message): void resolve(NewMessage::class)->persist(new NewMessageDTO( tenantId: $tenantProvider->tenant_id, provider: IdentityProvider::Discord, - providerUsername: $message->author->username.'#'.$message->author->discriminator, + providerUsername: $message->author->username, externalAccountId: $message->user_id, providerMessageId: $message->id, channelId: $message->channel_id, content: $message->content, - sentAt: $message->timestamp->toDateTimeImmutable() + sentAt: $message->timestamp->toDateTimeImmutable(), + avatar: $message->author->avatar, )); // Moderation pipeline — SubmitForModeration handles pre-screen (sync) + async AI. diff --git a/app-modules/he4rt/resources/views/components/base/logo.blade.php b/app-modules/he4rt/resources/views/components/base/logo.blade.php new file mode 100644 index 000000000..cf81318ba --- /dev/null +++ b/app-modules/he4rt/resources/views/components/base/logo.blade.php @@ -0,0 +1,7 @@ +
+ He4rt Developers +
+ He4rt + Developers +
+
diff --git a/app-modules/identity/src/ExternalIdentity/Actions/ResolveExternalIdentity.php b/app-modules/identity/src/ExternalIdentity/Actions/ResolveExternalIdentity.php index ba7ed3a5f..527193980 100644 --- a/app-modules/identity/src/ExternalIdentity/Actions/ResolveExternalIdentity.php +++ b/app-modules/identity/src/ExternalIdentity/Actions/ResolveExternalIdentity.php @@ -13,7 +13,7 @@ final class ResolveExternalIdentity { public function handle(ResolveUserProviderDTO $dto): ExternalIdentity { - return ExternalIdentity::query()->firstOrCreate( + $identity = ExternalIdentity::query()->firstOrCreate( [ 'provider' => $dto->provider, 'tenant_id' => $dto->tenantId, @@ -24,13 +24,22 @@ public function handle(ResolveUserProviderDTO $dto): ExternalIdentity 'type' => $dto->provider->getType(), 'credentials_type' => CredentialsType::OAuth2, 'credentials' => ClientAccessManager::make(), - 'metadata' => array_filter([ - 'username' => $dto->username, - 'email' => $dto->email, - 'avatar' => $dto->avatar, - ]), 'connected_at' => now(), ] ); + + $metadata = array_filter([ + 'username' => $dto->username, + 'email' => $dto->email, + 'avatar' => $dto->avatar, + ]); + + if ($metadata !== []) { + $identity->update([ + 'metadata' => array_merge($identity->metadata ?? [], $metadata), + ]); + } + + return $identity; } } diff --git a/app-modules/integration-discord/src/IntegrationDiscordServiceProvider.php b/app-modules/integration-discord/src/IntegrationDiscordServiceProvider.php index 7b06ab525..e8d53f5f1 100644 --- a/app-modules/integration-discord/src/IntegrationDiscordServiceProvider.php +++ b/app-modules/integration-discord/src/IntegrationDiscordServiceProvider.php @@ -10,6 +10,7 @@ use He4rt\IntegrationDiscord\ETL\Console\MergeDuplicateDiscordProfilesCommand; use He4rt\IntegrationDiscord\Models\DiscordEventLog; use He4rt\IntegrationDiscord\Sync\Console\SyncDiscordGuildCommand; +use He4rt\IntegrationDiscord\Sync\Console\SyncDiscordHistoryCommand; use He4rt\IntegrationDiscord\Sync\Observers\DiscordEventLogObserver; use He4rt\IntegrationDiscord\Transport\DiscordConnector; use He4rt\IntegrationDiscord\Transport\DiscordOAuthConnector; @@ -40,6 +41,7 @@ public function boot(): void ImportDiscordMessagesCommand::class, MergeDuplicateDiscordProfilesCommand::class, SyncDiscordGuildCommand::class, + SyncDiscordHistoryCommand::class, BackfillVoiceLogsCommand::class, ]); } diff --git a/app-modules/integration-discord/src/Sync/Console/SyncDiscordHistoryCommand.php b/app-modules/integration-discord/src/Sync/Console/SyncDiscordHistoryCommand.php new file mode 100644 index 000000000..1b9a3b33c --- /dev/null +++ b/app-modules/integration-discord/src/Sync/Console/SyncDiscordHistoryCommand.php @@ -0,0 +1,96 @@ +argument('channel_id'); + $limit = (int) $this->option('limit'); + + $botToken = config('discord.token') ?? env('HE4RT_DISCORD_BOT_KEY'); + $guildId = env('HE4RT_DISCORD_GUILD'); + + if (! $botToken) { + $this->error('Discord bot token not configured.'); + return self::FAILURE; + } + + // Find the tenant associated with this guild to ensure multi-tenancy rules are respected + $tenantProvider = ExternalIdentity::query() + ->where('model_type', (new Tenant())->getMorphClass()) + ->where('external_account_id', (string) $guildId) + ->first(); + + if (! $tenantProvider) { + $this->error("Tenant not found for guild {$guildId}. Check your environment configuration."); + return self::FAILURE; + } + + $this->info("Fetching last {$limit} messages from channel {$channelId}..."); + + try { + $connector = new DiscordConnector($botToken); + $response = $connector->send(new ListChannelMessages($channelId, limit: $limit)); + + if (! $response->successful()) { + $this->error("Failed to fetch messages from Discord: " . $response->body()); + return self::FAILURE; + } + + $messages = $response->json(); + $count = 0; + + $this->withProgressBar($messages, function (array $m) use ($tenantProvider, $newMessageAction, &$count) { + if (isset($m['author']['bot']) && $m['author']['bot']) { + return; + } + + $author = $m['author']; + + try { + $newMessageAction->persist(new NewMessageDTO( + tenantId: $tenantProvider->tenant_id, + provider: IdentityProvider::Discord, + providerUsername: $author['username'], + externalAccountId: $author['id'], + providerMessageId: $m['id'], + channelId: $m['channel_id'], + content: $m['content'] ?? '', + sentAt: new DateTimeImmutable($m['timestamp']), + avatar: $author['avatar'] ?? null, + )); + $count++; + } catch (Throwable $e) { + // Silently skip individual message failures to keep progress + } + }); + + $this->newLine(); + $this->info("Successfully synced {$count} messages and updated participant profiles."); + + return self::SUCCESS; + } catch (Throwable $e) { + $this->error("An error occurred during sync: " . $e->getMessage()); + return self::FAILURE; + } + } +} diff --git a/app-modules/panel-admin/src/Filament/Resources/ExternalIdentities/ExternalIdentityResource.php b/app-modules/panel-admin/src/Filament/Resources/ExternalIdentities/ExternalIdentityResource.php index 8319bd5bb..f595f64b4 100644 --- a/app-modules/panel-admin/src/Filament/Resources/ExternalIdentities/ExternalIdentityResource.php +++ b/app-modules/panel-admin/src/Filament/Resources/ExternalIdentities/ExternalIdentityResource.php @@ -66,6 +66,10 @@ public static function getEloquentQuery(): Builder ->withoutGlobalScopes([ SoftDeletingScope::class, ]) + ->when( + session('active_provider'), + fn (Builder $query, string $provider) => $query->where('provider', $provider) + ) ->with(['model', 'connectedByUser']); } diff --git a/app-modules/panel-admin/src/Http/Middleware/SetProviderScope.php b/app-modules/panel-admin/src/Http/Middleware/SetProviderScope.php new file mode 100644 index 000000000..555f98d42 --- /dev/null +++ b/app-modules/panel-admin/src/Http/Middleware/SetProviderScope.php @@ -0,0 +1,30 @@ +has('provider')) { + $provider = IdentityProvider::tryFrom($request->query('provider')); + + if ($provider) { + session(['active_provider' => $provider->value]); + } + } + + if (!session()->has('active_provider')) { + session(['active_provider' => IdentityProvider::Discord->value]); + } + + return $next($request); + } +} diff --git a/app-modules/panel-admin/src/Marketing/Pages/MeetingShowcasePage.php b/app-modules/panel-admin/src/Marketing/Pages/MeetingShowcasePage.php index 123a5a0e0..2ed3778aa 100644 --- a/app-modules/panel-admin/src/Marketing/Pages/MeetingShowcasePage.php +++ b/app-modules/panel-admin/src/Marketing/Pages/MeetingShowcasePage.php @@ -49,6 +49,17 @@ public function getTitle(): string return __('panel-admin::marketing.navigation.meeting_showcase'); } + public function mount(): void + { + // Default to today's meeting time as a helpful preset + $tz = config('app.display_timezone', 'America/Sao_Paulo'); + $this->startDate = now($tz)->setTime(22, 0)->format('Y-m-d\TH:i'); + $this->endDate = now($tz)->setTime(23, 40)->format('Y-m-d\TH:i'); + + // Default meeting channel + $this->channelId = '853401652471398400'; + } + public function loadParticipants(): void { if ($this->channelId === '' || $this->startDate === '' || $this->endDate === '') { @@ -68,17 +79,20 @@ public function loadParticipants(): void ->orderByDesc('total_messages') ->get(); - $identityIds = $messageStats->pluck('external_identity_id'); + $identityIds = $messageStats->pluck('external_identity_id')->unique()->filter()->values()->all(); $identities = ExternalIdentity::query() + ->withoutGlobalScopes() + ->withTrashed() ->whereIn('id', $identityIds) ->get() ->keyBy('id'); $this->participants = $messageStats->map(function (Message $stat) use ($identities): array { - $identity = $identities->get($stat->external_identity_id); + $identityId = (string) $stat->external_identity_id; + $identity = $identities->get($identityId); - return $this->extractDiscordData($identity, (int) $stat->total_messages); // @phpstan-ignore property.notFound + return $this->extractDiscordData($identity, (int) $stat->total_messages); })->all(); $this->loaded = true; diff --git a/app-modules/panel-admin/src/PanelAdminServiceProvider.php b/app-modules/panel-admin/src/PanelAdminServiceProvider.php index a8ff60d69..eb9e08010 100644 --- a/app-modules/panel-admin/src/PanelAdminServiceProvider.php +++ b/app-modules/panel-admin/src/PanelAdminServiceProvider.php @@ -113,7 +113,7 @@ private function moderationNavigation(NavigationBuilder $builder): NavigationBui NavigationItem::make(__('panel-admin::moderation.navigation.back_to_admin')) ->sort(0) ->icon('heroicon-o-arrow-left') - ->url(Dashboard::getUrl()), + ->url(Dashboard::getUrl(['tenant' => filament()->getTenant()])), ])->groups(resolve(ModerationCluster::class)->getCachedSubNavigation()); } @@ -124,7 +124,7 @@ private function marketingNavigation(NavigationBuilder $builder): NavigationBuil NavigationItem::make(__('panel-admin::marketing.navigation.back_to_admin')) ->sort(0) ->icon('heroicon-o-arrow-left') - ->url(Dashboard::getUrl()), + ->url(Dashboard::getUrl(['tenant' => filament()->getTenant()])), ])->groups(resolve(MarketingCluster::class)->getCachedSubNavigation()); } @@ -135,7 +135,7 @@ private function twitchNavigation(NavigationBuilder $builder): NavigationBuilder NavigationItem::make(__('panel-admin::twitch.navigation.back_to_admin')) ->sort(0) ->icon('heroicon-o-arrow-left') - ->url(Dashboard::getUrl()), + ->url(Dashboard::getUrl(['tenant' => filament()->getTenant()])), ])->groups(resolve(TwitchCluster::class)->getCachedSubNavigation()); } diff --git a/app-modules/panel-app/resources/views/auth/login.blade.php b/app-modules/panel-app/resources/views/auth/login.blade.php index 67b2fdb93..b102beb98 100644 --- a/app-modules/panel-app/resources/views/auth/login.blade.php +++ b/app-modules/panel-app/resources/views/auth/login.blade.php @@ -1,124 +1,172 @@ - -
- {{-- Left: Brand panel --}} +