From 0517a966a3b81ecced658bf0f8262484f891cf26 Mon Sep 17 00:00:00 2001 From: Hampus Kraft Date: Wed, 18 Feb 2026 15:38:51 +0000 Subject: [PATCH] fix: various fixes to sentry-reported errors and more --- .github/workflows/deploy-queue.yaml | 92 - .gitignore | 2 +- config/config.dev.template.json | 11 +- config/config.production.template.json | 14 +- config/config.test.json | 8 - devenv.nix | 21 + fluxer_app/src/Endpoints.tsx | 5 + .../src/components/channel/Messages.tsx | 2 +- .../MessageSearchBar.module.css | 2 +- .../src/components/emojis/EmojiListItem.tsx | 107 +- .../src/components/form/Input.module.css | 4 +- fluxer_app/src/components/layout/UserArea.tsx | 28 +- .../components/modals/AddConnectionModal.tsx | 5 +- .../components/modals/GuildSettingsModal.tsx | 7 +- .../src/components/modals/InviteModal.tsx | 14 +- .../discovery/DiscoveryGuildCard.module.css | 2 +- .../modals/discovery/DiscoveryGuildCard.tsx | 6 + .../modals/guild_tabs/GuildDiscoveryTab.tsx | 55 +- .../modals/guild_tabs/GuildEmojiTab.tsx | 95 +- .../modals/guild_tabs/GuildStickersTab.tsx | 67 +- .../modals/utils/GuildSettingsConstants.tsx | 8 +- .../components/stickers/StickerGridItem.tsx | 42 +- .../context_menu/items/GuildMenuData.tsx | 7 +- .../context_menu/items/GuildMenuItems.tsx | 7 +- fluxer_app/src/lib/ChunkedUploadService.tsx | 195 +++ fluxer_app/src/lib/CloudUpload.tsx | 1 + fluxer_app/src/lib/MessageQueue.tsx | 98 ++ fluxer_app/src/lib/ScrollManager.tsx | 5 +- fluxer_app/src/locales/ar/messages.po | 535 +++--- fluxer_app/src/locales/bg/messages.po | 535 +++--- fluxer_app/src/locales/cs/messages.po | 535 +++--- fluxer_app/src/locales/da/messages.po | 535 +++--- fluxer_app/src/locales/de/messages.po | 535 +++--- fluxer_app/src/locales/el/messages.po | 535 +++--- fluxer_app/src/locales/en-GB/messages.po | 537 +++--- fluxer_app/src/locales/en-US/messages.po | 535 +++--- fluxer_app/src/locales/es-419/messages.po | 535 +++--- fluxer_app/src/locales/es-ES/messages.po | 535 +++--- fluxer_app/src/locales/fi/messages.po | 535 +++--- fluxer_app/src/locales/fr/messages.po | 535 +++--- fluxer_app/src/locales/he/messages.po | 535 +++--- fluxer_app/src/locales/hi/messages.po | 535 +++--- fluxer_app/src/locales/hr/messages.po | 535 +++--- fluxer_app/src/locales/hu/messages.po | 535 +++--- fluxer_app/src/locales/id/messages.po | 535 +++--- fluxer_app/src/locales/it/messages.po | 535 +++--- fluxer_app/src/locales/ja/messages.po | 535 +++--- fluxer_app/src/locales/ko/messages.po | 535 +++--- fluxer_app/src/locales/lt/messages.po | 535 +++--- fluxer_app/src/locales/nl/messages.po | 535 +++--- fluxer_app/src/locales/no/messages.po | 535 +++--- fluxer_app/src/locales/pl/messages.po | 535 +++--- fluxer_app/src/locales/pt-BR/messages.po | 535 +++--- fluxer_app/src/locales/ro/messages.po | 535 +++--- fluxer_app/src/locales/ru/messages.po | 535 +++--- fluxer_app/src/locales/sv-SE/messages.po | 535 +++--- fluxer_app/src/locales/th/messages.po | 535 +++--- fluxer_app/src/locales/tr/messages.po | 535 +++--- fluxer_app/src/locales/uk/messages.po | 535 +++--- fluxer_app/src/locales/vi/messages.po | 535 +++--- fluxer_app/src/locales/zh-CN/messages.po | 535 +++--- fluxer_app/src/locales/zh-TW/messages.po | 535 +++--- .../stores/gateway/GatewayConnectionStore.tsx | 3 +- .../ChannelSearchBottomSheet.module.css | 2 +- .../src/utils/MessageAttachmentUtils.tsx | 4 +- fluxer_app/src/utils/MessageRequestUtils.tsx | 1 + fluxer_app/src/utils/PermissionUtils.tsx | 6 + fluxer_app/src/utils/SelectUtils.tsx | 2 +- .../backups/ghost-blog-glide-theme.tar.gz.age | Bin 377170 -> 0 bytes .../ghost-help-guidepost-theme.tar.gz.age | Bin 2238324 -> 0 bytes .../backups/photo_dna_hash_service.tar.gz.age | Bin 50982 -> 0 bytes fluxer_devops/nats_core/compose.yaml | 29 + fluxer_devops/nats_core/nats.conf | 6 + fluxer_devops/nats_jetstream/compose.yaml | 34 + fluxer_devops/nats_jetstream/nats.conf | 12 + .../photo_dna_hash_service/.gitignore | 2 - .../signoz/alerts/critical-alerts.json | 407 ----- .../signoz/alerts/default-alerts.yaml | 329 ---- fluxer_docs/api-reference/openapi.json | 1006 ++++++++++- fluxer_docs/resources/admin.mdx | 42 + fluxer_docs/resources/channels.mdx | 77 + fluxer_docs/resources/guilds.mdx | 3 + fluxer_docs/resources/overview.mdx | 15 +- fluxer_docs/resources/users.mdx | 10 - fluxer_docs/self_hosting/configuration.mdx | 4 +- fluxer_gateway/rebar.config | 6 +- fluxer_gateway/rebar.lock | 9 + fluxer_gateway/src/call/call.erl | 2 - .../src/gateway/fluxer_gateway_app.erl | 3 +- .../src/gateway/fluxer_gateway_config.erl | 35 +- .../src/gateway/fluxer_gateway_sup.erl | 2 +- .../src/gateway/gateway_compress.erl | 2 - .../src/gateway/gateway_handler.erl | 15 +- .../src/gateway/gateway_nats_rpc.erl | 260 +++ .../src/gateway/gateway_rpc_guild.erl | 226 ++- .../src/gateway/gateway_rpc_http_handler.erl | 167 -- .../gateway/gateway_rpc_tcp_connection.erl | 446 ----- .../src/gateway/gateway_rpc_tcp_server.erl | 108 -- fluxer_gateway/src/gateway/hot_reload.erl | 1 + fluxer_gateway/src/gateway/rpc_client.erl | 132 +- fluxer_gateway/src/guild/guild.erl | 984 +++-------- .../src/guild/guild_availability.erl | 21 +- fluxer_gateway/src/guild/guild_client.erl | 54 +- fluxer_gateway/src/guild/guild_common.erl | 278 +++ .../src/guild/guild_counts_cache.erl | 128 ++ fluxer_gateway/src/guild/guild_data_index.erl | 292 ++++ fluxer_gateway/src/guild/guild_dispatch.erl | 195 ++- fluxer_gateway/src/guild/guild_ets_utils.erl | 53 + fluxer_gateway/src/guild/guild_manager.erl | 59 +- .../src/guild/guild_manager_shard.erl | 192 ++- .../src/guild/guild_member_list.erl | 1239 ++++++-------- .../src/guild/guild_member_list_common.erl | 1522 +++++++++++++++++ fluxer_gateway/src/guild/guild_members.erl | 443 +++++ .../src/guild/guild_passive_sync.erl | 62 +- .../src/guild/guild_permission_cache.erl | 136 +- fluxer_gateway/src/guild/guild_presence.erl | 170 +- .../src/guild/guild_query_handler.erl | 251 +++ .../src/guild/guild_request_members.erl | 317 ++++ fluxer_gateway/src/guild/guild_sessions.erl | 241 ++- fluxer_gateway/src/guild/guild_state.erl | 288 ++++ .../src/guild/guild_subscription_handler.erl | 327 ++++ fluxer_gateway/src/guild/guild_sync.erl | 24 - .../src/guild/guild_unified_subscriptions.erl | 2 +- fluxer_gateway/src/guild/guild_visibility.erl | 344 ++++ .../src/guild/guild_voice_handler.erl | 159 ++ .../src/guild/passive_sync_registry.erl | 225 +++ fluxer_gateway/src/guild/very_large_guild.erl | 126 +- .../guild/very_large_guild_member_list.erl | 668 +++++++- fluxer_gateway/src/guild/voice/dm_voice.erl | 4 +- .../src/guild/voice/guild_voice.erl | 24 +- .../guild/voice/guild_voice_connection.erl | 8 +- .../guild/voice/guild_voice_disconnect.erl | 20 - .../src/guild/voice/guild_voice_move.erl | 23 +- .../src/guild/voice/guild_voice_region.erl | 20 +- .../src/guild/voice/guild_voice_server.erl | 360 ++++ fluxer_gateway/src/presence/presence.erl | 40 +- .../src/presence/presence_payload.erl | 20 +- .../src/session/session_manager_shard.erl | 104 +- .../src/session/session_monitor.erl | 160 +- .../src/session/session_passive.erl | 1 + fluxer_gateway/src/session/session_voice.erl | 6 +- .../src/telemetry/process_registry.erl | 68 +- fluxer_integration/docker/config.json | 9 +- fluxer_marketing/src/App.tsx | 4 +- fluxer_marketing/src/Config.tsx | 4 +- fluxer_queue/Dockerfile | 72 - fluxer_queue/package.json | 31 - fluxer_queue/src/App.tsx | 110 -- fluxer_queue/src/Config.tsx | 40 - fluxer_queue/tsconfig.json | 10 - fluxer_server/package.json | 3 +- fluxer_server/src/HealthCheck.tsx | 51 +- fluxer_server/src/Routes.tsx | 7 - fluxer_server/src/ServiceInitializer.tsx | 82 +- fluxer_server/src/index.tsx | 75 +- .../src/utils/GatewayProcessManager.tsx | 1 - packages/admin/src/App.tsx | 2 +- packages/admin/src/api/Users.tsx | 27 + .../admin/src/middleware/ErrorHandler.tsx | 20 +- packages/admin/src/pages/SearchIndexPage.tsx | 8 + packages/admin/src/pages/UserDetailPage.tsx | 10 + .../src/pages/user_detail/tabs/AccountTab.tsx | 86 +- packages/admin/src/routes/Users.tsx | 17 + packages/api/package.json | 6 +- packages/api/src/Config.tsx | 12 +- packages/api/src/admin/AdminService.tsx | 21 +- .../controllers/DiscoveryAdminController.tsx | 4 +- .../admin/controllers/UserAdminController.tsx | 50 + packages/api/src/admin/models/UserTypes.tsx | 3 +- .../src/admin/services/AdminSearchService.tsx | 13 +- .../services/AdminUserSecurityService.tsx | 63 + .../src/admin/services/AdminUserService.tsx | 18 + .../AdminEndpointsAuthorization.test.tsx | 2 + .../tests/AdminSearchFieldCoverage.test.tsx | 565 ++++++ packages/api/src/app/APILifecycle.tsx | 57 +- packages/api/src/app/ControllerRegistry.tsx | 2 - .../src/auth/services/AuthLoginService.tsx | 3 +- .../auth/services/AuthRegistrationService.tsx | 9 +- .../src/bluesky/BlueskyOAuthController.tsx | 23 +- .../api/src/bluesky/BlueskyOAuthService.tsx | 2 +- .../src/bluesky/tests/BlueskyOAuth.test.tsx | 64 + .../controllers/ChunkedUploadController.tsx | 119 ++ .../api/src/channel/controllers/index.tsx | 2 + .../repositories/ChannelDataRepository.tsx | 6 +- .../message/MessageDataRepository.tsx | 2 +- .../channel/services/ChunkedUploadService.tsx | 227 +++ .../channel/services/StreamPreviewService.tsx | 24 +- .../message/AttachmentProcessingService.tsx | 18 +- .../message/MessageOperationsHelpers.tsx | 22 +- .../services/message/MessageRequestParser.tsx | 123 +- .../services/message/MessageSendService.tsx | 9 + .../src/channel/tests/AttachmentTestUtils.tsx | 10 - .../src/channel/tests/ChunkedUpload.test.tsx | 396 +++++ packages/api/src/config/APIConfig.tsx | 8 +- .../BlueskyOAuthAuthorizationFailedError.tsx | 27 + packages/api/src/database/Cassandra.tsx | 4 +- .../api/src/download/DownloadController.tsx | 13 +- packages/api/src/download/DownloadService.tsx | 43 +- .../src/favorite_meme/FavoriteMemeService.tsx | 15 +- .../tests/FavoriteMemeExtended.test.tsx | 19 +- .../gateway/tests/GatewayRpcService.test.tsx | 91 +- .../tests/GatewayTcpFrameCodec.test.tsx | 48 - .../tests/GatewayTcpRpcTransport.test.tsx | 309 ---- .../controllers/GuildDiscoveryController.tsx | 4 +- .../repositories/GuildContentRepository.tsx | 18 +- .../repositories/GuildDataRepository.tsx | 6 +- .../repositories/GuildMemberRepository.tsx | 9 +- .../repositories/GuildRoleRepository.tsx | 9 +- .../guild/services/GuildDiscoveryService.tsx | 99 +- .../infrastructure/DirectS3StorageService.tsx | 41 + .../src/infrastructure/GatewayRpcClient.tsx | 147 +- .../src/infrastructure/GatewayRpcError.tsx | 1 + .../api/src/infrastructure/GatewayService.tsx | 26 +- .../infrastructure/GatewayTcpFrameCodec.tsx | 77 - .../infrastructure/GatewayTcpRpcTransport.tsx | 528 ------ .../src/infrastructure/IGatewayService.tsx | 4 + .../src/infrastructure/IStorageService.tsx | 19 + .../NatsGatewayRpcTransport.tsx | 75 + .../api/src/infrastructure/StorageService.tsx | 69 +- .../src/infrastructure/UserCacheService.tsx | 21 +- .../MessageForwardingAccessControl.test.tsx | 670 ++++++++ .../RequireXForwardedForMiddleware.tsx | 1 - .../api/src/middleware/ServiceMiddleware.tsx | 25 +- .../api/src/middleware/ServiceRegistry.tsx | 3 +- .../repositories/ApplicationRepository.tsx | 6 +- .../tests/OAuth2ApplicationDelete.test.tsx | 13 +- .../ChannelRateLimitConfig.tsx | 15 + packages/api/src/rpc/NatsApiRpcListener.tsx | 116 ++ packages/api/src/rpc/RpcController.tsx | 90 - .../rpc/tests/RpcGatewayResilience.test.tsx | 220 --- .../search/guild/GuildSearchSerializer.tsx | 3 - .../tests/MessageSearchEndpoint.test.tsx | 8 +- packages/api/src/test/NoopGatewayService.tsx | 6 + .../api/src/test/TestHarnessController.tsx | 46 + packages/api/src/test/TestHarnessReset.tsx | 7 - .../test/mocks/MockGatewayRpcTransport.tsx | 60 + .../api/src/test/mocks/MockGatewayService.tsx | 5 + .../api/src/test/mocks/MockStorageService.tsx | 67 + .../test/msw/handlers/GatewayRpcHandlers.tsx | 141 -- packages/api/src/types/HonoEnv.tsx | 6 +- .../src/unfurler/resolvers/TenorResolver.tsx | 51 + .../src/unfurler/tests/TenorResolver.test.tsx | 165 +- packages/api/src/user/UserMappers.tsx | 2 +- .../account/crud/UserDataRepository.tsx | 8 +- .../AccountDeleteMentionResolution.test.tsx | 161 ++ .../tests/UserAccountAndSettings.test.tsx | 17 +- .../src/user/tests/UserFlagsResponse.test.tsx | 131 ++ packages/api/src/utils/IpUtils.tsx | 148 +- .../src/webhook/SendGridWebhookService.tsx | 250 --- .../api/src/webhook/SweegoWebhookService.tsx | 193 +++ .../api/src/webhook/WebhookController.tsx | 9 +- .../api/src/webhook/WebhookRepository.tsx | 6 +- .../api/src/webhook/WebhookRequestService.tsx | 16 +- .../tests/SendGridWebhookService.test.tsx | 194 --- .../tests/SweegoWebhookService.test.tsx | 198 +++ packages/api/src/worker/CronScheduler.tsx | 162 ++ .../api/src/worker/JetStreamWorkerQueue.tsx | 136 ++ packages/api/src/worker/WorkerMain.tsx | 66 +- packages/api/src/worker/WorkerRunner.tsx | 130 +- packages/api/src/worker/WorkerService.tsx | 38 +- .../api/src/worker/WorkerTaskRegistry.tsx | 8 +- .../src/worker/tasks/RefreshSearchIndex.tsx | 56 +- .../src/worker/tasks/SyncDiscoveryIndex.tsx | 12 +- packages/config/src/ConfigSchema.json | 111 +- .../src/__tests__/ConfigLoader.test.tsx | 4 - packages/config/src/schema/defs/auth.json | 17 - .../config/src/schema/defs/discovery.json | 2 +- .../src/schema/defs/integrations/email.json | 2 +- .../src/schema/defs/services/gateway.json | 16 +- .../config/src/schema/defs/services/nats.json | 24 + .../src/schema/defs/services/queue.json | 44 +- .../src/schema/defs/services/services.json | 4 + packages/config/src/schema/root.json | 6 +- packages/constants/src/ApiErrorCodes.tsx | 5 + .../src/ApiErrorCodesDescriptions.tsx | 5 + packages/constants/src/GatewayConstants.tsx | 1 - packages/constants/src/LimitConstants.tsx | 4 + ...ChunkedUploadChunkIndexOutOfRangeError.tsx | 27 + .../channel/ChunkedUploadIncompleteError.tsx | 27 + .../channel/ChunkedUploadNotFoundError.tsx | 27 + .../channel/ChunkedUploadNotOwnedError.tsx | 27 + .../errors/src/i18n/ErrorCodeMappings.tsx | 6 + .../src/i18n/ErrorI18nTypes.generated.tsx | 5 + packages/errors/src/i18n/locales/ar.yaml | 5 + packages/errors/src/i18n/locales/bg.yaml | 5 + packages/errors/src/i18n/locales/cs.yaml | 5 + packages/errors/src/i18n/locales/da.yaml | 5 + packages/errors/src/i18n/locales/de.yaml | 5 + packages/errors/src/i18n/locales/el.yaml | 5 + packages/errors/src/i18n/locales/en-GB.yaml | 5 + packages/errors/src/i18n/locales/es-419.yaml | 5 + packages/errors/src/i18n/locales/es-ES.yaml | 5 + packages/errors/src/i18n/locales/fi.yaml | 5 + packages/errors/src/i18n/locales/fr.yaml | 5 + packages/errors/src/i18n/locales/he.yaml | 7 +- packages/errors/src/i18n/locales/hi.yaml | 7 +- packages/errors/src/i18n/locales/hr.yaml | 5 + packages/errors/src/i18n/locales/hu.yaml | 5 + packages/errors/src/i18n/locales/id.yaml | 5 + packages/errors/src/i18n/locales/it.yaml | 7 +- packages/errors/src/i18n/locales/ja.yaml | 5 + packages/errors/src/i18n/locales/ko.yaml | 5 + packages/errors/src/i18n/locales/lt.yaml | 5 + .../errors/src/i18n/locales/messages.yaml | 5 + packages/errors/src/i18n/locales/nl.yaml | 5 + packages/errors/src/i18n/locales/no.yaml | 5 + packages/errors/src/i18n/locales/pl.yaml | 5 + packages/errors/src/i18n/locales/pt-BR.yaml | 5 + packages/errors/src/i18n/locales/ro.yaml | 5 + packages/errors/src/i18n/locales/ru.yaml | 5 + packages/errors/src/i18n/locales/sv-SE.yaml | 5 + packages/errors/src/i18n/locales/th.yaml | 5 + packages/errors/src/i18n/locales/tr.yaml | 5 + packages/errors/src/i18n/locales/uk.yaml | 5 + packages/errors/src/i18n/locales/vi.yaml | 5 + packages/errors/src/i18n/locales/zh-CN.yaml | 5 + packages/errors/src/i18n/locales/zh-TW.yaml | 5 + packages/geoip/package.json | 21 + packages/geoip/src/GeoipLookup.tsx | 149 ++ packages/geoip/tsconfig.json | 5 + packages/marketing/package.json | 6 +- packages/marketing/src/App.tsx | 15 - packages/marketing/src/GeoIp.tsx | 64 +- packages/marketing/src/MarketingConfig.tsx | 4 +- packages/marketing/src/MarketingTelemetry.tsx | 4 +- .../src/app/MarketingMiddlewareStack.tsx | 82 +- packages/marketing/src/components/Hero.tsx | 4 +- .../marketing/src/components/Navigation.tsx | 17 +- .../src/content/policies/Metadata.tsx | 2 +- .../marketing/src/content/policies/privacy.md | 6 +- packages/marketing/src/pages/DonatePage.tsx | 3 +- packages/marketing/src/pages/DownloadPage.tsx | 4 +- .../src/pages/donations/DonationForm.tsx | 47 +- .../pages/donations/DonationManageForm.tsx | 10 +- .../src/controllers/AttachmentsController.tsx | 32 +- .../controllers/ExternalMediaController.tsx | 31 +- .../media_proxy/src/lib/ImageProcessing.tsx | 106 +- .../src/lib/MediaTransformService.tsx | 40 +- .../src/MeilisearchIndexDefinitions.tsx | 4 +- .../src/adapters/MeilisearchGuildAdapter.tsx | 5 - packages/nats/package.json | 16 + .../nats/src/INatsConnectionManager.tsx | 13 +- .../nats/src/JetStreamConnectionManager.tsx | 31 + packages/nats/src/NatsConnectionManager.tsx | 71 + .../nats/src/NatsConnectionOptions.tsx | 12 +- packages/nats/tsconfig.json | 5 + .../contracts/search/SearchDocumentTypes.tsx | 6 +- .../schema/src/domains/admin/AdminSchemas.tsx | 1 + .../src/domains/admin/AdminUserSchemas.tsx | 13 + .../src/domains/channel/ChannelSchemas.tsx | 9 +- .../domains/channel/ChunkedUploadSchemas.tsx | 79 + .../src/domains/message/AttachmentSchemas.tsx | 1 + .../schema/src/primitives/FileValidators.tsx | 2 +- pnpm-lock.yaml | 123 +- pnpm-workspace.yaml | 3 +- scripts/ci/workflows/deploy_api.py | 7 +- scripts/ci/workflows/deploy_marketing.py | 1 + 357 files changed, 25420 insertions(+), 16281 deletions(-) delete mode 100644 .github/workflows/deploy-queue.yaml create mode 100644 fluxer_app/src/lib/ChunkedUploadService.tsx delete mode 100644 fluxer_devops/backups/ghost-blog-glide-theme.tar.gz.age delete mode 100644 fluxer_devops/backups/ghost-help-guidepost-theme.tar.gz.age delete mode 100644 fluxer_devops/backups/photo_dna_hash_service.tar.gz.age create mode 100644 fluxer_devops/nats_core/compose.yaml create mode 100644 fluxer_devops/nats_core/nats.conf create mode 100644 fluxer_devops/nats_jetstream/compose.yaml create mode 100644 fluxer_devops/nats_jetstream/nats.conf delete mode 100644 fluxer_devops/photo_dna_hash_service/.gitignore delete mode 100644 fluxer_devops/signoz/alerts/critical-alerts.json delete mode 100644 fluxer_devops/signoz/alerts/default-alerts.yaml create mode 100644 fluxer_gateway/src/gateway/gateway_nats_rpc.erl delete mode 100644 fluxer_gateway/src/gateway/gateway_rpc_http_handler.erl delete mode 100644 fluxer_gateway/src/gateway/gateway_rpc_tcp_connection.erl delete mode 100644 fluxer_gateway/src/gateway/gateway_rpc_tcp_server.erl create mode 100644 fluxer_gateway/src/guild/guild_common.erl create mode 100644 fluxer_gateway/src/guild/guild_counts_cache.erl create mode 100644 fluxer_gateway/src/guild/guild_ets_utils.erl create mode 100644 fluxer_gateway/src/guild/guild_member_list_common.erl create mode 100644 fluxer_gateway/src/guild/guild_query_handler.erl create mode 100644 fluxer_gateway/src/guild/guild_subscription_handler.erl delete mode 100644 fluxer_gateway/src/guild/guild_sync.erl create mode 100644 fluxer_gateway/src/guild/guild_voice_handler.erl create mode 100644 fluxer_gateway/src/guild/passive_sync_registry.erl create mode 100644 fluxer_gateway/src/guild/voice/guild_voice_server.erl delete mode 100644 fluxer_queue/Dockerfile delete mode 100644 fluxer_queue/package.json delete mode 100644 fluxer_queue/src/App.tsx delete mode 100644 fluxer_queue/src/Config.tsx delete mode 100644 fluxer_queue/tsconfig.json create mode 100644 packages/api/src/admin/tests/AdminSearchFieldCoverage.test.tsx create mode 100644 packages/api/src/channel/controllers/ChunkedUploadController.tsx create mode 100644 packages/api/src/channel/services/ChunkedUploadService.tsx create mode 100644 packages/api/src/channel/tests/ChunkedUpload.test.tsx create mode 100644 packages/api/src/connection/errors/BlueskyOAuthAuthorizationFailedError.tsx delete mode 100644 packages/api/src/gateway/tests/GatewayTcpFrameCodec.test.tsx delete mode 100644 packages/api/src/gateway/tests/GatewayTcpRpcTransport.test.tsx delete mode 100644 packages/api/src/infrastructure/GatewayTcpFrameCodec.tsx delete mode 100644 packages/api/src/infrastructure/GatewayTcpRpcTransport.tsx create mode 100644 packages/api/src/infrastructure/NatsGatewayRpcTransport.tsx create mode 100644 packages/api/src/message/tests/MessageForwardingAccessControl.test.tsx create mode 100644 packages/api/src/rpc/NatsApiRpcListener.tsx delete mode 100644 packages/api/src/rpc/RpcController.tsx delete mode 100644 packages/api/src/rpc/tests/RpcGatewayResilience.test.tsx create mode 100644 packages/api/src/test/mocks/MockGatewayRpcTransport.tsx delete mode 100644 packages/api/src/test/msw/handlers/GatewayRpcHandlers.tsx create mode 100644 packages/api/src/user/tests/AccountDeleteMentionResolution.test.tsx create mode 100644 packages/api/src/user/tests/UserFlagsResponse.test.tsx delete mode 100644 packages/api/src/webhook/SendGridWebhookService.tsx create mode 100644 packages/api/src/webhook/SweegoWebhookService.tsx delete mode 100644 packages/api/src/webhook/tests/SendGridWebhookService.test.tsx create mode 100644 packages/api/src/webhook/tests/SweegoWebhookService.test.tsx create mode 100644 packages/api/src/worker/CronScheduler.tsx create mode 100644 packages/api/src/worker/JetStreamWorkerQueue.tsx create mode 100644 packages/config/src/schema/defs/services/nats.json create mode 100644 packages/errors/src/domains/channel/ChunkedUploadChunkIndexOutOfRangeError.tsx create mode 100644 packages/errors/src/domains/channel/ChunkedUploadIncompleteError.tsx create mode 100644 packages/errors/src/domains/channel/ChunkedUploadNotFoundError.tsx create mode 100644 packages/errors/src/domains/channel/ChunkedUploadNotOwnedError.tsx create mode 100644 packages/geoip/package.json create mode 100644 packages/geoip/src/GeoipLookup.tsx create mode 100644 packages/geoip/tsconfig.json create mode 100644 packages/nats/package.json rename fluxer_queue/src/Instrument.tsx => packages/nats/src/INatsConnectionManager.tsx (73%) create mode 100644 packages/nats/src/JetStreamConnectionManager.tsx create mode 100644 packages/nats/src/NatsConnectionManager.tsx rename fluxer_queue/src/Logger.tsx => packages/nats/src/NatsConnectionOptions.tsx (80%) create mode 100644 packages/nats/tsconfig.json create mode 100644 packages/schema/src/domains/channel/ChunkedUploadSchemas.tsx diff --git a/.github/workflows/deploy-queue.yaml b/.github/workflows/deploy-queue.yaml deleted file mode 100644 index 5fffa437..00000000 --- a/.github/workflows/deploy-queue.yaml +++ /dev/null @@ -1,92 +0,0 @@ -name: deploy queue - -on: - push: - branches: - - canary - paths: - - fluxer_queue/** - - .github/workflows/deploy-queue.yaml - workflow_dispatch: - inputs: - ref: - type: string - required: false - default: '' - description: Optional git ref (defaults to the triggering branch) - -concurrency: - group: deploy-fluxer-queue - cancel-in-progress: true - -permissions: - contents: read - -jobs: - deploy: - name: Deploy queue - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 25 - env: - IS_CANARY: true - STACK: fluxer-queue - CACHE_SCOPE: deploy-fluxer-queue - RELEASE_CHANNEL: canary - - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ inputs.ref || '' }} - fetch-depth: 0 - - - name: Record deploy commit - run: python3 scripts/ci/workflows/deploy_queue.py --step record_deploy_commit - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - - - name: Set build timestamp - run: python3 scripts/ci/workflows/deploy_queue.py --step set_build_timestamp - - - name: Build image - uses: docker/build-push-action@v6 - with: - context: . - file: fluxer_queue/Dockerfile - tags: | - ${{ env.STACK }}:${{ env.DEPLOY_SHA }} - load: true - platforms: linux/amd64 - cache-from: type=gha,scope=${{ env.CACHE_SCOPE }} - cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }} - build-args: | - BUILD_SHA=${{ env.DEPLOY_SHA }} - BUILD_NUMBER=${{ github.run_number }} - BUILD_TIMESTAMP=${{ env.BUILD_TIMESTAMP }} - RELEASE_CHANNEL=${{ env.RELEASE_CHANNEL }} - env: - DOCKER_BUILD_SUMMARY: false - DOCKER_BUILD_RECORD_UPLOAD: false - - - name: Install docker-pussh - run: python3 scripts/ci/workflows/deploy_queue.py --step install_docker_pussh - - - name: Set up SSH agent - uses: webfactory/ssh-agent@v0.9.1 - with: - ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY_SERVER }} - - - name: Add server to known hosts - run: python3 scripts/ci/workflows/deploy_queue.py --step add_known_hosts --server-ip ${{ secrets.SERVER_IP }} - - - name: Push image and deploy - env: - SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} - IMAGE_TAG: ${{ env.STACK }}:${{ env.DEPLOY_SHA }} - run: python3 scripts/ci/workflows/deploy_queue.py --step push_and_deploy diff --git a/.gitignore b/.gitignore index 7825f1b3..0de62abf 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,7 @@ devenv.local.nix /dev/livekit.yaml /dev/bluesky_oauth_key.pem /dev/meilisearch_master_key -/dev/data/meilisearch/ +/dev/data/ **/.dev.vars **/.DS_Store **/.env diff --git a/config/config.dev.template.json b/config/config.dev.template.json index f2c567cb..9a984841 100644 --- a/config/config.dev.template.json +++ b/config/config.dev.template.json @@ -37,20 +37,15 @@ }, "gateway": { "port": 49107, - "rpc_tcp_port": 49108, - "api_host": "http://localhost:49319/api", "admin_reload_secret": "", "media_proxy_endpoint": "http://localhost:49319/media", "logger_level": "debug" }, - "queue": { - "secret": "" + "nats": { + "core_url": "nats://127.0.0.1:4222", + "jetstream_url": "nats://127.0.0.1:4223" } }, - "gateway": { - "rpc_endpoint": "http://localhost:49107", - "rpc_secret": "" - }, "auth": { "sudo_mode_secret": "", "connection_initiation_secret": "", diff --git a/config/config.production.template.json b/config/config.production.template.json index b7866495..5bf09f23 100644 --- a/config/config.production.template.json +++ b/config/config.production.template.json @@ -16,7 +16,7 @@ "s3": { "access_key_id": "YOUR_S3_ACCESS_KEY", "secret_access_key": "YOUR_S3_SECRET_KEY", - "endpoint": "http://127.0.0.1:3900" + "endpoint": "http://127.0.0.1:8080/s3" }, "services": { "server": { @@ -36,19 +36,15 @@ }, "gateway": { "port": 8082, - "rpc_tcp_port": 8083, - "api_host": "http://127.0.0.1:8080/api", "admin_reload_secret": "GENERATE_A_64_CHAR_HEX_SECRET", "media_proxy_endpoint": "http://127.0.0.1:8080/media" }, - "queue": { - "secret": "GENERATE_A_64_CHAR_HEX_SECRET" + "nats": { + "core_url": "nats://nats:4222", + "jetstream_url": "nats://nats:4222", + "auth_token": "GENERATE_A_NATS_AUTH_TOKEN" } }, - "gateway": { - "rpc_endpoint": "http://127.0.0.1:8082", - "rpc_secret": "GENERATE_A_64_CHAR_HEX_SECRET" - }, "auth": { "sudo_mode_secret": "GENERATE_A_64_CHAR_HEX_SECRET", "connection_initiation_secret": "GENERATE_A_64_CHAR_HEX_SECRET", diff --git a/config/config.test.json b/config/config.test.json index 8dbc577c..6678f98f 100644 --- a/config/config.test.json +++ b/config/config.test.json @@ -23,18 +23,10 @@ "oauth_client_secret": "test-oauth-client-secret" }, "gateway": { - "rpc_tcp_port": 8089, - "api_host": "http://localhost:8088/api", "admin_reload_secret": "test-gateway-admin-reload-secret-32-chars", "media_proxy_endpoint": "http://localhost:8088/media" - }, - "queue": { - "secret": "test-queue-secret-key-minimum-32-chars" } }, - "gateway": { - "rpc_secret": "test-gateway-rpc-secret-minimum-32-chars" - }, "auth": { "sudo_mode_secret": "test-sudo-mode-secret-minimum-32-chars", "connection_initiation_secret": "test-connection-initiation-secret-32ch", diff --git a/devenv.nix b/devenv.nix index 242c25e9..885bc98c 100644 --- a/devenv.nix +++ b/devenv.nix @@ -96,6 +96,20 @@ restart = "always"; }; }; + nats_core = { + command = lib.mkForce "exec ${config.git.root}/scripts/dev_process_entry.sh nats_core nats-server -p 4222 -a 127.0.0.1"; + log_location = "${config.git.root}/dev/logs/nats_core.log"; + availability = { + restart = "always"; + }; + }; + nats_jetstream = { + command = lib.mkForce "exec ${config.git.root}/scripts/dev_process_entry.sh nats_jetstream nats-server -p 4223 -js -sd ${config.git.root}/dev/data/nats_jetstream -a 127.0.0.1"; + log_location = "${config.git.root}/dev/logs/nats_jetstream.log"; + availability = { + restart = "always"; + }; + }; }; }; }; @@ -107,6 +121,7 @@ rebar3 valkey meilisearch + nats-server ffmpeg exiftool caddy @@ -143,6 +158,8 @@ "devenv:processes:mailpit" "devenv:processes:valkey" "devenv:processes:caddy" + "devenv:processes:nats_core" + "devenv:processes:nats_jetstream" ]; }; @@ -229,5 +246,9 @@ caddy.exec = '' exec caddy run --config ${config.git.root}/dev/Caddyfile.dev --adapter caddyfile ''; + nats_core.exec = "exec nats-server -p 4222 -a 127.0.0.1"; + nats_jetstream.exec = '' + exec nats-server -p 4223 -js -sd ${config.git.root}/dev/data/nats_jetstream -a 127.0.0.1 + ''; }; } diff --git a/fluxer_app/src/Endpoints.tsx b/fluxer_app/src/Endpoints.tsx index b357a1eb..f97b081d 100644 --- a/fluxer_app/src/Endpoints.tsx +++ b/fluxer_app/src/Endpoints.tsx @@ -96,6 +96,11 @@ export const Endpoints = { CHANNEL_TYPING: (channelId: string) => `/channels/${channelId}/typing`, CHANNEL_WEBHOOKS: (channelId: string) => `/channels/${channelId}/webhooks`, CHANNEL_RTC_REGIONS: (channelId: string) => `/channels/${channelId}/rtc-regions`, + CHANNEL_CHUNKED_UPLOADS: (channelId: string) => `/channels/${channelId}/chunked-uploads`, + CHANNEL_CHUNKED_UPLOAD_CHUNK: (channelId: string, uploadId: string, chunkIndex: number) => + `/channels/${channelId}/chunked-uploads/${uploadId}/chunks/${chunkIndex}`, + CHANNEL_CHUNKED_UPLOAD_COMPLETE: (channelId: string, uploadId: string) => + `/channels/${channelId}/chunked-uploads/${uploadId}/complete`, CHANNEL_CALL: (channelId: string) => `/channels/${channelId}/call`, CHANNEL_CALL_RING: (channelId: string) => `/channels/${channelId}/call/ring`, CHANNEL_CALL_STOP_RINGING: (channelId: string) => `/channels/${channelId}/call/stop-ringing`, diff --git a/fluxer_app/src/components/channel/Messages.tsx b/fluxer_app/src/components/channel/Messages.tsx index 7d41eb53..3c9054d6 100644 --- a/fluxer_app/src/components/channel/Messages.tsx +++ b/fluxer_app/src/components/channel/Messages.tsx @@ -191,7 +191,7 @@ export const Messages = observer(function Messages({channel, onBottomBarVisibili canAutoAck, }); - useEffect(() => { + useLayoutEffect(() => { const node = messagesWrapperRef.current; if (node) { node.style.setProperty('--message-group-spacing', `${state.messageGroupSpacing}px`); diff --git a/fluxer_app/src/components/channel/message_search_bar/MessageSearchBar.module.css b/fluxer_app/src/components/channel/message_search_bar/MessageSearchBar.module.css index 028d500c..f2885bcc 100644 --- a/fluxer_app/src/components/channel/message_search_bar/MessageSearchBar.module.css +++ b/fluxer_app/src/components/channel/message_search_bar/MessageSearchBar.module.css @@ -36,7 +36,7 @@ --search-scope-badge-hover-color: var(--text-primary); --search-scope-badge-border-color: var(--background-modifier-accent); --search-input-text-color: var(--text-primary); - --search-input-placeholder-color: var(--text-primary-muted); + --search-input-placeholder-color: var(--text-tertiary); --search-clear-button-color: var(--text-tertiary); --search-clear-button-hover-color: var(--text-primary); --search-clear-button-hover-background: var(--background-modifier-hover); diff --git a/fluxer_app/src/components/emojis/EmojiListItem.tsx b/fluxer_app/src/components/emojis/EmojiListItem.tsx index 06ca6f8e..6f9aab36 100644 --- a/fluxer_app/src/components/emojis/EmojiListItem.tsx +++ b/fluxer_app/src/components/emojis/EmojiListItem.tsx @@ -150,9 +150,10 @@ export const EmojiListItem: React.FC<{ guildId: string; emoji: GuildEmojiWithUser; layout: 'list' | 'grid'; + canModify: boolean; onRename: (emojiId: string, newName: string) => void; onRemove: (emojiId: string) => void; -}> = observer(({guildId, emoji, layout, onRename, onRemove}) => { +}> = observer(({guildId, emoji, layout, canModify, onRename, onRemove}) => { const {t} = useLingui(); const avatarUrl = emoji.user ? AvatarUtils.getUserAvatarURL(emoji.user, false) : null; const gridNameButtonRef = useRef(null); @@ -222,38 +223,44 @@ export const EmojiListItem: React.FC<{
- ( - - )} - > - - + + + ) : ( + :{emoji.name}: + )}
- - - - - + {canModify && ( + + + + + + )} ); } @@ -266,17 +273,21 @@ export const EmojiListItem: React.FC<{
- + {canModify ? ( + + ) : ( + :{emoji.name}: + )}
@@ -293,13 +304,15 @@ export const EmojiListItem: React.FC<{
- - - - - + {canModify && ( + + + + + + )} ); }); diff --git a/fluxer_app/src/components/form/Input.module.css b/fluxer_app/src/components/form/Input.module.css index 9e6d896d..ba0575ca 100644 --- a/fluxer_app/src/components/form/Input.module.css +++ b/fluxer_app/src/components/form/Input.module.css @@ -37,7 +37,7 @@ } .input::placeholder { - color: var(--text-primary-muted); + color: var(--text-tertiary); } .input.minHeight { @@ -209,7 +209,7 @@ } .textarea::placeholder { - color: var(--text-primary-muted); + color: var(--text-tertiary); } .textareaActions { diff --git a/fluxer_app/src/components/layout/UserArea.tsx b/fluxer_app/src/components/layout/UserArea.tsx index d194441c..7f586dd9 100644 --- a/fluxer_app/src/components/layout/UserArea.tsx +++ b/fluxer_app/src/components/layout/UserArea.tsx @@ -121,14 +121,28 @@ const UserAreaInner = observer( return; } - const height = voiceConnectionRef.current?.getBoundingClientRect().height ?? 0; - if (height > 0) { - root.style.setProperty(VOICE_CONNECTION_HEIGHT_VARIABLE, `${Math.round(height)}px`); - } else { + const element = voiceConnectionRef.current; + if (!element) { root.style.removeProperty(VOICE_CONNECTION_HEIGHT_VARIABLE); + return; } + const updateHeight = () => { + const height = element.getBoundingClientRect().height; + if (height > 0) { + root.style.setProperty(VOICE_CONNECTION_HEIGHT_VARIABLE, `${Math.round(height)}px`); + } else { + root.style.removeProperty(VOICE_CONNECTION_HEIGHT_VARIABLE); + } + }; + + updateHeight(); + + const observer = new ResizeObserver(updateHeight); + observer.observe(element); + return () => { + observer.disconnect(); root.style.removeProperty(VOICE_CONNECTION_HEIGHT_VARIABLE); }; }, [hasVoiceConnection]); @@ -164,13 +178,13 @@ const UserAreaInner = observer( return (
{hasVoiceConnection && ( - <> +
-
+
- +
)} {!hasVoiceConnection &&
}
diff --git a/fluxer_app/src/components/modals/AddConnectionModal.tsx b/fluxer_app/src/components/modals/AddConnectionModal.tsx index 886fd7d3..49c7b2ee 100644 --- a/fluxer_app/src/components/modals/AddConnectionModal.tsx +++ b/fluxer_app/src/components/modals/AddConnectionModal.tsx @@ -93,7 +93,10 @@ export const AddConnectionModal = observer(({defaultType}: AddConnectionModalPro const onSubmitInitiate = useCallback( async (data: InitiateFormInputs) => { - const identifier = data.identifier.trim(); + let identifier = data.identifier.trim(); + if (type === ConnectionTypes.BLUESKY) { + identifier = identifier.replace(/^https?:\/\/bsky\.app\/profile\//i, '').replace(/^@/, ''); + } if (UserConnectionStore.hasConnectionByTypeAndName(type, identifier)) { initiateForm.setError('identifier', {type: 'validate', message: t`You already have this connection.`}); return; diff --git a/fluxer_app/src/components/modals/GuildSettingsModal.tsx b/fluxer_app/src/components/modals/GuildSettingsModal.tsx index 12a4209f..a069686d 100644 --- a/fluxer_app/src/components/modals/GuildSettingsModal.tsx +++ b/fluxer_app/src/components/modals/GuildSettingsModal.tsx @@ -56,8 +56,11 @@ export const GuildSettingsModal: React.FC = observer( if (!guild) return guildSettingsTabs; return guildSettingsTabs.filter((tab) => { - if (tab.permission && !PermissionStore.can(tab.permission, {guildId})) { - return false; + if (tab.permission) { + const perms = Array.isArray(tab.permission) ? tab.permission : [tab.permission]; + if (!perms.some((p) => PermissionStore.can(p, {guildId}))) { + return false; + } } if (tab.requireFeature && !guild.features.has(tab.requireFeature)) { return false; diff --git a/fluxer_app/src/components/modals/InviteModal.tsx b/fluxer_app/src/components/modals/InviteModal.tsx index 38be6b39..741782df 100644 --- a/fluxer_app/src/components/modals/InviteModal.tsx +++ b/fluxer_app/src/components/modals/InviteModal.tsx @@ -192,9 +192,6 @@ export const InviteModal = observer(({channelId}: {channelId: string}) => { }; const getExpirationText = () => { - if (maxAge === '0') { - return never expires; - } const option = maxAgeOptions.find((opt) => opt.value === maxAge); if (option) { switch (option.value) { @@ -310,9 +307,16 @@ export const InviteModal = observer(({channelId}: {channelId: string}) => { onInputClick={(e) => e.currentTarget.select()} inputProps={{placeholder: t`Invite link`}} > - {isUsingVanityUrl ? ( + {isUsingVanityUrl || maxAge === '0' ? (

- This invite link never expires. + This invite link never expires.{' '} + {!isUsingVanityUrl && ( + + + + )}

) : (

diff --git a/fluxer_app/src/components/modals/discovery/DiscoveryGuildCard.module.css b/fluxer_app/src/components/modals/discovery/DiscoveryGuildCard.module.css index 23b4d9bf..8c8bdd6f 100644 --- a/fluxer_app/src/components/modals/discovery/DiscoveryGuildCard.module.css +++ b/fluxer_app/src/components/modals/discovery/DiscoveryGuildCard.module.css @@ -87,7 +87,7 @@ margin: 0; overflow: hidden; display: -webkit-box; - -webkit-line-clamp: 2; + -webkit-line-clamp: 4; -webkit-box-orient: vertical; flex: 1; } diff --git a/fluxer_app/src/components/modals/discovery/DiscoveryGuildCard.tsx b/fluxer_app/src/components/modals/discovery/DiscoveryGuildCard.tsx index c927d313..65862950 100644 --- a/fluxer_app/src/components/modals/discovery/DiscoveryGuildCard.tsx +++ b/fluxer_app/src/components/modals/discovery/DiscoveryGuildCard.tsx @@ -19,11 +19,14 @@ import type {DiscoveryGuild} from '@app/actions/DiscoveryActionCreators'; import * as DiscoveryActionCreators from '@app/actions/DiscoveryActionCreators'; +import * as ModalActionCreators from '@app/actions/ModalActionCreators'; +import * as NavigationActionCreators from '@app/actions/NavigationActionCreators'; import * as ToastActionCreators from '@app/actions/ToastActionCreators'; import {GuildBadge} from '@app/components/guild/GuildBadge'; import styles from '@app/components/modals/discovery/DiscoveryGuildCard.module.css'; import {GuildIcon} from '@app/components/popouts/GuildIcon'; import {Button} from '@app/components/uikit/button/Button'; +import DiscoveryStore from '@app/stores/DiscoveryStore'; import GuildStore from '@app/stores/GuildStore'; import {getApiErrorMessage} from '@app/utils/ApiErrorUtils'; import {getCurrentLocale} from '@app/utils/LocaleUtils'; @@ -51,6 +54,9 @@ export const DiscoveryGuildCard = observer(function DiscoveryGuildCard({guild}: setJoining(true); try { await DiscoveryActionCreators.joinGuild(guild.id); + DiscoveryStore.reset(); + ModalActionCreators.pop(); + NavigationActionCreators.selectGuild(guild.id); } catch (error) { setJoining(false); const message = getApiErrorMessage(error) ?? t`Failed to join this community. Please try again.`; diff --git a/fluxer_app/src/components/modals/guild_tabs/GuildDiscoveryTab.tsx b/fluxer_app/src/components/modals/guild_tabs/GuildDiscoveryTab.tsx index 270313e2..d5699516 100644 --- a/fluxer_app/src/components/modals/guild_tabs/GuildDiscoveryTab.tsx +++ b/fluxer_app/src/components/modals/guild_tabs/GuildDiscoveryTab.tsx @@ -121,22 +121,22 @@ const GuildDiscoveryTab: React.FC<{guildId: string}> = ({guildId}) => { application.status === DiscoveryApplicationStatus.REJECTED || application.status === DiscoveryApplicationStatus.REMOVED); + const formValues = useMemo( + () => + hasActiveApplication && application + ? {description: application.description, category_type: application.category_type} + : undefined, + [hasActiveApplication, application], + ); + const form = useForm({ defaultValues: { - description: hasActiveApplication ? application.description : '', - category_type: hasActiveApplication ? application.category_type : 0, + description: '', + category_type: 0, }, + values: formValues, }); - useEffect(() => { - if (hasActiveApplication && application) { - form.reset({ - description: application.description, - category_type: application.category_type, - }); - } - }, [application, hasActiveApplication, form]); - const setApplicationFromResponse = useCallback((response: DiscoveryApplicationResponse) => { setStatus((prev) => (prev ? {...prev, application: response} : prev)); }, []); @@ -291,8 +291,10 @@ const GuildDiscoveryTab: React.FC<{guildId: string}> = ({guildId}) => {

Description
-