mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-08 03:32:27 +09:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03d1354562 | ||
|
|
964845d7a7 | ||
|
|
3bc5dd8e0f | ||
|
|
17292fd6a5 | ||
|
|
f753659899 |
@@ -28,9 +28,6 @@ f:media_proxy:
|
||||
f:messages:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: fluxer_messages/**/*
|
||||
f:recon:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: fluxer_recon/**/*
|
||||
f:snowflakes:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: fluxer_snowflakes/**/*
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
name: build recon
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build-version:
|
||||
description: "Explicit Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes) to use instead of automatic UTC clock allocation"
|
||||
type: string
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
approve:
|
||||
name: approve build release
|
||||
permissions: {}
|
||||
runs-on: ubuntu-24.04
|
||||
environment: builds
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: approved
|
||||
run: echo "Build release approved."
|
||||
|
||||
image:
|
||||
needs: approve
|
||||
uses: ./.github/workflows/_build-image.yaml
|
||||
secrets: inherit
|
||||
with:
|
||||
image: fluxer-recon
|
||||
dockerfile: fluxer_recon/Dockerfile
|
||||
build-version: ${{ inputs['build-version'] }}
|
||||
Generated
-19
@@ -1868,25 +1868,6 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fluxer-recon"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"base64",
|
||||
"fluxer-svc",
|
||||
"hmac 0.13.0",
|
||||
"reqwest",
|
||||
"scylla",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.11.0",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fluxer-snowflakes"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -15,7 +15,6 @@ members = [
|
||||
"fluxer_users",
|
||||
"fluxer_unfurl",
|
||||
"packages/markdown_parser/rust",
|
||||
"fluxer_recon",
|
||||
]
|
||||
exclude = [
|
||||
"packages/markdown_parser/rust/fuzz",
|
||||
|
||||
Vendored
-5
@@ -65,11 +65,6 @@ FLUXER_LIVEKIT_API_SECRET=fluxer-livekit-development-secret
|
||||
FLUXER_LIVEKIT_WEBHOOK_URL=http://localhost:8088/api/webhooks/livekit
|
||||
FLUXER_LIVEKIT_DEFAULT_REGION={"id":"local","name":"Local","emoji":"LC","latitude":59.3293,"longitude":18.0686}
|
||||
|
||||
FLUXER_RECON_MODE=observing
|
||||
FLUXER_RECON_EXPECTED_ROOMS=64
|
||||
FLUXER_RECON_WARMUP_SECONDS=15
|
||||
FLUXER_RECON_MAX_HOT_ROOMS=8
|
||||
|
||||
FLUXER_API_PORT=8080
|
||||
FLUXER_API_PRESIGNED_ATTACHMENT_UPLOADS_ENABLED=true
|
||||
FLUXER_API_WORKER_MODE=all_lanes
|
||||
|
||||
@@ -413,7 +413,6 @@ services:
|
||||
NODE_OPTIONS: --enable-source-maps${FLUXER_WORKER_NODE_HEAP_MB:+ --max-old-space-size=$FLUXER_WORKER_NODE_HEAP_MB}
|
||||
FLUXER_API_WORKER_MODE: all_lanes
|
||||
FLUXER_API_WORKER_ENABLE_CRON_SCHEDULER: "true"
|
||||
FLUXER_API_WORKER_ENABLE_VOICE_RECONCILIATION: "true"
|
||||
FLUXER_POSTGRES_MAX_CONNECTIONS: "25"
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "const age=Date.now()-require('node:fs').statSync('/tmp/fluxer-worker-heartbeat').mtimeMs;if(age>30000){console.error('worker heartbeat is '+Math.round(age)+'ms old');process.exit(1)}"]
|
||||
|
||||
@@ -10671,7 +10671,9 @@
|
||||
"logo_url": {"nullable": true, "type": "string"},
|
||||
"wordmark_url": {"nullable": true, "type": "string"},
|
||||
"favicon_url": {"nullable": true, "type": "string"},
|
||||
"theme_color": {"nullable": true, "type": "string"}
|
||||
"theme_color": {"nullable": true, "type": "string"},
|
||||
"status_page_url": {"nullable": true, "type": "string"},
|
||||
"status_page_incident_history_url": {"nullable": true, "type": "string"}
|
||||
},
|
||||
"required": [
|
||||
"product_name",
|
||||
@@ -10680,7 +10682,9 @@
|
||||
"logo_url",
|
||||
"wordmark_url",
|
||||
"favicon_url",
|
||||
"theme_color"
|
||||
"theme_color",
|
||||
"status_page_url",
|
||||
"status_page_incident_history_url"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
@@ -11144,7 +11148,9 @@
|
||||
"logo_url": {"nullable": true, "type": "string", "maxLength": 2048},
|
||||
"wordmark_url": {"nullable": true, "type": "string", "maxLength": 2048},
|
||||
"favicon_url": {"nullable": true, "type": "string", "maxLength": 2048},
|
||||
"theme_color": {"nullable": true, "type": "string", "maxLength": 64}
|
||||
"theme_color": {"nullable": true, "type": "string", "maxLength": 64},
|
||||
"status_page_url": {"nullable": true, "type": "string", "maxLength": 2048},
|
||||
"status_page_incident_history_url": {"nullable": true, "type": "string", "maxLength": 2048}
|
||||
}
|
||||
},
|
||||
"setup": {"nullable": true, "type": "object", "properties": {"configured": {"type": "boolean"}}},
|
||||
|
||||
@@ -326,6 +326,8 @@ pub struct AppBrandingConfigResponse {
|
||||
pub wordmark_url: Option<String>,
|
||||
pub favicon_url: Option<String>,
|
||||
pub theme_color: Option<String>,
|
||||
pub status_page_url: Option<String>,
|
||||
pub status_page_incident_history_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for AppBrandingConfigResponse {
|
||||
@@ -338,6 +340,8 @@ impl Default for AppBrandingConfigResponse {
|
||||
wordmark_url: None,
|
||||
favicon_url: None,
|
||||
theme_color: None,
|
||||
status_page_url: None,
|
||||
status_page_incident_history_url: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -858,6 +862,10 @@ pub struct AppBrandingConfigUpdateRequest {
|
||||
pub favicon_url: Option<Option<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub theme_color: Option<Option<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status_page_url: Option<Option<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status_page_incident_history_url: Option<Option<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
|
||||
@@ -683,6 +683,8 @@ fn build_app_public_update(form: &MultiValueForm) -> InstanceConfigUpdateRequest
|
||||
wordmark_url: optional("app_wordmark_url"),
|
||||
favicon_url: optional("app_favicon_url"),
|
||||
theme_color: optional("app_theme_color"),
|
||||
status_page_url: optional("app_status_page_url"),
|
||||
status_page_incident_history_url: optional("app_status_page_incident_history_url"),
|
||||
}),
|
||||
setup: Some(AppSetupConfigUpdateRequest {
|
||||
configured: Some(form.bool_value("app_setup_configured")),
|
||||
|
||||
@@ -848,6 +848,18 @@ fn app_public_config_section(
|
||||
app_public.branding.favicon_url.as_deref().unwrap_or(""),
|
||||
"https://example.com/favicon.ico",
|
||||
))
|
||||
(text_input(
|
||||
"app_status_page_url",
|
||||
"Status page URL",
|
||||
app_public.branding.status_page_url.as_deref().unwrap_or(""),
|
||||
"https://fluxerstatus.com",
|
||||
))
|
||||
(text_input(
|
||||
"app_status_page_incident_history_url",
|
||||
"Status page history URL",
|
||||
app_public.branding.status_page_incident_history_url.as_deref().unwrap_or(""),
|
||||
"https://fluxerstatus.com/history",
|
||||
))
|
||||
}
|
||||
div class="space-y-2" {
|
||||
(checkbox(
|
||||
|
||||
@@ -479,6 +479,8 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
wordmarkUrl: master.instance.branding.wordmark_url,
|
||||
faviconUrl: master.instance.branding.favicon_url,
|
||||
themeColor: master.instance.branding.theme_color,
|
||||
statusPageUrl: master.instance.branding.status_page_url,
|
||||
statusPageIncidentHistoryUrl: master.instance.branding.status_page_incident_history_url,
|
||||
},
|
||||
setup: {
|
||||
configured: master.instance.setup.configured,
|
||||
|
||||
@@ -305,6 +305,11 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
wordmark_url: readOptionalField(data.app_public.branding, 'wordmark_url'),
|
||||
favicon_url: readOptionalField(data.app_public.branding, 'favicon_url'),
|
||||
theme_color: readOptionalField(data.app_public.branding, 'theme_color'),
|
||||
status_page_url: readOptionalField(data.app_public.branding, 'status_page_url'),
|
||||
status_page_incident_history_url: readOptionalField(
|
||||
data.app_public.branding,
|
||||
'status_page_incident_history_url',
|
||||
),
|
||||
})
|
||||
: undefined,
|
||||
legal: data.app_public.legal
|
||||
|
||||
@@ -324,6 +324,8 @@ export interface APIConfig {
|
||||
wordmarkUrl?: string;
|
||||
faviconUrl?: string;
|
||||
themeColor?: string;
|
||||
statusPageUrl?: string;
|
||||
statusPageIncidentHistoryUrl?: string;
|
||||
};
|
||||
setup: {
|
||||
configured: boolean;
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {ChannelID, GuildID, UserID} from '@app/api/BrandedTypes';
|
||||
import type {
|
||||
ILiveKitService,
|
||||
ListActiveRoomsResult,
|
||||
ListParticipantsResult,
|
||||
} from '@app/api/infrastructure/ILiveKitService';
|
||||
import type {ILiveKitService, ListParticipantsResult} from '@app/api/infrastructure/ILiveKitService';
|
||||
import type {VoiceRegionMetadata, VoiceServerRecord} from '@app/api/voice/VoiceModel';
|
||||
|
||||
interface CreateTokenParams {
|
||||
@@ -78,10 +74,6 @@ export class DisabledLiveKitService implements ILiveKitService {
|
||||
return {status: 'ok', participants: []};
|
||||
}
|
||||
|
||||
async listActiveRooms(): Promise<ListActiveRoomsResult> {
|
||||
return {rooms: [], errors: [], searchedServers: 0, completed: true};
|
||||
}
|
||||
|
||||
getDefaultRegionId(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {ChannelID, GuildID, MessageID, RoleID, UserID} from '@app/api/BrandedTypes';
|
||||
import {createChannelID, createGuildID, createRoleID, createUserID} from '@app/api/BrandedTypes';
|
||||
import {createChannelID, createRoleID, createUserID} from '@app/api/BrandedTypes';
|
||||
import {SYSTEM_USER_ID} from '@app/api/constants/Core';
|
||||
import type {GatewayDispatchEvent} from '@app/api/constants/Gateway';
|
||||
import {GatewayRpcClient} from '@app/api/infrastructure/GatewayRpcClient';
|
||||
import {GatewayRpcMethodError, GatewayRpcMethodErrorCodes} from '@app/api/infrastructure/GatewayRpcError';
|
||||
import type {
|
||||
CallData,
|
||||
GatewayActiveVoiceRooms,
|
||||
GatewayChannelMention,
|
||||
GatewayGuildMemoryStats,
|
||||
GatewayMentionSources,
|
||||
@@ -1066,26 +1065,6 @@ export class GatewayService {
|
||||
};
|
||||
}
|
||||
|
||||
async getActiveVoiceRooms(): Promise<GatewayActiveVoiceRooms> {
|
||||
const result = await this.call<{
|
||||
rooms?: Array<{
|
||||
guild_id?: string | null;
|
||||
channel_id: string;
|
||||
voice_state_count?: number;
|
||||
}>;
|
||||
node_count?: number;
|
||||
}>('process.active_voice_rooms', {});
|
||||
return {
|
||||
nodeCount: result.node_count ?? 0,
|
||||
rooms: (result.rooms ?? []).map((room) => ({
|
||||
guildId:
|
||||
room.guild_id === undefined || room.guild_id === null ? undefined : createGuildID(BigInt(room.guild_id)),
|
||||
channelId: createChannelID(BigInt(room.channel_id)),
|
||||
voiceStateCount: room.voice_state_count ?? 0,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async getUserPermissions({guildId, userId, channelId}: UserPermissionsParams): Promise<bigint> {
|
||||
const result = await this.call<{
|
||||
permissions: string;
|
||||
@@ -1638,41 +1617,6 @@ export class GatewayService {
|
||||
};
|
||||
}
|
||||
|
||||
async repairVoiceStateFromCache({
|
||||
guildId,
|
||||
channelId,
|
||||
userId,
|
||||
connectionId,
|
||||
}: {
|
||||
guildId?: GuildID;
|
||||
channelId: ChannelID;
|
||||
userId: UserID;
|
||||
connectionId: string;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
repaired?: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
const params: Record<string, string> = {
|
||||
channel_id: channelId.toString(),
|
||||
user_id: userId.toString(),
|
||||
connection_id: connectionId,
|
||||
};
|
||||
if (guildId !== undefined) {
|
||||
params['guild_id'] = guildId.toString();
|
||||
}
|
||||
const result = await this.call<{
|
||||
success: boolean;
|
||||
repaired?: boolean;
|
||||
error?: string;
|
||||
}>('voice.repair_state_from_cache', params);
|
||||
return {
|
||||
success: result.success,
|
||||
repaired: result.repaired,
|
||||
error: result.error,
|
||||
};
|
||||
}
|
||||
|
||||
async getVoiceStatesForChannel({guildId, channelId}: {guildId?: GuildID; channelId: ChannelID}): Promise<{
|
||||
voiceStates: Array<GatewayVoiceStateEntry>;
|
||||
}> {
|
||||
|
||||
@@ -88,17 +88,6 @@ export interface GatewayVoiceStateCounts {
|
||||
servers: Array<GatewayVoiceStateServerCount>;
|
||||
}
|
||||
|
||||
export interface GatewayActiveVoiceRoom {
|
||||
guildId?: GuildID;
|
||||
channelId: ChannelID;
|
||||
voiceStateCount: number;
|
||||
}
|
||||
|
||||
export interface GatewayActiveVoiceRooms {
|
||||
rooms: Array<GatewayActiveVoiceRoom>;
|
||||
nodeCount: number;
|
||||
}
|
||||
|
||||
export interface GatewayVoiceStateEntry {
|
||||
connectionId: string;
|
||||
userId: string;
|
||||
@@ -162,8 +151,6 @@ export abstract class IGatewayService {
|
||||
|
||||
abstract getVoiceStateCounts(): Promise<GatewayVoiceStateCounts>;
|
||||
|
||||
abstract getActiveVoiceRooms(): Promise<GatewayActiveVoiceRooms>;
|
||||
|
||||
abstract getUsersToMentionByRoles(params: {
|
||||
guildId: GuildID;
|
||||
channelId: ChannelID;
|
||||
@@ -345,17 +332,6 @@ export abstract class IGatewayService {
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
abstract repairVoiceStateFromCache(params: {
|
||||
guildId?: GuildID;
|
||||
channelId: ChannelID;
|
||||
userId: UserID;
|
||||
connectionId: string;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
repaired?: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
abstract getVoiceStatesForChannel(params: {guildId?: GuildID; channelId: ChannelID}): Promise<{
|
||||
voiceStates: Array<GatewayVoiceStateEntry>;
|
||||
}>;
|
||||
|
||||
@@ -61,13 +61,6 @@ interface ListParticipantsParams {
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
export interface LiveKitServerError {
|
||||
regionId: string;
|
||||
serverId: string;
|
||||
errorCode: string;
|
||||
retryable: boolean;
|
||||
}
|
||||
|
||||
export interface ListParticipantsSuccess {
|
||||
status: 'ok';
|
||||
participants: Array<{
|
||||
@@ -84,19 +77,6 @@ export interface ListParticipantsError {
|
||||
|
||||
export type ListParticipantsResult = ListParticipantsSuccess | ListParticipantsError;
|
||||
|
||||
export interface LiveKitRoomLocation {
|
||||
roomName: string;
|
||||
regionId: string;
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
export interface ListActiveRoomsResult {
|
||||
rooms: Array<LiveKitRoomLocation>;
|
||||
errors: Array<LiveKitServerError>;
|
||||
searchedServers: number;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
export abstract class ILiveKitService {
|
||||
abstract createToken(params: CreateTokenParams): Promise<{
|
||||
token: string;
|
||||
@@ -111,8 +91,6 @@ export abstract class ILiveKitService {
|
||||
|
||||
abstract listParticipants(params: ListParticipantsParams): Promise<ListParticipantsResult>;
|
||||
|
||||
abstract listActiveRooms(): Promise<ListActiveRoomsResult>;
|
||||
|
||||
abstract getDefaultRegionId(): string | null;
|
||||
|
||||
abstract getRegionMetadata(): Array<VoiceRegionMetadata>;
|
||||
|
||||
@@ -2,11 +2,7 @@
|
||||
|
||||
import type {ChannelID, GuildID, UserID} from '@app/api/BrandedTypes';
|
||||
import {Config} from '@app/api/Config';
|
||||
import type {
|
||||
ListActiveRoomsResult,
|
||||
ListParticipantsResult,
|
||||
LiveKitServerError,
|
||||
} from '@app/api/infrastructure/ILiveKitService';
|
||||
import type {ListParticipantsResult} from '@app/api/infrastructure/ILiveKitService';
|
||||
import {ILiveKitService} from '@app/api/infrastructure/ILiveKitService';
|
||||
import {Logger} from '@app/api/Logger';
|
||||
import type {VoiceRegionMetadata, VoiceServerRecord} from '@app/api/voice/VoiceModel';
|
||||
@@ -339,39 +335,6 @@ export class LiveKitService extends ILiveKitService {
|
||||
}
|
||||
}
|
||||
|
||||
async listActiveRooms(): Promise<ListActiveRoomsResult> {
|
||||
const rooms: ListActiveRoomsResult['rooms'] = [];
|
||||
const errors: Array<LiveKitServerError> = [];
|
||||
const servers = this.getActiveServerClients();
|
||||
for (const server of servers) {
|
||||
try {
|
||||
const liveRooms = await server.roomServiceClient.listRooms();
|
||||
for (const room of liveRooms) {
|
||||
if (typeof room.name !== 'string' || room.name.length === 0) {
|
||||
continue;
|
||||
}
|
||||
rooms.push({
|
||||
roomName: room.name,
|
||||
regionId: server.regionId,
|
||||
serverId: server.serverId,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.warn(
|
||||
{error, regionId: server.regionId, serverId: server.serverId},
|
||||
'LiveKit listRooms failed during voice reconciliation',
|
||||
);
|
||||
errors.push(this.toServerError(server.regionId, server.serverId, error));
|
||||
}
|
||||
}
|
||||
return {
|
||||
rooms,
|
||||
errors,
|
||||
searchedServers: servers.length,
|
||||
completed: errors.length === 0,
|
||||
};
|
||||
}
|
||||
|
||||
private static isHttp404(error: unknown): boolean {
|
||||
return LiveKitService.getHttpStatus(error) === 404;
|
||||
}
|
||||
@@ -396,34 +359,6 @@ export class LiveKitService extends ILiveKitService {
|
||||
return region.get(serverId) ?? null;
|
||||
}
|
||||
|
||||
private getActiveServerClients(): Array<ServerClientConfig & {regionId: string; serverId: string}> {
|
||||
const servers: Array<ServerClientConfig & {regionId: string; serverId: string}> = [];
|
||||
for (const [regionId, region] of this.serverClients.entries()) {
|
||||
for (const [serverId, server] of region.entries()) {
|
||||
if (server.isActive) {
|
||||
servers.push({...server, regionId, serverId});
|
||||
}
|
||||
}
|
||||
}
|
||||
return servers.sort((left, right) => {
|
||||
const regionComparison = left.regionId.localeCompare(right.regionId);
|
||||
if (regionComparison !== 0) {
|
||||
return regionComparison;
|
||||
}
|
||||
return left.serverId.localeCompare(right.serverId);
|
||||
});
|
||||
}
|
||||
|
||||
private toServerError(regionId: string, serverId: string, error: unknown): LiveKitServerError {
|
||||
const status = LiveKitService.getHttpStatus(error);
|
||||
return {
|
||||
regionId,
|
||||
serverId,
|
||||
errorCode: error instanceof Error ? error.message : 'unknown',
|
||||
retryable: status != null && status >= 500,
|
||||
};
|
||||
}
|
||||
|
||||
getDefaultRegionId(): string | null {
|
||||
return this.topology.getDefaultRegionId();
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ export class LiveKitWebhookService {
|
||||
roomName: data.event.room?.name ?? null,
|
||||
eventType: data.event.event,
|
||||
},
|
||||
'LiveKit webhook reports dropped events - reconciliation may be needed',
|
||||
'LiveKit webhook reports dropped events',
|
||||
);
|
||||
}
|
||||
await this.processEvent(data);
|
||||
@@ -256,7 +256,7 @@ export class LiveKitWebhookService {
|
||||
error: result.error,
|
||||
participantIdentity: participant.identity,
|
||||
},
|
||||
'LiveKit participant_joined did not match gateway state; leaving participant connected for reconciliation',
|
||||
'LiveKit participant_joined did not match gateway state; leaving participant connected',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -319,6 +319,8 @@ function getDefaultAppPublicConfig(): InstanceAppPublicConfig {
|
||||
wordmark_url: normalizeOptionalString(Config.instance.branding.wordmarkUrl),
|
||||
favicon_url: normalizeOptionalString(Config.instance.branding.faviconUrl),
|
||||
theme_color: normalizeOptionalString(Config.instance.branding.themeColor),
|
||||
status_page_url: normalizeOptionalString(Config.instance.branding.statusPageUrl),
|
||||
status_page_incident_history_url: normalizeOptionalString(Config.instance.branding.statusPageIncidentHistoryUrl),
|
||||
},
|
||||
setup: {
|
||||
configured: !Config.instance.selfHosted || Config.instance.setup.configured,
|
||||
@@ -519,6 +521,11 @@ function buildAppPublicConfig(config: z.infer<typeof StoredInstanceAppPublicSche
|
||||
wordmark_url: normalizeOptionalPublicString(branding.wordmark_url, defaults.branding.wordmark_url),
|
||||
favicon_url: normalizeOptionalPublicString(branding.favicon_url, defaults.branding.favicon_url),
|
||||
theme_color: normalizeOptionalPublicString(branding.theme_color, defaults.branding.theme_color),
|
||||
status_page_url: normalizeOptionalPublicString(branding.status_page_url, defaults.branding.status_page_url),
|
||||
status_page_incident_history_url: normalizeOptionalPublicString(
|
||||
branding.status_page_incident_history_url,
|
||||
defaults.branding.status_page_incident_history_url,
|
||||
),
|
||||
},
|
||||
setup: {
|
||||
configured: setup.configured ?? defaults.setup.configured,
|
||||
|
||||
@@ -29384,6 +29384,14 @@
|
||||
"theme_color": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Optional browser theme color"
|
||||
},
|
||||
"status_page_url": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Optional public status page URL"
|
||||
},
|
||||
"status_page_incident_history_url": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Optional public status page incident history URL"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -29393,7 +29401,9 @@
|
||||
"logo_url",
|
||||
"wordmark_url",
|
||||
"favicon_url",
|
||||
"theme_color"
|
||||
"theme_color",
|
||||
"status_page_url",
|
||||
"status_page_incident_history_url"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"description": "Branding values safe to expose to clients"
|
||||
|
||||
@@ -20,7 +20,6 @@ import {GuildRepository} from '@app/api/guild/repositories/GuildRepository';
|
||||
import {GuildRoleRepository} from '@app/api/guild/repositories/GuildRoleRepository';
|
||||
import {
|
||||
type CallData,
|
||||
type GatewayActiveVoiceRooms,
|
||||
type GatewayChannelMention,
|
||||
type GatewayGuildMemoryStats,
|
||||
type GatewayMentionSources,
|
||||
@@ -168,25 +167,6 @@ export class NoopGatewayService extends IGatewayService {
|
||||
};
|
||||
}
|
||||
|
||||
async getActiveVoiceRooms(): Promise<GatewayActiveVoiceRooms> {
|
||||
return {
|
||||
nodeCount: 1,
|
||||
rooms: Array.from(this.voiceStatesByChannel.entries()).flatMap(([key, voiceStates]) => {
|
||||
if (voiceStates.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const [guildIdText, channelIdText] = key.split(':');
|
||||
return [
|
||||
{
|
||||
guildId: guildIdText === 'dm' ? undefined : (BigInt(guildIdText) as GuildID),
|
||||
channelId: BigInt(channelIdText) as ChannelID,
|
||||
voiceStateCount: voiceStates.length,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async getUsersToMentionByRoles(_params: {
|
||||
guildId: GuildID;
|
||||
channelId: ChannelID;
|
||||
@@ -882,19 +862,6 @@ export class NoopGatewayService extends IGatewayService {
|
||||
return {success: false};
|
||||
}
|
||||
|
||||
async repairVoiceStateFromCache(_params: {
|
||||
guildId?: GuildID;
|
||||
channelId: ChannelID;
|
||||
userId: UserID;
|
||||
connectionId: string;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
repaired?: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
return {success: false};
|
||||
}
|
||||
|
||||
async getVoiceStatesForChannel(params: {guildId?: GuildID; channelId: ChannelID}): Promise<{
|
||||
voiceStates: Array<GatewayVoiceStateEntry>;
|
||||
}> {
|
||||
|
||||
@@ -633,6 +633,7 @@ img::before {
|
||||
|
||||
.emoji:not(img) {
|
||||
font-size: var(--emoji-size-emoji);
|
||||
font-variant-emoji: emoji;
|
||||
line-height: 1;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
import {canSwitchAccountFromStalledConnection} from '@app/features/app/ConnectionRecovery';
|
||||
import {isClientBooting, isClientReconnecting} from '@app/features/app/state/ClientReadiness';
|
||||
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
|
||||
import Nagbar from '@app/features/ui/state/Nagbar';
|
||||
import StatusPage from '@app/features/user/state/StatusPage';
|
||||
import {ExternalUrls} from '@fluxer/constants/src/ExternalUrls';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
const BOOT_NOTICE_DELAY_MS = 3_000;
|
||||
const STATUS_PAGE_URL = RuntimeConfig.statusPageUrl;
|
||||
|
||||
export const ConnectionNoticeTone = Object.freeze({
|
||||
NEUTRAL: 'neutral',
|
||||
@@ -131,7 +132,7 @@ export function useConnectionNotice(): ConnectionNotice | null {
|
||||
return {
|
||||
tone: ConnectionNoticeTone.NEUTRAL,
|
||||
message: i18n._(CONNECTION_ISSUES_DESCRIPTOR),
|
||||
action: {label: i18n._(VIEW_STATUS_PAGE_DESCRIPTOR), url: ExternalUrls.SERVICE_STATUS},
|
||||
action: STATUS_PAGE_URL ? {label: i18n._(VIEW_STATUS_PAGE_DESCRIPTOR), url: STATUS_PAGE_URL} : null,
|
||||
showSwitchAccount,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -144,6 +144,8 @@ export const DEFAULT_APP_PUBLIC_CONFIG: InstanceAppPublic = {
|
||||
wordmark_url: null,
|
||||
favicon_url: null,
|
||||
theme_color: null,
|
||||
status_page_url: null,
|
||||
status_page_incident_history_url: null,
|
||||
},
|
||||
setup: {
|
||||
configured: false,
|
||||
@@ -480,6 +482,14 @@ class RuntimeConfig {
|
||||
}
|
||||
}
|
||||
|
||||
get statusPageUrl(): string {
|
||||
return this.appPublic.branding.status_page_url ?? '';
|
||||
}
|
||||
|
||||
get statusPageIncidentHistoryUrl(): string {
|
||||
return this.appPublic.branding.status_page_incident_history_url ?? '';
|
||||
}
|
||||
|
||||
isSelfHosted(): boolean {
|
||||
return DeveloperOptions.selfHostedModeOverride || this.features.self_hosted;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,11 @@ const SCHEDULED_MAINTENANCE_NAGBAR_DESCRIPTOR = msg({
|
||||
comment:
|
||||
'Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the scheduled-maintenance banner.',
|
||||
});
|
||||
const SERVICE_INCIDENT_NAGBAR_DESCRIPTOR = msg({
|
||||
message: 'Service incident nagbar',
|
||||
comment:
|
||||
'Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.',
|
||||
});
|
||||
const UNCLAIMED_ACCOUNT_NAGBAR_DESCRIPTOR = msg({
|
||||
message: 'Unclaimed account nagbar',
|
||||
comment: 'Developer control label for the unclaimed-account banner.',
|
||||
@@ -344,6 +349,22 @@ export const getNagbarControls = (): Array<NagbarControlDefinition> => [
|
||||
forceShowDisabled: (state) => state.forceScheduledMaintenance,
|
||||
forceHideDisabled: (state) => state.forceHideScheduledMaintenance,
|
||||
},
|
||||
{
|
||||
key: 'forceIncident',
|
||||
label: SERVICE_INCIDENT_NAGBAR_DESCRIPTOR,
|
||||
forceKey: 'forceConnectionNotice',
|
||||
forceHideKey: 'forceHideConnectionNotice',
|
||||
resetKeys: ['forceConnectionNotice'],
|
||||
status: (state) =>
|
||||
state.forceConnectionNotice
|
||||
? FORCE_ENABLED
|
||||
: state.forceHideConnectionNotice
|
||||
? FORCE_DISABLED
|
||||
: USING_ACTUAL_STATE,
|
||||
useActualDisabled: (state) => !state.forceConnectionNotice && !state.forceHideConnectionNotice,
|
||||
forceShowDisabled: (state) => state.forceConnectionNotice,
|
||||
forceHideDisabled: (state) => state.forceHideConnectionNotice,
|
||||
},
|
||||
{
|
||||
key: 'forceVoiceSessionRestore',
|
||||
label: VOICE_SESSION_RESTORE_NAGBAR_DESCRIPTOR,
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "إشعارات رسائل المجتمع"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "شريط تذكير بمتطلبات المصادقة متعددة العوامل للمجتمع"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "معرف الاتصال"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "هل تواجه مشكلات في الاتصال؟"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "فُقد الاتصال"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "فُقد الاتصال. جارٍ إعادة الاتصال…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "الاستخدامات الحالية:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "مُغلق حاليًا"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "معروض حاليًا"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "مدة الخمول على سطح المكتب قبل إرسال الإشعارات إلى الجوال"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "شريط تنبيه إشعارات سطح المكتب"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "راسلنا عبر البريد الإلكتروني وسيقوم شخص بمراجعة حسابك. أخبرنا أنك لا تستطيع التحقق عبر الرسائل القصيرة وسنتولى الأمر. الردود تأتي عبر البريد الإلكتروني، لذا فهذه العملية ليست فورية."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "شريط تذكير التحقق من البريد الإلكتروني"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "حفاظًا على أمانك، أكّد هويتك للمتابعة."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "معطل قسريًا"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "مفعل قسريًا"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "مسار مخزون الهدايا"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "شريط تنبيه مخزون الهدايا"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "الدعوات معطلة"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "شريط تنبيه تعطيل الدعوات"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "شارة Premium"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "شريط انتهاء Premium"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "شريط فترة السماح لـ Premium"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "نموذج Premium"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "شريط التعريف بـ Premium"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "استضِف ملف الرمز المميّز"
|
||||
msgid "Service error"
|
||||
msgstr "خطأ في الخدمة"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "إشعار حادث الخدمة"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "تينيسي"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "شريط قبول الشروط"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "حساب غير مُطالَب به"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "شريط إشعارات الحساب غير المطالب به"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "الاستخدامات:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "يستخدم حالة الحساب الفعلية"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "يستخدم حالة مخزون الهدايا الفعلية"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "يستخدم حالة Premium الفعلية"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "يستخدم الحالة الفعلية"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "يستخدم حالة التحقق الفعلية"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "عرض مقال المساعدة"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "عرض تفاصيل العطل"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "عرض المكالمة الواردة"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "عرض تفاصيل الصيانة"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "عرض التفاعلات"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "عرض صفحة الحالة"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary ({premiumProductName} مدى الحياة)"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "شريط تنبيه المصادقة الثنائية لـ Visionary"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "منطقة الصوت"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "شريط تذكير استعادة الجلسة الصوتية"
|
||||
|
||||
|
||||
@@ -2538,6 +2538,9 @@
|
||||
{
|
||||
"msgid": "Serial send"
|
||||
},
|
||||
{
|
||||
"msgid": "Service incident nagbar"
|
||||
},
|
||||
{
|
||||
"msgid": "Set a community avatar."
|
||||
},
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Известия за съобщения в общности"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Лента за изискване за MFA на общността"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "ID на връзката"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Проблеми с връзката?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Връзката е изгубена"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Връзката е изгубена. Свързване отново…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Текущи използвания:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "В момента е отхвърлено"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "В момента се показва"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Неактивност на компютъра преди мобилно известие"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Лента за известия на работния плот"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Изпрати ни имейл и човек ще прегледа акаунта ти. Кажи ни, че не можеш да потвърдиш чрез SMS, и ние ще се погрижим за останалото. Отговорите идват по имейл, така че не е мигновено."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Лента за подкана за потвърждение на имейл"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "За твоята сигурност потвърди самоличността си, за да продължиш."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Принудително деактивирано"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Принудително активирано"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Процес за инвентара с подаръци"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Лента за инвентара с подаръци"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Поканите са деактивирани"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Лента за деактивирани покани"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Премиум значка"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Лента за изтекъл премиум"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Лента за гратисен период на премиум"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Премиум модел"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Лента за първоначално включване към премиум"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Предоставяне на файла с токена"
|
||||
msgid "Service error"
|
||||
msgstr "Грешка в услугата"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Инцидент със услугата"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Тенеси"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Лента за приемане на условията"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Незаявен акаунт"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Лента за напомняне за незаявен акаунт"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Използвания:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Използва се действителното състояние на акаунта"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Използва се действителното състояние на инвентара с подаръци"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Използва се действителното премиум състояние"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Използва се действителното състояние"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Използва се действителното състояние на потвърждение"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Преглед на помощна статия"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Виж подробности за инцидента"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Преглед на входящото повикване"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Виж подробности за поддръжката"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Преглед на реакциите"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Преглед на страницата със състоянието"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Визионер (доживотен {premiumProductName})"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Лента за Visionary 2FA"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Гласов регион"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Лента за възстановяване на гласова сесия"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Oznámení zpráv v komunitě"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Lišta s požadavkem komunity na vícefaktorové ověřování"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "ID připojení"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Problémy s připojením?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Připojení ztraceno"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Připojení ztraceno. Obnovování připojení…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Aktuální počet použití:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Momentálně skryto"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Aktuálně se zobrazuje"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Neaktivita na počítači před odesíláním push oznámení na mobil"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Lišta s upozorněním na oznámení na počítači"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Napište nám e-mail a váš účet osobně posoudíme. Uveďte, že nemůžete provést ověření pomocí SMS, a my se o zbytek postaráme. Odpovíme e-mailem, takže vyřízení nebude okamžité."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Lišta s výzvou k ověření e-mailu"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "Pro vaši bezpečnost prosím potvrďte, že jste to vy, abyste mohli pokračovat."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Vynuceně vypnuto"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Vynuceně zapnuto"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Průchod inventářem dárků"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Lišta s upozorněním na inventář dárků"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Pozvánky zakázány"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Lišta s upozorněním na vypnuté pozvánky"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Odznak Premium"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Lišta s upozorněním na vypršení předplatného"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Lišta s upozorněním na dodatečnou lhůtu předplatného"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Model prémiových funkcí"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Lišta s představením prémiových funkcí"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Poskytnout soubor tokenu"
|
||||
msgid "Service error"
|
||||
msgstr "Chyba služby"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Upozornění na incident služby"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Tennessee"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Upozornění na přijetí podmínek"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Účet s nedokončenou registrací"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Upozornění na nedokončenou registraci účtu"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Použití:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Používá skutečný stav účtu"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Používá se skutečný stav inventáře dárků"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Používá skutečný stav prémiového účtu"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Používá skutečný stav"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Používá skutečný stav ověření"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Zobrazit článek nápovědy"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Zobrazit podrobnosti incidentu"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Zobrazit příchozí hovor"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Zobrazit podrobnosti údržby"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Zobrazit reakce"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Zobrazit stavovou stránku"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary (doživotní {premiumProductName})"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Upozornění na dvoufaktorové ověřování pro Visionary"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Hlasový region"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Oznamovací lišta pro obnovení hlasové relace"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Notifikationer for fællesskabsbeskeder"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Banner for fællesskabets MFA-krav"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "Forbindelses-id"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Forbindelsesproblemer?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Forbindelsen blev afbrudt"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Forbindelsen er mistet. Opretter forbindelse igen…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Antal anvendelser:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Skjult i øjeblikket"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Vises i øjeblikket"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Inaktivitet på computeren før push-notifikationer på mobilen"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Banner om computernotifikationer"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Send os en e-mail, så gennemgår en medarbejder din konto. Fortæl os, at du ikke kan bekræfte dit telefonnummer via sms, så hjælper vi dig videre. Vi svarer via e-mail, så det sker ikke med det samme."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Banner til bekræftelse af e-mail"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "Af hensyn til din sikkerhed skal du bekræfte, at det er dig, for at fortsætte."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Fremtving deaktivering"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Fremtving aktivering"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Forløb for gaveoversigt"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Banner for gaveoversigt"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Invitationer deaktiveret"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Beskedbjælke om deaktiverede invitationer"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Premium-badge"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Beskedbjælke om udløbet premium"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Beskedbjælke om overgangsperiode for premium"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Premium-model"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Beskedbjælke med introduktion til premium"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Gør tokenfilen tilgængelig"
|
||||
msgid "Service error"
|
||||
msgstr "Tjenestefejl"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Servicehændelses-nagbar"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Tennessee"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Beskedbjælke til accept af servicevilkår"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Kontooprettelse ikke fuldført"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Beskedbjælke om ufuldført kontooprettelse"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Anvendelser:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Bruger faktisk kontostatus"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Bruger gaveoversigtens faktiske status"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Bruger faktisk premium-status"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Bruger faktisk tilstand"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Bruger faktisk bekræftelsesstatus"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Se hjælpeartikel"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Se hændelsesdetaljer"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Se indgående opkald"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Se vedligeholdelsesdetaljer"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Se reaktioner"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Se statusside"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary ({premiumProductName} på livstid)"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Beskedbjælke om 2FA for Visionary"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Taleregion"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Beskedbjælke om gendannelse af taleopkald"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Benachrichtigungen für Community-Nachrichten"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Hinweisleiste zur MFA-Pflicht in der Community"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "Verbindungs-ID"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Verbindungsprobleme?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Verbindung unterbrochen"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Verbindung unterbrochen. Verbindung wird wiederhergestellt …"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Aktuelle Nutzungen:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Derzeit ausgeblendet"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Wird gerade angezeigt"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Desktop-Inaktivität vor mobilen Push-Benachrichtigungen"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Desktop-Benachrichtigungsleiste"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Schreib uns eine E-Mail und jemand aus unserem Team wird deinen Account überprüfen. Sag uns, dass du dich nicht per SMS verifizieren kannst, und wir kümmern uns um den Rest. Du erhältst die Antwort per E-Mail, daher kann es etwas dauern."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Hinweisleiste zur E-Mail-Verifizierung"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "Bestätige bitte zu deiner Sicherheit, dass du es bist, um fortzufahren."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Erzwungen deaktiviert"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Erzwungen aktiviert"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Geschenkinventar-Flow"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Hinweisleiste zum Geschenkinventar"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Einladungen deaktiviert"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Hinweisleiste zu deaktivierten Einladungen"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Premium-Abzeichen"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Premium-Ablauf-Hinweisbanner"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Hinweisleiste zur Premium-Übergangsfrist"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Premium-Modell"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Hinweisleiste zur Premium-Einführung"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Token-Datei bereitstellen"
|
||||
msgid "Service error"
|
||||
msgstr "Dienstfehler"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Service-Störungsmeldung"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Tennessee"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "AGB-Hinweisbanner"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Nicht beanspruchter Account"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Banner für nicht beanspruchten Account"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Nutzungen:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Verwendet den tatsächlichen Accountstatus"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Verwendet den tatsächlichen Status des Geschenkinventars"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Verwendet den tatsächlichen Premium-Status"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Verwendet den tatsächlichen Status"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Verwendet den tatsächlichen Verifizierungsstatus"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Hilfeartikel ansehen"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Details zur Störung anzeigen"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Eingehenden Anruf ansehen"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Wartungsdetails anzeigen"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Reaktionen ansehen"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Statusseite anzeigen"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary (lebenslanges {premiumProductName})"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Visionary-2FA-Hinweisleiste"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Sprachregion"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Hinweisleiste zur Wiederherstellung der Sprachsitzung"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Ειδοποιήσεις μηνυμάτων κοινότητας"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Γραμμή ειδοποιήσεων απαίτησης MFA κοινότητας"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "Αναγνωριστικό σύνδεσης"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Πρόβλημα σύνδεσης;"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Η σύνδεση χάθηκε"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Χάθηκε η σύνδεση. Γίνεται επανασύνδεση…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Τρέχουσες χρήσεις:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Έχει απορριφθεί προς το παρόν"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Εμφανίζεται τώρα"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Αδράνεια στην επιφάνεια εργασίας πριν από την αποστολή ειδοποίησης στην κινητή συσκευή"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Γραμμή ειδοποιήσεων για ειδοποιήσεις επιφάνειας εργασίας"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Στείλτε μας email και ένα άτομο θα ελέγξει τον λογαριασμό σας. Πείτε μας ότι δεν μπορείτε να επαληθεύσετε μέσω SMS και θα αναλάβουμε εμείς. Οι απαντήσεις έρχονται μέσω email, οπότε αυτό δεν είναι άμεσο."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Μπάρα ειδοποιήσεων επαλήθευσης email"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "Για την ασφάλειά σας, επιβεβαιώστε ότι είστε εσείς για να συνεχίσετε."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Αναγκαστική απενεργοποίηση"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Αναγκαστική ενεργοποίηση"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Ροή αποθέματος δώρων"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Γραμμή ειδοποιήσεων αποθέματος δώρων"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Οι προσκλήσεις απενεργοποιήθηκαν"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Μπάρα ειδοποιήσεων απενεργοποιημένων προσκλήσεων"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Σήμα Premium"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Γραμμή ειδοποιήσεων για λήξη Premium"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Γραμμή ειδοποιήσεων περιόδου χάριτος Premium"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Μοντέλο Premium"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Γραμμή ειδοποιήσεων καλωσορίσματος Premium"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Παροχή του αρχείου token"
|
||||
msgid "Service error"
|
||||
msgstr "Σφάλμα υπηρεσίας"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Αναφορά περιστατικού υπηρεσίας"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Τενεσί"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Γραμμή ειδοποιήσεων αποδοχής όρων"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Μη διεκδικημένος λογαριασμός"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Μπάρα ειδοποιήσεων για μη διεκδικημένο λογαριασμό"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Χρήσεις:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Χρήση της πραγματικής κατάστασης λογαριασμού"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Χρήση της πραγματικής κατάστασης αποθέματος δώρων"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Χρήση της πραγματικής κατάστασης premium"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Χρήση της πραγματικής κατάστασης"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Χρήση της πραγματικής κατάστασης επαλήθευσης"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Προβολή άρθρου βοήθειας"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Προβολή λεπτομερειών συμβάντος"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Προβολή εισερχόμενης κλήσης"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Προβολή λεπτομερειών συντήρησης"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Προβολή αντιδράσεων"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Προβολή σελίδας κατάστασης"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary ({premiumProductName} εφ’ όρου ζωής)"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Μπάρα ειδοποιήσεων 2FA Visionary"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Περιοχή φωνής"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Γραμμή ειδοποιήσεων επαναφοράς φωνητικής συνεδρίας"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Community message notifications"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Community MFA requirement nagbar"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "Connection ID"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Connection issues?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Connection lost"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Connection lost. Reconnecting…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Current uses:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Currently dismissed"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Currently showing"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Desktop inactivity before mobile push"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Desktop notification nagbar"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Email us and a person will review your account. Tell us you cannot verify by SMS and we will take it from there. Replies come by email, so this is not instant."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Email verification nagbar"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "For your security, please confirm it's you to continue."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Force disabled"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Force enabled"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Gift inventory flow"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Gift inventory nagbar"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Invites disabled"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Invites disabled nagbar"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Premium badge"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Premium expired nagbar"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Premium grace period nagbar"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Premium model"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Premium onboarding nagbar"
|
||||
|
||||
@@ -29497,6 +29497,12 @@ msgstr "Serve the token file"
|
||||
msgid "Service error"
|
||||
msgstr "Service error"
|
||||
|
||||
# auto-i18n: reviewed unchanged
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Service incident nagbar"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32781,7 @@ msgid "Tennessee"
|
||||
msgstr "Tennessee"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Terms acceptance nagbar"
|
||||
|
||||
@@ -35293,7 +35299,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Unclaimed account"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Unclaimed account nagbar"
|
||||
|
||||
@@ -36815,27 +36821,27 @@ msgid "Uses:"
|
||||
msgstr "Uses:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Using actual account state"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Using actual gift inventory state"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Using actual premium state"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Using actual state"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Using actual verification state"
|
||||
|
||||
@@ -37422,7 +37428,7 @@ msgid "View help article"
|
||||
msgstr "View help article"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "View incident details"
|
||||
|
||||
@@ -37432,7 +37438,7 @@ msgid "View incoming call"
|
||||
msgstr "View incoming call"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "View maintenance details"
|
||||
|
||||
@@ -37474,7 +37480,7 @@ msgid "View reactions"
|
||||
msgstr "View reactions"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "View status page"
|
||||
|
||||
@@ -37554,7 +37560,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary (lifetime {premiumProductName})"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Visionary 2FA nagbar"
|
||||
|
||||
@@ -37812,7 +37818,7 @@ msgid "Voice region"
|
||||
msgstr "Voice region"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Voice session restore nagbar"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Community message notifications"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Community MFA requirement nagbar"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "Connection ID"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Connection issues?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Connection lost"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Connection lost. Reconnecting…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Current uses:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Currently dismissed"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Currently showing"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Desktop inactivity before mobile push"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Desktop notification nagbar"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Email us and a person will review your account. Tell us you cannot verify by SMS and we will take it from there. Replies come by email, so this is not instant."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Email verification nagbar"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "For your security, please confirm it's you to continue."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Force disabled"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Force enabled"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Gift inventory flow"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Gift inventory nagbar"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Invites disabled"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Invites disabled nagbar"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Premium badge"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Premium expired nagbar"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Premium grace period nagbar"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Premium model"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Premium onboarding nagbar"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Serve the token file"
|
||||
msgid "Service error"
|
||||
msgstr "Service error"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Service incident nagbar"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Tennessee"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Terms acceptance nagbar"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Unclaimed account"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Unclaimed account nagbar"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Uses:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Using actual account state"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Using actual gift inventory state"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Using actual premium state"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Using actual state"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Using actual verification state"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "View help article"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "View incident details"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "View incoming call"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "View maintenance details"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "View reactions"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "View status page"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary (lifetime {premiumProductName})"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Visionary 2FA nagbar"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Voice region"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Voice session restore nagbar"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Notificaciones de mensajes de la comunidad"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Barra de notificación de requisito de MFA de la comunidad"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "ID de conexión"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "¿Problemas de conexión?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Conexión perdida"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Conexión perdida. Reconectando…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Usos actuales:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Actualmente descartado"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Mostrando actualmente"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Inactividad en la computadora antes de enviar notificaciones al celular"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Barra de notificaciones de escritorio"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Envíanos un correo y una persona revisará tu cuenta. Indica que no puedes verificar por SMS y nos encargaremos del resto. Las respuestas llegan por correo electrónico, así que esto no es instantáneo."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Barra de verificación de correo electrónico"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "Para tu seguridad, confirma que eres tú para continuar."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Forzar desactivación"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Forzar activación"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Flujo de inventario de regalos"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Barra de notificación de inventario de regalos"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Invitaciones deshabilitadas"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Barra de notificaciones de invitaciones deshabilitadas"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Insignia prémium"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Barra de notificación de premium vencido"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Barra de notificación del período de gracia prémium"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Modelo prémium"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Barra de notificación de incorporación a Premium"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Proporciona el archivo de token"
|
||||
msgid "Service error"
|
||||
msgstr "Error del servicio"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Incidente de servicio"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Tennessee"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Barra de aviso para aceptar los Términos del servicio"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Cuenta con registro pendiente"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Barra de aviso de registro pendiente"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Usos:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Usando el estado real de la cuenta"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Usando el estado real del inventario de regalos"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Usando el estado premium real"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Usando el estado real"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Usando el estado de verificación real"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Ver artículo de ayuda"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Ver detalles del incidente"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Ver llamada entrante"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Ver detalles del mantenimiento"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Ver reacciones"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Ver página de estado"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary ({premiumProductName} de por vida)"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Barra de notificación de 2FA Visionary"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Región de voz"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Barra de notificación de restauración de sesión de voz"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Notificaciones de mensajes de la comunidad"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Barra de notificación de requisito de MFA de la comunidad"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "ID de conexión"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "¿Problemas de conexión?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Conexión perdida"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Conexión perdida. Reconectando…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Usos actuales:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Actualmente descartado"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Mostrando actualmente"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Inactividad en el escritorio antes de enviar notificaciones al móvil."
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Barra de notificaciones de escritorio"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Envíanos un correo y una persona revisará tu cuenta. Indica que no puedes verificar por SMS y nos encargaremos del resto. Las respuestas llegan por correo electrónico, así que no es instantáneo."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Barra de notificación de verificación de correo electrónico"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "Para tu seguridad, confirma que eres tú para continuar."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Forzar desactivación"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Forzar activación"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Flujo de inventario de regalos"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Barra de notificaciones de inventario de regalos"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Invitaciones deshabilitadas"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Barra de notificaciones de invitaciones deshabilitadas"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Insignia prémium"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Barra de notificación de Premium caducado"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Barra de notificación del período de gracia prémium"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Modelo prémium"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Barra de notificación de incorporación a Premium"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Proporcionar el archivo de token"
|
||||
msgid "Service error"
|
||||
msgstr "Error del servicio"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Aviso de incidente del servicio"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Tennessee"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Barra de aviso para aceptar las Condiciones del servicio"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Cuenta con registro pendiente"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Barra de aviso de registro pendiente"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Usos:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Usando el estado real de la cuenta"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Usando el estado real del inventario de regalos"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Usando el estado prémium real"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Usando el estado real"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Usando el estado de verificación real"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Ver artículo de ayuda"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Ver detalles del incidente"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Ver llamada entrante"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Ver detalles del mantenimiento"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Ver reacciones"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Ver página de estado"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary ({premiumProductName} de por vida)"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Barra de notificación de 2FA de Visionary"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Región de voz"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Barra de notificación de restauración de sesión de voz"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Yhteisön viesti-ilmoitukset"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Yhteisön MFA-vaatimuksen ilmoituspalkki"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "Yhteyden tunnus"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Yhteysongelmia?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Yhteys katkesi"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Yhteys katkesi. Yhdistetään uudelleen…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Nykyiset käyttökerrat:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Tällä hetkellä suljettu"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Tällä hetkellä näkyvissä"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Työpöytäsovelluksen käyttämättömyysaika ennen mobiili-ilmoitusta"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Työpöytäilmoitusten ilmoituspalkki"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Lähetä meille sähköpostia, niin ihminen tarkistaa tilisi. Kerro, ettet voi vahvistaa tekstiviestillä, niin hoidamme loput. Vastaukset tulevat sähköpostitse, joten vastaus ei tule heti."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Sähköpostivahvistuksen ilmoituspalkki"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "Vahvista turvallisuussyistä henkilöllisyytesi, jotta voit jatkaa."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Pakotettu pois käytöstä"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Pakotettu käyttöön"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Lahjavaraston kulku"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Lahjavaraston ilmoituspalkki"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Kutsut poissa käytöstä"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Kutsut poissa käytöstä -ilmoituspalkki"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Premium-merkki"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Plutonium-tilaus vanhentunut -ilmoituspalkki"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Plutonium-lisäajan ilmoituspalkki"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Premium-malli"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Plutonium-perehdytyksen ilmoituspalkki"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Julkaise tunnustiedosto"
|
||||
msgid "Service error"
|
||||
msgstr "Palveluvirhe"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Palvelun häiriöilmoitus"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Tennessee"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Käyttöehtojen hyväksymisen ilmoituspalkki"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Lunastamaton tili"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Lunastamaton tili -ilmoituspalkki"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Käyttökerrat:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Käytetään todellista tilin tilaa"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Käytetään todellista lahjavaraston tilaa"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Käytetään todellista premium-tilaa"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Käytetään todellista tilaa"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Käytetään todellista vahvistustilaa"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Näytä ohjeartikkeli"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Näytä häiriön tiedot"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Näytä saapuva puhelu"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Näytä huoltokatkon tiedot"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Näytä reaktiot"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Näytä tilasivu"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary (elinikäinen {premiumProductName})"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Visionary 2FA -ilmoituspalkki"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Puhealue"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Äänipuhelun palautusilmoituspalkki"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Notifications des messages de communauté"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Bannière d’exigence MFA de la communauté"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "ID de connexion"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Problèmes de connexion ?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Connexion perdue"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Connexion perdue. Reconnexion…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Utilisations actuelles :"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Actuellement ignoré"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Affiché actuellement"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Délai d’inactivité sur ordinateur avant les notifications push sur mobile"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Bandeau d’activation des notifications de bureau"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Envoyez-nous un e-mail et une personne examinera votre compte. Indiquez-nous que vous ne pouvez pas vérifier par SMS et nous nous en chargerons. Les réponses arrivent par e-mail, ce n'est donc pas instantané."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Bannière de vérification de l'e-mail"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "Pour votre sécurité, veuillez confirmer votre identité pour continuer."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Désactivé de force"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Activé de force"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Parcours de l’inventaire des cadeaux"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Bannière d'inventaire de cadeaux"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Invitations désactivées"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Bannière \" Invitations désactivées \""
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Badge Premium"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Bannière d'expiration de l'abonnement Premium"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Bannière de période de grâce Premium"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Modèle premium"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Bannière d'intégration Premium"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Fournir le fichier de jeton"
|
||||
msgid "Service error"
|
||||
msgstr "Erreur de service"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Bannière d'incident de service"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Tennessee"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Bannière d'acceptation des conditions d'utilisation"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Compte non finalisé"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Bandeau d’inscription incomplète"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Utilisations :"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Utilise l'état réel du compte"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Utilisation de l’état réel de l’inventaire des cadeaux"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Utilise l'état réel de l'abonnement premium"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Utilise l'état réel"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Utilisation de l'état de vérification réel"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Afficher l'article d'aide"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Voir les détails de l'incident"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Voir l'appel entrant"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Voir les détails de la maintenance"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Voir les réactions"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Voir la page d'état"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary ({premiumProductName} à vie)"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Bannière 2FA Visionary"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Région vocale"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Barre de rappel de restauration de session vocale"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "התראות על הודעות קהילה"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "שורת התראה על דרישת אימות דו-שלבי בקהילה"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "מזהה חיבור"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "בעיות חיבור?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "החיבור אבד"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "החיבור אבד. מתחבר מחדש…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "שימושים נוכחיים:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "מוסתר כעת"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "מוצג כעת"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "חוסר פעילות במחשב לפני שליחת התראה לנייד"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "שורת התראה של התראות שולחן עבודה"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "שלח לנו אימייל ונציג יבדוק את חשבונך. ציין שאינך יכול לאמת באמצעות SMS ונטפל בזה. התשובות מגיעות באימייל, לכן זה לא מיידי."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "באנר אימות אימייל"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "למען אבטחתך, יש לאמת את זהותך כדי להמשיך."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "מושבת בכפייה"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "מופעל בכפייה"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "תהליך מלאי מתנות"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "סרגל התראות של מלאי מתנות"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "ההזמנות מושבתות"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "שורת התראה להזמנות מושבתות"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "תג פרימיום"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "באנר פרימיום שפג תוקפו"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "באנר תקופת חסד של פרימיום"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "מודל פרימיום"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "באנר הצטרפות לפרימיום"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "הגשת קובץ האסימון"
|
||||
msgid "Service error"
|
||||
msgstr "שגיאת שירות"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "הודעת תקלה בשירות"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "טנסי"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "באנר קבלת תנאים"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "חשבון שלא נתבע"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "באנר חשבון שלא נתבע"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "שימושים:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "משתמש במצב החשבון האמיתי"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "משתמש במצב מלאי המתנות האמיתי"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "משתמש במצב פרימיום אמיתי"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "משתמש במצב אמיתי"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "משתמש במצב אימות אמיתי"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "הצגת מאמר עזרה"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "הצג פרטי תקרית"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "הצג שיחה נכנסת"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "הצג פרטי תחזוקה"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "הצגת ריאקציות"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "הצג דף סטטוס"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary ({premiumProductName} לכל החיים)"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "באנר 2FA של Visionary"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "אזור קולי"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "שורת התראה לשחזור שיחה קולית"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "कम्युनिटी मैसेज की सूचनाएँ"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "कम्युनिटी MFA आवश्यकता नागबार"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "कनेक्शन आईडी"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "कनेक्शन में समस्या है?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "कनेक्शन टूट गया"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "कनेक्शन टूट गया। पुनः कनेक्ट हो रहा है…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "मौजूदा उपयोग:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "फ़िलहाल खारिज है"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "फ़िलहाल दिख रहा है"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "मोबाइल पुश से पहले डेस्कटॉप निष्क्रियता"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "डेस्कटॉप नोटिफ़िकेशन नागबार"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "हमें ईमेल करें और कोई व्यक्ति आपके अकाउंट की समीक्षा करेगा। हमें बताएं कि आप SMS से वेरिफ़ाई नहीं कर सकते और हम आगे संभाल लेंगे। जवाब ईमेल से आते हैं, इसलिए यह तुरंत नहीं होता।"
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "ईमेल वेरिफ़िकेशन नागबार"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "आपकी सुरक्षा के लिए, आगे बढ़ने से पहले कृपया पुष्टि करें कि यह आप ही हैं।"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "ज़बरदस्ती अक्षम किया गया"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "ज़बरदस्ती चालू किया गया"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "उपहार इन्वेंट्री फ़्लो"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "उपहार इन्वेंट्री नागबार"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "इनवाइट बंद हैं"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "इनवाइट बंद नागबार"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "प्रीमियम बैज"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "प्रीमियम की सदस्यता समाप्त होने का नागबार"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "प्रीमियम ग्रेस पीरियड नागबार"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "प्रीमियम मॉडल"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "प्रीमियम ऑनबोर्डिंग नागबार"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "टोकन फ़ाइल होस्ट करें"
|
||||
msgid "Service error"
|
||||
msgstr "सेवा में गड़बड़ी"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "सेवा घटना नैगबार"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "टेनेसी"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "नियम स्वीकार करने वाला नागबार"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "क्लेम न किया गया अकाउंट"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "क्लेम न किया गया अकाउंट नागबार"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "उपयोग:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "वास्तविक अकाउंट स्थिति का उपयोग किया जा रहा है"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "उपहार इन्वेंट्री की वास्तविक स्थिति का उपयोग किया जा रहा है"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "वास्तविक प्रीमियम स्थिति का उपयोग किया जा रहा है"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "वास्तविक स्थिति का उपयोग किया जा रहा है"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "वास्तविक वेरिफ़िकेशन स्थिति का उपयोग किया जा रहा है"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "सहायता लेख देखें"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "घटना का विवरण देखें"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "इनकमिंग कॉल देखें"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "रखरखाव का विवरण देखें"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "रिएक्शन देखें"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "स्टेटस पेज देखें"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "विजनरी (लाइफटाइम {premiumProductName})"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "विजनरी 2FA नागबार"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "वॉइस रीजन"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "वॉइस सेशन रीस्टोर नागबार"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Obavijesti o porukama u zajednici"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Traka za obveznu MFA u zajednici"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "ID veze"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Problemi s vezom?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Veza prekinuta"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Veza izgubljena. Ponovno povezivanje…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Trenutni broj korištenja:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Trenutačno odbačeno"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Trenutačno prikazano"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Neaktivnost na računalu prije mobilne push obavijesti"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Traka s obavijestima za radnu površinu"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Pošaljite nam e-poruku i osoba će pregledati vaš račun. Navedite da ne možete potvrditi putem SMS-a i mi ćemo se pobrinuti za ostalo. Odgovori stižu e-poštom, tako da ovo nije trenutno."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Traka za provjeru e-pošte"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "Radi vaše sigurnosti, potvrdite svoj identitet za nastavak."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Prisilno onemogućeno"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Prisilno omogućeno"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Tijek inventara poklona"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Traka s obavijestima o inventaru poklona"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Pozivnice onemogućene"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Traka s obavijestima o onemogućenim pozivnicama"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Premium značka"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Traka s obavijestima o isteku Premiuma"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Traka s obavijestima o razdoblju počeka za Premium"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Premium model"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Traka s obavijestima o uvođenju u Premium"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Posluži datoteku tokena"
|
||||
msgid "Service error"
|
||||
msgstr "Pogreška usluge"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Obavijest o incidentu usluge"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Tennessee"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Traka za prihvaćanje uvjeta"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Nepreuzet račun"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Traka s obavijestima o nepreuzetom računu"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Broj korištenja:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Koristi se stvarno stanje računa"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Koristi se stvarno stanje inventara poklona"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Koristi se stvarno premium stanje"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Koristi se stvarno stanje"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Koristi se stvarno stanje provjere"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Prikaži članak pomoći"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Prikaži detalje incidenta"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Prikaži dolazni poziv"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Prikaži detalje održavanja"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Pogledaj reakcije"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Prikaži stranicu statusa"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary (doživotni {premiumProductName})"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Visionary 2FA traka s obavijestima"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Glasovna regija"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Traka s obavijestima o vraćanju glasovne sesije"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Közösségi üzenetek értesítései"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Közösségi kétlépcsős azonosítást kérő sáv"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "Kapcsolat azonosítója"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Kapcsolódási problémák?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Kapcsolat megszakadt"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "A kapcsolat megszakadt. Újracsatlakozás…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Jelenlegi használatok:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Jelenleg elvetve"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Jelenleg látható"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Asztali inaktivitás a mobilos pushértesítések előtt"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Asztali értesítési sáv"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Írj nekünk e-mailt, és egy ember átnézi a fiókodat. Mondd el, hogy nem tudod igazolni magad SMS-ben, és mi intézzük a többit. A válaszok e-mailben érkeznek, így ez nem azonnali."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "E-mail-ellenőrzési emlékeztető sáv"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "A biztonságod érdekében kérjük, erősítsd meg, hogy te vagy az, mielőtt folytatnád."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Kényszerített letiltás"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Kényszerített engedélyezés"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Ajándékok kezelésének folyamata"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Ajándékokra figyelmeztető sáv"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Meghívók letiltva"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Meghívók letiltva (értesítősáv)"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Prémium jelvény"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Lejárt prémiumcsomag figyelmeztető sávja"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Prémium türelmi időszak figyelmeztető sávja"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Prémium modell"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Prémium bevezető figyelmeztető sáv"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Tokenfájl kiszolgálása"
|
||||
msgid "Service error"
|
||||
msgstr "Szolgáltatási hiba"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Szolgáltatási incidens értesítés"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Tennessee"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Szolgáltatási feltételek elfogadására figyelmeztető sáv"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Befejezetlen regisztrációjú fiók"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Befejezetlen regisztrációjú fiók figyelmeztető sávja"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Használat:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Valós fiókállapot használata"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Valós ajándékállapot használata"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Tényleges prémium állapot használata"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Valós állapot használata"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Tényleges ellenőrzési állapot használata"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Súgócikk megtekintése"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Incidens részleteinek megtekintése"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Bejövő hívás megtekintése"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Karbantartási részletek megtekintése"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Reakciók megtekintése"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Állapotoldal megtekintése"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary (élethosszig tartó {premiumProductName})"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Visionary 2FA értesítősáv"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Hangrégió"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Hanghívás-munkamenet visszaállítási értesítő"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Notifikasi pesan komunitas"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Bilah notifikasi persyaratan MFA komunitas"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "ID koneksi"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Ada masalah koneksi?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Koneksi terputus"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Koneksi terputus. Menyambungkan ulang…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Penggunaan saat ini:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Saat ini ditutup"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Saat ini ditampilkan"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Waktu tidak aktif di desktop sebelum notifikasi seluler"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Bilah notifikasi desktop"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Kirim email kepada kami dan seseorang akan meninjau akunmu. Beri tahu kami bahwa kamu tidak dapat memverifikasi melalui SMS dan kami akan menanganinya. Balasan dikirim melalui email, jadi ini tidak instan."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Bilah notifikasi verifikasi email"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "Demi keamananmu, konfirmasi dulu bahwa ini benar kamu untuk melanjutkan."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Paksa dinonaktifkan"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Paksa diaktifkan"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Alur inventaris hadiah"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Bilah notifikasi inventaris hadiah"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Undangan dinonaktifkan"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Bilah notifikasi saat undangan dinonaktifkan"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Lencana premium"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Bilah notifikasi premium kedaluwarsa"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Bilah notifikasi masa tenggang premium"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Model premium"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Bilah notifikasi orientasi premium"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Sajikan file token"
|
||||
msgid "Service error"
|
||||
msgstr "Kesalahan layanan"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Insiden layanan"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Tennessee"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Bilah notifikasi persetujuan ketentuan"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Akun belum diklaim"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Bilah notifikasi akun belum diklaim"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Penggunaan:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Menggunakan status akun sebenarnya"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Menggunakan status inventaris hadiah sebenarnya"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Menggunakan status premium sebenarnya"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Menggunakan status sebenarnya"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Menggunakan status verifikasi sebenarnya"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Lihat artikel bantuan"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Lihat detail insiden"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Lihat panggilan masuk"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Lihat detail pemeliharaan"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Lihat reaksi"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Lihat halaman status"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary ({premiumProductName} seumur hidup)"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Bilah notifikasi 2FA Visionary"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Wilayah suara"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Bilah notifikasi pemulihan sesi suara"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Notifiche messaggi community"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Banner requisito MFA della community"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "ID connessione"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Problemi di connessione?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Connessione persa"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Connessione persa. Riconnessione in corso…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Utilizzi attuali:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Attualmente ignorato"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Attualmente visualizzato"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Inattività desktop prima delle notifiche push su dispositivo mobile"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Banner di notifica desktop"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Inviaci un'email e una persona esaminerà il tuo account. Dicci che non puoi verificare tramite SMS e ci occuperemo noi del resto. Le risposte arrivano via email, quindi non è istantaneo."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Banner di verifica email"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "Per la tua sicurezza, conferma la tua identità per continuare."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Disattivazione forzata"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Attivazione forzata"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Flusso inventario regali"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Banner inventario regali"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Inviti disabilitati"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Barra di notifica inviti disabilitati"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Badge Premium"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Banner scadenza Premium"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Banner del periodo di tolleranza Premium"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Modello premium"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Banner introduttivo di Premium"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Rendi disponibile il file del token sul server"
|
||||
msgid "Service error"
|
||||
msgstr "Errore del servizio"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Notifica incidente servizio"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Tennessee"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Banner di accettazione dei termini"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Account con registrazione incompleta"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Banner account con registrazione incompleta"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Utilizzi:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Utilizzo dello stato effettivo dell'account"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Utilizzo dello stato effettivo dell'inventario regali"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Utilizzo dello stato premium effettivo"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Utilizzo dello stato attuale"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Utilizzo dello stato di verifica effettivo"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Visualizza articolo della guida"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Visualizza i dettagli dell'incidente"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Visualizza chiamata in arrivo"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Visualizza i dettagli della manutenzione"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Visualizza reazioni"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Visualizza la pagina di stato"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary ({premiumProductName} a vita)"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Banner 2FA Visionary"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Regione vocale"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Banner di ripristino della sessione vocale"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "コミュニティメッセージ通知"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "コミュニティのMFA必須設定ナグバー"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "接続ID"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "接続に問題がありますか?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "接続が切れました"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "接続が切れました。再接続中…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "現在の使用回数:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "現在非表示"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "現在表示中"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "モバイルプッシュ前のデスクトップでの非アクティブ時間"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "デスクトップ通知ナグバー"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "メールでご連絡ください。担当者がアカウントを確認します。SMSでの本人確認ができない旨をお伝えいただければ、その後の対応をいたします。返信はメールで行われるため、すぐには対応できません。"
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "メール認証ナグバー"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "セキュリティ保護のため、ご本人確認をお願いします。"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "強制的に無効"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "強制的に有効"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "ギフトインベントリフロー"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "ギフトインベントリナグバー"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "招待が無効です"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "招待無効のナグバー"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "プレミアムバッジ"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "プレミアムの有効期限切れナグバー"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "プレミアム猶予期間のナグバー"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "プレミアムモデル"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "プレミアムオンボーディングのナグバー"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "トークンファイルを配信"
|
||||
msgid "Service error"
|
||||
msgstr "サービスエラー"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "サービスインシデント通知"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "テネシー州"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "利用規約同意のナグバー"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "未登録アカウント"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "未登録アカウントのナグバー"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "使用回数:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "実際のアカウントの状態を使用中"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "実際のギフト在庫状況を使用中"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "実際のプレミアム状態を使用中"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "実際の状態を使用中"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "実際の認証状態を使用中"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "ヘルプ記事を見る"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "インシデントの詳細を表示"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "着信を表示"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "メンテナンスの詳細を表示"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "リアクションを見る"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "ステータスページを表示"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary (生涯{premiumProductName})"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Visionary 2FA ナグバー"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "ボイスリージョン"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "音声セッション復元ナグバー"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "커뮤니티 메시지 알림"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "커뮤니티 MFA 필수 알림 표시줄"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "연결 ID"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "연결에 문제가 있나요?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "연결 끊김"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "연결이 끊겼습니다. 다시 연결 중…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "현재 사용 횟수:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "현재 닫힘"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "현재 표시 중"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "모바일 푸시 알림 전 데스크톱 비활성 시간"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "데스크톱 알림 표시줄"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "이메일로 문의하시면 담당자가 계정을 검토합니다. SMS 인증이 불가능하다고 알려주시면 저희가 처리하겠습니다. 답장은 이메일로 발송되므로 즉시 확인은 어렵습니다."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "이메일 인증 알림 표시줄"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "안전을 위해 본인 확인 후 계속해 주세요."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "강제 비활성화됨"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "강제 활성화됨"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "선물함 플로우"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "선물함 알림 표시줄"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "초대 비활성화됨"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "초대 비활성화 알림 표시줄"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "프리미엄 배지"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "프리미엄 만료 알림 표시줄"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "프리미엄 유예 기간 알림 표시줄"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "프리미엄 모델"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "프리미엄 온보딩 알림 표시줄"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "토큰 파일 제공"
|
||||
msgid "Service error"
|
||||
msgstr "서비스 오류"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "서비스 장애 알림"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "테네시"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "약관 동의 알림 표시줄"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "미등록 계정"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "미등록 계정 알림 표시줄"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "사용 횟수:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "실제 계정 상태 사용 중"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "실제 선물 재고 상태 사용 중"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "실제 프리미엄 상태 사용 중"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "실제 상태 사용 중"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "실제 인증 상태 사용 중"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "도움말 보기"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "장애 세부 정보 보기"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "수신 전화 보기"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "점검 상세 보기"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "반응 보기"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "상태 페이지 보기"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary (평생 {premiumProductName})"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Visionary 2단계 인증 알림 표시줄"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "음성 지역"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "음성 세션 복원 알림 표시줄"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Bendruomenės žinučių pranešimai"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Bendruomenės MFA reikalavimo pranešimų juosta"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "Ryšio ID"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Ryšio problemos?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Ryšys nutrūko"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Ryšys nutrūko. Bandoma prisijungti iš naujo…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Panaudojimų:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Šiuo metu atmesta"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Šiuo metu rodoma"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Neaktyvumo kompiuteryje trukmė prieš pranešimus į mobilųjį įrenginį"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Darbalaukio pranešimų juosta"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Parašykite mums el. paštu ir jūsų paskyrą peržiūrės žmogus. Nurodykite, kad negalite patvirtinti SMS žinute, o toliau viskuo pasirūpinsime mes. Atsakymai siunčiami el. paštu, todėl jų teks palaukti."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "El. pašto patvirtinimo pranešimų juosta"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "Siekdami užtikrinti jūsų saugumą, patvirtinkite, kad tai jūs."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Priverstinai išjungta"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Priverstinai įjungta"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Dovanų inventoriaus eiga"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Dovanų inventoriaus pranešimų juosta"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Kvietimai išjungti"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Kvietimų išjungimo pranešimų juosta"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "„Premium“ ženklelis"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Pasibaigusios „Premium“ prenumeratos juosta"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "„Premium“ lengvatinio laikotarpio juosta"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Premium modelis"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "„Premium“ supažindinimo juosta"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Pateikti prieigos rakto failą"
|
||||
msgid "Service error"
|
||||
msgstr "Paslaugos klaida"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Pranešimo apie incidentą juosta"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Tenesis"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Sąlygų priėmimo priminimas"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Neužregistruota paskyra"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Neužregistruotos paskyros priminimo juosta"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Naudojimų skaičius:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Naudojama tikroji paskyros būsena"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Naudojama tikroji dovanų atsargų būsena"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Naudojama tikroji „Premium“ būsena"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Naudojama tikroji būsena"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Naudojama tikroji patvirtinimo būsena"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Peržiūrėti pagalbos straipsnį"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Peržiūrėti incidento informaciją"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Peržiūrėti gaunamą skambutį"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Peržiūrėti techninės priežiūros informaciją"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Peržiūrėti reakcijas"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Peržiūrėti būsenos puslapį"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary ({premiumProductName} visam laikui)"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Visionary 2FA pranešimų juosta"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Balso regionas"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Balso seanso atkūrimo pranešimų juosta"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Meldingen voor communityberichten"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Banner voor MFA-vereiste van community"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "Verbindings-ID"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Verbindingsproblemen?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Verbinding verbroken"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Verbinding verbroken. Opnieuw verbinding maken…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Aantal keer gebruikt:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Momenteel weggeklikt"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Wordt nu weergegeven"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Duur van desktopinactiviteit vóór mobiele pushmeldingen"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Meldingenbalk voor desktopmeldingen"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Stuur ons een e-mail en een medewerker zal je account beoordelen. Geef aan dat je niet via sms kunt verifiëren, dan helpen wij je verder. Je krijgt per e-mail antwoord, dus je krijgt niet meteen een reactie."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Banner e-mailverificatie"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "Bevestig voor je veiligheid dat jij het bent om verder te gaan."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Geforceerd uitgeschakeld"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Geforceerd ingeschakeld"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Cadeau-inventarisflow"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Banner voor cadeau-inventaris"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Uitnodigingen uitgeschakeld"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Banner voor uitgeschakelde uitnodigingen"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Premium-badge"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Meldingsbalk voor verlopen Premium"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Meldingsbalk voor de Premium-respijtperiode"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Premiummodel"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Meldingsbalk voor kennismaking met Premium"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Tokenbestand aanbieden"
|
||||
msgid "Service error"
|
||||
msgstr "Servicefout"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Service-incidentmelding"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Tennessee"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Meldingsbalk voor het accepteren van de voorwaarden"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Account met onvoltooide registratie"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Meldingsbalk voor onvoltooide registratie"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Aantal keren gebruikt:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Gebruikt de werkelijke accountstatus"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Werkelijke status van cadeau-inventaris wordt gebruikt"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Gebruikt de daadwerkelijke premiumstatus"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Gebruikt de werkelijke status"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Werkelijke verificatiestatus gebruiken"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Helpartikel bekijken"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Bekijk incidentdetails"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Inkomende oproep bekijken"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Bekijk onderhoudsdetails"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Reacties bekijken"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Statuspagina bekijken"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary (levenslang {premiumProductName})"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Meldingsbalk voor Visionary-2FA"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Spraakregio"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Meldingsbalk voor herstel van spraaksessie"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Varsler for fellesskapsmeldinger"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Varsellinje for fellesskapets MFA-krav"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "Tilkoblings-ID"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Tilkoblingsproblemer?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Tilkoblingen ble brutt"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Tilkoblingen ble brutt. Kobler til igjen …"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Antall ganger brukt:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Avvist for øyeblikket"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Viser nå"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Inaktivitet på datamaskin før push-varsler på mobilen"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Varsellinje for skrivebordsvarsler"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Send oss en e-post, så vil en person gjennomgå kontoen din. Fortell oss at du ikke kan bekrefte kontoen via SMS, så tar vi det derfra. Vi svarer på e-post, så du får ikke svar med én gang."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Varsellinje for e-postbekreftelse"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "Av sikkerhetshensyn må du bekrefte at det er deg, for å fortsette."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Tvangsdeaktivert"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Tvangsaktivert"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Gavebeholdningsflyt"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Varsellinje for gavebeholdning"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Invitasjoner deaktivert"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Varsellinje for deaktiverte invitasjoner"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Premium-merke"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Varselbanner for utløpt Premium-abonnement"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Varselbanner for Premium-respittperiode"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Premiummodell"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Varselbanner for introduksjon til Premium"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Gjør token-filen tilgjengelig på nettstedet"
|
||||
msgid "Service error"
|
||||
msgstr "Tjenestefeil"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Varsel om tjenestehendelse"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Tennessee"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Varselbanner for godkjenning av vilkår"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Uregistrert konto"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Varsellinje for uregistrert konto"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Antall bruk:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Bruker faktisk kontostatus"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Bruker faktisk gavebeholdningsstatus"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Bruker faktisk premiumstatus"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Bruker faktisk tilstand"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Bruker faktisk bekreftelsesstatus"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Vis hjelpeartikkel"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Vis detaljer om hendelsen"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Se innkommende anrop"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Vis detaljer om vedlikeholdet"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Se reaksjoner"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Vis statussiden"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary ({premiumProductName} på livstid)"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Varsellinje for Visionary-2FA"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Taleregion"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Varsellinje for gjenoppretting av taleøkt"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Powiadomienia o wiadomościach społeczności"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Pasek z wymogiem MFA dla społeczności"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "ID połączenia"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Problemy z połączeniem?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Utracono połączenie"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Połączenie utracone. Ponowne nawiązywanie połączenia…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Dotychczasowa liczba użyć:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Obecnie ukryty"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Aktualnie wyświetlane"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Czas bezczynności na komputerze przed wysłaniem powiadomienia push na telefon"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Pasek zachęcający do włączenia powiadomień na komputerze"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Napisz do nas e-mailem, a ktoś z naszego zespołu sprawdzi Twoje konto. Poinformuj nas, że nie możesz ukończyć weryfikacji przez SMS, a my zajmiemy się resztą. Odpowiemy e-mailem, więc nie nastąpi to od razu."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Pasek weryfikacji e-maila"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "Dla Twojego bezpieczeństwa potwierdź swoją tożsamość, aby kontynuować."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Wymuszone wyłączenie"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Wymuszone włączenie"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Przebieg obsługi ekwipunku prezentów"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Pasek przypominający o ekwipunku prezentów"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Zaproszenia wyłączone"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Pasek informujący o wyłączonych zaproszeniach"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Odznaka Premium"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Pasek informujący o wygaśnięciu Premium"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Pasek informujący o okresie karencji Premium"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Model Premium"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Pasek powitalny Premium"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Udostępnij plik tokenu"
|
||||
msgid "Service error"
|
||||
msgstr "Błąd usługi"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Powiadomienie o awarii usługi"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Tennessee"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Pasek akceptacji warunków"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Konto z niedokończoną rejestracją"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Pasek przypomnienia o dokończeniu rejestracji konta"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Użycia:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Używasz rzeczywistego stanu konta"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Używanie rzeczywistego stanu ekwipunku prezentów"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Używasz rzeczywistego stanu Premium"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Używasz rzeczywistego stanu"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Używasz rzeczywistego stanu weryfikacji"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Wyświetl artykuł pomocy"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Zobacz szczegóły incydentu"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Wyświetl połączenie przychodzące"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Zobacz szczegóły konserwacji"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Wyświetl reakcje"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Zobacz stronę statusu"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary (dożywotni dostęp do {premiumProductName})"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Pasek przypomnienia o 2FA dla Visionary"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Region głosowy"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Pasek przywracania sesji głosowej"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Notificações de mensagens da comunidade"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Barra de aviso de exigência de MFA na comunidade"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "ID da conexão"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Problemas de conexão?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Conexão perdida"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Conexão perdida. Reconectando…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Usos atuais:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Atualmente dispensado"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Exibindo agora"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Inatividade no computador antes das notificações no celular"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Barra de aviso de notificações no computador"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Envie um e-mail para nós e uma pessoa analisará sua conta. Informe que você não consegue verificar por SMS e nós cuidaremos do resto. As respostas chegam por e-mail, então não é instantâneo."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Barra de notificação de verificação de e-mail"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "Para sua segurança, confirme que é você para continuar."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Forçar desativação"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Forçar ativação"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Fluxo de inventário de presentes"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Banner de inventário de presentes"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Convites desativados"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Barra de convites desativados"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Selo premium"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Barra de aviso de acesso premium expirado"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Barra de notificação do período de carência premium"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Modelo premium"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Barra de boas-vindas ao premium"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Disponibilizar o arquivo do token"
|
||||
msgid "Service error"
|
||||
msgstr "Erro no serviço"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Incidente de serviço na barra de avisos"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Tennessee"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Barra de notificação de aceitação dos termos"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Conta não reivindicada"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Barra de notificação de conta não reivindicada"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Usos:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Usando o estado real da conta"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Usando o status real do inventário de presentes"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Usando o status premium real"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Usando o estado real"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Usando o estado de verificação real"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Ver artigo de ajuda"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Ver detalhes do incidente"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Ver chamada recebida"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Ver detalhes da manutenção"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Ver reações"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Ver página de status"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary ({premiumProductName} vitalício)"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Barra de aviso de 2FA do Visionary"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Região de voz"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Barra de notificação de restauração de sessão de voz"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Notificări pentru mesajele comunității"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Bara de notificare pentru cerința MFA a comunității"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "ID conexiune"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Probleme de conexiune?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Conexiune pierdută"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Conexiune pierdută. Se reconectează…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Utilizări curente:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Ascuns în prezent"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Afișat în prezent"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Inactivitate pe desktop înainte de notificarea push pe mobil"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Bara de notificare desktop"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Trimite-ne un e-mail și o persoană îți va verifica contul. Spune-ne că nu poți verifica prin SMS și ne vom ocupa noi de restul. Răspunsurile vin prin e-mail, deci nu este instant."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Bara de notificare pentru verificarea e-mailului"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "Pentru siguranța ta, te rugăm să confirmi că ești tu pentru a continua."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Dezactivat forțat"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Activat forțat"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Flux inventar cadouri"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Bara de notificări pentru inventarul de cadouri"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Invitații dezactivate"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Bara de notificare „Invitații dezactivate”"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Insignă Premium"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Bara de notificare pentru abonamentul Premium expirat"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Bara de notificare pentru perioada de grație Premium"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Model premium"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Bara de notificare pentru integrarea Premium"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Publică fișierul token"
|
||||
msgid "Service error"
|
||||
msgstr "Eroare de serviciu"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Notificare incident serviciu"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Tennessee"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Bara de notificare pentru acceptarea termenilor"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Cont nerevendicat"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Bara de notificare pentru cont nerevendicat"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Utilizări:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Se folosește starea reală a contului"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Se folosește starea reală a inventarului de cadouri"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Se folosește starea reală de premium"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Se folosește starea reală"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Se folosește starea reală de verificare"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Vezi articolul de ajutor"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Vezi detaliile incidentului"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Vezi apelul primit"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Vezi detaliile mentenanței"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Vezi reacțiile"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Vezi pagina de stare"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary ({premiumProductName} pe viață)"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Bara de notificare 2FA Visionary"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Regiune vocală"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Bara de notificare pentru restaurarea sesiunii vocale"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Уведомления о сообщениях в сообществах"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Баннер с требованием MFA для сообщества"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "ID подключения"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Проблемы с подключением?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Соединение потеряно"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Соединение потеряно. Переподключение…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Использований:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Сейчас скрыт"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Сейчас показывается"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Время бездействия на компьютере перед отправкой уведомления на мобильное устройство"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Баннер с предложением включить уведомления"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Напишите нам по электронной почте, и ваш аккаунт проверит сотрудник. Сообщите, что вы не можете пройти проверку по SMS, и мы разберемся. Ответы приходят по электронной почте, поэтому мгновенного ответа не будет."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Панель уведомлений о подтверждении почты"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "Для вашей безопасности подтвердите, что это вы, чтобы продолжить."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Принудительно отключено"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Принудительно включено"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Сценарий инвентаря подарков"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Баннер инвентаря подарков"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Приглашения отключены"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Баннер «Приглашения отключены»"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Значок премиум"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Баннер об истечении срока действия подписки Premium"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Баннер льготного периода Premium"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Премиум-модель"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Баннер онбординга премиум-подписки"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Разместите файл токена"
|
||||
msgid "Service error"
|
||||
msgstr "Ошибка сервиса"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Уведомление об инциденте службы"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Теннесси"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Баннер принятия условий"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Неподтвержденный аккаунт"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Панель уведомлений о неподтвержденном аккаунте"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Число использований:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Используется фактическое состояние аккаунта"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Используется фактическое состояние инвентаря подарков"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Используется фактическое состояние премиум-подписки"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Используется фактическое состояние"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Используется фактическое состояние проверки"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Посмотреть статью справки"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Посмотреть детали инцидента"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Посмотреть входящий звонок"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Подробнее о технических работах"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Посмотреть реакции"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Открыть страницу статуса"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary (пожизненная подписка {premiumProductName})"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Баннер 2FA для Visionary"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Регион для голосовых чатов"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Баннер восстановления голосовой сессии"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Aviseringar för communitymeddelanden"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Meddelandefält för MFA-krav i communityn"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "Anslutnings-ID"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Anslutningsproblem?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Anslutningen bröts"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Anslutningen bröts. Återansluter…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Antal användningar hittills:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Avfärdad för närvarande"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Visas nu"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Inaktivitet på datorn innan pushaviseringar skickas till mobilen"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Meddelandefält för datoraviseringar"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Skicka ett mejl till oss så granskar en person ditt konto. Berätta att du inte kan verifiera via SMS så tar vi hand om resten. Svaret kommer via e-post, så det kan ta en stund."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Meddelandefält för e-postverifiering"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "För din säkerhet, bekräfta att det är du för att fortsätta."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Tvinga inaktivering"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Tvinga aktivering"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Flöde för presentlagret"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Meddelandefält för presentlagret"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Inbjudningar inaktiverade"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Meddelandefält för inaktiverade inbjudningar"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Premiummärke"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Meddelandefält för utgånget Premium"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Meddelandefält för respitperiod för Premium"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Premiummodell"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Meddelandefält för introduktion till Premium"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Tillhandahåll tokenfilen"
|
||||
msgid "Service error"
|
||||
msgstr "Tjänstefel"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Serviceincident i nagbar"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Tennessee"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Meddelandefält för godkännande av villkor"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Osäkrat konto"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Meddelandefält för osäkrat konto"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Användningar:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Använder faktisk kontostatus"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Använder presentlagrets faktiska status"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Använder faktisk premiumstatus"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Använder faktiskt tillstånd"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Använder faktisk verifieringsstatus"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Visa hjälpartikel"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Visa incidentdetaljer"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Visa inkommande samtal"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Visa underhållsdetaljer"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Visa reaktioner"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Visa statussida"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary ({premiumProductName} på livstid)"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Visionary 2FA-meddelandefält"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Röstregion"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Meddelandefält för återställning av röstsession"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "การแจ้งเตือนข้อความของคอมมูนิตี้"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "แถบแจ้งเตือนข้อกำหนด MFA ของคอมมูนิตี้"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "ID การเชื่อมต่อ"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "มีปัญหาการเชื่อมต่อใช่ไหม"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "การเชื่อมต่อขาดหาย"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "การเชื่อมต่อขาดหาย กำลังเชื่อมต่อใหม่…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "จำนวนครั้งที่ใช้ไปแล้ว:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "ปิดอยู่"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "กำลังแสดง"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "ระยะเวลาไม่ได้ใช้งานบนเดสก์ท็อปก่อนส่งการแจ้งเตือนไปยังมือถือ"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "แถบแจ้งเตือนสำหรับการแจ้งเตือนเดสก์ท็อป"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "ส่งอีเมลถึงเรา แล้วจะมีเจ้าหน้าที่มาตรวจสอบบัญชีของคุณ แจ้งเราว่าคุณไม่สามารถยืนยันด้วย SMS ได้ แล้วเราจะดำเนินการต่อ การตอบกลับจะส่งทางอีเมล ดังนั้นจึงไม่ใช่การดำเนินการทันที"
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "แถบแจ้งเตือนการยืนยันอีเมล"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "เพื่อความปลอดภัย โปรดยืนยันตัวตนเพื่อดำเนินการต่อ"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "บังคับปิดใช้งาน"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "บังคับเปิดใช้งาน"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "ขั้นตอนคลังของขวัญ"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "แถบแจ้งเตือนคลังของขวัญ"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "ปิดใช้งานคำเชิญแล้ว"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "แถบแจ้งเตือนปิดใช้งานคำเชิญ"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "ป้ายพรีเมียม"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "แถบแจ้งเตือนพรีเมียมหมดอายุ"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "แถบแจ้งเตือนช่วงผ่อนผันสำหรับสมาชิกพรีเมียม"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "โมเดลพรีเมียม"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "แถบแจ้งเตือนการเริ่มต้นใช้งานพรีเมียม"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "วางไฟล์โทเค็นบนเว็บไซต์"
|
||||
msgid "Service error"
|
||||
msgstr "ข้อผิดพลาดของบริการ"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "แถบแจ้งเตือนเหตุขัดข้องของบริการ"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "เทนเนสซี"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "แถบแจ้งเตือนการยอมรับข้อกำหนด"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "บัญชีที่ยังไม่ได้ยืนยัน"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "แถบแจ้งเตือนบัญชีที่ยังไม่ได้ยืนยัน"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "จำนวนครั้งที่ใช้:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "กำลังใช้สถานะบัญชีจริง"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "กำลังใช้สถานะคลังของขวัญจริง"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "กำลังใช้สถานะพรีเมียมจริง"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "กำลังใช้สถานะจริง"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "กำลังใช้สถานะการยืนยันจริง"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "ดูบทความช่วยเหลือ"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "ดูรายละเอียดเหตุขัดข้อง"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "ดูสายเรียกเข้า"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "ดูรายละเอียดการบำรุงรักษา"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "ดูรีแอ็กชัน"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "ดูหน้าสถานะ"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary (ตลอดชีพ {premiumProductName})"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "แถบแจ้งเตือน 2FA สำหรับ Visionary"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "ภูมิภาคเสียง"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "แถบแจ้งเตือนการกู้คืนเซสชันเสียง"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Topluluk mesajı bildirimleri"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Topluluk MFA gereksinimi bildirim çubuğu"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "Bağlantı kimliği"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Bağlantı sorunları mı yaşıyorsunuz?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Bağlantı kesildi"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Bağlantı kesildi. Yeniden bağlanılıyor…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Mevcut kullanım sayısı:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Şu anda kapatıldı"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Şu anda gösteriliyor"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Mobil bildirimden önce masaüstü cihazında boşta kalma süresi"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Masaüstü bildirimleri bildirim çubuğu"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "E-posta gönderin, bir kişi hesabınızı inceleyecektir. SMS ile doğrulama yapamadığınızı bize bildirin, gerisini biz hallederiz. Yanıtlar e-posta ile gelir, bu nedenle anında değildir."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "E-posta doğrulama bildirim çubuğu"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "Güvenliğiniz için devam etmeden önce lütfen siz olduğunuzu onaylayın."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Zorla devre dışı"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Zorla etkin"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Hediye envanteri akışı"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Hediye envanteri bildirim çubuğu"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Davetler devre dışı bırakıldı"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Davetler devre dışı bırakıldı bildirim çubuğu"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Premium rozeti"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Premium süresi doldu bildirim çubuğu"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Premium ödemesiz dönem bildirim çubuğu"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Premium model"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Premium başlangıç bildirim çubuğu"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Belirteç dosyasını sun"
|
||||
msgid "Service error"
|
||||
msgstr "Hizmet hatası"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Hizmet kesintisi bildirimi"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Tennessee"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Şartlar kabulü bildirim çubuğu"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Sahiplenilmemiş hesap"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Sahiplenilmemiş hesap uyarı çubuğu"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Kullanım:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Gerçek hesap durumu kullanılıyor"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Gerçek hediye envanteri durumu kullanılıyor"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Gerçek premium durumu kullanılıyor"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Gerçek durum kullanılıyor"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Gerçek doğrulama durumu kullanılıyor"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Yardım makalesini görüntüle"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Olay ayrıntılarını görüntüle"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Gelen aramayı görüntüle"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Bakım ayrıntılarını görüntüle"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Tepkileri görüntüle"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Durum sayfasını görüntüle"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary (ömür boyu {premiumProductName})"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Visionary 2FA bildirim çubuğu"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Ses bölgesi"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Ses oturumu geri yükleme bildirim çubuğu"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Сповіщення про повідомлення спільноти"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Панель нагадування про вимогу MFA для спільноти"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "Ідентифікатор підключення"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Проблеми зі з'єднанням?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "З'єднання втрачено"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "З'єднання втрачено. Повторне підключення…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Поточна кількість використань:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Наразі відхилено"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Зараз показується"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Неактивність на комп'ютері перед надсиланням push-сповіщень на мобільний пристрій"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Нагадування про сповіщення на комп'ютері"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Напишіть нам на електронну пошту, і людина перегляне ваш обліковий запис. Повідомте нам, що ви не можете пройти перевірку за допомогою SMS, і ми розберемося. Відповіді надходять електронною поштою, тому це не миттєво."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Панель сповіщень про підтвердження електронної пошти"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "Для вашої безпеки підтвердьте, що це ви, щоб продовжити."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Примусово вимкнено"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Примусово ввімкнено"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Потік інвентарю подарунків"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Панель сповіщень про подарунки"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Запрошення вимкнено"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Нагадування про вимкнені запрошення"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Значок преміуму"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Панель сповіщень про закінчення преміум-підписки"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Панель сповіщень про пільговий період преміуму"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Преміум-модель"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Панель сповіщень про початок роботи з преміумом"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Розмістіть файл токена"
|
||||
msgid "Service error"
|
||||
msgstr "Помилка служби"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Сповіщення про інцидент служби"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Теннессі"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Банер про прийняття умов"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Незареєстрований обліковий запис"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Панель сповіщень про незареєстрований обліковий запис"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Кількість використань:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Використовується фактичний стан облікового запису"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Використовується фактичний стан інвентарю подарунків"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Використовується фактичний стан преміуму"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Використовується фактичний стан"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Використовується фактичний стан перевірки"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Переглянути довідкову статтю"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Переглянути деталі інциденту"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Переглянути вхідний дзвінок"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Переглянути деталі технічного обслуговування"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Переглянути реакції"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Переглянути сторінку стану"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary (довічний {premiumProductName})"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Банер Visionary 2FA"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Голосовий регіон"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Панель відновлення голосового сеансу"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "Thông báo tin nhắn cộng đồng"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "Thanh thông báo yêu cầu MFA của cộng đồng"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "ID kết nối"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "Gặp vấn đề kết nối?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "Mất kết nối"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "Mất kết nối. Đang kết nối lại…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "Lượt dùng hiện tại:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "Đang ẩn"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "Đang hiển thị"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "Thời gian không hoạt động trên máy tính trước khi gửi thông báo đẩy đến di động"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "Thanh thông báo trên máy tính"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "Gửi email cho chúng tôi và một người sẽ xem xét tài khoản của bạn. Hãy cho chúng tôi biết bạn không thể xác minh qua SMS và chúng tôi sẽ xử lý tiếp. Phản hồi sẽ được gửi qua email, vì vậy việc này không diễn ra ngay lập tức."
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "Thanh thông báo xác minh email"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "Để bảo mật, vui lòng xác nhận danh tính của bạn để tiếp tục."
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "Buộc tắt"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "Buộc bật"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "Luồng kho quà tặng"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "Thanh thông báo kho quà tặng"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "Đã tắt lời mời"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "Thanh thông báo lời mời bị tắt"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "Huy hiệu Premium"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "Thanh thông báo hết hạn gói Premium"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "Thanh thông báo thời gian ân hạn Premium"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "Mô hình cao cấp"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "Thanh thông báo giới thiệu gói Premium"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "Cung cấp tệp token"
|
||||
msgid "Service error"
|
||||
msgstr "Lỗi dịch vụ"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "Thông báo sự cố dịch vụ"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "Tennessee"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "Thanh thông báo chấp nhận điều khoản"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "Tài khoản chưa xác nhận"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "Thanh thông báo tài khoản chưa xác nhận"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "Lượt dùng:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "Đang sử dụng trạng thái tài khoản thực tế"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "Đang sử dụng trạng thái kho quà tặng thực tế"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "Đang sử dụng trạng thái premium thực tế"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "Đang sử dụng trạng thái thực tế"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "Đang sử dụng trạng thái xác minh thực tế"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "Xem bài viết trợ giúp"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "Xem chi tiết sự cố"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "Xem cuộc gọi đến"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "Xem chi tiết bảo trì"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "Xem cảm xúc"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "Xem trang trạng thái"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary (trọn đời {premiumProductName})"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Thanh thông báo 2FA Visionary"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "Vùng thoại"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "Thanh thông báo khôi phục phiên thoại"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "社区消息通知"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "社区 MFA 要求提示栏"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "连接 ID"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "连接有问题?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "连接已断开"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "连接已断开,正在重新连接…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "当前使用次数:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "当前已忽略"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "当前显示"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "发送移动推送前的桌面闲置时长"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "桌面通知提示栏"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "给我们发邮件,会有专人审核你的账号。告诉我们你无法通过短信验证,后续由我们处理。回复通过邮件发送,因此不会即时收到。"
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "邮箱验证提醒栏"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "为保障你的账号安全,请先验证身份。"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "强制禁用"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "强制启用"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "礼物库存流程"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "礼物库存提示栏"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "邀请已禁用"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "邀请已禁用提示栏"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "会员徽章"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "会员过期提示栏"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "会员宽限期提示栏"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "会员模式"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "会员引导提示栏"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "提供令牌文件"
|
||||
msgid "Service error"
|
||||
msgstr "服务错误"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "服务事件通知栏"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "田纳西州"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "条款接受提示栏"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "未认领账号"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "未认领账号提示栏"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "使用次数:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "正在使用实际账号状态"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "正在使用实际礼物库存状态"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "正在使用实际会员状态"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "正在使用实际状态"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "正在使用实际验证状态"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "查看帮助文章"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "查看事件详情"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "查看来电"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "查看维护详情"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "查看反应"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "查看状态页"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "Visionary(终身 {premiumProductName})"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Visionary 双重认证提示栏"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "语音地区"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "语音会话恢复提示栏"
|
||||
|
||||
|
||||
@@ -8561,7 +8561,7 @@ msgid "Community message notifications"
|
||||
msgstr "社群訊息通知"
|
||||
|
||||
#. Developer control label for the community MFA requirement banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:71
|
||||
#: src/features/devtools/components/NagbarControls.ts:76
|
||||
msgid "Community MFA requirement nagbar"
|
||||
msgstr "社群 MFA 要求提示列"
|
||||
|
||||
@@ -8995,7 +8995,7 @@ msgid "Connection ID"
|
||||
msgstr "連線 ID"
|
||||
|
||||
#. Banner shown while the app is still loading and the connection is taking unusually long. Phrased as a question offering help.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:33
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:34
|
||||
msgid "Connection issues?"
|
||||
msgstr "連線有問題?"
|
||||
|
||||
@@ -9009,7 +9009,7 @@ msgid "Connection lost"
|
||||
msgstr "連線中斷"
|
||||
|
||||
#. Banner shown when the app loses its gateway connection after having loaded. The app stays usable, so keep the tone calm.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:38
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:39
|
||||
msgid "Connection lost. Reconnecting…"
|
||||
msgstr "連線已中斷。正在重新連線…"
|
||||
|
||||
@@ -10569,12 +10569,12 @@ msgid "Current uses:"
|
||||
msgstr "目前使用次數:"
|
||||
|
||||
#. Developer nagbar status: the banner is dismissed in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:104
|
||||
#: src/features/devtools/components/NagbarControls.ts:109
|
||||
msgid "Currently dismissed"
|
||||
msgstr "目前已關閉"
|
||||
|
||||
#. Developer nagbar status: the banner is showing in the current real state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:108
|
||||
#: src/features/devtools/components/NagbarControls.ts:113
|
||||
msgid "Currently showing"
|
||||
msgstr "目前顯示"
|
||||
|
||||
@@ -11735,7 +11735,7 @@ msgid "Desktop inactivity before mobile push"
|
||||
msgstr "電腦閒置多久後才傳送行動裝置推播通知"
|
||||
|
||||
#. Developer control label for the desktop-notification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:35
|
||||
#: src/features/devtools/components/NagbarControls.ts:40
|
||||
msgid "Desktop notification nagbar"
|
||||
msgstr "桌面通知提示列"
|
||||
|
||||
@@ -13252,7 +13252,7 @@ msgid "Email us and a person will review your account. Tell us you cannot verify
|
||||
msgstr "請透過電子郵件與我們聯繫,將由專人審核您的帳號。請告知我們您無法透過 SMS 驗證,後續事宜將由我們處理。我們將透過電子郵件回覆,因此處理過程並非即時。"
|
||||
|
||||
#. Developer control label for the email-verification banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:31
|
||||
#: src/features/devtools/components/NagbarControls.ts:36
|
||||
msgid "Email verification nagbar"
|
||||
msgstr "電子郵件驗證提示列"
|
||||
|
||||
@@ -15421,12 +15421,12 @@ msgid "For your security, please confirm it's you to continue."
|
||||
msgstr "為了您的帳戶安全,請驗證是您本人操作,才能繼續。"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to hide.
|
||||
#: src/features/devtools/components/NagbarControls.ts:92
|
||||
#: src/features/devtools/components/NagbarControls.ts:97
|
||||
msgid "Force disabled"
|
||||
msgstr "強制停用"
|
||||
|
||||
#. Developer nagbar override status: the banner is forced to show.
|
||||
#: src/features/devtools/components/NagbarControls.ts:88
|
||||
#: src/features/devtools/components/NagbarControls.ts:93
|
||||
msgid "Force enabled"
|
||||
msgstr "強制啟用"
|
||||
|
||||
@@ -16165,7 +16165,7 @@ msgid "Gift inventory flow"
|
||||
msgstr "禮物庫存流程"
|
||||
|
||||
#. Developer control label for the gift-inventory banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:51
|
||||
#: src/features/devtools/components/NagbarControls.ts:56
|
||||
msgid "Gift inventory nagbar"
|
||||
msgstr "禮物庫存提醒列"
|
||||
|
||||
@@ -18454,7 +18454,7 @@ msgid "Invites disabled"
|
||||
msgstr "邀請已停用"
|
||||
|
||||
#. Developer control label for the invites-disabled banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:67
|
||||
#: src/features/devtools/components/NagbarControls.ts:72
|
||||
msgid "Invites disabled nagbar"
|
||||
msgstr "邀請已停用提示列"
|
||||
|
||||
@@ -25161,12 +25161,12 @@ msgid "Premium badge"
|
||||
msgstr "進階方案徽章"
|
||||
|
||||
#. Developer control label for the premium-expired banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:43
|
||||
#: src/features/devtools/components/NagbarControls.ts:48
|
||||
msgid "Premium expired nagbar"
|
||||
msgstr "進階方案過期提示列"
|
||||
|
||||
#. Developer control label for the premium grace-period banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:39
|
||||
#: src/features/devtools/components/NagbarControls.ts:44
|
||||
msgid "Premium grace period nagbar"
|
||||
msgstr "進階方案寬限期提示列"
|
||||
|
||||
@@ -25176,7 +25176,7 @@ msgid "Premium model"
|
||||
msgstr "進階方案模式"
|
||||
|
||||
#. Developer control label for the premium-onboarding banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:47
|
||||
#: src/features/devtools/components/NagbarControls.ts:52
|
||||
msgid "Premium onboarding nagbar"
|
||||
msgstr "進階方案上手提示列"
|
||||
|
||||
@@ -29497,6 +29497,11 @@ msgstr "提供權杖檔案"
|
||||
msgid "Service error"
|
||||
msgstr "服務錯誤"
|
||||
|
||||
#. Developer / debug surface — keep terse and technical. Label in the developer Nagbar controls panel for the service incident banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
msgid "Service incident nagbar"
|
||||
msgstr "服務事件通知列"
|
||||
|
||||
#. Section heading in the voice connection status popout for current call/session debug stats.
|
||||
#: src/features/voice/components/voice_connection_status/shared.ts:62
|
||||
msgid "Session"
|
||||
@@ -32775,7 +32780,7 @@ msgid "Tennessee"
|
||||
msgstr "田納西州"
|
||||
|
||||
#. Developer control label for the terms-acceptance banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:59
|
||||
#: src/features/devtools/components/NagbarControls.ts:64
|
||||
msgid "Terms acceptance nagbar"
|
||||
msgstr "條款接受提示列"
|
||||
|
||||
@@ -35293,7 +35298,7 @@ msgid "Unclaimed account"
|
||||
msgstr "未認領的帳號"
|
||||
|
||||
#. Developer control label for the unclaimed-account banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:27
|
||||
#: src/features/devtools/components/NagbarControls.ts:32
|
||||
msgid "Unclaimed account nagbar"
|
||||
msgstr "未認領帳號提示列"
|
||||
|
||||
@@ -36815,27 +36820,27 @@ msgid "Uses:"
|
||||
msgstr "使用次數:"
|
||||
|
||||
#. Developer nagbar status: banner follows real account state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:96
|
||||
#: src/features/devtools/components/NagbarControls.ts:101
|
||||
msgid "Using actual account state"
|
||||
msgstr "使用實際帳號狀態"
|
||||
|
||||
#. Developer nagbar status: banner follows real gift inventory state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:116
|
||||
#: src/features/devtools/components/NagbarControls.ts:121
|
||||
msgid "Using actual gift inventory state"
|
||||
msgstr "使用實際禮物庫存狀態"
|
||||
|
||||
#. Developer nagbar status: banner follows real premium subscription state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:112
|
||||
#: src/features/devtools/components/NagbarControls.ts:117
|
||||
msgid "Using actual premium state"
|
||||
msgstr "使用實際的進階方案狀態"
|
||||
|
||||
#. Developer nagbar status: banner follows the real app state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:120
|
||||
#: src/features/devtools/components/NagbarControls.ts:125
|
||||
msgid "Using actual state"
|
||||
msgstr "使用實際狀態"
|
||||
|
||||
#. Developer nagbar status: banner follows real email verification state.
|
||||
#: src/features/devtools/components/NagbarControls.ts:100
|
||||
#: src/features/devtools/components/NagbarControls.ts:105
|
||||
msgid "Using actual verification state"
|
||||
msgstr "使用實際驗證狀態"
|
||||
|
||||
@@ -37422,7 +37427,7 @@ msgid "View help article"
|
||||
msgstr "查看說明文章"
|
||||
|
||||
#. Button on the connection banner when an incident is ongoing. Opens that incident on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:47
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:48
|
||||
msgid "View incident details"
|
||||
msgstr "查看事件詳情"
|
||||
|
||||
@@ -37432,7 +37437,7 @@ msgid "View incoming call"
|
||||
msgstr "查看來電"
|
||||
|
||||
#. Button on the connection banner when maintenance is ongoing. Opens that maintenance on the status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:51
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:52
|
||||
msgid "View maintenance details"
|
||||
msgstr "查看維護詳情"
|
||||
|
||||
@@ -37474,7 +37479,7 @@ msgid "View reactions"
|
||||
msgstr "查看反應"
|
||||
|
||||
#. Button on the connection banner. Opens the external service status page.
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:43
|
||||
#: src/features/app/hooks/useConnectionNotice.ts:44
|
||||
msgid "View status page"
|
||||
msgstr "查看狀態頁面"
|
||||
|
||||
@@ -37554,7 +37559,7 @@ msgid "Visionary (lifetime {premiumProductName})"
|
||||
msgstr "遠見者(終身享有 {premiumProductName})"
|
||||
|
||||
#. Developer control label for the Visionary two-factor-authentication banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:55
|
||||
#: src/features/devtools/components/NagbarControls.ts:60
|
||||
msgid "Visionary 2FA nagbar"
|
||||
msgstr "Visionary 雙重驗證提醒"
|
||||
|
||||
@@ -37812,7 +37817,7 @@ msgid "Voice region"
|
||||
msgstr "語音區域"
|
||||
|
||||
#. Developer control label for the voice-session-restore banner.
|
||||
#: src/features/devtools/components/NagbarControls.ts:63
|
||||
#: src/features/devtools/components/NagbarControls.ts:68
|
||||
msgid "Voice session restore nagbar"
|
||||
msgstr "語音連線還原提示列"
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ html,
|
||||
font-family: var(--font-sans);
|
||||
font-feature-settings: normal;
|
||||
font-variation-settings: normal;
|
||||
font-variant-emoji: unicode;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface NagbarSettings {
|
||||
guildMembershipCtaDismissed: boolean;
|
||||
visionaryMfaDismissed: boolean;
|
||||
claimAccountModalShownThisSession: boolean;
|
||||
forceOffline: boolean;
|
||||
forceConnectionNotice: boolean;
|
||||
forceEmailVerification: boolean;
|
||||
forceIOSInstall: boolean;
|
||||
forcePWAInstall: boolean;
|
||||
@@ -42,7 +42,7 @@ export interface NagbarSettings {
|
||||
forceScheduledMaintenance: boolean;
|
||||
forceVoiceSessionRestore: boolean;
|
||||
forceGuildMfaRequirement: boolean;
|
||||
forceHideOffline: boolean;
|
||||
forceHideConnectionNotice: boolean;
|
||||
forceHideEmailVerification: boolean;
|
||||
forceHideIOSInstall: boolean;
|
||||
forceHidePWAInstall: boolean;
|
||||
@@ -215,8 +215,8 @@ export class Nagbar implements NagbarSettings {
|
||||
return this.pushNotificationDismissed;
|
||||
}
|
||||
|
||||
getForceOffline(): boolean {
|
||||
return this.forceOffline;
|
||||
getForceConnectionNotice(): boolean {
|
||||
return this.forceConnectionNotice;
|
||||
}
|
||||
|
||||
getForceEmailVerification(): boolean {
|
||||
@@ -259,8 +259,8 @@ export class Nagbar implements NagbarSettings {
|
||||
return this.forceInvitesDisabled;
|
||||
}
|
||||
|
||||
getForceHideOffline(): boolean {
|
||||
return this.forceHideOffline;
|
||||
getForceHideConnectionNotice(): boolean {
|
||||
return this.forceHideConnectionNotice;
|
||||
}
|
||||
|
||||
getForceHideEmailVerification(): boolean {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import {ExternalUrls} from '@fluxer/constants/src/ExternalUrls';
|
||||
import {makeAutoObservable, runInAction} from 'mobx';
|
||||
|
||||
type IncidentStatus = 'investigating' | 'identified' | 'monitoring' | 'resolved';
|
||||
@@ -91,6 +90,7 @@ const POLL_AFTER_MAINTENANCE_START_MS = 5 * 1000;
|
||||
const POLL_MIN_DELAY_MS = 10 * 1000;
|
||||
const POLL_RESUME_STALE_MS = 30 * 1000;
|
||||
const STATUS_PAGE_FETCH_TIMEOUT_MS = 10 * 1000;
|
||||
const STATUS_PAGE_URL = RuntimeConfig.statusPageUrl;
|
||||
|
||||
function statusPageFetchOptions(): RequestInit {
|
||||
return {cache: 'no-store', signal: AbortSignal.timeout(STATUS_PAGE_FETCH_TIMEOUT_MS)};
|
||||
@@ -196,7 +196,6 @@ export class StatusPage {
|
||||
private checkInFlight: Promise<void> | null = null;
|
||||
private lastCheckedAt = 0;
|
||||
private pollingStarted = false;
|
||||
private readonly isSelfHosted = RuntimeConfig.isSelfHosted();
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable<StatusPage, 'checkInFlight' | 'lastCheckedAt' | 'pollingStarted' | 'pollTimerId'>(
|
||||
@@ -212,7 +211,7 @@ export class StatusPage {
|
||||
}
|
||||
|
||||
startPolling(): void {
|
||||
if (this.isSelfHosted || this.pollingStarted) {
|
||||
if (!STATUS_PAGE_URL || this.pollingStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -243,7 +242,7 @@ export class StatusPage {
|
||||
}
|
||||
|
||||
async checkIncidents(): Promise<void> {
|
||||
if (this.isSelfHosted) {
|
||||
if (!STATUS_PAGE_URL) {
|
||||
return;
|
||||
}
|
||||
if (this.checkInFlight) {
|
||||
@@ -260,7 +259,7 @@ export class StatusPage {
|
||||
|
||||
private async fetchIncidents(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(`${ExternalUrls.SERVICE_STATUS}/summary.json`, statusPageFetchOptions());
|
||||
const response = await fetch(`${STATUS_PAGE_URL}/summary.json`, statusPageFetchOptions());
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
@@ -301,7 +300,7 @@ export class StatusPage {
|
||||
|
||||
private async fetchComponentMaintenances(): Promise<Array<InstatusMaintenance>> {
|
||||
try {
|
||||
const response = await fetch(`${ExternalUrls.SERVICE_STATUS}/components.json`, statusPageFetchOptions());
|
||||
const response = await fetch(`${STATUS_PAGE_URL}/components.json`, statusPageFetchOptions());
|
||||
if (!response.ok) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -299,7 +299,7 @@ const ACCEPTED_TABLE_FINDINGS = new Map<string, Readonly<Partial<Record<TableRul
|
||||
['http-api/guild-moderation.mdx', {'table-cell': 1}],
|
||||
['http-api/guild-stickers.mdx', {'table-cell': 3}],
|
||||
['http-api/guilds.mdx', {'table-fit': 1, 'table-identifier': 4}],
|
||||
['http-api/instance.mdx', {'table-identifier': 4}],
|
||||
['http-api/instance.mdx', {'table-identifier': 5}],
|
||||
['http-api/invites.mdx', {'table-cell': 6}],
|
||||
['http-api/messages.mdx', {'table-fit': 1, 'table-cell': 20}],
|
||||
['http-api/permissions.mdx', {'table-cell': 8}],
|
||||
|
||||
@@ -374,7 +374,7 @@ Branding, setup state, legal documents, and registration fields a client reads b
|
||||
|
||||
## Public branding object
|
||||
|
||||
The instance identity a client renders: its name, its images, and its theme colour. The image URLs customise browser metadata, install metadata, and link previews.
|
||||
The instance identity a client renders: its name, its images, its theme colour, and its status page links. The image URLs customise browser metadata, install metadata, and link previews.
|
||||
|
||||
### Structure
|
||||
|
||||
@@ -387,6 +387,8 @@ The instance identity a client renders: its name, its images, and its theme colo
|
||||
| wordmark_url | ?string | Application wordmark URL, or null |
|
||||
| favicon_url | ?string | Browser favicon URL, or null |
|
||||
| theme_color | ?string | Browser theme colour, or null |
|
||||
| status_page_url | ?string | Public status page URL, or null |
|
||||
| status_page_incident_history_url | ?string | Status page incident history URL, or null |
|
||||
|
||||
## Public setup state object
|
||||
|
||||
|
||||
@@ -694,8 +694,6 @@ To rotate the key pair, set both names in `.env`, then run `docker compose up -d
|
||||
|
||||
The `Caddyfile` is a bind mount, so the edge reads the copy that sits on disk beside `docker-compose.yml`. Editing it takes `docker compose restart edge`, because `docker compose up -d` leaves a container alone when only a mounted file changed. An upgrade does that restart itself, which [What the script does](/operator/upgrading/#what-the-script-does) covers. A change to any LiveKit value in `.env` needs `docker compose up -d`, because `restart` reuses the existing container with its old environment.
|
||||
|
||||
Voice reconciliation has moved from `worker` to the separate recon service. The old `FLUXER_API_WORKER_ENABLE_VOICE_RECONCILIATION` and `FLUXER_API_WORKER_VOICE_RECONCILIATION_*` settings no longer control it. The bundled Compose stack does not start the recon service.
|
||||
|
||||
## Email
|
||||
|
||||
Email is off by default, and the conditions below turn it on. The email switch must be on. It is the value saved in the admin dashboard, or `FLUXER_EMAIL_ENABLED` when the dashboard has no saved value. Also, the provider must be `smtp` with a complete SMTP configuration, meaning `FLUXER_EMAIL_FROM_EMAIL`, `FLUXER_EMAIL_SMTP_HOST`, `FLUXER_EMAIL_SMTP_PORT`, `FLUXER_EMAIL_SMTP_USERNAME`, and `FLUXER_EMAIL_SMTP_PASSWORD` are all non-empty. All are optional.
|
||||
@@ -1243,6 +1241,14 @@ Default empty. The favicon. Its origin is added to the CSP by `app-proxy`.
|
||||
|
||||
Default empty. The theme colour.
|
||||
|
||||
#### `FLUXER_APP_STATUS_PAGE_URL`
|
||||
|
||||
Default empty. The public status page. The web app links to it when loading stalls, and polls its Instatus `summary.json` and `components.json` for incidents and maintenance. Empty removes the link and stops the polling. `app-proxy` does not add this origin to the CSP, so a status page off `fluxerstatus.com` also needs it in `FLUXER_CSP_EXTRA_CONNECT_SRC`.
|
||||
|
||||
#### `FLUXER_APP_STATUS_PAGE_INCIDENT_HISTORY_URL`
|
||||
|
||||
Default empty. The incident history page of the status page.
|
||||
|
||||
#### `FLUXER_INSTANCE_SETUP_CONFIGURED`
|
||||
|
||||
Default `false`. Whether setup is marked complete. The stored `app_public_config` row wins whenever its `setup.configured` is a boolean. This variable supplies the default only while no such row exists, and only on a self-hosted instance. Off a self-hosted instance the state is always true whatever this says.
|
||||
|
||||
@@ -75,7 +75,6 @@ route(M, P) ->
|
||||
<<"guild.disconnect_voice_user_if_in_channel">> => fun gateway_rpc_guild_voice:handle/2,
|
||||
<<"guild.disconnect_all_voice_users_in_channel">> => fun gateway_rpc_guild_voice:handle/2,
|
||||
<<"guild.confirm_voice_connection_from_livekit">> => fun gateway_rpc_guild_voice:handle/2,
|
||||
<<"guild.repair_voice_state_from_cache">> => fun gateway_rpc_guild_voice:handle/2,
|
||||
<<"guild.get_voice_states_for_channel">> => fun gateway_rpc_guild_voice:handle/2,
|
||||
<<"guild.get_pending_joins_for_channel">> => fun gateway_rpc_guild_voice:handle/2,
|
||||
<<"guild.move_member">> => fun gateway_rpc_guild_voice:handle/2,
|
||||
|
||||
@@ -24,8 +24,6 @@ handle(<<"guild.disconnect_all_voice_users_in_channel">>, P) ->
|
||||
handle_disconnect_all_in_channel(P);
|
||||
handle(<<"guild.confirm_voice_connection_from_livekit">>, P) ->
|
||||
handle_confirm_connection(P);
|
||||
handle(<<"guild.repair_voice_state_from_cache">>, P) ->
|
||||
handle_repair_state(P);
|
||||
handle(<<"guild.get_voice_states_for_channel">>, P) ->
|
||||
handle_get_voice_states(P);
|
||||
handle(<<"guild.get_pending_joins_for_channel">>, P) ->
|
||||
@@ -167,27 +165,6 @@ format_confirm_result({error, _, EA}) ->
|
||||
format_confirm_result(#{error := E}) ->
|
||||
raise_voice_error(E).
|
||||
|
||||
-spec handle_repair_state(map()) -> term().
|
||||
handle_repair_state(P) ->
|
||||
GuildId = validation:snowflake_or_throw(<<"guild_id">>, maps:get(<<"guild_id">>, P)),
|
||||
ChannelId = validation:snowflake_or_throw(<<"channel_id">>, maps:get(<<"channel_id">>, P)),
|
||||
UserId = validation:snowflake_or_throw(<<"user_id">>, maps:get(<<"user_id">>, P)),
|
||||
gateway_rpc_guild_infra:with_voice_server(GuildId, fun(VPid, _) ->
|
||||
repair_voice_state(VPid, P, ChannelId, UserId)
|
||||
end).
|
||||
|
||||
-spec repair_voice_state(pid(), map(), integer(), integer()) -> map().
|
||||
repair_voice_state(VPid, P, ChannelId, UserId) ->
|
||||
Req = #{
|
||||
connection_id => maps:get(<<"connection_id">>, P),
|
||||
channel_id => ChannelId,
|
||||
user_id => UserId
|
||||
},
|
||||
Result = gen_server:call(
|
||||
VPid, {repair_voice_state_from_guild_cache, Req}, ?GUILD_CALL_TIMEOUT
|
||||
),
|
||||
gateway_rpc_guild_voice_util:handle_repair_result(Result).
|
||||
|
||||
-spec handle_get_voice_states(map()) -> term().
|
||||
handle_get_voice_states(P) ->
|
||||
case validate_gc_params(P) of
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
-export([
|
||||
handle_move_member_result/4,
|
||||
handle_repair_result/1,
|
||||
normalize_voice_rpc_error/1,
|
||||
parse_voice_update/1,
|
||||
process_voice_update/1
|
||||
@@ -58,18 +57,6 @@ cleanup_virtual_access_on_disconnect(DisconnectedUserId, Pid) ->
|
||||
spawn(fun() -> guild_voice:cleanup_virtual_access_on_disconnect(DisconnectedUserId, Pid) end),
|
||||
ok.
|
||||
|
||||
-spec handle_repair_result(term()) -> map().
|
||||
handle_repair_result(#{success := true, repaired := Repaired}) ->
|
||||
#{<<"success">> => true, <<"repaired">> => Repaired};
|
||||
handle_repair_result(#{success := true}) ->
|
||||
#{<<"success">> => true};
|
||||
handle_repair_result(#{success := false, error := Error}) ->
|
||||
#{<<"success">> => false, <<"error">> => normalize_voice_rpc_error(Error)};
|
||||
handle_repair_result(#{error := Error}) ->
|
||||
gateway_rpc_error:raise(normalize_voice_rpc_error(Error));
|
||||
handle_repair_result(_) ->
|
||||
#{<<"success">> => false, <<"error">> => <<"repair_voice_state_error">>}.
|
||||
|
||||
-spec voice_session_data([term()]) -> [map()].
|
||||
voice_session_data(SessionData) ->
|
||||
[Entry || Entry <- SessionData, is_map(Entry)].
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
get_local_node_id/0,
|
||||
get_local_node_stats/0,
|
||||
get_local_memory_stats/1,
|
||||
get_local_voice_state_counts/0,
|
||||
get_local_active_voice_rooms/0
|
||||
get_local_voice_state_counts/0
|
||||
]).
|
||||
|
||||
-define(DEFAULT_MEMORY_STATS_LIMIT, 100).
|
||||
@@ -31,9 +30,6 @@ execute_method(<<"process.node_stats">>, _Params) ->
|
||||
execute_method(<<"process.voice_state_counts">>, _Params) ->
|
||||
ActiveNodes = voice_state_count_nodes(),
|
||||
gateway_rpc_misc_presence:collect_and_aggregate_voice_state_counts(ActiveNodes);
|
||||
execute_method(<<"process.active_voice_rooms">>, _Params) ->
|
||||
ActiveNodes = gateway_node_router:active_nodes(),
|
||||
gateway_rpc_misc_push:collect_and_aggregate_active_voice_rooms(ActiveNodes);
|
||||
execute_method(Method, _Params) ->
|
||||
gateway_rpc_error:raise(<<"Unknown method: ", Method/binary>>).
|
||||
|
||||
@@ -53,10 +49,6 @@ get_local_memory_stats(Limit) ->
|
||||
get_local_voice_state_counts() ->
|
||||
gateway_rpc_misc_presence:get_local_voice_state_counts().
|
||||
|
||||
-spec get_local_active_voice_rooms() -> map().
|
||||
get_local_active_voice_rooms() ->
|
||||
gateway_rpc_misc_push:get_local_active_voice_rooms().
|
||||
|
||||
-spec parse_memory_stats_limit(map()) -> pos_integer().
|
||||
parse_memory_stats_limit(Params) ->
|
||||
Limit =
|
||||
|
||||
@@ -1,356 +0,0 @@
|
||||
%% SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
-module(gateway_rpc_misc_push).
|
||||
-typing([eqwalizer]).
|
||||
|
||||
-export([
|
||||
get_local_active_voice_rooms/0,
|
||||
collect_and_aggregate_active_voice_rooms/1
|
||||
]).
|
||||
|
||||
-define(NODE_RPC_TIMEOUT, 10000).
|
||||
|
||||
-spec get_local_active_voice_rooms() -> map().
|
||||
get_local_active_voice_rooms() ->
|
||||
#{
|
||||
<<"node_id">> => gateway_rpc_misc_session:node_id(node()),
|
||||
<<"rooms">> => get_local_guild_active_voice_rooms() ++ get_local_dm_active_voice_rooms()
|
||||
}.
|
||||
|
||||
-spec collect_and_aggregate_active_voice_rooms([node()]) -> map().
|
||||
collect_and_aggregate_active_voice_rooms(Nodes) ->
|
||||
NodeRooms = [
|
||||
fetch_node_active_voice_rooms(N)
|
||||
|| N <- gateway_rpc_misc_session:normalize_nodes(Nodes)
|
||||
],
|
||||
aggregate_active_voice_rooms(NodeRooms).
|
||||
|
||||
-spec get_local_guild_active_voice_rooms() -> [map()].
|
||||
get_local_guild_active_voice_rooms() ->
|
||||
try
|
||||
ets:foldl(
|
||||
fun
|
||||
({GuildId, Pid}, Acc) when is_integer(GuildId), is_pid(Pid) ->
|
||||
get_guild_active_voice_rooms(GuildId, Pid) ++ Acc;
|
||||
(_Row, Acc) ->
|
||||
Acc
|
||||
end,
|
||||
[],
|
||||
guild_voice_registry
|
||||
)
|
||||
catch
|
||||
error:badarg ->
|
||||
[]
|
||||
end.
|
||||
|
||||
-spec get_guild_active_voice_rooms(integer(), pid()) -> [map()].
|
||||
get_guild_active_voice_rooms(GuildId, Pid) ->
|
||||
case process_liveness:is_alive(Pid) of
|
||||
true -> fetch_guild_voice_states(GuildId, Pid);
|
||||
false -> []
|
||||
end.
|
||||
|
||||
-spec fetch_guild_voice_states(integer(), pid()) -> [map()].
|
||||
fetch_guild_voice_states(GuildId, Pid) ->
|
||||
try gen_server:call(Pid, {get_voice_states_map}, 500) of
|
||||
VoiceStates when is_map(VoiceStates) ->
|
||||
build_guild_voice_room_entries(GuildId, VoiceStates);
|
||||
_ ->
|
||||
[]
|
||||
catch
|
||||
_:_ -> []
|
||||
end.
|
||||
|
||||
-spec build_guild_voice_room_entries(integer(), map()) -> [map()].
|
||||
build_guild_voice_room_entries(GuildId, VoiceStates) ->
|
||||
Counts = maps:fold(
|
||||
fun(_ConnId, VoiceState, Acc) ->
|
||||
fold_voice_state(VoiceState, Acc)
|
||||
end,
|
||||
#{},
|
||||
VoiceStates
|
||||
),
|
||||
[
|
||||
#{
|
||||
<<"guild_id">> => integer_to_binary(GuildId),
|
||||
<<"channel_id">> => ChannelId,
|
||||
<<"voice_state_count">> => Count
|
||||
}
|
||||
|| {ChannelId, Count} <- lists:sort(maps:to_list(Counts)),
|
||||
Count > 0
|
||||
].
|
||||
|
||||
-spec fold_voice_state(term(), map()) -> map().
|
||||
fold_voice_state(State, Acc) when is_map(State) ->
|
||||
ChannelId = normalize_id(maps:get(<<"channel_id">>, State, undefined)),
|
||||
increment_channel_count(ChannelId, Acc);
|
||||
fold_voice_state(_, Acc) ->
|
||||
Acc.
|
||||
|
||||
-spec increment_channel_count(binary() | undefined, map()) -> map().
|
||||
increment_channel_count(undefined, Acc) ->
|
||||
Acc;
|
||||
increment_channel_count(ChannelId, Acc) ->
|
||||
maps:update_with(ChannelId, fun(C) -> C + 1 end, 1, Acc).
|
||||
|
||||
-spec get_local_dm_active_voice_rooms() -> [map()].
|
||||
get_local_dm_active_voice_rooms() ->
|
||||
RawIds = shard_utils:safe_apply(fun call_manager:local_call_ids/0, []),
|
||||
CallIds = integer_entries(RawIds),
|
||||
lists:flatmap(fun get_dm_active_voice_room/1, CallIds).
|
||||
|
||||
-spec integer_entries(term()) -> [integer()].
|
||||
integer_entries(Values) when is_list(Values) ->
|
||||
[Value || Value <- Values, is_integer(Value)];
|
||||
integer_entries(_) ->
|
||||
[].
|
||||
|
||||
-spec get_dm_active_voice_room(integer()) -> [map()].
|
||||
get_dm_active_voice_room(ChannelId) ->
|
||||
case lookup_call(ChannelId) of
|
||||
{ok, Pid} -> get_dm_room_from_pid(ChannelId, Pid);
|
||||
_ -> []
|
||||
end.
|
||||
|
||||
-spec lookup_call(integer()) -> {ok, pid()} | error.
|
||||
lookup_call(ChannelId) ->
|
||||
try call_manager:lookup(ChannelId) of
|
||||
{ok, Pid} when is_pid(Pid) -> {ok, Pid};
|
||||
_ -> error
|
||||
catch
|
||||
_:_ -> error
|
||||
end.
|
||||
|
||||
-spec get_dm_room_from_pid(integer(), pid()) -> [map()].
|
||||
get_dm_room_from_pid(ChannelId, Pid) ->
|
||||
try gen_server:call(Pid, {get_state}, 500) of
|
||||
{ok, CallData} when is_map(CallData) ->
|
||||
build_dm_room_entry(ChannelId, CallData);
|
||||
_ ->
|
||||
[]
|
||||
catch
|
||||
_:_ -> []
|
||||
end.
|
||||
|
||||
-spec build_dm_room_entry(integer(), map()) -> [map()].
|
||||
build_dm_room_entry(ChannelId, CallData) ->
|
||||
VoiceStates = maps:get(
|
||||
voice_states,
|
||||
CallData,
|
||||
maps:get(<<"voice_states">>, CallData, [])
|
||||
),
|
||||
Count =
|
||||
case VoiceStates of
|
||||
Values when is_list(Values) -> length(Values);
|
||||
_ -> 0
|
||||
end,
|
||||
case Count > 0 of
|
||||
true ->
|
||||
[
|
||||
#{
|
||||
<<"guild_id">> => null,
|
||||
<<"channel_id">> => integer_to_binary(ChannelId),
|
||||
<<"voice_state_count">> => Count
|
||||
}
|
||||
];
|
||||
false ->
|
||||
[]
|
||||
end.
|
||||
|
||||
-spec fetch_node_active_voice_rooms(node()) -> map().
|
||||
fetch_node_active_voice_rooms(TargetNode) ->
|
||||
case
|
||||
gateway_rpc_misc_session:safe_node_call(
|
||||
TargetNode, get_local_active_voice_rooms, [], ?NODE_RPC_TIMEOUT
|
||||
)
|
||||
of
|
||||
#{<<"rooms">> := Rooms} = Result when is_list(Rooms) ->
|
||||
Result#{<<"node_id">> => gateway_rpc_misc_session:node_id(TargetNode)};
|
||||
_ ->
|
||||
#{
|
||||
<<"node_id">> => gateway_rpc_misc_session:node_id(TargetNode),
|
||||
<<"rooms">> => []
|
||||
}
|
||||
end.
|
||||
|
||||
-spec aggregate_active_voice_rooms([term()]) -> map().
|
||||
aggregate_active_voice_rooms(NodeRooms) ->
|
||||
{RoomCounts, NodeCount} = lists:foldl(
|
||||
fun merge_node_rooms/2,
|
||||
{#{}, 0},
|
||||
NodeRooms
|
||||
),
|
||||
Rooms = [
|
||||
build_room_entry(RoomKey, Count)
|
||||
|| {RoomKey, Count} <- lists:sort(maps:to_list(RoomCounts)),
|
||||
Count > 0
|
||||
],
|
||||
#{<<"node_count">> => NodeCount, <<"rooms">> => Rooms}.
|
||||
|
||||
-spec merge_node_rooms(term(), {map(), non_neg_integer()}) -> {map(), non_neg_integer()}.
|
||||
merge_node_rooms(NodeRooms, {RoomAcc, NodeCount}) when is_map(NodeRooms) ->
|
||||
Rooms = maps:get(<<"rooms">>, NodeRooms, []),
|
||||
MergedRooms =
|
||||
case Rooms of
|
||||
RoomList when is_list(RoomList) ->
|
||||
lists:foldl(fun merge_single_room/2, RoomAcc, RoomList);
|
||||
_ ->
|
||||
RoomAcc
|
||||
end,
|
||||
{MergedRooms, NodeCount + 1};
|
||||
merge_node_rooms(_, Acc) ->
|
||||
Acc.
|
||||
|
||||
-spec merge_single_room(term(), map()) -> map().
|
||||
merge_single_room(Room, Acc) when is_map(Room) ->
|
||||
ChannelId = normalize_id(maps:get(<<"channel_id">>, Room, undefined)),
|
||||
GuildIdResult = normalize_room_guild_id(maps:get(<<"guild_id">>, Room, null)),
|
||||
Count = gateway_rpc_misc_session:decode_integer(
|
||||
maps:get(<<"voice_state_count">>, Room, 0)
|
||||
),
|
||||
apply_room_merge(ChannelId, GuildIdResult, Count, Acc);
|
||||
merge_single_room(_, Acc) ->
|
||||
Acc.
|
||||
|
||||
-spec apply_room_merge(
|
||||
binary() | undefined, {ok, binary() | null} | error, non_neg_integer(), map()
|
||||
) -> map().
|
||||
apply_room_merge(undefined, _, _, Acc) ->
|
||||
Acc;
|
||||
apply_room_merge(_, error, _, Acc) ->
|
||||
Acc;
|
||||
apply_room_merge(_, _, Count, Acc) when Count =< 0 -> Acc;
|
||||
apply_room_merge(ChannelId, {ok, GuildId}, Count, Acc) ->
|
||||
Key =
|
||||
case GuildId of
|
||||
null -> {dm, ChannelId};
|
||||
_ -> {guild, GuildId, ChannelId}
|
||||
end,
|
||||
maps:update_with(Key, fun(Current) -> Current + Count end, Count, Acc).
|
||||
|
||||
-spec build_room_entry(tuple(), non_neg_integer()) -> map().
|
||||
build_room_entry({dm, ChannelId}, Count) ->
|
||||
#{
|
||||
<<"guild_id">> => null,
|
||||
<<"channel_id">> => ChannelId,
|
||||
<<"voice_state_count">> => Count
|
||||
};
|
||||
build_room_entry({guild, GuildId, ChannelId}, Count) ->
|
||||
#{
|
||||
<<"guild_id">> => GuildId,
|
||||
<<"channel_id">> => ChannelId,
|
||||
<<"voice_state_count">> => Count
|
||||
}.
|
||||
|
||||
-spec normalize_id(term()) -> binary() | undefined.
|
||||
normalize_id(Value) when is_binary(Value), byte_size(Value) > 0 ->
|
||||
case validation:validate_snowflake(Value) of
|
||||
{ok, Id} when Id > 0 -> integer_to_binary(Id);
|
||||
_ -> undefined
|
||||
end;
|
||||
normalize_id(Value) when is_integer(Value), Value > 0 ->
|
||||
integer_to_binary(Value);
|
||||
normalize_id(Value) when is_list(Value), Value =/= [] ->
|
||||
normalize_integer_id(type_conv:to_integer(Value));
|
||||
normalize_id(_) ->
|
||||
undefined.
|
||||
|
||||
-spec normalize_integer_id(integer() | undefined) -> binary() | undefined.
|
||||
normalize_integer_id(Id) when is_integer(Id), Id > 0 ->
|
||||
integer_to_binary(Id);
|
||||
normalize_integer_id(_) ->
|
||||
undefined.
|
||||
|
||||
-spec normalize_room_guild_id(term()) -> {ok, binary() | null} | error.
|
||||
normalize_room_guild_id(null) ->
|
||||
{ok, null};
|
||||
normalize_room_guild_id(undefined) ->
|
||||
{ok, null};
|
||||
normalize_room_guild_id(Value) ->
|
||||
case normalize_id(Value) of
|
||||
undefined -> error;
|
||||
Normalized -> {ok, Normalized}
|
||||
end.
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
malformed_test_node_rooms() ->
|
||||
[
|
||||
#{
|
||||
<<"rooms">> => [
|
||||
#{
|
||||
<<"guild_id">> => <<"10">>,
|
||||
<<"channel_id">> => <<"20">>,
|
||||
<<"voice_state_count">> => <<"2">>
|
||||
},
|
||||
#{
|
||||
<<"guild_id">> => 10,
|
||||
<<"channel_id">> => 20,
|
||||
<<"voice_state_count">> => 3
|
||||
},
|
||||
#{
|
||||
<<"guild_id">> => null,
|
||||
<<"channel_id">> => "30",
|
||||
<<"voice_state_count">> => "4"
|
||||
},
|
||||
#{
|
||||
<<"guild_id">> => <<"bad">>,
|
||||
<<"channel_id">> => <<"40">>,
|
||||
<<"voice_state_count">> => 5
|
||||
},
|
||||
#{
|
||||
<<"guild_id">> => <<"10">>,
|
||||
<<"channel_id">> => <<"bad">>,
|
||||
<<"voice_state_count">> => 5
|
||||
},
|
||||
#{
|
||||
<<"guild_id">> => <<"10">>,
|
||||
<<"channel_id">> => <<"50">>,
|
||||
<<"voice_state_count">> => 0
|
||||
},
|
||||
not_a_room
|
||||
]
|
||||
},
|
||||
#{<<"rooms">> => not_a_list},
|
||||
not_a_node
|
||||
].
|
||||
|
||||
aggregate_active_voice_rooms_ignores_malformed_nodes_and_rooms_test() ->
|
||||
Aggregate = aggregate_active_voice_rooms(malformed_test_node_rooms()),
|
||||
?assertEqual(2, maps:get(<<"node_count">>, Aggregate)),
|
||||
?assertEqual(
|
||||
[
|
||||
#{
|
||||
<<"guild_id">> => null,
|
||||
<<"channel_id">> => <<"30">>,
|
||||
<<"voice_state_count">> => 4
|
||||
},
|
||||
#{
|
||||
<<"guild_id">> => <<"10">>,
|
||||
<<"channel_id">> => <<"20">>,
|
||||
<<"voice_state_count">> => 5
|
||||
}
|
||||
],
|
||||
maps:get(<<"rooms">>, Aggregate)
|
||||
).
|
||||
|
||||
build_guild_voice_room_entries_ignores_malformed_voice_states_test() ->
|
||||
VoiceStates = #{
|
||||
<<"good">> => #{<<"channel_id">> => <<"20">>},
|
||||
<<"same">> => #{<<"channel_id">> => 20},
|
||||
<<"bad_channel">> => #{<<"channel_id">> => <<"bad">>},
|
||||
<<"not_a_map">> => not_a_map
|
||||
},
|
||||
?assertEqual(
|
||||
[
|
||||
#{
|
||||
<<"guild_id">> => <<"10">>,
|
||||
<<"channel_id">> => <<"20">>,
|
||||
<<"voice_state_count">> => 2
|
||||
}
|
||||
],
|
||||
build_guild_voice_room_entries(10, VoiceStates)
|
||||
).
|
||||
|
||||
-endif.
|
||||
@@ -13,8 +13,6 @@
|
||||
-spec execute_method(binary(), map()) -> term().
|
||||
execute_method(<<"voice.confirm_connection">>, P) ->
|
||||
handle_confirm_connection(P);
|
||||
execute_method(<<"voice.repair_state_from_cache">>, P) ->
|
||||
handle_repair_state(P);
|
||||
execute_method(<<"voice.disconnect_user_if_in_channel">>, P) ->
|
||||
handle_disconnect_if_in_channel(P);
|
||||
execute_method(<<"voice.get_voice_states_for_channel">>, P) ->
|
||||
@@ -46,18 +44,6 @@ handle_confirm_connection(Params) ->
|
||||
)
|
||||
end.
|
||||
|
||||
-spec handle_repair_state(map()) -> map() | term().
|
||||
handle_repair_state(Params) ->
|
||||
case parse_optional_guild_id(Params) of
|
||||
undefined ->
|
||||
#{<<"success">> => false, <<"error">> => <<"voice_not_supported">>};
|
||||
GuildId ->
|
||||
gateway_rpc_guild:execute_method(
|
||||
<<"guild.repair_voice_state_from_cache">>,
|
||||
Params#{<<"guild_id">> => integer_to_binary(GuildId)}
|
||||
)
|
||||
end.
|
||||
|
||||
-spec handle_disconnect_if_in_channel(map()) -> term().
|
||||
handle_disconnect_if_in_channel(Params) ->
|
||||
ChannelIdBin = maps:get(<<"channel_id">>, Params),
|
||||
|
||||
@@ -45,8 +45,6 @@ handle_call({session_connect, Request}, {CallerPid, _}, State) ->
|
||||
handle_session_connect_call(Request, CallerPid, State);
|
||||
handle_call(export_handoff_state, _From, State) ->
|
||||
{reply, {ok, guild_handoff:export_handoff_state(State)}, State};
|
||||
handle_call({get_cached_voice_state_by_connection, ConnectionId}, _From, State) ->
|
||||
handle_cached_voice_state_call(ConnectionId, State);
|
||||
handle_call({get_guild_id}, _From, State) ->
|
||||
{reply, maps:get(id, State, undefined), State};
|
||||
handle_call({get_voice_guild_state}, _From, State) ->
|
||||
@@ -239,10 +237,6 @@ session_connect_pid(#{session_pid := Pid}, _CallerPid) ->
|
||||
session_connect_pid(_Request, CallerPid) ->
|
||||
CallerPid.
|
||||
|
||||
-spec handle_cached_voice_state_call(term(), guild_state()) -> call_reply().
|
||||
handle_cached_voice_state_call(ConnectionId, State) when is_binary(ConnectionId) ->
|
||||
guild_voice_lifecycle:reply_cached_voice_state(ConnectionId, State).
|
||||
|
||||
-spec handle_reload_call(term(), guild_state()) -> call_reply().
|
||||
handle_reload_call(NewData, State) when is_map(NewData) ->
|
||||
guild_init:handle_reload(NewData, State).
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
ensure_voice_server/1,
|
||||
handle_voice_server_exit/3,
|
||||
reply_voice_server_pid/1,
|
||||
reply_cached_voice_state/2,
|
||||
clear_stale_cached_voice_states/2
|
||||
]).
|
||||
|
||||
@@ -61,15 +60,6 @@ reply_voice_server_pid(State) ->
|
||||
{{error, Reason}, NewState} -> {reply, {error, Reason}, NewState}
|
||||
end.
|
||||
|
||||
-spec reply_cached_voice_state(binary(), guild_state()) ->
|
||||
{reply, {ok, map()} | {error, not_found}, guild_state()}.
|
||||
reply_cached_voice_state(ConnectionId, State) ->
|
||||
VoiceStates = maps:get(voice_states, State, #{}),
|
||||
case maps:find(ConnectionId, VoiceStates) of
|
||||
{ok, VoiceState} -> {reply, {ok, VoiceState}, State};
|
||||
error -> {reply, {error, not_found}, State}
|
||||
end.
|
||||
|
||||
-spec clear_stale_cached_voice_states([binary()], guild_state()) -> guild_state().
|
||||
clear_stale_cached_voice_states(ConnectionIds, State) ->
|
||||
case read_authoritative_voice_states(State) of
|
||||
|
||||
@@ -189,13 +189,6 @@ voice_request_handler(_) ->
|
||||
error.
|
||||
|
||||
-spec handle_call_local(term(), server_state()) -> {reply, term(), server_state()}.
|
||||
handle_call_local({repair_voice_state_from_guild_cache, Request}, State) when is_map(Request) ->
|
||||
{Reply, NewState} = guild_voice_server_state:repair_voice_state_from_guild_cache(
|
||||
Request, State
|
||||
),
|
||||
{reply, Reply, NewState};
|
||||
handle_call_local({repair_voice_state_from_guild_cache, _Request}, State) ->
|
||||
{reply, #{success => false, error => voice_invalid_state}, State};
|
||||
handle_call_local({store_pending_connection, ConnectionId, Metadata}, State) ->
|
||||
Pending = maps:get(pending_voice_connections, State, #{}),
|
||||
NewPending = bounded_put(ConnectionId, Metadata, Pending, ?MAX_PENDING_CONNECTIONS),
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
local_voice_states_for_channel/2,
|
||||
local_pending_joins_for_channel/2,
|
||||
parse_voice_channel_id/1,
|
||||
repair_voice_state_from_guild_cache/2,
|
||||
voice_state_rpc_entries/1,
|
||||
pending_join_rpc_entries/1,
|
||||
fetch_guild_data/1,
|
||||
@@ -117,114 +116,6 @@ parse_voice_channel_id(ChannelId) ->
|
||||
error
|
||||
end.
|
||||
|
||||
-spec repair_voice_state_from_guild_cache(map(), server_state()) -> {map(), server_state()}.
|
||||
repair_voice_state_from_guild_cache(Request, State) ->
|
||||
ConnectionId = maps:get(connection_id, Request, undefined),
|
||||
UserId = maps:get(user_id, Request, undefined),
|
||||
ChannelId = maps:get(channel_id, Request, undefined),
|
||||
case {ConnectionId, UserId, ChannelId} of
|
||||
{Conn, UId, CId} when is_binary(Conn), is_integer(UId), is_integer(CId) ->
|
||||
do_repair(Conn, UId, CId, State);
|
||||
_ ->
|
||||
{#{success => false, error => voice_invalid_state}, State}
|
||||
end.
|
||||
|
||||
-spec do_repair(binary(), integer(), integer(), server_state()) -> {map(), server_state()}.
|
||||
do_repair(ConnectionId, UserId, ChannelId, State) ->
|
||||
VoiceStates = maps:get(voice_states, State, #{}),
|
||||
case maps:get(ConnectionId, VoiceStates, undefined) of
|
||||
ExistingVoiceState when is_map(ExistingVoiceState) ->
|
||||
repair_existing_voice_state(
|
||||
ExistingVoiceState, ConnectionId, UserId, ChannelId, State
|
||||
);
|
||||
_ ->
|
||||
repair_from_cached(ConnectionId, UserId, ChannelId, VoiceStates, State)
|
||||
end.
|
||||
|
||||
-spec repair_existing_voice_state(
|
||||
voice_state(), binary(), integer(), integer(), server_state()
|
||||
) ->
|
||||
{map(), server_state()}.
|
||||
repair_existing_voice_state(ExistingVoiceState, ConnectionId, UserId, ChannelId, State) ->
|
||||
case voice_state_matches(ExistingVoiceState, ConnectionId, UserId, ChannelId) of
|
||||
true -> {#{success => true, repaired => false}, State};
|
||||
false -> {#{success => false, error => voice_state_mismatch}, State}
|
||||
end.
|
||||
|
||||
-spec repair_from_cached(binary(), integer(), integer(), voice_state_map(), server_state()) ->
|
||||
{map(), server_state()}.
|
||||
repair_from_cached(ConnectionId, UserId, ChannelId, VoiceStates, State) ->
|
||||
case fetch_cached_voice_state(ConnectionId, State) of
|
||||
{ok, CachedVoiceState} ->
|
||||
repair_with_cached(
|
||||
ConnectionId, UserId, ChannelId, CachedVoiceState, VoiceStates, State
|
||||
);
|
||||
{error, _Reason} ->
|
||||
{#{success => false, error => voice_connection_not_found}, State}
|
||||
end.
|
||||
|
||||
-spec repair_with_cached(
|
||||
binary(), integer(), integer(), voice_state(), voice_state_map(), server_state()
|
||||
) -> {map(), server_state()}.
|
||||
repair_with_cached(ConnectionId, UserId, ChannelId, CachedVoiceState, VoiceStates, State) ->
|
||||
case voice_state_matches(CachedVoiceState, ConnectionId, UserId, ChannelId) of
|
||||
false ->
|
||||
{#{success => false, error => voice_state_mismatch}, State};
|
||||
true ->
|
||||
OldVoiceStates = maps:get(voice_states, State, #{}),
|
||||
NewVoiceStates = VoiceStates#{ConnectionId => CachedVoiceState},
|
||||
_ = guild_voice_server_sync:sync_replaced_voice_states(
|
||||
OldVoiceStates, NewVoiceStates
|
||||
),
|
||||
PendingConns = maps:remove(
|
||||
ConnectionId, maps:get(pending_voice_connections, State, #{})
|
||||
),
|
||||
RecentDisc = maps:remove(
|
||||
ConnectionId,
|
||||
maps:get(recently_disconnected_voice_states, State, #{})
|
||||
),
|
||||
NewState0 = State#{
|
||||
voice_states => NewVoiceStates,
|
||||
pending_voice_connections => PendingConns,
|
||||
recently_disconnected_voice_states => RecentDisc
|
||||
},
|
||||
GuildState = build_guild_state(NewState0),
|
||||
ChannelIdBin = maps:get(<<"channel_id">>, CachedVoiceState, null),
|
||||
guild_voice_broadcast:broadcast_voice_state_update(
|
||||
CachedVoiceState, GuildState, ChannelIdBin
|
||||
),
|
||||
logger:warning(
|
||||
voice_state_repaired_log_message(),
|
||||
[maps:get(guild_id, State), ChannelId, UserId, ConnectionId]
|
||||
),
|
||||
{#{success => true, repaired => true}, NewState0}
|
||||
end.
|
||||
|
||||
-spec voice_state_repaired_log_message() -> string().
|
||||
voice_state_repaired_log_message() ->
|
||||
"guild_voice_state_repaired_from_guild_cache: guild_id=~p "
|
||||
"channel_id=~p user_id=~p connection_id=~p".
|
||||
|
||||
-spec fetch_cached_voice_state(binary(), server_state()) ->
|
||||
{ok, voice_state()} | {error, not_found}.
|
||||
fetch_cached_voice_state(ConnectionId, #{guild_pid := GuildPid}) when is_pid(GuildPid) ->
|
||||
try gen_server:call(GuildPid, {get_cached_voice_state_by_connection, ConnectionId}, 1000) of
|
||||
{ok, VoiceState} when is_map(VoiceState) -> {ok, VoiceState};
|
||||
_ -> {error, not_found}
|
||||
catch
|
||||
throw:_ -> {error, not_found};
|
||||
error:_ -> {error, not_found};
|
||||
exit:_ -> {error, not_found}
|
||||
end;
|
||||
fetch_cached_voice_state(_ConnectionId, _State) ->
|
||||
{error, not_found}.
|
||||
|
||||
-spec voice_state_matches(voice_state(), binary(), integer(), integer()) -> boolean().
|
||||
voice_state_matches(VoiceState, ConnectionId, UserId, ChannelId) ->
|
||||
maps:get(<<"connection_id">>, VoiceState, ConnectionId) =:= ConnectionId andalso
|
||||
voice_state_utils:voice_state_user_id(VoiceState) =:= UserId andalso
|
||||
voice_state_utils:voice_state_channel_id(VoiceState) =:= ChannelId.
|
||||
|
||||
-spec voice_state_rpc_entries(term()) -> [map()].
|
||||
voice_state_rpc_entries(VoiceStates) when is_list(VoiceStates) ->
|
||||
lists:filtermap(fun voice_state_rpc_entry/1, VoiceStates);
|
||||
|
||||
@@ -269,60 +269,45 @@ dispatch_to_sessions(Payload, State) ->
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
expiry_timer_not_armed_while_disabled_test() ->
|
||||
application:unset_env(fluxer_gateway, custom_status_expiry_enabled),
|
||||
State = refresh_custom_status_expiry_timer(#{custom_status => future_custom_status()}),
|
||||
?assertNot(maps:is_key(?CUSTOM_STATUS_EXPIRY_TIMER, State)).
|
||||
|
||||
expiry_timer_not_armed_without_expires_at_test() ->
|
||||
with_expiry_enabled(fun() ->
|
||||
?assertNot(
|
||||
maps:is_key(
|
||||
?CUSTOM_STATUS_EXPIRY_TIMER,
|
||||
refresh_custom_status_expiry_timer(#{custom_status => null})
|
||||
)
|
||||
),
|
||||
?assertNot(
|
||||
maps:is_key(
|
||||
?CUSTOM_STATUS_EXPIRY_TIMER,
|
||||
refresh_custom_status_expiry_timer(#{
|
||||
custom_status => #{<<"text">> => <<"hi">>}
|
||||
})
|
||||
)
|
||||
?assertNot(
|
||||
maps:is_key(
|
||||
?CUSTOM_STATUS_EXPIRY_TIMER,
|
||||
refresh_custom_status_expiry_timer(#{custom_status => null})
|
||||
)
|
||||
end).
|
||||
),
|
||||
?assertNot(
|
||||
maps:is_key(
|
||||
?CUSTOM_STATUS_EXPIRY_TIMER,
|
||||
refresh_custom_status_expiry_timer(#{
|
||||
custom_status => #{<<"text">> => <<"hi">>}
|
||||
})
|
||||
)
|
||||
).
|
||||
|
||||
expiry_timer_armed_for_a_future_expiry_test() ->
|
||||
with_expiry_enabled(fun() ->
|
||||
State = refresh_custom_status_expiry_timer(#{custom_status => future_custom_status()}),
|
||||
?assert(is_reference(maps:get(?CUSTOM_STATUS_EXPIRY_TIMER, State))),
|
||||
ok = cancel_expiry_timer(maps:get(?CUSTOM_STATUS_EXPIRY_TIMER, State))
|
||||
end).
|
||||
State = refresh_custom_status_expiry_timer(#{custom_status => future_custom_status()}),
|
||||
?assert(is_reference(maps:get(?CUSTOM_STATUS_EXPIRY_TIMER, State))),
|
||||
ok = cancel_expiry_timer(maps:get(?CUSTOM_STATUS_EXPIRY_TIMER, State)).
|
||||
|
||||
expiry_timer_not_armed_for_an_already_expired_status_test() ->
|
||||
with_expiry_enabled(fun() ->
|
||||
Expired = #{<<"expires_at">> => <<"2020-01-01T00:00:00.000Z">>},
|
||||
State = refresh_custom_status_expiry_timer(#{custom_status => Expired}),
|
||||
?assertNot(maps:is_key(?CUSTOM_STATUS_EXPIRY_TIMER, State))
|
||||
end).
|
||||
Expired = #{<<"expires_at">> => <<"2020-01-01T00:00:00.000Z">>},
|
||||
State = refresh_custom_status_expiry_timer(#{custom_status => Expired}),
|
||||
?assertNot(maps:is_key(?CUSTOM_STATUS_EXPIRY_TIMER, State)).
|
||||
|
||||
expiry_timer_replaces_the_previous_one_test() ->
|
||||
with_expiry_enabled(fun() ->
|
||||
First = refresh_custom_status_expiry_timer(#{custom_status => future_custom_status()}),
|
||||
Second = refresh_custom_status_expiry_timer(First),
|
||||
?assertNotEqual(
|
||||
maps:get(?CUSTOM_STATUS_EXPIRY_TIMER, First),
|
||||
maps:get(?CUSTOM_STATUS_EXPIRY_TIMER, Second)
|
||||
),
|
||||
ok = cancel_expiry_timer(maps:get(?CUSTOM_STATUS_EXPIRY_TIMER, Second))
|
||||
end).
|
||||
First = refresh_custom_status_expiry_timer(#{custom_status => future_custom_status()}),
|
||||
Second = refresh_custom_status_expiry_timer(First),
|
||||
?assertNotEqual(
|
||||
maps:get(?CUSTOM_STATUS_EXPIRY_TIMER, First),
|
||||
maps:get(?CUSTOM_STATUS_EXPIRY_TIMER, Second)
|
||||
),
|
||||
ok = cancel_expiry_timer(maps:get(?CUSTOM_STATUS_EXPIRY_TIMER, Second)).
|
||||
|
||||
expiry_timer_is_dropped_when_the_status_is_cleared_test() ->
|
||||
with_expiry_enabled(fun() ->
|
||||
Armed = refresh_custom_status_expiry_timer(#{custom_status => future_custom_status()}),
|
||||
Cleared = refresh_custom_status_expiry_timer(Armed#{custom_status => null}),
|
||||
?assertNot(maps:is_key(?CUSTOM_STATUS_EXPIRY_TIMER, Cleared))
|
||||
end).
|
||||
Armed = refresh_custom_status_expiry_timer(#{custom_status => future_custom_status()}),
|
||||
Cleared = refresh_custom_status_expiry_timer(Armed#{custom_status => null}),
|
||||
?assertNot(maps:is_key(?CUSTOM_STATUS_EXPIRY_TIMER, Cleared)).
|
||||
|
||||
expiry_timer_delivers_a_reconcile_cast_test() ->
|
||||
State = arm_expiry_timer({ok, 1}, #{}),
|
||||
@@ -343,19 +328,4 @@ future_custom_status() ->
|
||||
),
|
||||
#{<<"text">> => <<"brb">>, <<"expires_at">> => list_to_binary(ExpiresAt)}.
|
||||
|
||||
with_expiry_enabled(Fun) ->
|
||||
Key = custom_status_expiry_enabled,
|
||||
Previous = application:get_env(fluxer_gateway, Key),
|
||||
application:set_env(fluxer_gateway, Key, true),
|
||||
try
|
||||
Fun()
|
||||
after
|
||||
restore_expiry_env(Key, Previous)
|
||||
end.
|
||||
|
||||
restore_expiry_env(Key, undefined) ->
|
||||
application:unset_env(fluxer_gateway, Key);
|
||||
restore_expiry_env(Key, {ok, Value}) ->
|
||||
application:set_env(fluxer_gateway, Key, Value).
|
||||
|
||||
-endif.
|
||||
|
||||
@@ -96,46 +96,30 @@ build_invisible_binary_normalized_to_offline_test() ->
|
||||
?assertEqual(null, maps:get(<<"custom_status">>, Result)).
|
||||
|
||||
build_clears_expired_custom_status_test() ->
|
||||
with_expiry_enabled(fun() ->
|
||||
Result = build(test_user(), online, false, false, expired_custom_status()),
|
||||
?assertEqual(<<"online">>, maps:get(<<"status">>, Result)),
|
||||
?assertEqual(null, maps:get(<<"custom_status">>, Result))
|
||||
end).
|
||||
Result = build(test_user(), online, false, false, expired_custom_status()),
|
||||
?assertEqual(<<"online">>, maps:get(<<"status">>, Result)),
|
||||
?assertEqual(null, maps:get(<<"custom_status">>, Result)).
|
||||
|
||||
build_coalesces_an_expired_status_with_no_status_test() ->
|
||||
with_expiry_enabled(fun() ->
|
||||
?assertEqual(
|
||||
build(test_user(), online, false, false, null),
|
||||
build(test_user(), online, false, false, expired_custom_status())
|
||||
)
|
||||
end).
|
||||
?assertEqual(
|
||||
build(test_user(), online, false, false, null),
|
||||
build(test_user(), online, false, false, expired_custom_status())
|
||||
).
|
||||
|
||||
build_keeps_unexpired_custom_status_test() ->
|
||||
with_expiry_enabled(fun() ->
|
||||
Live = future_custom_status(),
|
||||
Result = build(test_user(), dnd, false, false, Live),
|
||||
?assertEqual(Live, maps:get(<<"custom_status">>, Result))
|
||||
end).
|
||||
Live = future_custom_status(),
|
||||
Result = build(test_user(), dnd, false, false, Live),
|
||||
?assertEqual(Live, maps:get(<<"custom_status">>, Result)).
|
||||
|
||||
build_keeps_custom_status_without_expires_at_test() ->
|
||||
with_expiry_enabled(fun() ->
|
||||
Live = #{<<"text">> => <<"forever">>},
|
||||
Result = build(test_user(), idle, false, false, Live),
|
||||
?assertEqual(Live, maps:get(<<"custom_status">>, Result))
|
||||
end).
|
||||
Live = #{<<"text">> => <<"forever">>},
|
||||
Result = build(test_user(), idle, false, false, Live),
|
||||
?assertEqual(Live, maps:get(<<"custom_status">>, Result)).
|
||||
|
||||
build_keeps_malformed_expires_at_test() ->
|
||||
with_expiry_enabled(fun() ->
|
||||
Live = #{<<"text">> => <<"hi">>, <<"expires_at">> => <<"not-a-date">>},
|
||||
Result = build(test_user(), online, false, false, Live),
|
||||
?assertEqual(Live, maps:get(<<"custom_status">>, Result))
|
||||
end).
|
||||
|
||||
build_leaves_expired_status_alone_while_disabled_test() ->
|
||||
application:unset_env(fluxer_gateway, custom_status_expiry_enabled),
|
||||
Expired = expired_custom_status(),
|
||||
Result = build(test_user(), online, false, false, Expired),
|
||||
?assertEqual(Expired, maps:get(<<"custom_status">>, Result)).
|
||||
Live = #{<<"text">> => <<"hi">>, <<"expires_at">> => <<"not-a-date">>},
|
||||
Result = build(test_user(), online, false, false, Live),
|
||||
?assertEqual(Live, maps:get(<<"custom_status">>, Result)).
|
||||
|
||||
test_user() ->
|
||||
#{<<"id">> => <<"1">>, <<"username">> => <<"Test">>}.
|
||||
@@ -153,19 +137,4 @@ future_custom_status() ->
|
||||
erlang:system_time(millisecond) + 3600000, [{unit, millisecond}, {offset, "Z"}]
|
||||
),
|
||||
#{<<"text">> => <<"brb">>, <<"expires_at">> => list_to_binary(ExpiresAt)}.
|
||||
|
||||
with_expiry_enabled(Fun) ->
|
||||
Key = custom_status_expiry_enabled,
|
||||
Previous = application:get_env(fluxer_gateway, Key),
|
||||
application:set_env(fluxer_gateway, Key, true),
|
||||
try
|
||||
Fun()
|
||||
after
|
||||
restore_expiry_env(Key, Previous)
|
||||
end.
|
||||
|
||||
restore_expiry_env(Key, undefined) ->
|
||||
application:unset_env(fluxer_gateway, Key);
|
||||
restore_expiry_env(Key, {ok, Value}) ->
|
||||
application:set_env(fluxer_gateway, Key, Value).
|
||||
-endif.
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
-typing([eqwalizer]).
|
||||
|
||||
-export([
|
||||
enabled/0,
|
||||
clear_if_expired/1,
|
||||
clear_if_expired/2,
|
||||
remaining_ms/2,
|
||||
@@ -20,26 +19,13 @@
|
||||
-type wakeup() :: {ok, pos_integer()} | none.
|
||||
-type offset() :: {ok, integer()} | none.
|
||||
|
||||
-define(ENABLED_KEY, custom_status_expiry_enabled).
|
||||
-define(MAX_REPAIR_WINDOW_SECONDS, 86400).
|
||||
-define(MAX_TIMER_MS, 86400000).
|
||||
-define(WAKEUP_JITTER_MS, 5000).
|
||||
|
||||
-spec enabled() -> boolean().
|
||||
enabled() ->
|
||||
application:get_env(fluxer_gateway, ?ENABLED_KEY, false) =:= true.
|
||||
|
||||
-spec clear_if_expired(term()) -> custom_status().
|
||||
clear_if_expired(CustomStatus) ->
|
||||
clear_when_enabled(enabled(), CustomStatus).
|
||||
|
||||
-spec clear_when_enabled(boolean(), term()) -> custom_status().
|
||||
clear_when_enabled(true, CustomStatus) ->
|
||||
clear_if_expired(CustomStatus, erlang:system_time(millisecond));
|
||||
clear_when_enabled(false, CustomStatus) when is_map(CustomStatus) ->
|
||||
CustomStatus;
|
||||
clear_when_enabled(false, _CustomStatus) ->
|
||||
null.
|
||||
clear_if_expired(CustomStatus, erlang:system_time(millisecond)).
|
||||
|
||||
-spec clear_if_expired(term(), term()) -> custom_status().
|
||||
clear_if_expired(CustomStatus, NowMs) when is_map(CustomStatus), is_integer(NowMs) ->
|
||||
@@ -88,12 +74,6 @@ parse_rfc3339(Chars) ->
|
||||
|
||||
-spec next_wakeup_ms(term()) -> wakeup().
|
||||
next_wakeup_ms(CustomStatus) ->
|
||||
next_wakeup_ms(enabled(), CustomStatus).
|
||||
|
||||
-spec next_wakeup_ms(boolean(), term()) -> wakeup().
|
||||
next_wakeup_ms(false, _CustomStatus) ->
|
||||
none;
|
||||
next_wakeup_ms(true, CustomStatus) ->
|
||||
wakeup_delay(remaining_ms(CustomStatus, erlang:system_time(millisecond))).
|
||||
|
||||
-spec wakeup_delay(offset()) -> wakeup().
|
||||
@@ -125,8 +105,7 @@ repair(WindowSeconds) when
|
||||
#{
|
||||
scheduled => Total,
|
||||
considered => length(Considered),
|
||||
window_seconds => WindowSeconds,
|
||||
enabled => enabled()
|
||||
window_seconds => WindowSeconds
|
||||
};
|
||||
repair(_WindowSeconds) ->
|
||||
#{error => invalid_window_seconds}.
|
||||
@@ -334,21 +313,11 @@ wakeup_clamps_to_the_timer_limit_test() ->
|
||||
?assert(DelayMs > ?MAX_TIMER_MS),
|
||||
?assert(DelayMs =< ?MAX_TIMER_MS + ?WAKEUP_JITTER_MS).
|
||||
|
||||
disabled_by_default_test() ->
|
||||
application:unset_env(fluxer_gateway, ?ENABLED_KEY),
|
||||
Status = status(?LIVE_EXPIRES_AT),
|
||||
?assertEqual(false, enabled()),
|
||||
?assertEqual(Status, clear_if_expired(Status)),
|
||||
?assertEqual(none, next_wakeup_ms(future_status())).
|
||||
|
||||
enabled_clears_expired_and_arms_future_test() ->
|
||||
clears_expired_and_arms_future_test() ->
|
||||
Live = future_status(),
|
||||
with_expiry_enabled(fun() ->
|
||||
?assertEqual(true, enabled()),
|
||||
?assertEqual(null, clear_if_expired(status(?LIVE_EXPIRES_AT))),
|
||||
?assertEqual(Live, clear_if_expired(Live)),
|
||||
?assertMatch({ok, _}, next_wakeup_ms(Live))
|
||||
end).
|
||||
?assertEqual(null, clear_if_expired(status(?LIVE_EXPIRES_AT))),
|
||||
?assertEqual(Live, clear_if_expired(Live)),
|
||||
?assertMatch({ok, _}, next_wakeup_ms(Live)).
|
||||
|
||||
future_status() ->
|
||||
status(rfc3339(erlang:system_time(millisecond) + 3600000)).
|
||||
@@ -371,8 +340,7 @@ repair_reports_what_it_scheduled_test() ->
|
||||
Result = repair(0),
|
||||
?assert(is_integer(maps:get(scheduled, Result))),
|
||||
?assert(maps:get(scheduled, Result) >= 0),
|
||||
?assertEqual(0, maps:get(window_seconds, Result)),
|
||||
?assertEqual(enabled(), maps:get(enabled, Result)).
|
||||
?assertEqual(0, maps:get(window_seconds, Result)).
|
||||
|
||||
schedule_reconciles_delivers_the_cast_test() ->
|
||||
flush_mailbox(),
|
||||
@@ -388,18 +356,4 @@ flush_mailbox() ->
|
||||
after 0 -> ok
|
||||
end.
|
||||
|
||||
with_expiry_enabled(Fun) ->
|
||||
Previous = application:get_env(fluxer_gateway, ?ENABLED_KEY),
|
||||
application:set_env(fluxer_gateway, ?ENABLED_KEY, true),
|
||||
try
|
||||
Fun()
|
||||
after
|
||||
restore_expiry_env(Previous)
|
||||
end.
|
||||
|
||||
restore_expiry_env(undefined) ->
|
||||
application:unset_env(fluxer_gateway, ?ENABLED_KEY);
|
||||
restore_expiry_env({ok, Value}) ->
|
||||
application:set_env(fluxer_gateway, ?ENABLED_KEY, Value).
|
||||
|
||||
-endif.
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
[package]
|
||||
name = "fluxer-recon"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
fluxer-svc = { path = "../fluxer_svc" }
|
||||
anyhow = "1.0.104"
|
||||
thiserror = "2.0.18"
|
||||
tokio = { version = "1.52.3", features = ["macros", "net", "rt-multi-thread", "signal", "sync", "time"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.150"
|
||||
tracing = "0.1.44"
|
||||
reqwest = { version = "0.13.4", default-features = false, features = ["json", "rustls"] }
|
||||
hmac = "0.13.0"
|
||||
sha2 = "0.11.0"
|
||||
base64 = "0.22.1"
|
||||
axum = { version = "0.8.9", default-features = false, features = ["http1", "json", "tokio"] }
|
||||
scylla = { version = "1.6.0", optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
scylla = ["dep:scylla", "fluxer-svc/scylla"]
|
||||
@@ -1,42 +0,0 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
FROM rust:1-bookworm AS builder
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN cargo build --release -p fluxer-recon --features scylla
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
ARG BUILD_VERSION=""
|
||||
ARG SOURCE_SHA=""
|
||||
ARG SOURCE_DATE=""
|
||||
|
||||
LABEL org.opencontainers.image.title="fluxer-recon"
|
||||
LABEL org.opencontainers.image.description="Fluxer voice reconciliation service"
|
||||
LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later"
|
||||
LABEL org.opencontainers.image.vendor="Fluxer"
|
||||
LABEL org.opencontainers.image.url="https://fluxer.app"
|
||||
LABEL org.opencontainers.image.documentation="https://docs.fluxer.app"
|
||||
LABEL org.opencontainers.image.source="https://github.com/fluxerapp/fluxer"
|
||||
LABEL org.opencontainers.image.version="${BUILD_VERSION}"
|
||||
LABEL org.opencontainers.image.revision="${SOURCE_SHA}"
|
||||
LABEL org.opencontainers.image.created="${SOURCE_DATE}"
|
||||
LABEL app.fluxer.build-version="${BUILD_VERSION}"
|
||||
|
||||
WORKDIR /usr/local/bin
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /usr/src/app/target/release/fluxer-recon /usr/local/bin/fluxer-recon
|
||||
|
||||
ENV BUILD_VERSION="${BUILD_VERSION}"
|
||||
|
||||
USER 65532:65532
|
||||
|
||||
EXPOSE 8090
|
||||
|
||||
CMD ["/usr/local/bin/fluxer-recon"]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,308 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::config::ReconConfig;
|
||||
use crate::evidence::CensusCrossCheck;
|
||||
use crate::gateway::{ActiveVoiceRooms, GatewayApi, GatewayFault, GatewayMethod};
|
||||
use crate::ids::{Epoch, Millis, RoomKey};
|
||||
use crate::ledger::MAP_ENTRY_OVERHEAD_BYTES;
|
||||
|
||||
pub const CENSUS_METHOD: GatewayMethod = GatewayMethod::ActiveVoiceRooms;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum CensusRead {
|
||||
Counted(ActiveVoiceRooms),
|
||||
Unreadable(GatewayFault),
|
||||
}
|
||||
|
||||
impl CensusRead {
|
||||
pub const fn is_readable(&self) -> bool {
|
||||
matches!(self, Self::Counted(_))
|
||||
}
|
||||
|
||||
pub const fn counted(&self) -> Option<&ActiveVoiceRooms> {
|
||||
match self {
|
||||
Self::Counted(rooms) => Some(rooms),
|
||||
Self::Unreadable(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn fault(&self) -> Option<&GatewayFault> {
|
||||
match self {
|
||||
Self::Counted(_) => None,
|
||||
Self::Unreadable(fault) => Some(fault),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Counted(_) => "counted",
|
||||
Self::Unreadable(_) => "unreadable",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read_census<G>(gateway: &G) -> CensusRead
|
||||
where
|
||||
G: GatewayApi,
|
||||
{
|
||||
match gateway.active_voice_rooms().await {
|
||||
Ok(rooms) => CensusRead::Counted(rooms),
|
||||
Err(fault) => CensusRead::Unreadable(fault),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CensusSnapshot {
|
||||
counts: BTreeMap<RoomKey, u32>,
|
||||
node_count: u32,
|
||||
unparseable_rooms: u32,
|
||||
taken_at: Millis,
|
||||
epoch: Epoch,
|
||||
}
|
||||
|
||||
impl CensusSnapshot {
|
||||
pub fn rooms(&self) -> impl Iterator<Item = (RoomKey, u32)> + '_ {
|
||||
self.counts.iter().map(|(room, count)| (*room, *count))
|
||||
}
|
||||
|
||||
pub fn room_keys(&self) -> Vec<RoomKey> {
|
||||
self.counts.keys().copied().collect()
|
||||
}
|
||||
|
||||
pub fn voice_state_count(&self, room: &RoomKey) -> Option<u32> {
|
||||
self.counts.get(room).copied()
|
||||
}
|
||||
|
||||
pub const fn node_count(&self) -> u32 {
|
||||
self.node_count
|
||||
}
|
||||
|
||||
pub const fn unparseable_rooms(&self) -> u32 {
|
||||
self.unparseable_rooms
|
||||
}
|
||||
|
||||
pub const fn taken_at(&self) -> Millis {
|
||||
self.taken_at
|
||||
}
|
||||
|
||||
pub const fn epoch(&self) -> Epoch {
|
||||
self.epoch
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.counts.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.counts.is_empty()
|
||||
}
|
||||
|
||||
pub const fn age_ms(&self, now: Millis) -> u64 {
|
||||
now.saturating_since(self.taken_at)
|
||||
}
|
||||
|
||||
pub const fn is_fresh(&self, now: Millis, max_age_ms: u64) -> bool {
|
||||
self.age_ms(now) <= max_age_ms
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct CensusLimits {
|
||||
pub max_age_ms: u64,
|
||||
pub interval_ms: u64,
|
||||
}
|
||||
|
||||
impl CensusLimits {
|
||||
pub const fn from_config(config: &ReconConfig) -> Self {
|
||||
Self {
|
||||
max_age_ms: config.census_max_age_ms,
|
||||
interval_ms: config.census_interval_ms,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum CensusOutcome {
|
||||
Applied { epoch: Epoch, rooms: usize },
|
||||
Unchanged { epoch: Epoch },
|
||||
Failed,
|
||||
}
|
||||
|
||||
impl CensusOutcome {
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Applied { .. } => "applied",
|
||||
Self::Unchanged { .. } => "unchanged",
|
||||
Self::Failed => "failed",
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn kept_previous_snapshot(self) -> bool {
|
||||
match self {
|
||||
Self::Applied { .. } | Self::Unchanged { .. } => false,
|
||||
Self::Failed => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn epoch(self) -> Option<Epoch> {
|
||||
match self {
|
||||
Self::Applied { epoch, .. } | Self::Unchanged { epoch } => Some(epoch),
|
||||
Self::Failed => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CensusCache {
|
||||
limits: CensusLimits,
|
||||
snapshot: Option<CensusSnapshot>,
|
||||
epoch: Epoch,
|
||||
last_failure_at: Option<Millis>,
|
||||
consecutive_failures: u32,
|
||||
applied_total: u64,
|
||||
unchanged_total: u64,
|
||||
failed_total: u64,
|
||||
}
|
||||
|
||||
pub const CENSUS_ENTRY_BYTES: u64 =
|
||||
(size_of::<RoomKey>() + size_of::<u32>() + MAP_ENTRY_OVERHEAD_BYTES) as u64;
|
||||
|
||||
impl CensusCache {
|
||||
pub fn tracked_bytes(&self) -> u64 {
|
||||
self.snapshot.as_ref().map_or(0, |snapshot| {
|
||||
(snapshot.len() as u64).saturating_mul(CENSUS_ENTRY_BYTES)
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn new(limits: CensusLimits) -> Self {
|
||||
Self {
|
||||
limits,
|
||||
snapshot: None,
|
||||
epoch: Epoch::FIRST,
|
||||
last_failure_at: None,
|
||||
consecutive_failures: 0,
|
||||
applied_total: 0,
|
||||
unchanged_total: 0,
|
||||
failed_total: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn limits(&self) -> CensusLimits {
|
||||
self.limits
|
||||
}
|
||||
|
||||
pub const fn snapshot(&self) -> Option<&CensusSnapshot> {
|
||||
self.snapshot.as_ref()
|
||||
}
|
||||
|
||||
pub const fn epoch(&self) -> Epoch {
|
||||
self.epoch
|
||||
}
|
||||
|
||||
pub const fn last_failure_at(&self) -> Option<Millis> {
|
||||
self.last_failure_at
|
||||
}
|
||||
|
||||
pub const fn consecutive_failures(&self) -> u32 {
|
||||
self.consecutive_failures
|
||||
}
|
||||
|
||||
pub const fn applied_total(&self) -> u64 {
|
||||
self.applied_total
|
||||
}
|
||||
|
||||
pub const fn unchanged_total(&self) -> u64 {
|
||||
self.unchanged_total
|
||||
}
|
||||
|
||||
pub const fn failed_total(&self) -> u64 {
|
||||
self.failed_total
|
||||
}
|
||||
|
||||
pub fn age_ms(&self, now: Millis) -> Option<u64> {
|
||||
self.snapshot.as_ref().map(|snapshot| snapshot.age_ms(now))
|
||||
}
|
||||
|
||||
pub fn is_fresh(&self, now: Millis) -> bool {
|
||||
self.snapshot
|
||||
.as_ref()
|
||||
.is_some_and(|snapshot| snapshot.is_fresh(now, self.limits.max_age_ms))
|
||||
}
|
||||
|
||||
pub fn rooms(&self) -> Vec<RoomKey> {
|
||||
self.snapshot
|
||||
.as_ref()
|
||||
.map(CensusSnapshot::room_keys)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn node_count(&self) -> Option<u32> {
|
||||
self.snapshot.as_ref().map(CensusSnapshot::node_count)
|
||||
}
|
||||
|
||||
pub fn accept(&mut self, read: &CensusRead, now: Millis) -> CensusOutcome {
|
||||
let counted = match read {
|
||||
CensusRead::Unreadable(_) => {
|
||||
self.failed_total = self.failed_total.saturating_add(1);
|
||||
self.consecutive_failures = self.consecutive_failures.saturating_add(1);
|
||||
self.last_failure_at = Some(now);
|
||||
return CensusOutcome::Failed;
|
||||
}
|
||||
CensusRead::Counted(counted) => counted,
|
||||
};
|
||||
|
||||
self.consecutive_failures = 0;
|
||||
let counts: BTreeMap<RoomKey, u32> = counted
|
||||
.rooms
|
||||
.iter()
|
||||
.map(|entry| (entry.room, entry.voice_state_count))
|
||||
.collect();
|
||||
|
||||
let unchanged = self
|
||||
.snapshot
|
||||
.as_ref()
|
||||
.is_some_and(|snapshot| snapshot.counts == counts);
|
||||
if unchanged {
|
||||
self.unchanged_total = self.unchanged_total.saturating_add(1);
|
||||
if let Some(snapshot) = self.snapshot.as_mut() {
|
||||
snapshot.taken_at = now;
|
||||
snapshot.node_count = counted.node_count;
|
||||
snapshot.unparseable_rooms = counted.unparseable_rooms;
|
||||
}
|
||||
return CensusOutcome::Unchanged { epoch: self.epoch };
|
||||
}
|
||||
|
||||
self.epoch = self.epoch.next();
|
||||
self.applied_total = self.applied_total.saturating_add(1);
|
||||
let rooms = counts.len();
|
||||
self.snapshot = Some(CensusSnapshot {
|
||||
counts,
|
||||
node_count: counted.node_count,
|
||||
unparseable_rooms: counted.unparseable_rooms,
|
||||
taken_at: now,
|
||||
epoch: self.epoch,
|
||||
});
|
||||
CensusOutcome::Applied {
|
||||
epoch: self.epoch,
|
||||
rooms,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cross_check(&self, room: &RoomKey, now: Millis) -> CensusCrossCheck {
|
||||
let Some(snapshot) = self.snapshot.as_ref() else {
|
||||
return CensusCrossCheck::Missing;
|
||||
};
|
||||
if !snapshot.is_fresh(now, self.limits.max_age_ms) {
|
||||
return CensusCrossCheck::Stale;
|
||||
}
|
||||
CensusCrossCheck::Fresh {
|
||||
voice_state_count: snapshot.voice_state_count(room).unwrap_or(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn forget(&mut self) {
|
||||
self.snapshot = None;
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crate::ids::{Millis, WallMillis};
|
||||
|
||||
pub trait Clock: Send + Sync {
|
||||
fn now(&self) -> Millis;
|
||||
|
||||
fn wall_now(&self) -> WallMillis;
|
||||
|
||||
fn elapsed_since(&self, earlier: Millis) -> u64 {
|
||||
self.now().saturating_since(earlier)
|
||||
}
|
||||
}
|
||||
|
||||
pub type SharedClock = Arc<dyn Clock>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct WallAnchor {
|
||||
monotonic: Millis,
|
||||
wall: WallMillis,
|
||||
}
|
||||
|
||||
impl WallAnchor {
|
||||
pub const fn new(monotonic: Millis, wall: WallMillis) -> Self {
|
||||
Self { monotonic, wall }
|
||||
}
|
||||
|
||||
pub const fn projected(&self, monotonic: Millis) -> WallMillis {
|
||||
self.wall
|
||||
.saturating_add_millis(monotonic.saturating_since(self.monotonic))
|
||||
}
|
||||
|
||||
pub const fn guard(&self, monotonic: Millis, observed: WallMillis) -> WallMillis {
|
||||
let projected = self.projected(monotonic);
|
||||
if observed.get() < projected.get() {
|
||||
observed
|
||||
} else {
|
||||
projected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct SystemClock;
|
||||
|
||||
impl SystemClock {
|
||||
pub const fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn shared() -> SharedClock {
|
||||
Arc::new(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Clock for SystemClock {
|
||||
fn now(&self) -> Millis {
|
||||
Millis::new(u64::try_from(fluxer_svc::metrics::now_ms()).unwrap_or(0))
|
||||
}
|
||||
|
||||
fn wall_now(&self) -> WallMillis {
|
||||
let since_epoch = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default();
|
||||
WallMillis::new(u64::try_from(since_epoch.as_millis()).unwrap_or(u64::MAX))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct TestClockState {
|
||||
monotonic: AtomicU64,
|
||||
wall: AtomicU64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct TestClock {
|
||||
state: Arc<TestClockState>,
|
||||
}
|
||||
|
||||
impl TestClock {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn starting_at(monotonic: Millis, wall: WallMillis) -> Self {
|
||||
Self {
|
||||
state: Arc::new(TestClockState {
|
||||
monotonic: AtomicU64::new(monotonic.get()),
|
||||
wall: AtomicU64::new(wall.get()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shared(&self) -> SharedClock {
|
||||
Arc::new(self.clone())
|
||||
}
|
||||
|
||||
pub fn advance(&self, delta_ms: u64) -> Millis {
|
||||
let previous = self.state.monotonic.fetch_add(delta_ms, Ordering::SeqCst);
|
||||
Millis::new(previous.saturating_add(delta_ms))
|
||||
}
|
||||
|
||||
pub fn advance_wall(&self, delta_ms: u64) -> WallMillis {
|
||||
let previous = self.state.wall.fetch_add(delta_ms, Ordering::SeqCst);
|
||||
WallMillis::new(previous.saturating_add(delta_ms))
|
||||
}
|
||||
|
||||
pub fn set_wall(&self, wall: WallMillis) {
|
||||
self.state.wall.store(wall.get(), Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
impl Clock for TestClock {
|
||||
fn now(&self) -> Millis {
|
||||
Millis::new(self.state.monotonic.load(Ordering::SeqCst))
|
||||
}
|
||||
|
||||
fn wall_now(&self) -> WallMillis {
|
||||
WallMillis::new(self.state.wall.load(Ordering::SeqCst))
|
||||
}
|
||||
}
|
||||
@@ -1,402 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::env;
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::health::ReconMode;
|
||||
use crate::topology::default_region_id;
|
||||
|
||||
const MIN_COVERAGE_TARGET_MS: u64 = 1_000;
|
||||
const MIN_REQUIRED_CORROBORATIONS: u32 = 2;
|
||||
const READ_RPS_RANGE: (u32, u32) = (1, 200);
|
||||
const MUTATE_RPM_RANGE: (u32, u32) = (0, 600);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ConnectionIdGuard {
|
||||
AssumeAbsent,
|
||||
Honoured,
|
||||
}
|
||||
|
||||
impl ConnectionIdGuard {
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"assume_absent" => Some(Self::AssumeAbsent),
|
||||
"honored" | "honoured" => Some(Self::Honoured),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::AssumeAbsent => "assume_absent",
|
||||
Self::Honoured => "honored",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ReconConfig {
|
||||
pub mode: ReconMode,
|
||||
pub worker_threads: usize,
|
||||
pub tick_ms: u64,
|
||||
pub turns_per_tick: usize,
|
||||
pub coverage_target_ms: u64,
|
||||
pub coverage_period_hard_cap_ms: u64,
|
||||
pub expected_rooms: usize,
|
||||
pub hot_period_ms: u64,
|
||||
pub max_hot_rooms: usize,
|
||||
pub suspicion_hold_ms: u64,
|
||||
pub required_corroborations: u32,
|
||||
pub min_corroboration_gap_ms: u64,
|
||||
pub max_corroboration_window_ms: u64,
|
||||
pub min_divergence_ms: u64,
|
||||
pub preflight_max_age_ms: u64,
|
||||
pub warmup_seconds: u64,
|
||||
pub pending_join_skew_ms: u64,
|
||||
pub census_interval_ms: u64,
|
||||
pub census_max_age_ms: u64,
|
||||
pub discovery_interval_ms: u64,
|
||||
pub topology_refresh_ms: u64,
|
||||
pub topology_max_age_ms: u64,
|
||||
pub topology_drain_grace_ms: u64,
|
||||
pub room_cliff_hold_ms: u64,
|
||||
pub server_cliff_hold_ms: u64,
|
||||
pub server_cliff_fraction: f64,
|
||||
pub gateway_read_rps: u32,
|
||||
pub gateway_mutate_rpm: u32,
|
||||
pub livekit_read_rps: u32,
|
||||
pub livekit_read_rps_per_server: u32,
|
||||
pub livekit_mutate_rpm: u32,
|
||||
pub max_inflight_room_turns: usize,
|
||||
pub max_inflight_gateway: usize,
|
||||
pub max_inflight_livekit: usize,
|
||||
pub max_inflight_per_server: usize,
|
||||
pub max_mutations_per_room_per_min: u32,
|
||||
pub max_mutations_per_guild_per_min: u32,
|
||||
pub max_mutations_per_min: u32,
|
||||
pub max_action_attempts: u32,
|
||||
pub max_divergent_fraction: f64,
|
||||
pub breaker_auto_reset_ms: u64,
|
||||
pub max_breaker_auto_resets: u32,
|
||||
pub max_tracked_rooms: usize,
|
||||
pub max_tracked_connections: usize,
|
||||
pub max_connections_per_room: usize,
|
||||
pub memory_budget_bytes: u64,
|
||||
pub dm_media_eviction: bool,
|
||||
pub dm_gateway_eviction: bool,
|
||||
pub gateway_connection_id_guard: ConnectionIdGuard,
|
||||
pub webhook_enabled: bool,
|
||||
pub webhook_port: u16,
|
||||
pub livekit_internal_url: Option<String>,
|
||||
pub livekit_default_region_id: Option<String>,
|
||||
}
|
||||
|
||||
impl ReconConfig {
|
||||
pub fn from_env() -> anyhow::Result<Self> {
|
||||
Self::from_env_reader(|name| env::var(name).ok())
|
||||
}
|
||||
|
||||
pub fn from_env_reader<F>(get: F) -> anyhow::Result<Self>
|
||||
where
|
||||
F: Fn(&str) -> Option<String>,
|
||||
{
|
||||
let shard_count = number(&get, "FLUXER_SVC_SHARD_COUNT", 1u32)?;
|
||||
let shard_id = number(&get, "FLUXER_SVC_SHARD_ID", 0u32)?;
|
||||
if shard_count != 1 || shard_id != 0 {
|
||||
anyhow::bail!(
|
||||
"fluxer-recon runs as a single replica: FLUXER_SVC_SHARD_COUNT must be 1 and FLUXER_SVC_SHARD_ID must be 0, got {shard_count} and {shard_id}"
|
||||
);
|
||||
}
|
||||
|
||||
let config = Self {
|
||||
mode: mode(&get, "FLUXER_RECON_MODE", ReconMode::Observing)?,
|
||||
worker_threads: number(&get, "FLUXER_RECON_WORKER_THREADS", 2)?,
|
||||
tick_ms: number(&get, "FLUXER_RECON_TICK_MS", 50)?,
|
||||
turns_per_tick: number(&get, "FLUXER_RECON_TURNS_PER_TICK", 4)?,
|
||||
coverage_target_ms: number(&get, "FLUXER_RECON_COVERAGE_TARGET_MS", 30_000)?,
|
||||
coverage_period_hard_cap_ms: number(
|
||||
&get,
|
||||
"FLUXER_RECON_COVERAGE_PERIOD_HARD_CAP_MS",
|
||||
120_000,
|
||||
)?,
|
||||
expected_rooms: number(&get, "FLUXER_RECON_EXPECTED_ROOMS", 800)?,
|
||||
hot_period_ms: number(&get, "FLUXER_RECON_HOT_PERIOD_MS", 1_000)?,
|
||||
max_hot_rooms: number(&get, "FLUXER_RECON_MAX_HOT_ROOMS", 64)?,
|
||||
suspicion_hold_ms: number(&get, "FLUXER_RECON_SUSPICION_HOLD_MS", 30_000)?,
|
||||
required_corroborations: number(&get, "FLUXER_RECON_REQUIRED_CORROBORATIONS", 3)?,
|
||||
min_corroboration_gap_ms: number(&get, "FLUXER_RECON_MIN_CORROBORATION_GAP_MS", 1_000)?,
|
||||
max_corroboration_window_ms: number(
|
||||
&get,
|
||||
"FLUXER_RECON_MAX_CORROBORATION_WINDOW_MS",
|
||||
60_000,
|
||||
)?,
|
||||
min_divergence_ms: number(&get, "FLUXER_RECON_MIN_DIVERGENCE_MS", 15_000)?,
|
||||
preflight_max_age_ms: number(&get, "FLUXER_RECON_PREFLIGHT_MAX_AGE_MS", 1_000)?,
|
||||
warmup_seconds: number(&get, "FLUXER_RECON_WARMUP_SECONDS", 120)?,
|
||||
pending_join_skew_ms: number(&get, "FLUXER_RECON_PENDING_JOIN_SKEW_MS", 5_000)?,
|
||||
census_interval_ms: number(&get, "FLUXER_RECON_CENSUS_INTERVAL_MS", 60_000)?,
|
||||
census_max_age_ms: number(&get, "FLUXER_RECON_CENSUS_MAX_AGE_MS", 300_000)?,
|
||||
discovery_interval_ms: number(&get, "FLUXER_RECON_DISCOVERY_INTERVAL_MS", 15_000)?,
|
||||
topology_refresh_ms: number(&get, "FLUXER_RECON_TOPOLOGY_REFRESH_MS", 60_000)?,
|
||||
topology_max_age_ms: number(&get, "FLUXER_RECON_TOPOLOGY_MAX_AGE_MS", 600_000)?,
|
||||
topology_drain_grace_ms: number(&get, "FLUXER_RECON_TOPOLOGY_DRAIN_GRACE_MS", 600_000)?,
|
||||
room_cliff_hold_ms: number(&get, "FLUXER_RECON_ROOM_CLIFF_HOLD_MS", 60_000)?,
|
||||
server_cliff_hold_ms: number(&get, "FLUXER_RECON_SERVER_CLIFF_HOLD_MS", 300_000)?,
|
||||
server_cliff_fraction: number(&get, "FLUXER_RECON_SERVER_CLIFF_FRACTION", 0.5)?,
|
||||
gateway_read_rps: clamped(
|
||||
number(&get, "FLUXER_RECON_GATEWAY_READ_RPS", 40)?,
|
||||
READ_RPS_RANGE,
|
||||
),
|
||||
gateway_mutate_rpm: clamped(
|
||||
number(&get, "FLUXER_RECON_GATEWAY_MUTATE_RPM", 30)?,
|
||||
MUTATE_RPM_RANGE,
|
||||
),
|
||||
livekit_read_rps: clamped(
|
||||
number(&get, "FLUXER_RECON_LIVEKIT_READ_RPS", 40)?,
|
||||
READ_RPS_RANGE,
|
||||
),
|
||||
livekit_read_rps_per_server: clamped(
|
||||
number(&get, "FLUXER_RECON_LIVEKIT_READ_RPS_PER_SERVER", 40)?,
|
||||
READ_RPS_RANGE,
|
||||
),
|
||||
livekit_mutate_rpm: clamped(
|
||||
number(&get, "FLUXER_RECON_LIVEKIT_MUTATE_RPM", 30)?,
|
||||
MUTATE_RPM_RANGE,
|
||||
),
|
||||
max_inflight_room_turns: number(&get, "FLUXER_RECON_MAX_INFLIGHT_ROOM_TURNS", 4)?,
|
||||
max_inflight_gateway: number(&get, "FLUXER_RECON_MAX_INFLIGHT_GATEWAY", 8)?,
|
||||
max_inflight_livekit: number(&get, "FLUXER_RECON_MAX_INFLIGHT_LIVEKIT", 8)?,
|
||||
max_inflight_per_server: number(&get, "FLUXER_RECON_MAX_INFLIGHT_PER_SERVER", 2)?,
|
||||
max_mutations_per_room_per_min: number(
|
||||
&get,
|
||||
"FLUXER_RECON_MAX_MUTATIONS_PER_ROOM_PER_MIN",
|
||||
4,
|
||||
)?,
|
||||
max_mutations_per_guild_per_min: number(
|
||||
&get,
|
||||
"FLUXER_RECON_MAX_MUTATIONS_PER_GUILD_PER_MIN",
|
||||
8,
|
||||
)?,
|
||||
max_mutations_per_min: number(&get, "FLUXER_RECON_MAX_MUTATIONS_PER_MIN", 30)?,
|
||||
max_action_attempts: number(&get, "FLUXER_RECON_MAX_ACTION_ATTEMPTS", 3)?,
|
||||
max_divergent_fraction: number(&get, "FLUXER_RECON_MAX_DIVERGENT_FRACTION", 0.10)?,
|
||||
breaker_auto_reset_ms: number(&get, "FLUXER_RECON_BREAKER_AUTO_RESET_MS", 900_000)?,
|
||||
max_breaker_auto_resets: number(&get, "FLUXER_RECON_MAX_BREAKER_AUTO_RESETS", 2)?,
|
||||
max_tracked_rooms: number(&get, "FLUXER_RECON_MAX_TRACKED_ROOMS", 8_192)?,
|
||||
max_tracked_connections: number(&get, "FLUXER_RECON_MAX_TRACKED_CONNECTIONS", 65_536)?,
|
||||
max_connections_per_room: number(&get, "FLUXER_RECON_MAX_CONNECTIONS_PER_ROOM", 512)?,
|
||||
memory_budget_bytes: number(&get, "FLUXER_RECON_MEMORY_BUDGET_BYTES", 16_777_216)?,
|
||||
dm_media_eviction: boolean(&get, "FLUXER_RECON_DM_MEDIA_EVICTION", false)?,
|
||||
dm_gateway_eviction: boolean(&get, "FLUXER_RECON_DM_GATEWAY_EVICTION", true)?,
|
||||
gateway_connection_id_guard: connection_id_guard(
|
||||
&get,
|
||||
"FLUXER_RECON_GATEWAY_CONNECTION_ID_GUARD",
|
||||
ConnectionIdGuard::AssumeAbsent,
|
||||
)?,
|
||||
webhook_enabled: boolean(&get, "FLUXER_RECON_WEBHOOK_ENABLED", false)?,
|
||||
webhook_port: number(&get, "FLUXER_RECON_WEBHOOK_PORT", 8_092)?,
|
||||
livekit_internal_url: text(&get, "FLUXER_LIVEKIT_INTERNAL_URL"),
|
||||
livekit_default_region_id: text(&get, "FLUXER_LIVEKIT_DEFAULT_REGION")
|
||||
.as_deref()
|
||||
.and_then(default_region_id),
|
||||
};
|
||||
|
||||
validate(&config)?;
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
fn validate(config: &ReconConfig) -> anyhow::Result<()> {
|
||||
positive_usize(config.worker_threads, "FLUXER_RECON_WORKER_THREADS")?;
|
||||
positive_usize(config.turns_per_tick, "FLUXER_RECON_TURNS_PER_TICK")?;
|
||||
positive_usize(
|
||||
config.max_inflight_room_turns,
|
||||
"FLUXER_RECON_MAX_INFLIGHT_ROOM_TURNS",
|
||||
)?;
|
||||
positive_usize(
|
||||
config.max_inflight_gateway,
|
||||
"FLUXER_RECON_MAX_INFLIGHT_GATEWAY",
|
||||
)?;
|
||||
positive_usize(
|
||||
config.max_inflight_livekit,
|
||||
"FLUXER_RECON_MAX_INFLIGHT_LIVEKIT",
|
||||
)?;
|
||||
positive_usize(
|
||||
config.max_inflight_per_server,
|
||||
"FLUXER_RECON_MAX_INFLIGHT_PER_SERVER",
|
||||
)?;
|
||||
positive_usize(config.expected_rooms, "FLUXER_RECON_EXPECTED_ROOMS")?;
|
||||
positive_usize(config.max_tracked_rooms, "FLUXER_RECON_MAX_TRACKED_ROOMS")?;
|
||||
positive_usize(
|
||||
config.max_tracked_connections,
|
||||
"FLUXER_RECON_MAX_TRACKED_CONNECTIONS",
|
||||
)?;
|
||||
positive_usize(
|
||||
config.max_connections_per_room,
|
||||
"FLUXER_RECON_MAX_CONNECTIONS_PER_ROOM",
|
||||
)?;
|
||||
positive_millis(config.tick_ms, "FLUXER_RECON_TICK_MS")?;
|
||||
positive_millis(config.hot_period_ms, "FLUXER_RECON_HOT_PERIOD_MS")?;
|
||||
positive_millis(config.census_interval_ms, "FLUXER_RECON_CENSUS_INTERVAL_MS")?;
|
||||
positive_millis(
|
||||
config.discovery_interval_ms,
|
||||
"FLUXER_RECON_DISCOVERY_INTERVAL_MS",
|
||||
)?;
|
||||
positive_millis(
|
||||
config.topology_refresh_ms,
|
||||
"FLUXER_RECON_TOPOLOGY_REFRESH_MS",
|
||||
)?;
|
||||
positive_millis(
|
||||
config.preflight_max_age_ms,
|
||||
"FLUXER_RECON_PREFLIGHT_MAX_AGE_MS",
|
||||
)?;
|
||||
positive_millis(
|
||||
config.min_corroboration_gap_ms,
|
||||
"FLUXER_RECON_MIN_CORROBORATION_GAP_MS",
|
||||
)?;
|
||||
fraction(
|
||||
config.server_cliff_fraction,
|
||||
"FLUXER_RECON_SERVER_CLIFF_FRACTION",
|
||||
)?;
|
||||
fraction(
|
||||
config.max_divergent_fraction,
|
||||
"FLUXER_RECON_MAX_DIVERGENT_FRACTION",
|
||||
)?;
|
||||
|
||||
if config.required_corroborations < MIN_REQUIRED_CORROBORATIONS {
|
||||
anyhow::bail!(
|
||||
"FLUXER_RECON_REQUIRED_CORROBORATIONS must be at least {MIN_REQUIRED_CORROBORATIONS}, got {}",
|
||||
config.required_corroborations
|
||||
);
|
||||
}
|
||||
|
||||
let spread = config
|
||||
.min_corroboration_gap_ms
|
||||
.saturating_mul(u64::from(config.required_corroborations.saturating_sub(1)));
|
||||
if config.max_corroboration_window_ms <= spread {
|
||||
anyhow::bail!(
|
||||
"FLUXER_RECON_MAX_CORROBORATION_WINDOW_MS ({}) must exceed the gap times one fewer than the required corroborations ({spread})",
|
||||
config.max_corroboration_window_ms
|
||||
);
|
||||
}
|
||||
|
||||
if config.coverage_target_ms < MIN_COVERAGE_TARGET_MS {
|
||||
anyhow::bail!(
|
||||
"FLUXER_RECON_COVERAGE_TARGET_MS must be at least {MIN_COVERAGE_TARGET_MS}, got {}",
|
||||
config.coverage_target_ms
|
||||
);
|
||||
}
|
||||
|
||||
if config.coverage_target_ms > config.coverage_period_hard_cap_ms {
|
||||
anyhow::bail!(
|
||||
"FLUXER_RECON_COVERAGE_TARGET_MS ({}) must not exceed FLUXER_RECON_COVERAGE_PERIOD_HARD_CAP_MS ({})",
|
||||
config.coverage_target_ms,
|
||||
config.coverage_period_hard_cap_ms
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn text<F>(get: &F, name: &str) -> Option<String>
|
||||
where
|
||||
F: Fn(&str) -> Option<String>,
|
||||
{
|
||||
get(name)
|
||||
.map(|value| value.trim().to_owned())
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn positive_usize(value: usize, name: &str) -> anyhow::Result<()> {
|
||||
if value == 0 {
|
||||
anyhow::bail!("{name} must be greater than zero");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn positive_millis(value: u64, name: &str) -> anyhow::Result<()> {
|
||||
if value == 0 {
|
||||
anyhow::bail!("{name} must be greater than zero");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fraction(value: f64, name: &str) -> anyhow::Result<()> {
|
||||
if !(value > 0.0 && value <= 1.0) {
|
||||
anyhow::bail!("{name} must lie in (0, 1], got {value}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clamped(value: u32, range: (u32, u32)) -> u32 {
|
||||
let (low, high) = range;
|
||||
value.clamp(low, high)
|
||||
}
|
||||
|
||||
fn read<F>(get: &F, name: &str) -> Option<String>
|
||||
where
|
||||
F: Fn(&str) -> Option<String>,
|
||||
{
|
||||
get(name).filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn number<F, T>(get: &F, name: &str, default: T) -> anyhow::Result<T>
|
||||
where
|
||||
F: Fn(&str) -> Option<String>,
|
||||
T: FromStr,
|
||||
T::Err: Display,
|
||||
{
|
||||
match read(get, name) {
|
||||
None => Ok(default),
|
||||
Some(raw) => raw
|
||||
.trim()
|
||||
.parse::<T>()
|
||||
.map_err(|error| anyhow::anyhow!("invalid {name}: {raw} ({error})")),
|
||||
}
|
||||
}
|
||||
|
||||
fn boolean<F>(get: &F, name: &str, default: bool) -> anyhow::Result<bool>
|
||||
where
|
||||
F: Fn(&str) -> Option<String>,
|
||||
{
|
||||
match read(get, name) {
|
||||
None => Ok(default),
|
||||
Some(raw) => parse_bool(&raw)
|
||||
.ok_or_else(|| anyhow::anyhow!("invalid {name}: {raw} is not a boolean")),
|
||||
}
|
||||
}
|
||||
|
||||
fn mode<F>(get: &F, name: &str, default: ReconMode) -> anyhow::Result<ReconMode>
|
||||
where
|
||||
F: Fn(&str) -> Option<String>,
|
||||
{
|
||||
match read(get, name) {
|
||||
None => Ok(default),
|
||||
Some(raw) => ReconMode::parse(&raw).ok_or_else(|| anyhow::anyhow!("invalid {name}: {raw}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn connection_id_guard<F>(
|
||||
get: &F,
|
||||
name: &str,
|
||||
default: ConnectionIdGuard,
|
||||
) -> anyhow::Result<ConnectionIdGuard>
|
||||
where
|
||||
F: Fn(&str) -> Option<String>,
|
||||
{
|
||||
match read(get, name) {
|
||||
None => Ok(default),
|
||||
Some(raw) => {
|
||||
ConnectionIdGuard::parse(&raw).ok_or_else(|| anyhow::anyhow!("invalid {name}: {raw}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_bool(value: &str) -> Option<bool> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"1" | "true" | "yes" | "y" | "on" => Some(true),
|
||||
"0" | "false" | "no" | "n" | "off" => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1,784 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use fluxer_svc::transport::{Transport, TransportMessage, TransportSubscriber, reply_message};
|
||||
|
||||
use crate::budget::BudgetClass;
|
||||
use crate::health::{ModeClamp, ReconMode};
|
||||
use crate::ids::{ChannelId, GuildId, Millis, RoomKey};
|
||||
use crate::runtime::{Shared, monotonic_now};
|
||||
use crate::singleton::{RESUBSCRIBE_BACKOFF_MS, next_backoff_ms};
|
||||
use crate::suspicion::SuspicionSource;
|
||||
|
||||
pub const CONTROL_SUBJECT: &str = "svc.recon";
|
||||
pub const INSTANCE_SUBJECT: &str = "svc.recon.instance";
|
||||
pub const EXPLAIN_DECISION_LIMIT: usize = 16;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(untagged)]
|
||||
pub enum RawId {
|
||||
Number(u64),
|
||||
Text(String),
|
||||
}
|
||||
|
||||
impl RawId {
|
||||
pub fn value(&self) -> Option<u64> {
|
||||
match self {
|
||||
Self::Number(value) => Some(*value),
|
||||
Self::Text(text) => parse_u64_strict(text),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_u64_strict(value: &str) -> Option<u64> {
|
||||
if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
|
||||
return None;
|
||||
}
|
||||
value.parse::<u64>().ok()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(tag = "method", rename_all = "snake_case")]
|
||||
pub enum ControlRequest {
|
||||
Status,
|
||||
Explain {
|
||||
#[serde(default)]
|
||||
guild_id: Option<RawId>,
|
||||
channel_id: RawId,
|
||||
},
|
||||
Mode {
|
||||
#[serde(default)]
|
||||
mode: Option<String>,
|
||||
},
|
||||
Budget,
|
||||
Topology {
|
||||
#[serde(default)]
|
||||
refresh: Option<bool>,
|
||||
},
|
||||
Clamps {
|
||||
#[serde(default)]
|
||||
release: Option<String>,
|
||||
},
|
||||
Suspect {
|
||||
#[serde(default)]
|
||||
guild_id: Option<RawId>,
|
||||
channel_id: RawId,
|
||||
},
|
||||
Cliffs {
|
||||
#[serde(default)]
|
||||
release: Option<bool>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ControlError {
|
||||
DecodeFailed,
|
||||
UnknownMode,
|
||||
UnknownClamp,
|
||||
InvalidChannelId,
|
||||
InvalidGuildId,
|
||||
}
|
||||
|
||||
impl ControlError {
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::DecodeFailed => "decode_failed",
|
||||
Self::UnknownMode => "unknown_mode",
|
||||
Self::UnknownClamp => "unknown_clamp",
|
||||
Self::InvalidChannelId => "invalid_channel_id",
|
||||
Self::InvalidGuildId => "invalid_guild_id",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct ErrorResponse {
|
||||
pub ok: bool,
|
||||
pub error: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct ModeResponse {
|
||||
pub ok: bool,
|
||||
pub configured: &'static str,
|
||||
pub runtime_override: Option<&'static str>,
|
||||
pub effective: &'static str,
|
||||
pub clamps: Vec<&'static str>,
|
||||
pub changed: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct ServerHealthEntry {
|
||||
pub region: String,
|
||||
pub server: String,
|
||||
pub health: &'static str,
|
||||
pub consecutive_failures: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct PeerEntry {
|
||||
pub instance_id: String,
|
||||
pub started_at: u64,
|
||||
pub first_seen_ms_ago: u64,
|
||||
pub last_seen_ms_ago: u64,
|
||||
pub detections: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct StatusResponse {
|
||||
pub ok: bool,
|
||||
pub instance_id: String,
|
||||
pub started_at: u64,
|
||||
pub uptime_ms: u64,
|
||||
pub ready: bool,
|
||||
pub draining: bool,
|
||||
pub configured_mode: &'static str,
|
||||
pub runtime_override: Option<&'static str>,
|
||||
pub effective_mode: &'static str,
|
||||
pub clamps: Vec<&'static str>,
|
||||
pub warmup_elapsed: bool,
|
||||
pub coverage_pass_complete: bool,
|
||||
pub tracked_rooms: usize,
|
||||
pub tracked_connections: usize,
|
||||
pub tracked_bytes: u64,
|
||||
pub divergent_connections: usize,
|
||||
pub divergent_fraction: f64,
|
||||
pub coverage_period_ms: u64,
|
||||
pub breaker_tripped: bool,
|
||||
pub breaker_trips_total: u64,
|
||||
pub breaker_auto_resets_used: u32,
|
||||
pub breaker_held_for_manual_reset: bool,
|
||||
pub mutations_paused_ms_remaining: u64,
|
||||
pub topology_age_ms: Option<u64>,
|
||||
pub topology_servers: usize,
|
||||
pub census_age_ms: Option<u64>,
|
||||
pub last_turn: u64,
|
||||
pub servers: Vec<ServerHealthEntry>,
|
||||
pub peer: Option<PeerEntry>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct BucketEntry {
|
||||
pub class: &'static str,
|
||||
pub tokens: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct DenialEntry {
|
||||
pub reason: &'static str,
|
||||
pub count: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct BudgetResponse {
|
||||
pub ok: bool,
|
||||
pub effective_mode: &'static str,
|
||||
pub buckets: Vec<BucketEntry>,
|
||||
pub mutations_issued_total: u64,
|
||||
pub mutations_last_minute: u32,
|
||||
pub max_mutations_per_min: u32,
|
||||
pub max_action_attempts: u32,
|
||||
pub pending_actions: usize,
|
||||
pub scheduler_starved_total: u64,
|
||||
pub ledger_evicted_total: u64,
|
||||
pub denials: Vec<DenialEntry>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct ConnectionDump {
|
||||
pub connection_id: String,
|
||||
pub user_id: String,
|
||||
pub state: &'static str,
|
||||
pub corroborations: u32,
|
||||
pub divergence_since_ms: Option<u64>,
|
||||
pub fingerprint: Option<String>,
|
||||
pub last_seen_ms_ago: u64,
|
||||
pub action_attempts: u8,
|
||||
pub repair_attempts: u8,
|
||||
pub last_repair_verdict: Option<&'static str>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct ExplainResponse {
|
||||
pub ok: bool,
|
||||
pub scope: &'static str,
|
||||
pub guild_id: Option<String>,
|
||||
pub channel_id: String,
|
||||
pub tracked: bool,
|
||||
pub last_turn: Option<u64>,
|
||||
pub last_seen_ms_ago: Option<u64>,
|
||||
pub partially_unreadable: bool,
|
||||
pub no_candidates: bool,
|
||||
pub gateway_cliff_since_ms_ago: Option<u64>,
|
||||
pub media_cliff_since_ms_ago: Option<u64>,
|
||||
pub last_known_locations: Vec<String>,
|
||||
pub connections: Vec<ConnectionDump>,
|
||||
pub decisions: Vec<crate::observe::DecisionRecord>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct ClampEntry {
|
||||
pub clamp: &'static str,
|
||||
pub engaged: bool,
|
||||
pub self_clearing: bool,
|
||||
pub expires_in_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct ClampsResponse {
|
||||
pub ok: bool,
|
||||
pub configured_mode: &'static str,
|
||||
pub effective_mode: &'static str,
|
||||
pub ceiling: &'static str,
|
||||
pub clamps: Vec<ClampEntry>,
|
||||
pub released: Vec<&'static str>,
|
||||
pub breaker_tripped: bool,
|
||||
pub breaker_auto_resets_used: u32,
|
||||
pub breaker_held_for_manual_reset: bool,
|
||||
pub mutations_paused_windows: u32,
|
||||
pub mutations_paused_hold_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct CliffsResponse {
|
||||
pub ok: bool,
|
||||
pub cliffed: Vec<String>,
|
||||
pub disputed: Vec<String>,
|
||||
pub holed: Vec<String>,
|
||||
pub released: Vec<String>,
|
||||
pub hold_ms: u64,
|
||||
pub tripped_total: u64,
|
||||
pub disputed_total: u64,
|
||||
pub holes_total: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct SuspectResponse {
|
||||
pub ok: bool,
|
||||
pub scope: &'static str,
|
||||
pub guild_id: Option<String>,
|
||||
pub channel_id: String,
|
||||
pub admitted: &'static str,
|
||||
pub raised: &'static str,
|
||||
pub hold_ms: u64,
|
||||
pub suspect_rooms: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct TopologyResponse {
|
||||
pub ok: bool,
|
||||
pub servers: usize,
|
||||
pub age_ms: Option<u64>,
|
||||
pub max_age_ms: u64,
|
||||
pub stale: bool,
|
||||
pub refresh_requested: bool,
|
||||
}
|
||||
|
||||
fn clamp_labels(shared: &Shared) -> Vec<&'static str> {
|
||||
shared.with_state(|state| {
|
||||
state
|
||||
.governor
|
||||
.clamps()
|
||||
.engaged()
|
||||
.map(ModeClamp::label)
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
fn error(code: ControlError) -> serde_json::Value {
|
||||
serde_json::json!(ErrorResponse {
|
||||
ok: false,
|
||||
error: code.label(),
|
||||
})
|
||||
}
|
||||
|
||||
fn room_key(guild_id: Option<&RawId>, channel_id: &RawId) -> Result<RoomKey, ControlError> {
|
||||
let channel = channel_id.value().ok_or(ControlError::InvalidChannelId)?;
|
||||
match guild_id {
|
||||
None => Ok(RoomKey::Dm {
|
||||
channel_id: ChannelId::new(channel),
|
||||
}),
|
||||
Some(raw) => {
|
||||
let guild = raw.value().ok_or(ControlError::InvalidGuildId)?;
|
||||
Ok(RoomKey::Guild {
|
||||
guild_id: GuildId::new(guild),
|
||||
channel_id: ChannelId::new(channel),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(shared: &Shared, now: Millis) -> StatusResponse {
|
||||
shared.with_state(|state| StatusResponse {
|
||||
ok: true,
|
||||
instance_id: shared.instance().id.clone(),
|
||||
started_at: shared.instance().started_at,
|
||||
uptime_ms: now.saturating_since(shared.boot_at()),
|
||||
ready: shared.readiness().is_ready(),
|
||||
draining: shared.readiness().is_draining(),
|
||||
configured_mode: state.governor.configured_mode().as_str(),
|
||||
runtime_override: state.governor.runtime_override().map(ReconMode::as_str),
|
||||
effective_mode: state.governor.effective_mode().as_str(),
|
||||
clamps: state
|
||||
.governor
|
||||
.clamps()
|
||||
.engaged()
|
||||
.map(ModeClamp::label)
|
||||
.collect(),
|
||||
warmup_elapsed: state.governor.warmup().elapsed_complete(now),
|
||||
coverage_pass_complete: state.governor.warmup().coverage_pass_complete(),
|
||||
tracked_rooms: state.ledger.tracked_rooms(),
|
||||
tracked_connections: state.ledger.tracked_connections(),
|
||||
tracked_bytes: state.ledger.tracked_bytes(),
|
||||
divergent_connections: state.ledger.divergent_connections(),
|
||||
divergent_fraction: state.ledger.divergent_fraction(),
|
||||
coverage_period_ms: state.coverage_period_ms,
|
||||
breaker_tripped: state.governor.breaker().is_tripped(),
|
||||
breaker_trips_total: state.governor.breaker().trips_total(),
|
||||
breaker_auto_resets_used: state.governor.breaker().auto_resets_used(),
|
||||
breaker_held_for_manual_reset: state
|
||||
.governor
|
||||
.breaker()
|
||||
.held_for_manual_reset(state.governor.limits().breaker),
|
||||
mutations_paused_ms_remaining: state.mutations_pause.remaining_ms(now),
|
||||
topology_age_ms: state.topology_age_ms(now),
|
||||
topology_servers: state.topology_servers,
|
||||
census_age_ms: state.census_age_ms(now),
|
||||
last_turn: state.turn.get(),
|
||||
servers: state
|
||||
.server_health
|
||||
.tracked()
|
||||
.map(|(location, tracker)| ServerHealthEntry {
|
||||
region: location.region.to_string(),
|
||||
server: location.server.to_string(),
|
||||
health: tracker.state().label(),
|
||||
consecutive_failures: tracker.consecutive_failures(),
|
||||
})
|
||||
.collect(),
|
||||
peer: state.peer.as_ref().map(|peer| PeerEntry {
|
||||
instance_id: peer.instance_id.clone(),
|
||||
started_at: peer.started_at,
|
||||
first_seen_ms_ago: now.saturating_since(peer.first_seen_at),
|
||||
last_seen_ms_ago: now.saturating_since(peer.last_seen_at),
|
||||
detections: peer.detections,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn budget(shared: &Shared, now: Millis) -> BudgetResponse {
|
||||
let metrics = shared.metrics().clone();
|
||||
shared.with_state_mut(|state| {
|
||||
let buckets = [
|
||||
BudgetClass::GatewayRead,
|
||||
BudgetClass::GatewayMutate,
|
||||
BudgetClass::LiveKitRead,
|
||||
BudgetClass::LiveKitMutate,
|
||||
]
|
||||
.into_iter()
|
||||
.map(|class| BucketEntry {
|
||||
class: class.label(),
|
||||
tokens: state.governor.tokens(class, now),
|
||||
})
|
||||
.collect();
|
||||
|
||||
BudgetResponse {
|
||||
ok: true,
|
||||
effective_mode: state.governor.effective_mode().as_str(),
|
||||
buckets,
|
||||
mutations_issued_total: state.governor.mutations().issued_total(),
|
||||
mutations_last_minute: state.governor.mutations().global_count(now),
|
||||
max_mutations_per_min: state.governor.limits().max_mutations_per_min,
|
||||
max_action_attempts: state.governor.limits().max_action_attempts,
|
||||
pending_actions: state.governor.pending_actions(),
|
||||
scheduler_starved_total: metrics.scheduler_starved_total(),
|
||||
ledger_evicted_total: metrics.ledger_evicted_total(),
|
||||
denials: state
|
||||
.governor
|
||||
.denials()
|
||||
.map(|(reason, count)| DenialEntry {
|
||||
reason: reason.label(),
|
||||
count: *count,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn explain(shared: &Shared, room: RoomKey, now: Millis) -> ExplainResponse {
|
||||
let decisions = shared.journal().for_room(room, EXPLAIN_DECISION_LIMIT);
|
||||
shared.with_state(|state| {
|
||||
let entry = state.ledger.room(&room);
|
||||
ExplainResponse {
|
||||
ok: true,
|
||||
scope: crate::metrics::scope_label(room.scope()),
|
||||
guild_id: room.guild_id().map(|guild| guild.to_string()),
|
||||
channel_id: room.channel_id().to_string(),
|
||||
tracked: entry.is_some(),
|
||||
last_turn: entry.map(|room| room.last_turn().get()),
|
||||
last_seen_ms_ago: entry.map(|room| now.saturating_since(room.last_seen())),
|
||||
partially_unreadable: entry.is_some_and(|room| room.partially_unreadable()),
|
||||
no_candidates: entry.is_some_and(|room| room.no_candidates()),
|
||||
gateway_cliff_since_ms_ago: entry
|
||||
.and_then(|room| room.gateway_cliff().since())
|
||||
.map(|since| now.saturating_since(since)),
|
||||
media_cliff_since_ms_ago: entry
|
||||
.and_then(|room| room.media_cliff().since())
|
||||
.map(|since| now.saturating_since(since)),
|
||||
last_known_locations: entry
|
||||
.map(|room| {
|
||||
room.last_known_locations()
|
||||
.iter()
|
||||
.filter_map(|index| state.ledger.location(*index))
|
||||
.map(ToString::to_string)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
connections: entry
|
||||
.map(|room| {
|
||||
room.connections()
|
||||
.iter()
|
||||
.map(|connection| ConnectionDump {
|
||||
connection_id: connection.connection.to_string(),
|
||||
user_id: connection.user_id.to_string(),
|
||||
state: connection.state.label(),
|
||||
corroborations: connection.state.corroborations(),
|
||||
divergence_since_ms: connection
|
||||
.state
|
||||
.divergence_since()
|
||||
.map(|since| now.saturating_since(since)),
|
||||
fingerprint: connection
|
||||
.state
|
||||
.stored_fingerprint()
|
||||
.map(|fingerprint| format!("{:016x}", fingerprint.get())),
|
||||
last_seen_ms_ago: now.saturating_since(connection.last_seen),
|
||||
action_attempts: connection.action_attempts,
|
||||
repair_attempts: connection.repair_attempts,
|
||||
last_repair_verdict: connection
|
||||
.last_repair_verdict
|
||||
.map(|verdict| verdict.label()),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
decisions,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_mode(shared: &Shared, requested: Option<&str>) -> Result<ModeResponse, ControlError> {
|
||||
let parsed = match requested {
|
||||
None => None,
|
||||
Some(raw) => Some(ReconMode::parse(raw).ok_or(ControlError::UnknownMode)?),
|
||||
};
|
||||
|
||||
let transition = shared.set_mode_override(parsed);
|
||||
|
||||
tracing::warn!(
|
||||
configured = transition.configured.as_str(),
|
||||
previous_override = transition.previous_override.map(ReconMode::as_str),
|
||||
requested = transition.requested.map(ReconMode::as_str),
|
||||
effective_before = transition.effective_before.as_str(),
|
||||
effective_after = transition.effective_after.as_str(),
|
||||
"recon mode override applied over the control plane"
|
||||
);
|
||||
|
||||
Ok(ModeResponse {
|
||||
ok: true,
|
||||
configured: transition.configured.as_str(),
|
||||
runtime_override: transition.requested.map(ReconMode::as_str),
|
||||
effective: transition.effective_after.as_str(),
|
||||
clamps: clamp_labels(shared),
|
||||
changed: transition.changed(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_mode(shared: &Shared) -> ModeResponse {
|
||||
shared.with_state(|state| ModeResponse {
|
||||
ok: true,
|
||||
configured: state.governor.configured_mode().as_str(),
|
||||
runtime_override: state.governor.runtime_override().map(ReconMode::as_str),
|
||||
effective: state.governor.effective_mode().as_str(),
|
||||
clamps: state
|
||||
.governor
|
||||
.clamps()
|
||||
.engaged()
|
||||
.map(ModeClamp::label)
|
||||
.collect(),
|
||||
changed: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn topology(shared: &Shared, refresh: bool, now: Millis) -> TopologyResponse {
|
||||
let max_age_ms = shared.config().topology_max_age_ms;
|
||||
shared.with_state_mut(|state| {
|
||||
if refresh {
|
||||
state.topology_refresh_requested = true;
|
||||
}
|
||||
let age_ms = state.topology_age_ms(now);
|
||||
TopologyResponse {
|
||||
ok: true,
|
||||
servers: state.topology_servers,
|
||||
age_ms,
|
||||
max_age_ms,
|
||||
stale: age_ms.is_none_or(|age| age > max_age_ms),
|
||||
refresh_requested: state.topology_refresh_requested,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn release_clamp(shared: &Shared, clamp: ModeClamp) -> bool {
|
||||
shared.with_state_mut(|state| {
|
||||
let was_engaged = state.governor.clamps().is_engaged(clamp);
|
||||
match clamp {
|
||||
ModeClamp::BreakerTripped => {
|
||||
state.governor.reset_breaker();
|
||||
}
|
||||
ModeClamp::MutationsPaused => state.mutations_pause.clear(),
|
||||
ModeClamp::Warmup => state.governor.warmup_mut().force_complete(),
|
||||
ModeClamp::ColdStart => state.governor.warmup_mut().note_coverage_pass(),
|
||||
ModeClamp::SingletonConflict => state.peer = None,
|
||||
ModeClamp::TopologyStale | ModeClamp::NatsReconnect | ModeClamp::CoverageOverrun => {}
|
||||
}
|
||||
state.governor.clamps_mut().release(clamp);
|
||||
was_engaged
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clamps(
|
||||
shared: &Shared,
|
||||
release: Option<&str>,
|
||||
now: Millis,
|
||||
) -> Result<ClampsResponse, ControlError> {
|
||||
let wanted: Vec<ModeClamp> = match release {
|
||||
None => Vec::new(),
|
||||
Some(name) if name.trim().eq_ignore_ascii_case("all") => ModeClamp::ALL.to_vec(),
|
||||
Some(name) => vec![ModeClamp::parse(name).ok_or(ControlError::UnknownClamp)?],
|
||||
};
|
||||
|
||||
let mut released: Vec<&'static str> = Vec::new();
|
||||
for clamp in wanted {
|
||||
if release_clamp(shared, clamp) {
|
||||
released.push(clamp.label());
|
||||
}
|
||||
}
|
||||
|
||||
if !released.is_empty() {
|
||||
tracing::warn!(
|
||||
released = ?released,
|
||||
"an operator released mode clamps over the control plane, so the effective mode may \
|
||||
rise before the condition that engaged them is gone"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(shared.with_state_mut(|state| {
|
||||
let limits = state.governor.limits().breaker;
|
||||
ClampsResponse {
|
||||
ok: true,
|
||||
configured_mode: state.governor.configured_mode().as_str(),
|
||||
effective_mode: state.governor.effective_mode().as_str(),
|
||||
ceiling: state.governor.clamps().ceiling().as_str(),
|
||||
clamps: ModeClamp::ALL
|
||||
.into_iter()
|
||||
.map(|clamp| ClampEntry {
|
||||
clamp: clamp.label(),
|
||||
engaged: state.governor.clamps().is_engaged(clamp),
|
||||
self_clearing: clamp.is_self_clearing(),
|
||||
expires_in_ms: state
|
||||
.governor
|
||||
.clamps()
|
||||
.deadline(clamp)
|
||||
.map(|until| until.saturating_since(now)),
|
||||
})
|
||||
.collect(),
|
||||
released,
|
||||
breaker_tripped: state.governor.breaker().is_tripped(),
|
||||
breaker_auto_resets_used: state.governor.breaker().auto_resets_used(),
|
||||
breaker_held_for_manual_reset: state.governor.breaker().held_for_manual_reset(limits),
|
||||
mutations_paused_windows: state.mutations_pause.windows(),
|
||||
mutations_paused_hold_ms: state.mutations_pause.hold_ms(),
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn cliffs(shared: &Shared, release: bool, now: Millis) -> CliffsResponse {
|
||||
let hold_ms = shared.config().server_cliff_hold_ms;
|
||||
let response = shared.with_state_mut(|state| {
|
||||
let cliffed = state.cliffs.cliffed(now);
|
||||
let disputed = state.cliffs.disputed();
|
||||
let holed = state.suspicion.holed_servers();
|
||||
let mut released: Vec<String> = Vec::new();
|
||||
|
||||
if release {
|
||||
for location in cliffed.iter().chain(holed.iter()) {
|
||||
let forgotten = state.cliffs.forget(location);
|
||||
let filled = state.suspicion.clear_hole(location);
|
||||
if forgotten || filled {
|
||||
released.push(location.to_string());
|
||||
}
|
||||
}
|
||||
released.sort();
|
||||
released.dedup();
|
||||
}
|
||||
|
||||
CliffsResponse {
|
||||
ok: true,
|
||||
cliffed: cliffed.iter().map(ToString::to_string).collect(),
|
||||
disputed: disputed.iter().map(ToString::to_string).collect(),
|
||||
holed: holed.iter().map(ToString::to_string).collect(),
|
||||
released,
|
||||
hold_ms,
|
||||
tripped_total: state.cliffs.tripped_total(),
|
||||
disputed_total: state.cliffs.disputed_total(),
|
||||
holes_total: state.suspicion.holes_total(),
|
||||
}
|
||||
});
|
||||
|
||||
if !response.released.is_empty() {
|
||||
tracing::warn!(
|
||||
released = ?response.released,
|
||||
"an operator dropped server holds over the control plane, so those servers read as \
|
||||
authoritative again before their hold window ended"
|
||||
);
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
pub fn suspect(shared: &Shared, room: RoomKey, now: Millis) -> SuspectResponse {
|
||||
let (admitted, raised, suspect_rooms, hold_ms) = shared.with_state_mut(|state| {
|
||||
let admitted = state.directory.note_suspicion(room, now);
|
||||
let raised = state
|
||||
.suspicion
|
||||
.note_room(room, SuspicionSource::Control, now);
|
||||
(
|
||||
admitted.label(),
|
||||
raised.label(),
|
||||
state.suspicion.suspect_rooms(now).len(),
|
||||
state.suspicion.limits().hold_ms,
|
||||
)
|
||||
});
|
||||
|
||||
tracing::info!(
|
||||
channel_id = room.channel_id().get(),
|
||||
guild_id = room.guild_id().map(|guild| guild.get()),
|
||||
admitted,
|
||||
raised,
|
||||
"an operator raised suspicion over the control plane, which only reorders reads"
|
||||
);
|
||||
|
||||
SuspectResponse {
|
||||
ok: true,
|
||||
scope: crate::metrics::scope_label(room.scope()),
|
||||
guild_id: room.guild_id().map(|guild| guild.to_string()),
|
||||
channel_id: room.channel_id().to_string(),
|
||||
admitted,
|
||||
raised,
|
||||
hold_ms,
|
||||
suspect_rooms,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dispatch(shared: &Shared, request: &ControlRequest, now: Millis) -> serde_json::Value {
|
||||
match request {
|
||||
ControlRequest::Status => serde_json::json!(status(shared, now)),
|
||||
ControlRequest::Budget => serde_json::json!(budget(shared, now)),
|
||||
ControlRequest::Mode { mode } => match mode {
|
||||
None => serde_json::json!(read_mode(shared)),
|
||||
Some(raw) => match set_mode(shared, Some(raw)) {
|
||||
Ok(response) => serde_json::json!(response),
|
||||
Err(code) => error(code),
|
||||
},
|
||||
},
|
||||
ControlRequest::Explain {
|
||||
guild_id,
|
||||
channel_id,
|
||||
} => match room_key(guild_id.as_ref(), channel_id) {
|
||||
Ok(room) => serde_json::json!(explain(shared, room, now)),
|
||||
Err(code) => error(code),
|
||||
},
|
||||
ControlRequest::Topology { refresh } => {
|
||||
serde_json::json!(topology(shared, refresh.unwrap_or(false), now))
|
||||
}
|
||||
ControlRequest::Clamps { release } => match clamps(shared, release.as_deref(), now) {
|
||||
Ok(response) => serde_json::json!(response),
|
||||
Err(code) => error(code),
|
||||
},
|
||||
ControlRequest::Suspect {
|
||||
guild_id,
|
||||
channel_id,
|
||||
} => match room_key(guild_id.as_ref(), channel_id) {
|
||||
Ok(room) => serde_json::json!(suspect(shared, room, now)),
|
||||
Err(code) => error(code),
|
||||
},
|
||||
ControlRequest::Cliffs { release } => {
|
||||
serde_json::json!(cliffs(shared, release.unwrap_or(false), now))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle(shared: &Shared, payload: &[u8], now: Millis) -> Vec<u8> {
|
||||
let response = match serde_json::from_slice::<ControlRequest>(payload) {
|
||||
Ok(request) => dispatch(shared, &request, now),
|
||||
Err(parse_error) => {
|
||||
tracing::debug!(error = %parse_error, "rejecting an undecodable control request");
|
||||
error(ControlError::DecodeFailed)
|
||||
}
|
||||
};
|
||||
serde_json::to_vec(&response)
|
||||
.unwrap_or_else(|_| b"{\"ok\":false,\"error\":\"encode_failed\"}".to_vec())
|
||||
}
|
||||
|
||||
pub async fn run_control<T: Transport>(shared: Shared, transport: T) -> anyhow::Result<()> {
|
||||
let mut backoff_ms = RESUBSCRIBE_BACKOFF_MS;
|
||||
|
||||
loop {
|
||||
let mut subscription = match transport.subscribe(CONTROL_SUBJECT).await {
|
||||
Ok(subscription) => {
|
||||
backoff_ms = RESUBSCRIBE_BACKOFF_MS;
|
||||
subscription
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
error = %error,
|
||||
subject = CONTROL_SUBJECT,
|
||||
retry_in_ms = backoff_ms,
|
||||
"the control plane could not subscribe, retrying rather than ending the task, \
|
||||
because the escape hatch for a clamped service must outlive a broken \
|
||||
subscription"
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(backoff_ms)).await;
|
||||
backoff_ms = next_backoff_ms(backoff_ms);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
subject = CONTROL_SUBJECT,
|
||||
instance_id = shared.instance().id,
|
||||
"recon control plane listening"
|
||||
);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
message = subscription.next() => {
|
||||
let Some(message) = message else {
|
||||
tracing::warn!("control subscription ended, will re-subscribe");
|
||||
break;
|
||||
};
|
||||
if !message.has_reply() {
|
||||
continue;
|
||||
}
|
||||
let response = handle(&shared, message.payload(), monotonic_now());
|
||||
if let Err(error) = reply_message(&message, &transport, &response).await {
|
||||
tracing::debug!(error = %error, "failed to answer a control request");
|
||||
}
|
||||
}
|
||||
_ = transport.wait_for_reconnect() => {
|
||||
tracing::info!("NATS reconnected, re-subscribing the control plane");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,873 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::budget::BudgetView;
|
||||
use crate::config::ReconConfig;
|
||||
use crate::evidence::{PendingJoin, Presence, RoomObservation, Side, UnknownReason};
|
||||
use crate::gateway::Nonce;
|
||||
use crate::guards::{AbortReason, DmPosture};
|
||||
use crate::ids::{ConnectionId, ConnectionKey, Location, Millis, RoomKey, Scope, TurnId, UserId};
|
||||
use crate::ledger::{
|
||||
ActionKind, ConnectionState, Fingerprint, FingerprintInput, Ledger, LocationIndex,
|
||||
RepairVerdict, WedgedReason, fingerprint,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct DecideParams {
|
||||
pub required_corroborations: u32,
|
||||
pub min_corroboration_gap_ms: u64,
|
||||
pub max_corroboration_window_ms: u64,
|
||||
pub min_divergence_ms: u64,
|
||||
pub max_action_attempts: u32,
|
||||
pub pending_join_skew_ms: u64,
|
||||
pub dm_posture: DmPosture,
|
||||
}
|
||||
|
||||
impl Default for DecideParams {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
required_corroborations: 3,
|
||||
min_corroboration_gap_ms: 1_000,
|
||||
max_corroboration_window_ms: 60_000,
|
||||
min_divergence_ms: 15_000,
|
||||
max_action_attempts: 3,
|
||||
pending_join_skew_ms: 5_000,
|
||||
dm_posture: DmPosture::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DecideParams {
|
||||
pub const fn from_config(config: &ReconConfig) -> Self {
|
||||
Self {
|
||||
required_corroborations: config.required_corroborations,
|
||||
min_corroboration_gap_ms: config.min_corroboration_gap_ms,
|
||||
max_corroboration_window_ms: config.max_corroboration_window_ms,
|
||||
min_divergence_ms: config.min_divergence_ms,
|
||||
max_action_attempts: config.max_action_attempts,
|
||||
pending_join_skew_ms: config.pending_join_skew_ms,
|
||||
dm_posture: DmPosture {
|
||||
dm_gateway_eviction: config.dm_gateway_eviction,
|
||||
dm_media_eviction: config.dm_media_eviction,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ActionClass {
|
||||
None,
|
||||
Constructive,
|
||||
Destructive,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum DecisionAction {
|
||||
Hold,
|
||||
ConfirmConnection { nonce: Nonce },
|
||||
RepairState,
|
||||
RemoveGatewayState,
|
||||
RemoveParticipant { location: Location },
|
||||
}
|
||||
|
||||
impl DecisionAction {
|
||||
pub const fn class(&self) -> ActionClass {
|
||||
match self {
|
||||
Self::Hold => ActionClass::None,
|
||||
Self::ConfirmConnection { .. } | Self::RepairState => ActionClass::Constructive,
|
||||
Self::RemoveGatewayState | Self::RemoveParticipant { .. } => ActionClass::Destructive,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Hold => "hold",
|
||||
Self::ConfirmConnection { .. } => "confirm_connection",
|
||||
Self::RepairState => "repair_state",
|
||||
Self::RemoveGatewayState => "remove_gateway_state",
|
||||
Self::RemoveParticipant { .. } => "remove_participant",
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn kind(&self) -> Option<ActionKind> {
|
||||
match self {
|
||||
Self::Hold | Self::ConfirmConnection { .. } | Self::RepairState => None,
|
||||
Self::RemoveGatewayState => Some(ActionKind::RemoveGatewayState),
|
||||
Self::RemoveParticipant { .. } => Some(ActionKind::RemoveParticipant),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_destructive(&self) -> bool {
|
||||
matches!(self.class(), ActionClass::Destructive)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum DecisionReason {
|
||||
SideUnknown(UnknownReason),
|
||||
FingerprintChanged,
|
||||
BothSidesPresent,
|
||||
BothSidesAbsent,
|
||||
DivergenceOpened,
|
||||
CorroborationRecorded,
|
||||
CorroborationSpacedTooTightly,
|
||||
CorroborationWindowExpired,
|
||||
DivergenceTooYoung,
|
||||
EvidenceComplete,
|
||||
PendingJoinOutstanding,
|
||||
RepairAttempted,
|
||||
RepairSucceeded,
|
||||
RepairMadeNoChange,
|
||||
RepairNotPossible,
|
||||
RepairNotDefinitive,
|
||||
RepairExhausted,
|
||||
ActionUnresolved,
|
||||
ActionsExhausted,
|
||||
ParticipantJoinedAfterDivergence,
|
||||
LocationUnavailable,
|
||||
WedgedHolding,
|
||||
}
|
||||
|
||||
impl DecisionReason {
|
||||
pub const fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::SideUnknown(_) => "side_unknown",
|
||||
Self::FingerprintChanged => "fingerprint_changed",
|
||||
Self::BothSidesPresent => "both_sides_present",
|
||||
Self::BothSidesAbsent => "both_sides_absent",
|
||||
Self::DivergenceOpened => "divergence_opened",
|
||||
Self::CorroborationRecorded => "corroboration_recorded",
|
||||
Self::CorroborationSpacedTooTightly => "corroboration_spaced_too_tightly",
|
||||
Self::CorroborationWindowExpired => "corroboration_window_expired",
|
||||
Self::DivergenceTooYoung => "divergence_too_young",
|
||||
Self::EvidenceComplete => "evidence_complete",
|
||||
Self::PendingJoinOutstanding => "pending_join_outstanding",
|
||||
Self::RepairAttempted => "repair_attempted",
|
||||
Self::RepairSucceeded => "repair_succeeded",
|
||||
Self::RepairMadeNoChange => "repair_made_no_change",
|
||||
Self::RepairNotPossible => "repair_not_possible",
|
||||
Self::RepairNotDefinitive => "repair_not_definitive",
|
||||
Self::RepairExhausted => "repair_exhausted",
|
||||
Self::ActionUnresolved => "action_unresolved",
|
||||
Self::ActionsExhausted => "actions_exhausted",
|
||||
Self::ParticipantJoinedAfterDivergence => "participant_joined_after_divergence",
|
||||
Self::LocationUnavailable => "location_unavailable",
|
||||
Self::WedgedHolding => "wedged_holding",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Decision {
|
||||
pub connection: ConnectionKey,
|
||||
pub user_id: UserId,
|
||||
pub action: DecisionAction,
|
||||
pub reason: DecisionReason,
|
||||
pub from: &'static str,
|
||||
pub to: &'static str,
|
||||
pub blocked_by: Option<AbortReason>,
|
||||
}
|
||||
|
||||
impl Decision {
|
||||
pub const fn is_actionable(&self) -> bool {
|
||||
self.blocked_by.is_none() && !matches!(self.action, DecisionAction::Hold)
|
||||
}
|
||||
|
||||
pub const fn is_destructive(&self) -> bool {
|
||||
self.action.is_destructive()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DecisionSet {
|
||||
pub room: RoomKey,
|
||||
pub turn: TurnId,
|
||||
pub at: Millis,
|
||||
pub decisions: Vec<Decision>,
|
||||
}
|
||||
|
||||
impl DecisionSet {
|
||||
pub fn empty(room: RoomKey, turn: TurnId, at: Millis) -> Self {
|
||||
Self {
|
||||
room,
|
||||
turn,
|
||||
at,
|
||||
decisions: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn actionable(&self) -> impl Iterator<Item = &Decision> {
|
||||
self.decisions.iter().filter(|entry| entry.is_actionable())
|
||||
}
|
||||
|
||||
pub fn destructive(&self) -> impl Iterator<Item = &Decision> {
|
||||
self.decisions.iter().filter(|entry| entry.is_destructive())
|
||||
}
|
||||
|
||||
pub fn actionable_destructive(&self) -> impl Iterator<Item = &Decision> {
|
||||
self.decisions
|
||||
.iter()
|
||||
.filter(|entry| entry.is_actionable() && entry.is_destructive())
|
||||
}
|
||||
|
||||
pub fn blocked(&self) -> impl Iterator<Item = (&Decision, AbortReason)> {
|
||||
self.decisions
|
||||
.iter()
|
||||
.filter_map(|entry| entry.blocked_by.map(|reason| (entry, reason)))
|
||||
}
|
||||
|
||||
pub fn for_connection(&self, connection: &ConnectionId) -> Option<&Decision> {
|
||||
self.decisions
|
||||
.iter()
|
||||
.find(|entry| &entry.connection.connection == connection)
|
||||
}
|
||||
}
|
||||
|
||||
struct Context<'a> {
|
||||
params: &'a DecideParams,
|
||||
room: RoomKey,
|
||||
now: Millis,
|
||||
fingerprint: Fingerprint,
|
||||
location: Option<LocationIndex>,
|
||||
location_value: Option<&'a Location>,
|
||||
joined_at: Option<Millis>,
|
||||
pending_join: Option<&'a PendingJoin>,
|
||||
budget: &'a BudgetView,
|
||||
action_attempts: u32,
|
||||
repair_attempts: u32,
|
||||
repair_verdict: Option<RepairVerdict>,
|
||||
}
|
||||
|
||||
struct Outcome {
|
||||
state: ConnectionState,
|
||||
action: DecisionAction,
|
||||
reason: DecisionReason,
|
||||
blocked_by: Option<AbortReason>,
|
||||
}
|
||||
|
||||
impl Outcome {
|
||||
const fn hold(state: ConnectionState, reason: DecisionReason) -> Self {
|
||||
Self {
|
||||
state,
|
||||
action: DecisionAction::Hold,
|
||||
reason,
|
||||
blocked_by: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decide(
|
||||
ledger: &mut Ledger,
|
||||
observation: &RoomObservation,
|
||||
now: Millis,
|
||||
budget: &BudgetView,
|
||||
) -> DecisionSet {
|
||||
let params = *ledger.params();
|
||||
let room = observation.room;
|
||||
|
||||
if ledger.ensure_room(room, now).is_err() {
|
||||
return DecisionSet::empty(room, observation.turn, now);
|
||||
}
|
||||
|
||||
let no_candidates = observation.candidates.is_empty();
|
||||
let partially_unreadable = matches!(
|
||||
observation.authority.media.unknown_reason(),
|
||||
Some(UnknownReason::RoomPartiallyUnreadable)
|
||||
);
|
||||
if let Some(entry) = ledger.room_mut(&room) {
|
||||
entry.note_turn(observation.turn, now);
|
||||
entry.set_no_candidates(no_candidates);
|
||||
entry.set_partially_unreadable(partially_unreadable);
|
||||
}
|
||||
|
||||
let mut work: Vec<(ConnectionId, UserId)> = observation
|
||||
.connections
|
||||
.iter()
|
||||
.map(|entry| (entry.connection.clone(), entry.user_id))
|
||||
.collect();
|
||||
if let Some(entry) = ledger.room(&room) {
|
||||
for tracked in entry.connections() {
|
||||
if !work.iter().any(|(id, _)| id == &tracked.connection) {
|
||||
work.push((tracked.connection.clone(), tracked.user_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
work.sort();
|
||||
work.dedup();
|
||||
|
||||
let unsighted = observation.unsighted_presence();
|
||||
let mut decisions = Vec::with_capacity(work.len());
|
||||
|
||||
for (connection, user_id) in work {
|
||||
let observed = observation.observation_for(&connection);
|
||||
let presence: Side<Presence> =
|
||||
observed.map_or_else(|| unsighted.clone(), |entry| entry.presence.clone());
|
||||
let siblings = observed
|
||||
.map(|entry| entry.gateway_siblings.clone())
|
||||
.unwrap_or_default();
|
||||
let locations = observed
|
||||
.map(|entry| entry.media_locations.clone())
|
||||
.unwrap_or_default();
|
||||
let joined_at = observed.and_then(|entry| entry.participant_joined_at);
|
||||
|
||||
let print = fingerprint(&FingerprintInput {
|
||||
room,
|
||||
connection: &connection,
|
||||
user_id,
|
||||
gateway_siblings: &siblings,
|
||||
media_locations: &locations,
|
||||
participant_joined_at: joined_at,
|
||||
census_epoch: observation.census_epoch,
|
||||
topology_epoch: observation.topology_epoch,
|
||||
});
|
||||
|
||||
let location_index = locations
|
||||
.first()
|
||||
.and_then(|location| ledger.intern_location(location));
|
||||
if let (Some(index), Some(entry)) = (location_index, ledger.room_mut(&room)) {
|
||||
entry.note_location(index);
|
||||
}
|
||||
|
||||
let pending_join = observation.pending_join_for(
|
||||
&connection,
|
||||
observation.wall_at,
|
||||
params.pending_join_skew_ms,
|
||||
);
|
||||
|
||||
let Ok(entry) = ledger.upsert_connection(room, &connection, user_id, now) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(index) = location_index {
|
||||
entry.last_location = Some(index);
|
||||
}
|
||||
|
||||
let context = Context {
|
||||
params: ¶ms,
|
||||
room,
|
||||
now,
|
||||
fingerprint: print,
|
||||
location: location_index,
|
||||
location_value: locations.first(),
|
||||
joined_at,
|
||||
pending_join,
|
||||
budget,
|
||||
action_attempts: u32::from(entry.action_attempts),
|
||||
repair_attempts: u32::from(entry.repair_attempts),
|
||||
repair_verdict: entry.last_repair_verdict,
|
||||
};
|
||||
|
||||
let from = entry.state.label();
|
||||
let outcome = transition(&entry.state, &presence, &context);
|
||||
|
||||
if matches!(
|
||||
outcome.state,
|
||||
ConnectionState::Nascent | ConnectionState::Consistent
|
||||
) {
|
||||
entry.last_repair_verdict = None;
|
||||
}
|
||||
entry.state = outcome.state;
|
||||
|
||||
decisions.push(Decision {
|
||||
connection: ConnectionKey::new(room, connection),
|
||||
user_id,
|
||||
action: outcome.action,
|
||||
reason: outcome.reason,
|
||||
from,
|
||||
to: entry.state.label(),
|
||||
blocked_by: outcome.blocked_by,
|
||||
});
|
||||
}
|
||||
|
||||
DecisionSet {
|
||||
room,
|
||||
turn: observation.turn,
|
||||
at: now,
|
||||
decisions,
|
||||
}
|
||||
}
|
||||
|
||||
fn transition(prior: &ConnectionState, presence: &Side<Presence>, ctx: &Context<'_>) -> Outcome {
|
||||
match (&presence.gateway, &presence.media) {
|
||||
(Presence::Unknown(reason), Presence::Unknown(_))
|
||||
| (Presence::Unknown(reason), Presence::Present)
|
||||
| (Presence::Unknown(reason), Presence::Absent)
|
||||
| (Presence::Present, Presence::Unknown(reason))
|
||||
| (Presence::Absent, Presence::Unknown(reason)) => Outcome::hold(
|
||||
ConnectionState::Nascent,
|
||||
DecisionReason::SideUnknown(reason.clone()),
|
||||
),
|
||||
(Presence::Present, Presence::Present) => Outcome::hold(
|
||||
ConnectionState::Consistent,
|
||||
DecisionReason::BothSidesPresent,
|
||||
),
|
||||
(Presence::Absent, Presence::Absent) => Outcome::hold(
|
||||
ConnectionState::Retired {
|
||||
at: retired_at(prior, ctx.now),
|
||||
},
|
||||
DecisionReason::BothSidesAbsent,
|
||||
),
|
||||
(Presence::Present, Presence::Absent) => gateway_only_lane(prior, ctx),
|
||||
(Presence::Absent, Presence::Present) => media_only_lane(prior, ctx),
|
||||
}
|
||||
}
|
||||
|
||||
const fn retired_at(prior: &ConnectionState, now: Millis) -> Millis {
|
||||
match prior {
|
||||
ConnectionState::Retired { at } => *at,
|
||||
ConnectionState::Nascent
|
||||
| ConnectionState::Consistent
|
||||
| ConnectionState::PendingJoin { .. }
|
||||
| ConnectionState::GatewayOnly { .. }
|
||||
| ConnectionState::MediaOnly { .. }
|
||||
| ConnectionState::Repairing { .. }
|
||||
| ConnectionState::ActionTaken { .. }
|
||||
| ConnectionState::Wedged { .. } => now,
|
||||
}
|
||||
}
|
||||
|
||||
fn fingerprint_broke(prior: &ConnectionState, ctx: &Context<'_>) -> bool {
|
||||
prior
|
||||
.stored_fingerprint()
|
||||
.is_some_and(|stored| stored != ctx.fingerprint)
|
||||
}
|
||||
|
||||
fn nascent_on_fingerprint_change() -> Outcome {
|
||||
Outcome::hold(ConnectionState::Nascent, DecisionReason::FingerprintChanged)
|
||||
}
|
||||
|
||||
fn gateway_only_lane(prior: &ConnectionState, ctx: &Context<'_>) -> Outcome {
|
||||
if fingerprint_broke(prior, ctx) {
|
||||
return nascent_on_fingerprint_change();
|
||||
}
|
||||
|
||||
match prior {
|
||||
ConnectionState::Nascent
|
||||
| ConnectionState::Consistent
|
||||
| ConnectionState::PendingJoin { .. }
|
||||
| ConnectionState::Repairing { .. }
|
||||
| ConnectionState::MediaOnly { .. }
|
||||
| ConnectionState::Retired { .. } => open_gateway_divergence(ctx),
|
||||
ConnectionState::GatewayOnly {
|
||||
since,
|
||||
corroborations,
|
||||
last_corroboration,
|
||||
fingerprint: _,
|
||||
} => corroborate_gateway(*since, *corroborations, *last_corroboration, ctx),
|
||||
ConnectionState::ActionTaken { kind, since, .. } => match kind {
|
||||
ActionKind::RemoveGatewayState => {
|
||||
if ctx.action_attempts >= ctx.params.max_action_attempts {
|
||||
return wedged(
|
||||
WedgedReason::ActionIneffective,
|
||||
ctx,
|
||||
DecisionReason::ActionsExhausted,
|
||||
);
|
||||
}
|
||||
evaluate_gateway(
|
||||
*since,
|
||||
ctx.params.required_corroborations,
|
||||
ctx.now,
|
||||
ctx,
|
||||
DecisionReason::ActionUnresolved,
|
||||
)
|
||||
}
|
||||
ActionKind::RemoveParticipant => open_gateway_divergence(ctx),
|
||||
},
|
||||
ConnectionState::Wedged {
|
||||
reason,
|
||||
at,
|
||||
fingerprint: print,
|
||||
} => Outcome::hold(
|
||||
ConnectionState::Wedged {
|
||||
reason: *reason,
|
||||
at: *at,
|
||||
fingerprint: *print,
|
||||
},
|
||||
DecisionReason::WedgedHolding,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn open_gateway_divergence(ctx: &Context<'_>) -> Outcome {
|
||||
Outcome::hold(
|
||||
ConnectionState::GatewayOnly {
|
||||
since: ctx.now,
|
||||
corroborations: 1,
|
||||
fingerprint: ctx.fingerprint,
|
||||
last_corroboration: ctx.now,
|
||||
},
|
||||
DecisionReason::DivergenceOpened,
|
||||
)
|
||||
}
|
||||
|
||||
fn corroborate_gateway(
|
||||
since: Millis,
|
||||
corroborations: u32,
|
||||
last_corroboration: Millis,
|
||||
ctx: &Context<'_>,
|
||||
) -> Outcome {
|
||||
if ctx.now.saturating_since(since) > ctx.params.max_corroboration_window_ms {
|
||||
return Outcome::hold(
|
||||
ConnectionState::GatewayOnly {
|
||||
since: ctx.now,
|
||||
corroborations: 1,
|
||||
fingerprint: ctx.fingerprint,
|
||||
last_corroboration: ctx.now,
|
||||
},
|
||||
DecisionReason::CorroborationWindowExpired,
|
||||
);
|
||||
}
|
||||
|
||||
if ctx.now.saturating_since(last_corroboration) < ctx.params.min_corroboration_gap_ms {
|
||||
return evaluate_gateway(
|
||||
since,
|
||||
corroborations,
|
||||
last_corroboration,
|
||||
ctx,
|
||||
DecisionReason::CorroborationSpacedTooTightly,
|
||||
);
|
||||
}
|
||||
|
||||
evaluate_gateway(
|
||||
since,
|
||||
corroborations.saturating_add(1),
|
||||
ctx.now,
|
||||
ctx,
|
||||
DecisionReason::CorroborationRecorded,
|
||||
)
|
||||
}
|
||||
|
||||
fn evaluate_gateway(
|
||||
since: Millis,
|
||||
corroborations: u32,
|
||||
last_corroboration: Millis,
|
||||
ctx: &Context<'_>,
|
||||
progress: DecisionReason,
|
||||
) -> Outcome {
|
||||
let state = ConnectionState::GatewayOnly {
|
||||
since,
|
||||
corroborations,
|
||||
fingerprint: ctx.fingerprint,
|
||||
last_corroboration,
|
||||
};
|
||||
|
||||
if corroborations < ctx.params.required_corroborations {
|
||||
return Outcome::hold(state, progress);
|
||||
}
|
||||
|
||||
if ctx.now.saturating_since(since) < ctx.params.min_divergence_ms {
|
||||
return Outcome::hold(state, DecisionReason::DivergenceTooYoung);
|
||||
}
|
||||
|
||||
if ctx.action_attempts >= ctx.params.max_action_attempts {
|
||||
return wedged(
|
||||
WedgedReason::ActionIneffective,
|
||||
ctx,
|
||||
DecisionReason::ActionsExhausted,
|
||||
);
|
||||
}
|
||||
|
||||
let blocked_by = if ctx.params.dm_posture.allows_gateway_removal(ctx.room) {
|
||||
ctx.budget.destructive_block()
|
||||
} else {
|
||||
Some(AbortReason::DmPosture)
|
||||
};
|
||||
|
||||
Outcome {
|
||||
state,
|
||||
action: DecisionAction::RemoveGatewayState,
|
||||
reason: DecisionReason::EvidenceComplete,
|
||||
blocked_by,
|
||||
}
|
||||
}
|
||||
|
||||
fn media_only_lane(prior: &ConnectionState, ctx: &Context<'_>) -> Outcome {
|
||||
if fingerprint_broke(prior, ctx) {
|
||||
return nascent_on_fingerprint_change();
|
||||
}
|
||||
|
||||
match prior {
|
||||
ConnectionState::Nascent
|
||||
| ConnectionState::Consistent
|
||||
| ConnectionState::GatewayOnly { .. }
|
||||
| ConnectionState::Retired { .. } => open_media_divergence(ctx),
|
||||
ConnectionState::PendingJoin { last_confirm, .. } => match ctx.pending_join {
|
||||
None => open_media_divergence(ctx),
|
||||
Some(join) => {
|
||||
let state = ConnectionState::PendingJoin {
|
||||
expires_at: join.expires_at,
|
||||
nonce: join.nonce.clone(),
|
||||
last_confirm: *last_confirm,
|
||||
};
|
||||
if ctx.now.saturating_since(*last_confirm) < ctx.params.min_corroboration_gap_ms {
|
||||
return Outcome::hold(state, DecisionReason::PendingJoinOutstanding);
|
||||
}
|
||||
Outcome {
|
||||
state,
|
||||
action: DecisionAction::ConfirmConnection {
|
||||
nonce: join.nonce.clone(),
|
||||
},
|
||||
reason: DecisionReason::PendingJoinOutstanding,
|
||||
blocked_by: ctx.budget.constructive_block(),
|
||||
}
|
||||
}
|
||||
},
|
||||
ConnectionState::Repairing {
|
||||
since,
|
||||
last_attempt,
|
||||
} => repairing_lane(*since, *last_attempt, ctx),
|
||||
ConnectionState::MediaOnly {
|
||||
since,
|
||||
corroborations,
|
||||
last_corroboration,
|
||||
location,
|
||||
participant_joined_at,
|
||||
repair_verdict,
|
||||
fingerprint: _,
|
||||
} => corroborate_media(
|
||||
*since,
|
||||
*corroborations,
|
||||
*last_corroboration,
|
||||
*location,
|
||||
*participant_joined_at,
|
||||
*repair_verdict,
|
||||
ctx,
|
||||
),
|
||||
ConnectionState::ActionTaken { kind, since, .. } => match kind {
|
||||
ActionKind::RemoveParticipant => {
|
||||
if ctx.action_attempts >= ctx.params.max_action_attempts {
|
||||
return wedged(
|
||||
WedgedReason::ActionIneffective,
|
||||
ctx,
|
||||
DecisionReason::ActionsExhausted,
|
||||
);
|
||||
}
|
||||
reopen_media_divergence(*since, ctx, DecisionReason::ActionUnresolved)
|
||||
}
|
||||
ActionKind::RemoveGatewayState => open_media_divergence(ctx),
|
||||
},
|
||||
ConnectionState::Wedged {
|
||||
reason,
|
||||
at,
|
||||
fingerprint: print,
|
||||
} => Outcome::hold(
|
||||
ConnectionState::Wedged {
|
||||
reason: *reason,
|
||||
at: *at,
|
||||
fingerprint: *print,
|
||||
},
|
||||
DecisionReason::WedgedHolding,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn open_media_divergence(ctx: &Context<'_>) -> Outcome {
|
||||
if let Some(join) = ctx.pending_join {
|
||||
return Outcome {
|
||||
state: ConnectionState::PendingJoin {
|
||||
expires_at: join.expires_at,
|
||||
nonce: join.nonce.clone(),
|
||||
last_confirm: Millis::ZERO,
|
||||
},
|
||||
action: DecisionAction::ConfirmConnection {
|
||||
nonce: join.nonce.clone(),
|
||||
},
|
||||
reason: DecisionReason::PendingJoinOutstanding,
|
||||
blocked_by: ctx.budget.constructive_block(),
|
||||
};
|
||||
}
|
||||
|
||||
match ctx.room.scope() {
|
||||
Scope::Guild => {
|
||||
if ctx.repair_attempts > ctx.params.max_action_attempts {
|
||||
return wedged(
|
||||
WedgedReason::RepairExhausted,
|
||||
ctx,
|
||||
DecisionReason::RepairExhausted,
|
||||
);
|
||||
}
|
||||
Outcome {
|
||||
state: ConnectionState::Repairing {
|
||||
since: ctx.now,
|
||||
last_attempt: Millis::ZERO,
|
||||
},
|
||||
action: DecisionAction::RepairState,
|
||||
reason: DecisionReason::RepairAttempted,
|
||||
blocked_by: ctx.budget.constructive_block(),
|
||||
}
|
||||
}
|
||||
Scope::Dm => reopen_media_divergence(ctx.now, ctx, DecisionReason::DivergenceOpened),
|
||||
}
|
||||
}
|
||||
|
||||
fn repairing_lane(since: Millis, last_attempt: Millis, ctx: &Context<'_>) -> Outcome {
|
||||
match ctx.repair_verdict {
|
||||
Some(RepairVerdict::Repaired) => {
|
||||
Outcome::hold(ConnectionState::Consistent, DecisionReason::RepairSucceeded)
|
||||
}
|
||||
Some(RepairVerdict::NotRepairable) => {
|
||||
reopen_media_divergence(since, ctx, DecisionReason::RepairNotPossible)
|
||||
}
|
||||
Some(RepairVerdict::NoChange) | None => {
|
||||
if ctx.repair_attempts > ctx.params.max_action_attempts {
|
||||
return wedged(
|
||||
WedgedReason::RepairExhausted,
|
||||
ctx,
|
||||
DecisionReason::RepairExhausted,
|
||||
);
|
||||
}
|
||||
let state = ConnectionState::Repairing {
|
||||
since,
|
||||
last_attempt,
|
||||
};
|
||||
if ctx.now.saturating_since(last_attempt) < ctx.params.min_corroboration_gap_ms {
|
||||
return Outcome::hold(state, DecisionReason::RepairMadeNoChange);
|
||||
}
|
||||
Outcome {
|
||||
state,
|
||||
action: DecisionAction::RepairState,
|
||||
reason: DecisionReason::RepairAttempted,
|
||||
blocked_by: ctx.budget.constructive_block(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reopen_media_divergence(since: Millis, ctx: &Context<'_>, reason: DecisionReason) -> Outcome {
|
||||
let Some(location) = ctx.location else {
|
||||
return Outcome::hold(
|
||||
ConnectionState::Nascent,
|
||||
DecisionReason::LocationUnavailable,
|
||||
);
|
||||
};
|
||||
|
||||
Outcome::hold(
|
||||
ConnectionState::MediaOnly {
|
||||
since,
|
||||
corroborations: 1,
|
||||
fingerprint: ctx.fingerprint,
|
||||
last_corroboration: ctx.now,
|
||||
location,
|
||||
participant_joined_at: ctx.joined_at.unwrap_or(ctx.now),
|
||||
repair_verdict: ctx.repair_verdict,
|
||||
},
|
||||
reason,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn corroborate_media(
|
||||
since: Millis,
|
||||
corroborations: u32,
|
||||
last_corroboration: Millis,
|
||||
location: LocationIndex,
|
||||
participant_joined_at: Millis,
|
||||
repair_verdict: Option<RepairVerdict>,
|
||||
ctx: &Context<'_>,
|
||||
) -> Outcome {
|
||||
let joined_at = ctx.joined_at.unwrap_or(participant_joined_at);
|
||||
|
||||
if joined_at > since {
|
||||
return Outcome::hold(
|
||||
ConnectionState::MediaOnly {
|
||||
since: ctx.now,
|
||||
corroborations: 1,
|
||||
fingerprint: ctx.fingerprint,
|
||||
last_corroboration: ctx.now,
|
||||
location,
|
||||
participant_joined_at: joined_at,
|
||||
repair_verdict,
|
||||
},
|
||||
DecisionReason::ParticipantJoinedAfterDivergence,
|
||||
);
|
||||
}
|
||||
|
||||
if ctx.now.saturating_since(since) > ctx.params.max_corroboration_window_ms {
|
||||
return Outcome::hold(
|
||||
ConnectionState::MediaOnly {
|
||||
since: ctx.now,
|
||||
corroborations: 1,
|
||||
fingerprint: ctx.fingerprint,
|
||||
last_corroboration: ctx.now,
|
||||
location,
|
||||
participant_joined_at: joined_at,
|
||||
repair_verdict,
|
||||
},
|
||||
DecisionReason::CorroborationWindowExpired,
|
||||
);
|
||||
}
|
||||
|
||||
let spaced =
|
||||
ctx.now.saturating_since(last_corroboration) >= ctx.params.min_corroboration_gap_ms;
|
||||
let corroborations = if spaced {
|
||||
corroborations.saturating_add(1)
|
||||
} else {
|
||||
corroborations
|
||||
};
|
||||
let last_corroboration = if spaced { ctx.now } else { last_corroboration };
|
||||
let progress = if spaced {
|
||||
DecisionReason::CorroborationRecorded
|
||||
} else {
|
||||
DecisionReason::CorroborationSpacedTooTightly
|
||||
};
|
||||
|
||||
let state = ConnectionState::MediaOnly {
|
||||
since,
|
||||
corroborations,
|
||||
fingerprint: ctx.fingerprint,
|
||||
last_corroboration,
|
||||
location,
|
||||
participant_joined_at: joined_at,
|
||||
repair_verdict,
|
||||
};
|
||||
|
||||
if corroborations < ctx.params.required_corroborations {
|
||||
return Outcome::hold(state, progress);
|
||||
}
|
||||
|
||||
if ctx.now.saturating_since(since) < ctx.params.min_divergence_ms {
|
||||
return Outcome::hold(state, DecisionReason::DivergenceTooYoung);
|
||||
}
|
||||
|
||||
if matches!(ctx.room, RoomKey::Guild { .. })
|
||||
&& repair_verdict != Some(RepairVerdict::NotRepairable)
|
||||
{
|
||||
return Outcome::hold(state, DecisionReason::RepairNotDefinitive);
|
||||
}
|
||||
|
||||
if ctx.action_attempts >= ctx.params.max_action_attempts {
|
||||
return wedged(
|
||||
WedgedReason::ActionIneffective,
|
||||
ctx,
|
||||
DecisionReason::ActionsExhausted,
|
||||
);
|
||||
}
|
||||
|
||||
let Some(target) = ctx.location_value else {
|
||||
return Outcome::hold(state, DecisionReason::LocationUnavailable);
|
||||
};
|
||||
|
||||
let blocked_by = if ctx.params.dm_posture.allows_media_removal(ctx.room) {
|
||||
ctx.budget.destructive_block()
|
||||
} else {
|
||||
Some(AbortReason::DmPosture)
|
||||
};
|
||||
|
||||
Outcome {
|
||||
state,
|
||||
action: DecisionAction::RemoveParticipant {
|
||||
location: target.clone(),
|
||||
},
|
||||
reason: DecisionReason::EvidenceComplete,
|
||||
blocked_by,
|
||||
}
|
||||
}
|
||||
|
||||
fn wedged(reason: WedgedReason, ctx: &Context<'_>, decision: DecisionReason) -> Outcome {
|
||||
Outcome::hold(
|
||||
ConnectionState::Wedged {
|
||||
reason,
|
||||
at: ctx.now,
|
||||
fingerprint: ctx.fingerprint,
|
||||
},
|
||||
decision,
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,758 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::gateway::{GatewayFault, Nonce};
|
||||
use crate::health::ServerHealth;
|
||||
use crate::ids::{
|
||||
ChannelId, ConnectionId, Epoch, GuildId, Location, Millis, RoomKey, TurnId, UserId, WallMillis,
|
||||
};
|
||||
use crate::livekit::LiveKitFault;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum SideKind {
|
||||
Gateway,
|
||||
Media,
|
||||
}
|
||||
|
||||
impl SideKind {
|
||||
pub const ALL: [Self; 2] = [Self::Gateway, Self::Media];
|
||||
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Gateway => "gateway",
|
||||
Self::Media => "media",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub struct Side<T> {
|
||||
pub gateway: T,
|
||||
pub media: T,
|
||||
}
|
||||
|
||||
impl<T> Side<T> {
|
||||
pub const fn new(gateway: T, media: T) -> Self {
|
||||
Self { gateway, media }
|
||||
}
|
||||
|
||||
pub const fn get(&self, kind: SideKind) -> &T {
|
||||
match kind {
|
||||
SideKind::Gateway => &self.gateway,
|
||||
SideKind::Media => &self.media,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map<U, F: Fn(SideKind, &T) -> U>(&self, f: F) -> Side<U> {
|
||||
Side {
|
||||
gateway: f(SideKind::Gateway, &self.gateway),
|
||||
media: f(SideKind::Media, &self.media),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum UnknownReason {
|
||||
GatewayReadFailed(GatewayFault),
|
||||
GatewayCliff,
|
||||
CensusDisagrees,
|
||||
MediaLocationErrored(LiveKitFault),
|
||||
MediaServerUnhealthy,
|
||||
MediaServerCliff,
|
||||
NoCandidateLocations,
|
||||
TopologyStale,
|
||||
UnparseableIdentityInRoom,
|
||||
RoomPartiallyUnreadable,
|
||||
}
|
||||
|
||||
impl UnknownReason {
|
||||
pub const fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::GatewayReadFailed(_) => "gateway_read_failed",
|
||||
Self::GatewayCliff => "gateway_cliff",
|
||||
Self::CensusDisagrees => "census_disagrees",
|
||||
Self::MediaLocationErrored(_) => "media_location_errored",
|
||||
Self::MediaServerUnhealthy => "media_server_unhealthy",
|
||||
Self::MediaServerCliff => "media_server_cliff",
|
||||
Self::NoCandidateLocations => "no_candidate_locations",
|
||||
Self::TopologyStale => "topology_stale",
|
||||
Self::UnparseableIdentityInRoom => "unparseable_identity_in_room",
|
||||
Self::RoomPartiallyUnreadable => "room_partially_unreadable",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Presence {
|
||||
Present,
|
||||
Absent,
|
||||
Unknown(UnknownReason),
|
||||
}
|
||||
|
||||
impl Presence {
|
||||
pub const fn is_present(&self) -> bool {
|
||||
matches!(self, Self::Present)
|
||||
}
|
||||
|
||||
pub const fn is_absent(&self) -> bool {
|
||||
matches!(self, Self::Absent)
|
||||
}
|
||||
|
||||
pub const fn is_unknown(&self) -> bool {
|
||||
matches!(self, Self::Unknown(_))
|
||||
}
|
||||
|
||||
pub const fn unknown_reason(&self) -> Option<&UnknownReason> {
|
||||
match self {
|
||||
Self::Unknown(reason) => Some(reason),
|
||||
Self::Present | Self::Absent => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Present => "present",
|
||||
Self::Absent => "absent",
|
||||
Self::Unknown(_) => "unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum SideAuthority {
|
||||
Authoritative,
|
||||
Unknown(UnknownReason),
|
||||
}
|
||||
|
||||
impl SideAuthority {
|
||||
pub const fn is_authoritative(&self) -> bool {
|
||||
matches!(self, Self::Authoritative)
|
||||
}
|
||||
|
||||
pub const fn unknown_reason(&self) -> Option<&UnknownReason> {
|
||||
match self {
|
||||
Self::Authoritative => None,
|
||||
Self::Unknown(reason) => Some(reason),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn project(&self, sighted: bool) -> Presence {
|
||||
match self {
|
||||
Self::Unknown(reason) => Presence::Unknown(reason.clone()),
|
||||
Self::Authoritative => {
|
||||
if sighted {
|
||||
Presence::Present
|
||||
} else {
|
||||
Presence::Absent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct RoomCliffInput {
|
||||
pub dropped: usize,
|
||||
pub remaining: usize,
|
||||
}
|
||||
|
||||
pub const fn room_cliff_trips(input: RoomCliffInput) -> bool {
|
||||
input.dropped >= 2 || (input.dropped >= 1 && input.remaining == 0)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PriorConnection {
|
||||
pub connection: ConnectionId,
|
||||
pub was_consistent: bool,
|
||||
}
|
||||
|
||||
pub fn room_cliff_input(
|
||||
prior: &[PriorConnection],
|
||||
gateway_now: &[ConnectionId],
|
||||
media_now: &[ConnectionId],
|
||||
) -> RoomCliffInput {
|
||||
let dropped = prior
|
||||
.iter()
|
||||
.filter(|entry| entry.was_consistent)
|
||||
.filter(|entry| !gateway_now.contains(&entry.connection))
|
||||
.filter(|entry| media_now.contains(&entry.connection))
|
||||
.count();
|
||||
|
||||
RoomCliffInput {
|
||||
dropped,
|
||||
remaining: gateway_now.len(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct CliffWindow {
|
||||
since: Option<Millis>,
|
||||
last: Option<Millis>,
|
||||
}
|
||||
|
||||
impl CliffWindow {
|
||||
pub const fn clear() -> Self {
|
||||
Self {
|
||||
since: None,
|
||||
last: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn since(&self) -> Option<Millis> {
|
||||
self.since
|
||||
}
|
||||
|
||||
pub const fn trip(&mut self, now: Millis) {
|
||||
if self.since.is_none() {
|
||||
self.since = Some(now);
|
||||
}
|
||||
self.last = Some(now);
|
||||
}
|
||||
|
||||
pub const fn release(&mut self) {
|
||||
self.since = None;
|
||||
self.last = None;
|
||||
}
|
||||
|
||||
pub const fn is_held(&self, now: Millis, hold_ms: u64) -> bool {
|
||||
match self.last {
|
||||
None => false,
|
||||
Some(last) => now.saturating_since(last) < hold_ms,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn expire(&mut self, now: Millis, hold_ms: u64) {
|
||||
if !self.is_held(now, hold_ms) {
|
||||
self.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct ServerCliffConfig {
|
||||
pub fraction: f64,
|
||||
pub hold_ms: u64,
|
||||
}
|
||||
|
||||
pub const SERVER_CLIFF_TRAILING_PASSES: usize = 5;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct ServerCliffDetector {
|
||||
trailing: [u32; SERVER_CLIFF_TRAILING_PASSES],
|
||||
filled: usize,
|
||||
cursor: usize,
|
||||
window: CliffWindow,
|
||||
}
|
||||
|
||||
impl ServerCliffDetector {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
trailing: [0; SERVER_CLIFF_TRAILING_PASSES],
|
||||
filled: 0,
|
||||
cursor: 0,
|
||||
window: CliffWindow::clear(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn observe(
|
||||
&mut self,
|
||||
participants_seen: u32,
|
||||
now: Millis,
|
||||
config: ServerCliffConfig,
|
||||
) -> bool {
|
||||
let trailing_max = self.trailing_max();
|
||||
let tripped = if trailing_max == 0 {
|
||||
false
|
||||
} else {
|
||||
let drop =
|
||||
f64::from(trailing_max.saturating_sub(participants_seen)) / f64::from(trailing_max);
|
||||
drop > config.fraction
|
||||
};
|
||||
|
||||
if tripped {
|
||||
self.window.trip(now);
|
||||
} else {
|
||||
self.record(participants_seen);
|
||||
self.window.expire(now, config.hold_ms);
|
||||
}
|
||||
|
||||
self.is_cliffed(now, config)
|
||||
}
|
||||
|
||||
pub const fn release(&mut self) {
|
||||
self.trailing = [0; SERVER_CLIFF_TRAILING_PASSES];
|
||||
self.filled = 0;
|
||||
self.cursor = 0;
|
||||
self.window.release();
|
||||
}
|
||||
|
||||
fn record(&mut self, participants_seen: u32) {
|
||||
self.trailing[self.cursor] = participants_seen;
|
||||
self.cursor = (self.cursor + 1) % SERVER_CLIFF_TRAILING_PASSES;
|
||||
self.filled = self
|
||||
.filled
|
||||
.saturating_add(1)
|
||||
.min(SERVER_CLIFF_TRAILING_PASSES);
|
||||
}
|
||||
|
||||
pub const fn is_cliffed(&self, now: Millis, config: ServerCliffConfig) -> bool {
|
||||
self.window.is_held(now, config.hold_ms)
|
||||
}
|
||||
|
||||
pub const fn cliffed_since(&self) -> Option<Millis> {
|
||||
self.window.since()
|
||||
}
|
||||
|
||||
fn trailing_max(&self) -> u32 {
|
||||
self.trailing
|
||||
.iter()
|
||||
.take(self.filled)
|
||||
.copied()
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum GatewayRead {
|
||||
Ok { states: Vec<GatewayVoiceState> },
|
||||
Failed(GatewayFault),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct GatewayVoiceState {
|
||||
pub connection: Option<ConnectionId>,
|
||||
pub user_id: UserId,
|
||||
pub channel_id: ChannelId,
|
||||
pub guild_id: Option<GuildId>,
|
||||
pub hint: Option<Location>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PendingJoin {
|
||||
pub connection: ConnectionId,
|
||||
pub user_id: UserId,
|
||||
pub expires_at: WallMillis,
|
||||
pub nonce: Nonce,
|
||||
}
|
||||
|
||||
impl PendingJoin {
|
||||
pub const fn is_unexpired(&self, wall_now: WallMillis, skew_ms: u64) -> bool {
|
||||
wall_now.get() <= self.expires_at.get().saturating_add(skew_ms)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum CensusCrossCheck {
|
||||
Fresh { voice_state_count: u32 },
|
||||
Stale,
|
||||
Missing,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct TopologyFreshness {
|
||||
pub age_ms: u64,
|
||||
pub max_age_ms: u64,
|
||||
}
|
||||
|
||||
impl TopologyFreshness {
|
||||
pub const fn is_stale(self) -> bool {
|
||||
self.age_ms > self.max_age_ms
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct GatewayAuthorityInput<'a> {
|
||||
pub read: &'a GatewayRead,
|
||||
pub room_cliffed: bool,
|
||||
pub census: CensusCrossCheck,
|
||||
pub topology: TopologyFreshness,
|
||||
}
|
||||
|
||||
pub fn gateway_authority(input: &GatewayAuthorityInput<'_>) -> SideAuthority {
|
||||
let states = match input.read {
|
||||
GatewayRead::Failed(fault) => {
|
||||
return SideAuthority::Unknown(UnknownReason::GatewayReadFailed(fault.clone()));
|
||||
}
|
||||
GatewayRead::Ok { states } => states,
|
||||
};
|
||||
|
||||
if input.room_cliffed {
|
||||
return SideAuthority::Unknown(UnknownReason::GatewayCliff);
|
||||
}
|
||||
|
||||
if let CensusCrossCheck::Fresh { voice_state_count } = input.census
|
||||
&& voice_state_count > 0
|
||||
&& states.is_empty()
|
||||
{
|
||||
return SideAuthority::Unknown(UnknownReason::CensusDisagrees);
|
||||
}
|
||||
|
||||
if input.topology.is_stale() {
|
||||
return SideAuthority::Unknown(UnknownReason::TopologyStale);
|
||||
}
|
||||
|
||||
SideAuthority::Authoritative
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ProbeResult {
|
||||
Ok {
|
||||
participants: Vec<MediaParticipant>,
|
||||
unparseable_identities: usize,
|
||||
},
|
||||
Failed(LiveKitFault),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct MediaParticipant {
|
||||
pub connection: ConnectionId,
|
||||
pub user_id: UserId,
|
||||
pub location: Location,
|
||||
pub joined_at: Millis,
|
||||
pub is_publisher: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct MediaSighting {
|
||||
pub connection: ConnectionId,
|
||||
pub user_id: UserId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct LocationProbe {
|
||||
pub location: Location,
|
||||
pub result: ProbeResult,
|
||||
pub health: ServerHealth,
|
||||
pub cliffed: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct CandidateSet {
|
||||
locations: Vec<Location>,
|
||||
}
|
||||
|
||||
impl CandidateSet {
|
||||
pub fn from_locations(mut locations: Vec<Location>) -> Self {
|
||||
locations.sort();
|
||||
locations.dedup();
|
||||
Self { locations }
|
||||
}
|
||||
|
||||
pub fn locations(&self) -> &[Location] {
|
||||
&self.locations
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.locations.is_empty()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.locations.len()
|
||||
}
|
||||
|
||||
pub fn contains(&self, location: &Location) -> bool {
|
||||
self.locations.binary_search(location).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct CandidateSources {
|
||||
pub discovered: Vec<Location>,
|
||||
pub gateway_hints: Vec<Location>,
|
||||
pub pinned: Option<Location>,
|
||||
pub ledger_last_known: Vec<Location>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ServerRecord {
|
||||
pub location: Location,
|
||||
pub removed_at: Option<Millis>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct TopologyLens<'a> {
|
||||
pub servers: &'a [ServerRecord],
|
||||
pub now: Millis,
|
||||
pub drain_grace_ms: u64,
|
||||
}
|
||||
|
||||
impl TopologyLens<'_> {
|
||||
pub fn live_locations(&self) -> Vec<Location> {
|
||||
let mut locations: Vec<Location> = self
|
||||
.servers
|
||||
.iter()
|
||||
.filter(|record| !self.is_drained(&record.location))
|
||||
.map(|record| record.location.clone())
|
||||
.collect();
|
||||
locations.sort();
|
||||
locations.dedup();
|
||||
locations
|
||||
}
|
||||
|
||||
fn is_drained(&self, location: &Location) -> bool {
|
||||
self.servers
|
||||
.iter()
|
||||
.find(|record| &record.location == location)
|
||||
.and_then(|record| record.removed_at)
|
||||
.is_some_and(|removed_at| self.now.saturating_since(removed_at) > self.drain_grace_ms)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn candidate_set(sources: &CandidateSources, topology: &TopologyLens<'_>) -> CandidateSet {
|
||||
let mut locations: Vec<Location> = sources
|
||||
.discovered
|
||||
.iter()
|
||||
.chain(sources.gateway_hints.iter())
|
||||
.chain(sources.pinned.iter())
|
||||
.chain(sources.ledger_last_known.iter())
|
||||
.filter(|location| !topology.is_drained(location))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
locations.sort();
|
||||
locations.dedup();
|
||||
CandidateSet { locations }
|
||||
}
|
||||
|
||||
pub fn unread_believed_home<'a>(
|
||||
homes: &'a [Location],
|
||||
candidates: &CandidateSet,
|
||||
probes: &[LocationProbe],
|
||||
) -> Option<&'a Location> {
|
||||
homes.iter().find(|home| {
|
||||
!candidates.contains(home)
|
||||
|| !probes.iter().any(|probe| {
|
||||
&&probe.location == home && matches!(probe.result, ProbeResult::Ok { .. })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct MediaAuthorityInput<'a> {
|
||||
pub candidates: &'a CandidateSet,
|
||||
pub probes: &'a [LocationProbe],
|
||||
pub connections_in_room: usize,
|
||||
pub max_connections_per_room: usize,
|
||||
}
|
||||
|
||||
pub fn media_authority(input: &MediaAuthorityInput<'_>) -> SideAuthority {
|
||||
if input.candidates.is_empty() {
|
||||
return SideAuthority::Unknown(UnknownReason::NoCandidateLocations);
|
||||
}
|
||||
|
||||
for location in input.candidates.locations() {
|
||||
let Some(probe) = input
|
||||
.probes
|
||||
.iter()
|
||||
.find(|probe| &probe.location == location)
|
||||
else {
|
||||
return SideAuthority::Unknown(UnknownReason::MediaLocationErrored(
|
||||
LiveKitFault::ServerMissing,
|
||||
));
|
||||
};
|
||||
|
||||
match &probe.result {
|
||||
ProbeResult::Failed(fault) => {
|
||||
return SideAuthority::Unknown(UnknownReason::MediaLocationErrored(*fault));
|
||||
}
|
||||
ProbeResult::Ok {
|
||||
unparseable_identities,
|
||||
participants: _,
|
||||
} => {
|
||||
if *unparseable_identities > 0 {
|
||||
return SideAuthority::Unknown(UnknownReason::UnparseableIdentityInRoom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !probe.health.is_healthy() {
|
||||
return SideAuthority::Unknown(UnknownReason::MediaServerUnhealthy);
|
||||
}
|
||||
|
||||
if probe.cliffed {
|
||||
return SideAuthority::Unknown(UnknownReason::MediaServerCliff);
|
||||
}
|
||||
}
|
||||
|
||||
if input.connections_in_room > input.max_connections_per_room {
|
||||
return SideAuthority::Unknown(UnknownReason::RoomPartiallyUnreadable);
|
||||
}
|
||||
|
||||
SideAuthority::Authoritative
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ConnectionObservation {
|
||||
pub connection: ConnectionId,
|
||||
pub user_id: UserId,
|
||||
pub presence: Side<Presence>,
|
||||
pub gateway_siblings: Vec<ConnectionId>,
|
||||
pub media_locations: Vec<Location>,
|
||||
pub participant_joined_at: Option<Millis>,
|
||||
pub gateway_state_has_connection_id: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RoomObservation {
|
||||
pub room: RoomKey,
|
||||
pub turn: TurnId,
|
||||
pub at: Millis,
|
||||
pub wall_at: WallMillis,
|
||||
pub authority: Side<SideAuthority>,
|
||||
pub connections: Vec<ConnectionObservation>,
|
||||
pub pending_joins: Vec<PendingJoin>,
|
||||
pub candidates: CandidateSet,
|
||||
pub census_epoch: Epoch,
|
||||
pub topology_epoch: Epoch,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RoomReads<'a> {
|
||||
pub room: RoomKey,
|
||||
pub turn: TurnId,
|
||||
pub at: Millis,
|
||||
pub wall_at: WallMillis,
|
||||
pub gateway: &'a GatewayRead,
|
||||
pub probes: &'a [LocationProbe],
|
||||
pub pending_joins: Vec<PendingJoin>,
|
||||
pub candidates: CandidateSet,
|
||||
pub census_epoch: Epoch,
|
||||
pub topology_epoch: Epoch,
|
||||
}
|
||||
|
||||
impl RoomObservation {
|
||||
pub fn assemble(reads: &RoomReads<'_>, authority: Side<SideAuthority>) -> Self {
|
||||
let gateway_states: &[GatewayVoiceState] = match reads.gateway {
|
||||
GatewayRead::Ok { states } => states,
|
||||
GatewayRead::Failed(_) => &[],
|
||||
};
|
||||
|
||||
let participants: Vec<&MediaParticipant> = reads
|
||||
.probes
|
||||
.iter()
|
||||
.filter_map(|probe| match &probe.result {
|
||||
ProbeResult::Ok { participants, .. } => Some(participants.iter()),
|
||||
ProbeResult::Failed(_) => None,
|
||||
})
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
let mut keys: Vec<(ConnectionId, UserId)> = gateway_states
|
||||
.iter()
|
||||
.filter_map(|state| {
|
||||
state
|
||||
.connection
|
||||
.as_ref()
|
||||
.map(|connection| (connection.clone(), state.user_id))
|
||||
})
|
||||
.chain(
|
||||
participants
|
||||
.iter()
|
||||
.map(|participant| (participant.connection.clone(), participant.user_id)),
|
||||
)
|
||||
.collect();
|
||||
keys.sort();
|
||||
keys.dedup();
|
||||
|
||||
let connections = keys
|
||||
.into_iter()
|
||||
.map(|(connection, user_id)| {
|
||||
let gateway_sighted = gateway_states
|
||||
.iter()
|
||||
.any(|state| state.connection.as_ref() == Some(&connection));
|
||||
|
||||
let media_sightings: Vec<&&MediaParticipant> = participants
|
||||
.iter()
|
||||
.filter(|participant| participant.connection == connection)
|
||||
.collect();
|
||||
|
||||
let media_sighted = !media_sightings.is_empty();
|
||||
|
||||
let mut gateway_siblings: Vec<ConnectionId> = gateway_states
|
||||
.iter()
|
||||
.filter(|state| state.user_id == user_id)
|
||||
.filter_map(|state| state.connection.clone())
|
||||
.collect();
|
||||
gateway_siblings.sort();
|
||||
gateway_siblings.dedup();
|
||||
|
||||
let mut media_locations: Vec<Location> = media_sightings
|
||||
.iter()
|
||||
.map(|participant| participant.location.clone())
|
||||
.collect();
|
||||
media_locations.sort();
|
||||
media_locations.dedup();
|
||||
|
||||
let participant_joined_at = media_sightings
|
||||
.iter()
|
||||
.map(|participant| participant.joined_at)
|
||||
.min();
|
||||
|
||||
let gateway_state_has_connection_id = !gateway_states
|
||||
.iter()
|
||||
.any(|state| state.user_id == user_id && state.connection.is_none());
|
||||
|
||||
ConnectionObservation {
|
||||
presence: Side::new(
|
||||
authority.gateway.project(gateway_sighted),
|
||||
authority.media.project(media_sighted),
|
||||
),
|
||||
connection,
|
||||
user_id,
|
||||
gateway_siblings,
|
||||
media_locations,
|
||||
participant_joined_at,
|
||||
gateway_state_has_connection_id,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
room: reads.room,
|
||||
turn: reads.turn,
|
||||
at: reads.at,
|
||||
wall_at: reads.wall_at,
|
||||
authority,
|
||||
connections,
|
||||
pending_joins: reads.pending_joins.clone(),
|
||||
candidates: reads.candidates.clone(),
|
||||
census_epoch: reads.census_epoch,
|
||||
topology_epoch: reads.topology_epoch,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn observation_for(&self, connection: &ConnectionId) -> Option<&ConnectionObservation> {
|
||||
self.connections
|
||||
.iter()
|
||||
.find(|entry| &entry.connection == connection)
|
||||
}
|
||||
|
||||
pub fn unsighted_presence(&self) -> Side<Presence> {
|
||||
Side::new(
|
||||
self.authority.gateway.project(false),
|
||||
self.authority.media.project(false),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn pending_join_for(
|
||||
&self,
|
||||
connection: &ConnectionId,
|
||||
wall_now: WallMillis,
|
||||
skew_ms: u64,
|
||||
) -> Option<&PendingJoin> {
|
||||
self.pending_joins
|
||||
.iter()
|
||||
.find(|join| &join.connection == connection && join.is_unexpired(wall_now, skew_ms))
|
||||
}
|
||||
|
||||
pub fn user_has_pending_join(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
wall_now: WallMillis,
|
||||
skew_ms: u64,
|
||||
) -> bool {
|
||||
self.pending_joins
|
||||
.iter()
|
||||
.any(|join| join.user_id == user_id && join.is_unexpired(wall_now, skew_ms))
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Confidence {
|
||||
Transient,
|
||||
Definitive,
|
||||
Refused,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Confidence {
|
||||
pub const ALL: [Self; 4] = [
|
||||
Self::Transient,
|
||||
Self::Definitive,
|
||||
Self::Refused,
|
||||
Self::Unknown,
|
||||
];
|
||||
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Transient => "transient",
|
||||
Self::Definitive => "definitive",
|
||||
Self::Refused => "refused",
|
||||
Self::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_definitive(self) -> bool {
|
||||
matches!(self, Self::Definitive)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub enum GatewayErrorCode {
|
||||
Timeout,
|
||||
NoResponders,
|
||||
Overloaded,
|
||||
GuildNotFound,
|
||||
VoiceStatesError,
|
||||
PendingJoinsError,
|
||||
CallLookupError,
|
||||
CallStateError,
|
||||
CallPendingJoinsError,
|
||||
DisconnectUserError,
|
||||
ConfirmConnectionError,
|
||||
ConnectionNotFound,
|
||||
VoiceStateMismatch,
|
||||
VoiceInvalidState,
|
||||
VoiceNotSupported,
|
||||
EventMutationsPaused,
|
||||
InvalidParams,
|
||||
}
|
||||
|
||||
impl GatewayErrorCode {
|
||||
pub const ALL: [Self; 17] = [
|
||||
Self::Timeout,
|
||||
Self::NoResponders,
|
||||
Self::Overloaded,
|
||||
Self::GuildNotFound,
|
||||
Self::VoiceStatesError,
|
||||
Self::PendingJoinsError,
|
||||
Self::CallLookupError,
|
||||
Self::CallStateError,
|
||||
Self::CallPendingJoinsError,
|
||||
Self::DisconnectUserError,
|
||||
Self::ConfirmConnectionError,
|
||||
Self::ConnectionNotFound,
|
||||
Self::VoiceStateMismatch,
|
||||
Self::VoiceInvalidState,
|
||||
Self::VoiceNotSupported,
|
||||
Self::EventMutationsPaused,
|
||||
Self::InvalidParams,
|
||||
];
|
||||
|
||||
pub const fn wire(self) -> &'static str {
|
||||
match self {
|
||||
Self::Timeout => "timeout",
|
||||
Self::NoResponders => "no_responders",
|
||||
Self::Overloaded => "overloaded",
|
||||
Self::GuildNotFound => "guild_not_found",
|
||||
Self::VoiceStatesError => "voice_states_error",
|
||||
Self::PendingJoinsError => "pending_joins_error",
|
||||
Self::CallLookupError => "call_lookup_error",
|
||||
Self::CallStateError => "call_state_error",
|
||||
Self::CallPendingJoinsError => "call_pending_joins_error",
|
||||
Self::DisconnectUserError => "disconnect_user_error",
|
||||
Self::ConfirmConnectionError => "confirm_connection_error",
|
||||
Self::ConnectionNotFound => "connection_not_found",
|
||||
Self::VoiceStateMismatch => "voice_state_mismatch",
|
||||
Self::VoiceInvalidState => "voice_invalid_state",
|
||||
Self::VoiceNotSupported => "voice_not_supported",
|
||||
Self::EventMutationsPaused => "event_mutations_paused",
|
||||
Self::InvalidParams => "invalid_params",
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn confidence(self) -> Confidence {
|
||||
match self {
|
||||
Self::Timeout
|
||||
| Self::NoResponders
|
||||
| Self::Overloaded
|
||||
| Self::GuildNotFound
|
||||
| Self::VoiceStatesError
|
||||
| Self::PendingJoinsError
|
||||
| Self::CallLookupError
|
||||
| Self::CallStateError
|
||||
| Self::CallPendingJoinsError
|
||||
| Self::DisconnectUserError
|
||||
| Self::ConfirmConnectionError => Confidence::Transient,
|
||||
Self::ConnectionNotFound
|
||||
| Self::VoiceStateMismatch
|
||||
| Self::VoiceInvalidState
|
||||
| Self::VoiceNotSupported => Confidence::Definitive,
|
||||
Self::EventMutationsPaused | Self::InvalidParams => Confidence::Refused,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const MISSING_ERROR_LABEL: &str = "missing_error";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum GatewayError {
|
||||
Known(GatewayErrorCode),
|
||||
Unrecognised(Box<str>),
|
||||
Missing,
|
||||
}
|
||||
|
||||
impl GatewayError {
|
||||
pub fn from_wire(value: Option<&str>) -> Self {
|
||||
match value {
|
||||
None => Self::Missing,
|
||||
Some(raw) => Self::from_code(raw),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_code(raw: &str) -> Self {
|
||||
GatewayErrorCode::ALL
|
||||
.into_iter()
|
||||
.find(|code| code.wire() == raw)
|
||||
.map_or_else(|| Self::Unrecognised(Box::from(raw)), Self::Known)
|
||||
}
|
||||
|
||||
pub const fn confidence(&self) -> Confidence {
|
||||
match self {
|
||||
Self::Known(code) => code.confidence(),
|
||||
Self::Unrecognised(_) | Self::Missing => Confidence::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(&self) -> &str {
|
||||
match self {
|
||||
Self::Known(code) => code.wire(),
|
||||
Self::Unrecognised(raw) => raw,
|
||||
Self::Missing => MISSING_ERROR_LABEL,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn code(&self) -> Option<GatewayErrorCode> {
|
||||
match self {
|
||||
Self::Known(code) => Some(*code),
|
||||
Self::Unrecognised(_) | Self::Missing => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,316 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
pub mod codes;
|
||||
pub mod nats;
|
||||
|
||||
use std::future::Future;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::evidence::{GatewayVoiceState, PendingJoin};
|
||||
use crate::gateway::codes::{Confidence, GatewayError};
|
||||
use crate::ids::{ConnectionId, RoomKey, UserId};
|
||||
use crate::ledger::RepairVerdict;
|
||||
|
||||
pub const SUBJECT_PREFIX: &str = "rpc.gateway.";
|
||||
|
||||
pub const MUTATION_DEADLINE: Duration = Duration::from_secs(4);
|
||||
pub const CHANNEL_READ_DEADLINE: Duration = Duration::from_secs(5);
|
||||
pub const CENSUS_DEADLINE: Duration = Duration::from_secs(12);
|
||||
|
||||
pub const GATEWAY_GUILD_CALL_BUDGET: Duration = Duration::from_secs(4);
|
||||
pub const GATEWAY_CENSUS_NODE_RPC_BUDGET: Duration = Duration::from_secs(10);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum MethodClass {
|
||||
Read,
|
||||
Constructive,
|
||||
Destructive,
|
||||
}
|
||||
|
||||
impl MethodClass {
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Read => "read",
|
||||
Self::Constructive => "constructive",
|
||||
Self::Destructive => "destructive",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum GatewayMethod {
|
||||
ActiveVoiceRooms,
|
||||
VoiceStatesForChannel,
|
||||
PendingJoinsForChannel,
|
||||
ConfirmConnection,
|
||||
RepairStateFromCache,
|
||||
DisconnectUserIfInChannel,
|
||||
}
|
||||
|
||||
impl GatewayMethod {
|
||||
pub const ALL: [Self; 6] = [
|
||||
Self::ActiveVoiceRooms,
|
||||
Self::VoiceStatesForChannel,
|
||||
Self::PendingJoinsForChannel,
|
||||
Self::ConfirmConnection,
|
||||
Self::RepairStateFromCache,
|
||||
Self::DisconnectUserIfInChannel,
|
||||
];
|
||||
|
||||
pub const fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::ActiveVoiceRooms => "process.active_voice_rooms",
|
||||
Self::VoiceStatesForChannel => "voice.get_voice_states_for_channel",
|
||||
Self::PendingJoinsForChannel => "voice.get_pending_joins_for_channel",
|
||||
Self::ConfirmConnection => "voice.confirm_connection",
|
||||
Self::RepairStateFromCache => "voice.repair_state_from_cache",
|
||||
Self::DisconnectUserIfInChannel => "voice.disconnect_user_if_in_channel",
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn deadline(self) -> Duration {
|
||||
match self {
|
||||
Self::ActiveVoiceRooms => CENSUS_DEADLINE,
|
||||
Self::VoiceStatesForChannel | Self::PendingJoinsForChannel => CHANNEL_READ_DEADLINE,
|
||||
Self::ConfirmConnection
|
||||
| Self::RepairStateFromCache
|
||||
| Self::DisconnectUserIfInChannel => MUTATION_DEADLINE,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn class(self) -> MethodClass {
|
||||
match self {
|
||||
Self::ActiveVoiceRooms | Self::VoiceStatesForChannel | Self::PendingJoinsForChannel => {
|
||||
MethodClass::Read
|
||||
}
|
||||
Self::ConfirmConnection | Self::RepairStateFromCache => MethodClass::Constructive,
|
||||
Self::DisconnectUserIfInChannel => MethodClass::Destructive,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subject(self) -> String {
|
||||
let mut subject = String::with_capacity(SUBJECT_PREFIX.len() + self.name().len());
|
||||
subject.push_str(SUBJECT_PREFIX);
|
||||
subject.push_str(self.name());
|
||||
subject
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum FaultOrigin {
|
||||
CouldNotAsk,
|
||||
Unintelligible,
|
||||
GatewaySaidNo,
|
||||
}
|
||||
|
||||
impl FaultOrigin {
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::CouldNotAsk => "could_not_ask",
|
||||
Self::Unintelligible => "unintelligible",
|
||||
Self::GatewaySaidNo => "gateway_said_no",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum GatewayFault {
|
||||
Timeout,
|
||||
NoResponders,
|
||||
TransportFailed,
|
||||
DecodeFailed,
|
||||
NotOk(GatewayError),
|
||||
}
|
||||
|
||||
impl GatewayFault {
|
||||
pub const fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Timeout => "timeout",
|
||||
Self::NoResponders => "no_responders",
|
||||
Self::TransportFailed => "transport_failed",
|
||||
Self::DecodeFailed => "decode_failed",
|
||||
Self::NotOk(_) => "not_ok",
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn origin(&self) -> FaultOrigin {
|
||||
match self {
|
||||
Self::Timeout | Self::NoResponders | Self::TransportFailed => FaultOrigin::CouldNotAsk,
|
||||
Self::DecodeFailed => FaultOrigin::Unintelligible,
|
||||
Self::NotOk(_) => FaultOrigin::GatewaySaidNo,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_evidence(&self) -> bool {
|
||||
matches!(self.origin(), FaultOrigin::GatewaySaidNo)
|
||||
}
|
||||
|
||||
pub const fn confidence(&self) -> Confidence {
|
||||
match self {
|
||||
Self::Timeout | Self::NoResponders | Self::TransportFailed => Confidence::Transient,
|
||||
Self::DecodeFailed => Confidence::Unknown,
|
||||
Self::NotOk(error) => error.confidence(),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn error(&self) -> Option<&GatewayError> {
|
||||
match self {
|
||||
Self::Timeout | Self::NoResponders | Self::TransportFailed | Self::DecodeFailed => None,
|
||||
Self::NotOk(error) => Some(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub enum Nonce {
|
||||
#[default]
|
||||
Empty,
|
||||
Value(Box<str>),
|
||||
}
|
||||
|
||||
impl Nonce {
|
||||
pub fn from_wire(value: Option<&str>) -> Self {
|
||||
match value {
|
||||
None => Self::Empty,
|
||||
Some("") => Self::Empty,
|
||||
Some(raw) => Self::Value(Box::from(raw)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Empty => None,
|
||||
Self::Value(value) => Some(value),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_empty(&self) -> bool {
|
||||
matches!(self, Self::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ActiveVoiceRoom {
|
||||
pub room: RoomKey,
|
||||
pub voice_state_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct ActiveVoiceRooms {
|
||||
pub node_count: u32,
|
||||
pub rooms: Vec<ActiveVoiceRoom>,
|
||||
pub unparseable_rooms: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ConfirmOutcome {
|
||||
Confirmed,
|
||||
AlreadyConfirmed,
|
||||
CallNotFound,
|
||||
Failed(GatewayError),
|
||||
}
|
||||
|
||||
impl ConfirmOutcome {
|
||||
pub const fn succeeded(&self) -> bool {
|
||||
matches!(self, Self::Confirmed | Self::AlreadyConfirmed)
|
||||
}
|
||||
|
||||
pub const fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Confirmed => "confirmed",
|
||||
Self::AlreadyConfirmed => "already_confirmed",
|
||||
Self::CallNotFound => "call_not_found",
|
||||
Self::Failed(_) => "failed",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum RepairOutcome {
|
||||
Repaired,
|
||||
NoChange,
|
||||
Failed(GatewayError),
|
||||
}
|
||||
|
||||
impl RepairOutcome {
|
||||
pub const fn verdict(&self) -> RepairVerdict {
|
||||
match self {
|
||||
Self::Repaired => RepairVerdict::Repaired,
|
||||
Self::NoChange => RepairVerdict::NoChange,
|
||||
Self::Failed(error) => match error.confidence() {
|
||||
Confidence::Definitive => RepairVerdict::NotRepairable,
|
||||
Confidence::Transient | Confidence::Refused | Confidence::Unknown => {
|
||||
RepairVerdict::NoChange
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Repaired => "repaired",
|
||||
Self::NoChange => "no_change",
|
||||
Self::Failed(_) => "failed",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum DisconnectOutcome {
|
||||
Removed,
|
||||
Ignored { reason: Option<Box<str>> },
|
||||
CallNotFound,
|
||||
Failed(GatewayError),
|
||||
}
|
||||
|
||||
impl DisconnectOutcome {
|
||||
pub const fn removed_state(&self) -> bool {
|
||||
matches!(self, Self::Removed)
|
||||
}
|
||||
|
||||
pub const fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Removed => "removed",
|
||||
Self::Ignored { .. } => "ignored",
|
||||
Self::CallNotFound => "call_not_found",
|
||||
Self::Failed(_) => "failed",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait GatewayApi {
|
||||
fn active_voice_rooms(
|
||||
&self,
|
||||
) -> impl Future<Output = Result<ActiveVoiceRooms, GatewayFault>> + Send;
|
||||
|
||||
fn voice_states_for_channel(
|
||||
&self,
|
||||
room: RoomKey,
|
||||
) -> impl Future<Output = Result<Vec<GatewayVoiceState>, GatewayFault>> + Send;
|
||||
|
||||
fn pending_joins_for_channel(
|
||||
&self,
|
||||
room: RoomKey,
|
||||
) -> impl Future<Output = Result<Vec<PendingJoin>, GatewayFault>> + Send;
|
||||
|
||||
fn confirm_connection(
|
||||
&self,
|
||||
room: RoomKey,
|
||||
connection: &ConnectionId,
|
||||
nonce: &Nonce,
|
||||
) -> impl Future<Output = Result<ConfirmOutcome, GatewayFault>> + Send;
|
||||
|
||||
fn repair_state_from_cache(
|
||||
&self,
|
||||
room: RoomKey,
|
||||
user_id: UserId,
|
||||
connection: &ConnectionId,
|
||||
) -> impl Future<Output = Result<RepairOutcome, GatewayFault>> + Send;
|
||||
|
||||
fn disconnect_user_if_in_channel(
|
||||
&self,
|
||||
room: RoomKey,
|
||||
user_id: UserId,
|
||||
connection: Option<&ConnectionId>,
|
||||
) -> impl Future<Output = Result<DisconnectOutcome, GatewayFault>> + Send;
|
||||
}
|
||||
@@ -1,468 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use fluxer_svc::transport::Transport;
|
||||
|
||||
use crate::evidence::{GatewayVoiceState, PendingJoin};
|
||||
use crate::gateway::codes::GatewayError;
|
||||
use crate::gateway::{
|
||||
ActiveVoiceRoom, ActiveVoiceRooms, ConfirmOutcome, DisconnectOutcome, GatewayApi, GatewayFault,
|
||||
GatewayMethod, Nonce, RepairOutcome,
|
||||
};
|
||||
use crate::ids::{
|
||||
ChannelId, ConnectionId, GuildId, Location, RegionId, RoomKey, ServerId, UserId, WallMillis,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct NoParams {}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ChannelReadParams {
|
||||
channel_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
guild_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ConfirmParams {
|
||||
channel_id: String,
|
||||
connection_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
guild_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
token_nonce: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct RepairParams {
|
||||
channel_id: String,
|
||||
user_id: String,
|
||||
connection_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
guild_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct DisconnectParams {
|
||||
channel_id: String,
|
||||
user_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
guild_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
connection_id: Option<String>,
|
||||
}
|
||||
|
||||
fn guild_param(room: RoomKey) -> Option<String> {
|
||||
room.guild_id().map(|guild_id| guild_id.to_string())
|
||||
}
|
||||
|
||||
fn channel_read_params(room: RoomKey) -> ChannelReadParams {
|
||||
ChannelReadParams {
|
||||
channel_id: room.channel_id().to_string(),
|
||||
guild_id: guild_param(room),
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm_params(room: RoomKey, connection: &ConnectionId, nonce: &Nonce) -> ConfirmParams {
|
||||
ConfirmParams {
|
||||
channel_id: room.channel_id().to_string(),
|
||||
connection_id: connection.as_str().to_owned(),
|
||||
guild_id: guild_param(room),
|
||||
token_nonce: nonce.as_str().map(str::to_owned),
|
||||
}
|
||||
}
|
||||
|
||||
fn repair_params(room: RoomKey, user_id: UserId, connection: &ConnectionId) -> RepairParams {
|
||||
RepairParams {
|
||||
channel_id: room.channel_id().to_string(),
|
||||
user_id: user_id.to_string(),
|
||||
connection_id: connection.as_str().to_owned(),
|
||||
guild_id: guild_param(room),
|
||||
}
|
||||
}
|
||||
|
||||
fn disconnect_params(
|
||||
room: RoomKey,
|
||||
user_id: UserId,
|
||||
connection: Option<&ConnectionId>,
|
||||
) -> DisconnectParams {
|
||||
DisconnectParams {
|
||||
channel_id: room.channel_id().to_string(),
|
||||
user_id: user_id.to_string(),
|
||||
guild_id: guild_param(room),
|
||||
connection_id: connection.map(|id| id.as_str().to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn classify_transport_error(error: &anyhow::Error) -> GatewayFault {
|
||||
if error.chain().any(|cause| {
|
||||
cause
|
||||
.to_string()
|
||||
.to_ascii_lowercase()
|
||||
.contains("no responders")
|
||||
}) {
|
||||
return GatewayFault::NoResponders;
|
||||
}
|
||||
if error
|
||||
.chain()
|
||||
.any(|cause| cause.is::<tokio::time::error::Elapsed>())
|
||||
{
|
||||
return GatewayFault::Timeout;
|
||||
}
|
||||
if error.chain().any(|cause| {
|
||||
let text = cause.to_string().to_ascii_lowercase();
|
||||
text.contains("timed out") || text.contains("timeout") || text.contains("deadline")
|
||||
}) {
|
||||
return GatewayFault::Timeout;
|
||||
}
|
||||
GatewayFault::TransportFailed
|
||||
}
|
||||
|
||||
pub fn decode_envelope(bytes: &[u8]) -> Result<Value, GatewayFault> {
|
||||
let Ok(body) = serde_json::from_slice::<Value>(bytes) else {
|
||||
return Err(GatewayFault::DecodeFailed);
|
||||
};
|
||||
let Some(object) = body.as_object() else {
|
||||
return Err(GatewayFault::DecodeFailed);
|
||||
};
|
||||
let Some(ok) = object.get("ok").and_then(Value::as_bool) else {
|
||||
return Err(GatewayFault::DecodeFailed);
|
||||
};
|
||||
if ok {
|
||||
return Ok(object.get("result").cloned().unwrap_or(Value::Null));
|
||||
}
|
||||
let error = object.get("error").and_then(Value::as_str);
|
||||
Err(GatewayFault::NotOk(GatewayError::from_wire(error)))
|
||||
}
|
||||
|
||||
fn required<'a>(result: &'a Value, key: &str) -> Result<&'a Value, GatewayFault> {
|
||||
result.get(key).ok_or(GatewayFault::DecodeFailed)
|
||||
}
|
||||
|
||||
fn optional<'a>(result: &'a Value, key: &str) -> Option<&'a Value> {
|
||||
match result.get(key) {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(value) => Some(value),
|
||||
}
|
||||
}
|
||||
|
||||
fn entries(result: &Value, key: &str) -> Result<Vec<Value>, GatewayFault> {
|
||||
required(result, key)?
|
||||
.as_array()
|
||||
.cloned()
|
||||
.ok_or(GatewayFault::DecodeFailed)
|
||||
}
|
||||
|
||||
fn decimal_u64(value: &str) -> Option<u64> {
|
||||
if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
|
||||
return None;
|
||||
}
|
||||
value.parse::<u64>().ok()
|
||||
}
|
||||
|
||||
fn snowflake(value: &Value) -> Result<u64, GatewayFault> {
|
||||
match value {
|
||||
Value::String(raw) => decimal_u64(raw).ok_or(GatewayFault::DecodeFailed),
|
||||
Value::Number(number) => number.as_u64().ok_or(GatewayFault::DecodeFailed),
|
||||
Value::Null | Value::Bool(_) | Value::Array(_) | Value::Object(_) => {
|
||||
Err(GatewayFault::DecodeFailed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn text(value: &Value) -> Result<&str, GatewayFault> {
|
||||
value.as_str().ok_or(GatewayFault::DecodeFailed)
|
||||
}
|
||||
|
||||
fn count(value: &Value) -> Result<u32, GatewayFault> {
|
||||
let number = value.as_u64().ok_or(GatewayFault::DecodeFailed)?;
|
||||
u32::try_from(number).map_err(|_| GatewayFault::DecodeFailed)
|
||||
}
|
||||
|
||||
fn flag(result: &Value, key: &str) -> Result<bool, GatewayFault> {
|
||||
match optional(result, key) {
|
||||
None => Ok(false),
|
||||
Some(value) => value.as_bool().ok_or(GatewayFault::DecodeFailed),
|
||||
}
|
||||
}
|
||||
|
||||
fn reason(result: &Value) -> Result<Option<Box<str>>, GatewayFault> {
|
||||
match optional(result, "reason") {
|
||||
None => Ok(None),
|
||||
Some(value) => Ok(Some(Box::from(text(value)?))),
|
||||
}
|
||||
}
|
||||
|
||||
fn error_of(result: &Value) -> GatewayError {
|
||||
GatewayError::from_wire(optional(result, "error").and_then(Value::as_str))
|
||||
}
|
||||
|
||||
fn connection_of(value: &Value) -> Result<Option<ConnectionId>, GatewayFault> {
|
||||
let raw = text(value)?;
|
||||
if raw.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
ConnectionId::new(raw)
|
||||
.map(Some)
|
||||
.map_err(|_| GatewayFault::DecodeFailed)
|
||||
}
|
||||
|
||||
fn location_hint(entry: &Value) -> Result<Option<Location>, GatewayFault> {
|
||||
let region = match optional(entry, "region_id") {
|
||||
None => None,
|
||||
Some(value) => Some(RegionId::new(text(value)?).map_err(|_| GatewayFault::DecodeFailed)?),
|
||||
};
|
||||
let server = match optional(entry, "server_id") {
|
||||
None => None,
|
||||
Some(value) => Some(ServerId::new(text(value)?).map_err(|_| GatewayFault::DecodeFailed)?),
|
||||
};
|
||||
match (region, server) {
|
||||
(Some(region), Some(server)) => Ok(Some(Location::new(region, server))),
|
||||
(Some(_), None) | (None, Some(_)) | (None, None) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn room_of(entry: &Value) -> Result<RoomKey, GatewayFault> {
|
||||
let channel_id = ChannelId::new(snowflake(required(entry, "channel_id")?)?);
|
||||
match optional(entry, "guild_id") {
|
||||
None => Ok(RoomKey::Dm { channel_id }),
|
||||
Some(value) => Ok(RoomKey::Guild {
|
||||
guild_id: GuildId::new(snowflake(value)?),
|
||||
channel_id,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_active_voice_room(entry: &Value) -> Result<ActiveVoiceRoom, GatewayFault> {
|
||||
Ok(ActiveVoiceRoom {
|
||||
room: room_of(entry)?,
|
||||
voice_state_count: count(required(entry, "voice_state_count")?)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decode_active_voice_rooms(result: &Value) -> Result<ActiveVoiceRooms, GatewayFault> {
|
||||
let node_count = count(required(result, "node_count")?)?;
|
||||
let mut rooms = Vec::new();
|
||||
let mut unparseable_rooms = 0u32;
|
||||
|
||||
for entry in entries(result, "rooms")? {
|
||||
if let Ok(room) = decode_active_voice_room(&entry) {
|
||||
rooms.push(room);
|
||||
} else {
|
||||
unparseable_rooms = unparseable_rooms.saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ActiveVoiceRooms {
|
||||
node_count,
|
||||
rooms,
|
||||
unparseable_rooms,
|
||||
})
|
||||
}
|
||||
|
||||
fn decode_voice_state(entry: &Value) -> Result<GatewayVoiceState, GatewayFault> {
|
||||
let connection = match optional(entry, "connection_id") {
|
||||
None => None,
|
||||
Some(value) => connection_of(value)?,
|
||||
};
|
||||
let guild_id = match optional(entry, "guild_id") {
|
||||
None => None,
|
||||
Some(value) => Some(GuildId::new(snowflake(value)?)),
|
||||
};
|
||||
|
||||
Ok(GatewayVoiceState {
|
||||
connection,
|
||||
user_id: UserId::new(snowflake(required(entry, "user_id")?)?),
|
||||
channel_id: ChannelId::new(snowflake(required(entry, "channel_id")?)?),
|
||||
guild_id,
|
||||
hint: location_hint(entry)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decode_voice_states(result: &Value) -> Result<Vec<GatewayVoiceState>, GatewayFault> {
|
||||
entries(result, "voice_states")?
|
||||
.iter()
|
||||
.map(decode_voice_state)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn decode_pending_join(entry: &Value) -> Result<PendingJoin, GatewayFault> {
|
||||
let connection = ConnectionId::new(text(required(entry, "connection_id")?)?)
|
||||
.map_err(|_| GatewayFault::DecodeFailed)?;
|
||||
let expires_at = required(entry, "expires_at")?
|
||||
.as_i64()
|
||||
.ok_or(GatewayFault::DecodeFailed)?;
|
||||
let nonce = match optional(entry, "token_nonce") {
|
||||
None => Nonce::Empty,
|
||||
Some(value) => Nonce::from_wire(Some(text(value)?)),
|
||||
};
|
||||
|
||||
Ok(PendingJoin {
|
||||
connection,
|
||||
user_id: UserId::new(snowflake(required(entry, "user_id")?)?),
|
||||
expires_at: WallMillis::new(u64::try_from(expires_at).unwrap_or(0)),
|
||||
nonce,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decode_pending_joins(result: &Value) -> Result<Vec<PendingJoin>, GatewayFault> {
|
||||
entries(result, "pending_joins")?
|
||||
.iter()
|
||||
.map(decode_pending_join)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn decode_confirm(result: &Value) -> Result<ConfirmOutcome, GatewayFault> {
|
||||
if !flag(result, "success")? {
|
||||
return Ok(ConfirmOutcome::Failed(error_of(result)));
|
||||
}
|
||||
if flag(result, "call_not_found")? {
|
||||
return Ok(ConfirmOutcome::CallNotFound);
|
||||
}
|
||||
if flag(result, "already_confirmed")? {
|
||||
return Ok(ConfirmOutcome::AlreadyConfirmed);
|
||||
}
|
||||
Ok(ConfirmOutcome::Confirmed)
|
||||
}
|
||||
|
||||
pub fn decode_repair(result: &Value) -> Result<RepairOutcome, GatewayFault> {
|
||||
if !flag(result, "success")? {
|
||||
return Ok(RepairOutcome::Failed(error_of(result)));
|
||||
}
|
||||
match optional(result, "repaired") {
|
||||
None => Ok(RepairOutcome::NoChange),
|
||||
Some(value) => match value.as_bool().ok_or(GatewayFault::DecodeFailed)? {
|
||||
true => Ok(RepairOutcome::Repaired),
|
||||
false => Ok(RepairOutcome::NoChange),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode_disconnect(result: &Value) -> Result<DisconnectOutcome, GatewayFault> {
|
||||
if !flag(result, "success")? {
|
||||
return Ok(DisconnectOutcome::Failed(error_of(result)));
|
||||
}
|
||||
if flag(result, "ignored")? {
|
||||
return Ok(DisconnectOutcome::Ignored {
|
||||
reason: reason(result)?,
|
||||
});
|
||||
}
|
||||
if flag(result, "call_not_found")? {
|
||||
return Ok(DisconnectOutcome::CallNotFound);
|
||||
}
|
||||
Ok(DisconnectOutcome::Removed)
|
||||
}
|
||||
|
||||
pub struct NatsGateway<T> {
|
||||
transport: T,
|
||||
}
|
||||
|
||||
impl<T> NatsGateway<T> {
|
||||
pub const fn new(transport: T) -> Self {
|
||||
Self { transport }
|
||||
}
|
||||
|
||||
pub const fn transport(&self) -> &T {
|
||||
&self.transport
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Transport> NatsGateway<T> {
|
||||
async fn call<P: Serialize>(
|
||||
&self,
|
||||
method: GatewayMethod,
|
||||
params: &P,
|
||||
) -> Result<Value, GatewayFault> {
|
||||
let Ok(payload) = serde_json::to_vec(params) else {
|
||||
return Err(GatewayFault::TransportFailed);
|
||||
};
|
||||
let bytes = self
|
||||
.transport
|
||||
.request(&method.subject(), &payload, method.deadline())
|
||||
.await
|
||||
.map_err(|error| classify_transport_error(&error))?;
|
||||
decode_envelope(&bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Transport> GatewayApi for NatsGateway<T> {
|
||||
async fn active_voice_rooms(&self) -> Result<ActiveVoiceRooms, GatewayFault> {
|
||||
let result = self
|
||||
.call(GatewayMethod::ActiveVoiceRooms, &NoParams {})
|
||||
.await?;
|
||||
decode_active_voice_rooms(&result)
|
||||
}
|
||||
|
||||
async fn voice_states_for_channel(
|
||||
&self,
|
||||
room: RoomKey,
|
||||
) -> Result<Vec<GatewayVoiceState>, GatewayFault> {
|
||||
let result = self
|
||||
.call(
|
||||
GatewayMethod::VoiceStatesForChannel,
|
||||
&channel_read_params(room),
|
||||
)
|
||||
.await?;
|
||||
decode_voice_states(&result)
|
||||
}
|
||||
|
||||
async fn pending_joins_for_channel(
|
||||
&self,
|
||||
room: RoomKey,
|
||||
) -> Result<Vec<PendingJoin>, GatewayFault> {
|
||||
let result = self
|
||||
.call(
|
||||
GatewayMethod::PendingJoinsForChannel,
|
||||
&channel_read_params(room),
|
||||
)
|
||||
.await?;
|
||||
decode_pending_joins(&result)
|
||||
}
|
||||
|
||||
async fn confirm_connection(
|
||||
&self,
|
||||
room: RoomKey,
|
||||
connection: &ConnectionId,
|
||||
nonce: &Nonce,
|
||||
) -> Result<ConfirmOutcome, GatewayFault> {
|
||||
let result = self
|
||||
.call(
|
||||
GatewayMethod::ConfirmConnection,
|
||||
&confirm_params(room, connection, nonce),
|
||||
)
|
||||
.await?;
|
||||
decode_confirm(&result)
|
||||
}
|
||||
|
||||
async fn repair_state_from_cache(
|
||||
&self,
|
||||
room: RoomKey,
|
||||
user_id: UserId,
|
||||
connection: &ConnectionId,
|
||||
) -> Result<RepairOutcome, GatewayFault> {
|
||||
let result = self
|
||||
.call(
|
||||
GatewayMethod::RepairStateFromCache,
|
||||
&repair_params(room, user_id, connection),
|
||||
)
|
||||
.await?;
|
||||
decode_repair(&result)
|
||||
}
|
||||
|
||||
async fn disconnect_user_if_in_channel(
|
||||
&self,
|
||||
room: RoomKey,
|
||||
user_id: UserId,
|
||||
connection: Option<&ConnectionId>,
|
||||
) -> Result<DisconnectOutcome, GatewayFault> {
|
||||
let result = self
|
||||
.call(
|
||||
GatewayMethod::DisconnectUserIfInChannel,
|
||||
&disconnect_params(room, user_id, connection),
|
||||
)
|
||||
.await?;
|
||||
decode_disconnect(&result)
|
||||
}
|
||||
}
|
||||
@@ -1,572 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::config::ConnectionIdGuard;
|
||||
use crate::evidence::{MediaSighting, PendingJoin};
|
||||
use crate::health::ServerHealth;
|
||||
use crate::ids::{ConnectionId, Location, Millis, RoomKey, TurnId, UserId, WallMillis};
|
||||
use crate::ledger::RepairVerdict;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum AbortReason {
|
||||
SiblingConnection,
|
||||
NoConnectionId,
|
||||
PendingJoin,
|
||||
PreflightStale,
|
||||
PreflightDisagreed,
|
||||
JoinedAfterDivergence,
|
||||
UnknownGateway,
|
||||
UnknownMedia,
|
||||
ServerUnhealthy,
|
||||
Budget,
|
||||
Mode,
|
||||
Warmup,
|
||||
DmPosture,
|
||||
Breaker,
|
||||
}
|
||||
|
||||
impl AbortReason {
|
||||
pub const ALL: [Self; 14] = [
|
||||
Self::SiblingConnection,
|
||||
Self::NoConnectionId,
|
||||
Self::PendingJoin,
|
||||
Self::PreflightStale,
|
||||
Self::PreflightDisagreed,
|
||||
Self::JoinedAfterDivergence,
|
||||
Self::UnknownGateway,
|
||||
Self::UnknownMedia,
|
||||
Self::ServerUnhealthy,
|
||||
Self::Budget,
|
||||
Self::Mode,
|
||||
Self::Warmup,
|
||||
Self::DmPosture,
|
||||
Self::Breaker,
|
||||
];
|
||||
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::SiblingConnection => "sibling_connection",
|
||||
Self::NoConnectionId => "no_connection_id",
|
||||
Self::PendingJoin => "pending_join",
|
||||
Self::PreflightStale => "preflight_stale",
|
||||
Self::PreflightDisagreed => "preflight_disagreed",
|
||||
Self::JoinedAfterDivergence => "joined_after_divergence",
|
||||
Self::UnknownGateway => "unknown_gateway",
|
||||
Self::UnknownMedia => "unknown_media",
|
||||
Self::ServerUnhealthy => "server_unhealthy",
|
||||
Self::Budget => "budget",
|
||||
Self::Mode => "mode",
|
||||
Self::Warmup => "warmup",
|
||||
Self::DmPosture => "dm_posture",
|
||||
Self::Breaker => "breaker",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PreflightVoiceState {
|
||||
pub user_id: UserId,
|
||||
pub connection: Option<ConnectionId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct GatewayPreflight {
|
||||
pub turn: TurnId,
|
||||
pub taken_at: Millis,
|
||||
pub voice_states: Vec<PreflightVoiceState>,
|
||||
pub pending_joins: Vec<PendingJoin>,
|
||||
pub media_present: Vec<MediaSighting>,
|
||||
}
|
||||
|
||||
impl GatewayPreflight {
|
||||
pub const fn is_fresh(&self, now: Millis, max_age_ms: u64) -> bool {
|
||||
now.saturating_since(self.taken_at) <= max_age_ms
|
||||
}
|
||||
|
||||
pub fn states_for(&self, user_id: UserId) -> impl Iterator<Item = &PreflightVoiceState> {
|
||||
self.voice_states
|
||||
.iter()
|
||||
.filter(move |state| state.user_id == user_id)
|
||||
}
|
||||
|
||||
pub fn user_has_pending_join(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
wall_now: WallMillis,
|
||||
skew_ms: u64,
|
||||
) -> bool {
|
||||
self.pending_joins
|
||||
.iter()
|
||||
.any(|join| join.user_id == user_id && join.is_unexpired(wall_now, skew_ms))
|
||||
}
|
||||
|
||||
pub fn media_holds(&self, connection: &ConnectionId) -> bool {
|
||||
self.media_present
|
||||
.iter()
|
||||
.any(|sighting| &sighting.connection == connection)
|
||||
}
|
||||
|
||||
pub fn media_legs_of(&self, user_id: UserId) -> Vec<ConnectionId> {
|
||||
self.media_present
|
||||
.iter()
|
||||
.filter(|sighting| sighting.user_id == user_id)
|
||||
.map(|sighting| sighting.connection.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn joining_connections_of(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
wall_now: WallMillis,
|
||||
skew_ms: u64,
|
||||
) -> Vec<ConnectionId> {
|
||||
self.pending_joins
|
||||
.iter()
|
||||
.filter(|join| join.user_id == user_id && join.is_unexpired(wall_now, skew_ms))
|
||||
.map(|join| join.connection.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn live_connections_of(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
window: PreflightWindow,
|
||||
) -> Vec<ConnectionId> {
|
||||
let joining =
|
||||
self.joining_connections_of(user_id, window.wall_now, window.pending_join_skew_ms);
|
||||
let mut live: Vec<ConnectionId> = self
|
||||
.states_for(user_id)
|
||||
.filter_map(|state| state.connection.clone())
|
||||
.collect();
|
||||
live.extend(self.media_legs_of(user_id));
|
||||
live.retain(|connection| !joining.contains(connection));
|
||||
live.sort();
|
||||
live.dedup();
|
||||
live
|
||||
}
|
||||
|
||||
pub fn gateway_holds(&self, connection: &ConnectionId) -> bool {
|
||||
self.voice_states
|
||||
.iter()
|
||||
.any(|state| state.connection.as_ref() == Some(connection))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum UnknownMediaCause {
|
||||
UnreadRequired,
|
||||
Unasked,
|
||||
NoCustody,
|
||||
UserHomeSilent,
|
||||
NoStamp,
|
||||
LiveServerSilent,
|
||||
}
|
||||
|
||||
impl UnknownMediaCause {
|
||||
pub const ALL: [Self; 6] = [
|
||||
Self::UnreadRequired,
|
||||
Self::Unasked,
|
||||
Self::NoCustody,
|
||||
Self::UserHomeSilent,
|
||||
Self::NoStamp,
|
||||
Self::LiveServerSilent,
|
||||
];
|
||||
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::UnreadRequired => "unread_required",
|
||||
Self::Unasked => "unasked",
|
||||
Self::NoCustody => "no_custody",
|
||||
Self::UserHomeSilent => "user_home_silent",
|
||||
Self::NoStamp => "no_stamp",
|
||||
Self::LiveServerSilent => "live_server_silent",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct MediaCoverage {
|
||||
fleet: Vec<Location>,
|
||||
required: Vec<Location>,
|
||||
read: Vec<Location>,
|
||||
absent: Vec<Location>,
|
||||
unreachable: Vec<Location>,
|
||||
}
|
||||
|
||||
impl MediaCoverage {
|
||||
pub fn over(fleet: Vec<Location>, required: Vec<Location>) -> Self {
|
||||
let mut fleet = fleet;
|
||||
fleet.sort();
|
||||
fleet.dedup();
|
||||
let mut required = required;
|
||||
required.sort();
|
||||
required.dedup();
|
||||
required.retain(|location| fleet.contains(location));
|
||||
Self {
|
||||
fleet,
|
||||
required,
|
||||
read: Vec::new(),
|
||||
absent: Vec::new(),
|
||||
unreachable: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn note_read(&mut self, location: &Location) {
|
||||
if self.fleet.contains(location) && !self.read.contains(location) {
|
||||
self.read.push(location.clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn note_absent(&mut self, location: &Location) {
|
||||
if self.fleet.contains(location)
|
||||
&& !self.read.contains(location)
|
||||
&& !self.absent.contains(location)
|
||||
{
|
||||
self.absent.push(location.clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn note_unreachable(&mut self, location: &Location) {
|
||||
if self.fleet.contains(location) && !self.unreachable.contains(location) {
|
||||
self.unreachable.push(location.clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fleet(&self) -> &[Location] {
|
||||
&self.fleet
|
||||
}
|
||||
|
||||
pub fn required(&self) -> &[Location] {
|
||||
&self.required
|
||||
}
|
||||
|
||||
pub fn read(&self) -> &[Location] {
|
||||
&self.read
|
||||
}
|
||||
|
||||
pub fn absent(&self) -> &[Location] {
|
||||
&self.absent
|
||||
}
|
||||
|
||||
pub fn unreachable(&self) -> &[Location] {
|
||||
&self.unreachable
|
||||
}
|
||||
|
||||
pub fn answered(&self) -> Vec<Location> {
|
||||
let mut answered = self.read.clone();
|
||||
for location in &self.absent {
|
||||
if !answered.contains(location) {
|
||||
answered.push(location.clone());
|
||||
}
|
||||
}
|
||||
answered.sort();
|
||||
answered
|
||||
}
|
||||
|
||||
pub fn silent(&self) -> Vec<Location> {
|
||||
self.fleet
|
||||
.iter()
|
||||
.filter(|location| !self.read.contains(location) && !self.absent.contains(location))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn unasked(&self) -> Option<&Location> {
|
||||
self.fleet.iter().find(|location| {
|
||||
!self.read.contains(location)
|
||||
&& !self.absent.contains(location)
|
||||
&& !self.unreachable.contains(location)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn unread(&self) -> Option<&Location> {
|
||||
self.required
|
||||
.iter()
|
||||
.find(|location| !self.read.contains(location) && !self.absent.contains(location))
|
||||
}
|
||||
|
||||
pub fn is_complete(&self) -> bool {
|
||||
!self.read.is_empty() && self.unasked().is_none() && self.unread().is_none()
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn connection_id_is_honoured(posture: ConnectionIdGuard, room: RoomKey) -> bool {
|
||||
match room {
|
||||
RoomKey::Guild { .. } => matches!(posture, ConnectionIdGuard::Honoured),
|
||||
RoomKey::Dm { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn media_coverage_cause(
|
||||
coverage: &MediaCoverage,
|
||||
posture: ConnectionIdGuard,
|
||||
room: RoomKey,
|
||||
) -> Option<UnknownMediaCause> {
|
||||
if connection_id_is_honoured(posture, room) {
|
||||
return None;
|
||||
}
|
||||
if coverage.unread().is_some() {
|
||||
return Some(UnknownMediaCause::UnreadRequired);
|
||||
}
|
||||
if coverage.is_complete() {
|
||||
None
|
||||
} else {
|
||||
Some(UnknownMediaCause::Unasked)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn media_coverage_refusal(
|
||||
coverage: &MediaCoverage,
|
||||
posture: ConnectionIdGuard,
|
||||
room: RoomKey,
|
||||
) -> Option<AbortReason> {
|
||||
media_coverage_cause(coverage, posture, room).map(|_| AbortReason::UnknownMedia)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct MediaCustody<'a> {
|
||||
pub room: RoomKey,
|
||||
pub posture: ConnectionIdGuard,
|
||||
pub fleet: &'a [Location],
|
||||
pub answered: &'a [Location],
|
||||
pub read: &'a [Location],
|
||||
pub target_home: Option<&'a Location>,
|
||||
pub user_homes: &'a [Location],
|
||||
pub stamped: &'a [Location],
|
||||
}
|
||||
|
||||
impl MediaCustody<'_> {
|
||||
fn silent(&self) -> Vec<&Location> {
|
||||
self.fleet
|
||||
.iter()
|
||||
.filter(|location| !self.answered.contains(location))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn target_is_held(&self) -> bool {
|
||||
self.target_home
|
||||
.is_some_and(|home| self.fleet.contains(home) && self.read.contains(home))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn media_custody_cause(custody: &MediaCustody<'_>) -> Option<UnknownMediaCause> {
|
||||
let silent = custody.silent();
|
||||
if silent.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if connection_id_is_honoured(custody.posture, custody.room) {
|
||||
return if silent
|
||||
.iter()
|
||||
.all(|location| custody.stamped.contains(location))
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(UnknownMediaCause::NoStamp)
|
||||
};
|
||||
}
|
||||
if !custody.target_is_held() {
|
||||
return Some(UnknownMediaCause::NoCustody);
|
||||
}
|
||||
if custody.user_homes.iter().any(|home| silent.contains(&home)) {
|
||||
return Some(UnknownMediaCause::UserHomeSilent);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn media_custody_refusal(custody: &MediaCustody<'_>) -> Option<AbortReason> {
|
||||
media_custody_cause(custody).map(|_| AbortReason::UnknownMedia)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct FleetSilence<'a> {
|
||||
pub room: RoomKey,
|
||||
pub posture: ConnectionIdGuard,
|
||||
pub fleet: &'a [Location],
|
||||
pub answered: &'a [Location],
|
||||
pub written_off: &'a [Location],
|
||||
}
|
||||
|
||||
impl FleetSilence<'_> {
|
||||
fn silent_and_still_expected(&self) -> Option<&Location> {
|
||||
self.fleet.iter().find(|location| {
|
||||
!self.answered.contains(location) && !self.written_off.contains(location)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fleet_silence_cause(silence: &FleetSilence<'_>) -> Option<UnknownMediaCause> {
|
||||
if connection_id_is_honoured(silence.posture, silence.room) {
|
||||
return None;
|
||||
}
|
||||
silence
|
||||
.silent_and_still_expected()
|
||||
.map(|_| UnknownMediaCause::LiveServerSilent)
|
||||
}
|
||||
|
||||
pub fn fleet_silence_refusal(silence: &FleetSilence<'_>) -> Option<AbortReason> {
|
||||
fleet_silence_cause(silence).map(|_| AbortReason::UnknownMedia)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct PreflightWindow {
|
||||
pub now: Millis,
|
||||
pub wall_now: WallMillis,
|
||||
pub preflight_max_age_ms: u64,
|
||||
pub pending_join_skew_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct DmPosture {
|
||||
pub dm_gateway_eviction: bool,
|
||||
pub dm_media_eviction: bool,
|
||||
}
|
||||
|
||||
impl Default for DmPosture {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
dm_gateway_eviction: true,
|
||||
dm_media_eviction: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DmPosture {
|
||||
pub const fn allows_gateway_removal(self, room: RoomKey) -> bool {
|
||||
match room {
|
||||
RoomKey::Guild { .. } => true,
|
||||
RoomKey::Dm { .. } => self.dm_gateway_eviction,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn allows_media_removal(self, room: RoomKey) -> bool {
|
||||
match room {
|
||||
RoomKey::Guild { .. } => true,
|
||||
RoomKey::Dm { .. } => self.dm_media_eviction,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct GatewayRemovalGuard<'a> {
|
||||
pub room: RoomKey,
|
||||
pub target: &'a ConnectionId,
|
||||
pub user_id: UserId,
|
||||
pub posture: ConnectionIdGuard,
|
||||
pub dm_posture: DmPosture,
|
||||
pub preflight: &'a GatewayPreflight,
|
||||
pub window: PreflightWindow,
|
||||
}
|
||||
|
||||
pub fn gateway_removal_preflight(guard: &GatewayRemovalGuard<'_>) -> Result<(), AbortReason> {
|
||||
if !guard
|
||||
.preflight
|
||||
.is_fresh(guard.window.now, guard.window.preflight_max_age_ms)
|
||||
{
|
||||
return Err(AbortReason::PreflightStale);
|
||||
}
|
||||
|
||||
if !guard.dm_posture.allows_gateway_removal(guard.room) {
|
||||
return Err(AbortReason::DmPosture);
|
||||
}
|
||||
|
||||
let states: Vec<&PreflightVoiceState> = guard.preflight.states_for(guard.user_id).collect();
|
||||
|
||||
if states.iter().any(|state| state.connection.is_none()) {
|
||||
return Err(AbortReason::NoConnectionId);
|
||||
}
|
||||
|
||||
if connection_id_is_honoured(guard.posture, guard.room) {
|
||||
if !states
|
||||
.iter()
|
||||
.any(|state| state.connection.as_ref() == Some(guard.target))
|
||||
{
|
||||
return Err(AbortReason::PreflightDisagreed);
|
||||
}
|
||||
} else {
|
||||
let live = guard
|
||||
.preflight
|
||||
.live_connections_of(guard.user_id, guard.window);
|
||||
if live.len() != 1 {
|
||||
return Err(AbortReason::SiblingConnection);
|
||||
}
|
||||
if &live[0] != guard.target {
|
||||
return Err(AbortReason::SiblingConnection);
|
||||
}
|
||||
}
|
||||
|
||||
if guard.preflight.user_has_pending_join(
|
||||
guard.user_id,
|
||||
guard.window.wall_now,
|
||||
guard.window.pending_join_skew_ms,
|
||||
) {
|
||||
return Err(AbortReason::PendingJoin);
|
||||
}
|
||||
|
||||
if guard.preflight.media_holds(guard.target) {
|
||||
return Err(AbortReason::PreflightDisagreed);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct MediaRemovalGuard<'a> {
|
||||
pub room: RoomKey,
|
||||
pub target: &'a ConnectionId,
|
||||
pub user_id: UserId,
|
||||
pub dm_posture: DmPosture,
|
||||
pub divergence_since: Millis,
|
||||
pub participant_joined_at: Millis,
|
||||
pub repair_verdict: Option<RepairVerdict>,
|
||||
pub server_health: ServerHealth,
|
||||
pub preflight: &'a GatewayPreflight,
|
||||
pub window: PreflightWindow,
|
||||
}
|
||||
|
||||
pub fn media_removal_preflight(guard: &MediaRemovalGuard<'_>) -> Result<(), AbortReason> {
|
||||
if !guard
|
||||
.preflight
|
||||
.is_fresh(guard.window.now, guard.window.preflight_max_age_ms)
|
||||
{
|
||||
return Err(AbortReason::PreflightStale);
|
||||
}
|
||||
|
||||
if !guard.dm_posture.allows_media_removal(guard.room) {
|
||||
return Err(AbortReason::DmPosture);
|
||||
}
|
||||
|
||||
if guard
|
||||
.preflight
|
||||
.states_for(guard.user_id)
|
||||
.any(|state| state.connection.is_none())
|
||||
{
|
||||
return Err(AbortReason::NoConnectionId);
|
||||
}
|
||||
|
||||
if guard.participant_joined_at > guard.divergence_since {
|
||||
return Err(AbortReason::JoinedAfterDivergence);
|
||||
}
|
||||
|
||||
if matches!(guard.room, RoomKey::Guild { .. })
|
||||
&& guard.repair_verdict != Some(RepairVerdict::NotRepairable)
|
||||
{
|
||||
return Err(AbortReason::PreflightDisagreed);
|
||||
}
|
||||
|
||||
if guard.preflight.gateway_holds(guard.target) {
|
||||
return Err(AbortReason::PreflightDisagreed);
|
||||
}
|
||||
|
||||
if guard.preflight.user_has_pending_join(
|
||||
guard.user_id,
|
||||
guard.window.wall_now,
|
||||
guard.window.pending_join_skew_ms,
|
||||
) {
|
||||
return Err(AbortReason::PendingJoin);
|
||||
}
|
||||
|
||||
if !guard.server_health.is_healthy() {
|
||||
return Err(AbortReason::ServerUnhealthy);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,502 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::ids::{Location, Millis};
|
||||
|
||||
pub const DEGRADED_AT_FAILURES: u32 = 3;
|
||||
pub const UNREACHABLE_AT_FAILURES: u32 = 10;
|
||||
pub const QUARANTINED_AT_FAILURES: u32 = 30;
|
||||
pub const SUCCESSES_TO_RECOVER: u32 = 3;
|
||||
pub const UNREACHABLE_PROBE_INTERVAL_MS: u64 = 10_000;
|
||||
pub const QUARANTINED_PROBE_INTERVAL_MS: u64 = 60_000;
|
||||
pub const MUTATIONS_PAUSED_BACKOFF_MS: u64 = 15_000;
|
||||
pub const MUTATIONS_PAUSED_MAX_BACKOFF_MS: u64 = 120_000;
|
||||
pub const MUTATIONS_PAUSED_QUIET_MS: u64 = 300_000;
|
||||
pub const MUTATIONS_PAUSED_MAX_DOUBLINGS: u32 = 3;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ReconMode {
|
||||
Halted,
|
||||
Observing,
|
||||
Constructive,
|
||||
Enforcing,
|
||||
}
|
||||
|
||||
impl ReconMode {
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"halted" => Some(Self::Halted),
|
||||
"observing" => Some(Self::Observing),
|
||||
"constructive" => Some(Self::Constructive),
|
||||
"enforcing" => Some(Self::Enforcing),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Halted => "halted",
|
||||
Self::Observing => "observing",
|
||||
Self::Constructive => "constructive",
|
||||
Self::Enforcing => "enforcing",
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn allows_constructive(self) -> bool {
|
||||
matches!(self, Self::Constructive | Self::Enforcing)
|
||||
}
|
||||
|
||||
pub const fn allows_destructive(self) -> bool {
|
||||
matches!(self, Self::Enforcing)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ModeClamp {
|
||||
Warmup,
|
||||
ColdStart,
|
||||
BreakerTripped,
|
||||
SingletonConflict,
|
||||
TopologyStale,
|
||||
NatsReconnect,
|
||||
MutationsPaused,
|
||||
CoverageOverrun,
|
||||
}
|
||||
|
||||
impl ModeClamp {
|
||||
pub const ALL: [Self; 8] = [
|
||||
Self::Warmup,
|
||||
Self::ColdStart,
|
||||
Self::BreakerTripped,
|
||||
Self::SingletonConflict,
|
||||
Self::TopologyStale,
|
||||
Self::NatsReconnect,
|
||||
Self::MutationsPaused,
|
||||
Self::CoverageOverrun,
|
||||
];
|
||||
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Warmup => "warmup",
|
||||
Self::ColdStart => "cold_start",
|
||||
Self::BreakerTripped => "breaker_tripped",
|
||||
Self::SingletonConflict => "singleton_conflict",
|
||||
Self::TopologyStale => "topology_stale",
|
||||
Self::NatsReconnect => "nats_reconnect",
|
||||
Self::MutationsPaused => "mutations_paused",
|
||||
Self::CoverageOverrun => "coverage_overrun",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
let wanted = value.trim().to_ascii_lowercase();
|
||||
Self::ALL.into_iter().find(|clamp| clamp.label() == wanted)
|
||||
}
|
||||
|
||||
pub const fn is_self_clearing(self) -> bool {
|
||||
match self {
|
||||
Self::Warmup
|
||||
| Self::ColdStart
|
||||
| Self::SingletonConflict
|
||||
| Self::TopologyStale
|
||||
| Self::NatsReconnect
|
||||
| Self::MutationsPaused
|
||||
| Self::CoverageOverrun => true,
|
||||
Self::BreakerTripped => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct ModeClamps {
|
||||
active: BTreeMap<ModeClamp, Option<Millis>>,
|
||||
}
|
||||
|
||||
impl ModeClamps {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn set(&mut self, clamp: ModeClamp, engaged: bool) {
|
||||
if engaged {
|
||||
self.active.insert(clamp, None);
|
||||
} else {
|
||||
self.active.remove(&clamp);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn engage_until(&mut self, clamp: ModeClamp, until: Millis) {
|
||||
self.active.insert(clamp, Some(until));
|
||||
}
|
||||
|
||||
pub fn release_expired(&mut self, now: Millis) {
|
||||
self.active
|
||||
.retain(|_, until| until.is_none_or(|deadline| now < deadline));
|
||||
}
|
||||
|
||||
pub fn is_engaged(&self, clamp: ModeClamp) -> bool {
|
||||
self.active.contains_key(&clamp)
|
||||
}
|
||||
|
||||
pub fn deadline(&self, clamp: ModeClamp) -> Option<Millis> {
|
||||
self.active.get(&clamp).copied().flatten()
|
||||
}
|
||||
|
||||
pub fn release(&mut self, clamp: ModeClamp) -> bool {
|
||||
self.active.remove(&clamp).is_some()
|
||||
}
|
||||
|
||||
pub fn engaged(&self) -> impl Iterator<Item = ModeClamp> + '_ {
|
||||
self.active.keys().copied()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.active.is_empty()
|
||||
}
|
||||
|
||||
pub fn ceiling(&self) -> ReconMode {
|
||||
if self.active.is_empty() {
|
||||
ReconMode::Enforcing
|
||||
} else {
|
||||
ReconMode::Constructive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn effective_mode(
|
||||
configured: ReconMode,
|
||||
runtime_override: Option<ReconMode>,
|
||||
clamps: &ModeClamps,
|
||||
) -> ReconMode {
|
||||
let requested = match runtime_override {
|
||||
None => configured,
|
||||
Some(override_mode) => configured.min(override_mode),
|
||||
};
|
||||
requested.min(clamps.ceiling())
|
||||
}
|
||||
|
||||
pub const fn mutations_paused_hold_ms(windows: u32) -> u64 {
|
||||
if windows == 0 {
|
||||
return 0;
|
||||
}
|
||||
let doublings = if windows - 1 > MUTATIONS_PAUSED_MAX_DOUBLINGS {
|
||||
MUTATIONS_PAUSED_MAX_DOUBLINGS
|
||||
} else {
|
||||
windows - 1
|
||||
};
|
||||
let hold = MUTATIONS_PAUSED_BACKOFF_MS.saturating_mul(1u64 << doublings);
|
||||
if hold > MUTATIONS_PAUSED_MAX_BACKOFF_MS {
|
||||
MUTATIONS_PAUSED_MAX_BACKOFF_MS
|
||||
} else {
|
||||
hold
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct PauseBackoff {
|
||||
windows: u32,
|
||||
until: Option<Millis>,
|
||||
}
|
||||
|
||||
impl PauseBackoff {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
windows: 0,
|
||||
until: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn windows(&self) -> u32 {
|
||||
self.windows
|
||||
}
|
||||
|
||||
pub const fn until(&self) -> Option<Millis> {
|
||||
self.until
|
||||
}
|
||||
|
||||
pub const fn hold_ms(&self) -> u64 {
|
||||
mutations_paused_hold_ms(self.windows)
|
||||
}
|
||||
|
||||
pub const fn is_holding(&self, now: Millis) -> bool {
|
||||
match self.until {
|
||||
None => false,
|
||||
Some(until) => now.get() < until.get(),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn remaining_ms(&self, now: Millis) -> u64 {
|
||||
match self.until {
|
||||
None => 0,
|
||||
Some(until) => until.saturating_since(now),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn note_refused(&mut self, now: Millis) -> Option<Millis> {
|
||||
if self.is_holding(now) {
|
||||
return None;
|
||||
}
|
||||
self.windows = self.windows.saturating_add(1);
|
||||
let until = now.saturating_add_millis(mutations_paused_hold_ms(self.windows));
|
||||
self.until = Some(until);
|
||||
Some(until)
|
||||
}
|
||||
|
||||
pub const fn clear(&mut self) {
|
||||
self.windows = 0;
|
||||
self.until = None;
|
||||
}
|
||||
|
||||
pub const fn decay(&mut self, now: Millis) -> bool {
|
||||
let Some(until) = self.until else {
|
||||
return false;
|
||||
};
|
||||
if now.get() < until.get() {
|
||||
return false;
|
||||
}
|
||||
if now.saturating_since(until) < MUTATIONS_PAUSED_QUIET_MS {
|
||||
return false;
|
||||
}
|
||||
self.clear();
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct WarmupGate {
|
||||
started_at: Millis,
|
||||
warmup_ms: u64,
|
||||
coverage_pass_complete: bool,
|
||||
}
|
||||
|
||||
impl WarmupGate {
|
||||
pub const fn new(started_at: Millis, warmup_ms: u64) -> Self {
|
||||
Self {
|
||||
started_at,
|
||||
warmup_ms,
|
||||
coverage_pass_complete: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn note_coverage_pass(&mut self) {
|
||||
self.coverage_pass_complete = true;
|
||||
}
|
||||
|
||||
pub const fn coverage_pass_complete(&self) -> bool {
|
||||
self.coverage_pass_complete
|
||||
}
|
||||
|
||||
pub const fn elapsed_complete(&self, now: Millis) -> bool {
|
||||
now.saturating_since(self.started_at) >= self.warmup_ms
|
||||
}
|
||||
|
||||
pub const fn is_complete(&self, now: Millis) -> bool {
|
||||
self.elapsed_complete(now) && self.coverage_pass_complete
|
||||
}
|
||||
|
||||
pub const fn force_complete(&mut self) {
|
||||
self.warmup_ms = 0;
|
||||
self.coverage_pass_complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ServerHealth {
|
||||
Healthy,
|
||||
Degraded,
|
||||
Unreachable,
|
||||
Quarantined,
|
||||
}
|
||||
|
||||
impl ServerHealth {
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Healthy => "healthy",
|
||||
Self::Degraded => "degraded",
|
||||
Self::Unreachable => "unreachable",
|
||||
Self::Quarantined => "quarantined",
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_healthy(self) -> bool {
|
||||
matches!(self, Self::Healthy)
|
||||
}
|
||||
|
||||
pub const fn is_written_off(self) -> bool {
|
||||
matches!(self, Self::Unreachable | Self::Quarantined)
|
||||
}
|
||||
|
||||
pub const fn severity(self) -> u8 {
|
||||
match self {
|
||||
Self::Healthy => 0,
|
||||
Self::Degraded => 1,
|
||||
Self::Unreachable => 2,
|
||||
Self::Quarantined => 3,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn worse_of(self, other: Self) -> Self {
|
||||
if self.severity() >= other.severity() {
|
||||
self
|
||||
} else {
|
||||
other
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn probe_interval_ms(self) -> Option<u64> {
|
||||
match self {
|
||||
Self::Healthy | Self::Degraded => None,
|
||||
Self::Unreachable => Some(UNREACHABLE_PROBE_INTERVAL_MS),
|
||||
Self::Quarantined => Some(QUARANTINED_PROBE_INTERVAL_MS),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ProbeOutcome {
|
||||
Success,
|
||||
Failure,
|
||||
AuthFailure,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct ServerHealthTracker {
|
||||
state: ServerHealth,
|
||||
consecutive_failures: u32,
|
||||
consecutive_successes: u32,
|
||||
last_probe_at: Option<Millis>,
|
||||
ever_answered: bool,
|
||||
}
|
||||
|
||||
impl Default for ServerHealthTracker {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerHealthTracker {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
state: ServerHealth::Healthy,
|
||||
consecutive_failures: 0,
|
||||
consecutive_successes: 0,
|
||||
last_probe_at: None,
|
||||
ever_answered: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn state(&self) -> ServerHealth {
|
||||
self.state
|
||||
}
|
||||
|
||||
pub const fn consecutive_failures(&self) -> u32 {
|
||||
self.consecutive_failures
|
||||
}
|
||||
|
||||
pub const fn ever_answered(&self) -> bool {
|
||||
self.ever_answered
|
||||
}
|
||||
|
||||
pub const fn record(&mut self, outcome: ProbeOutcome, now: Millis) {
|
||||
self.last_probe_at = Some(now);
|
||||
match outcome {
|
||||
ProbeOutcome::Success => {
|
||||
self.ever_answered = true;
|
||||
self.consecutive_failures = 0;
|
||||
self.consecutive_successes = self.consecutive_successes.saturating_add(1);
|
||||
if self.consecutive_successes >= SUCCESSES_TO_RECOVER {
|
||||
self.state = ServerHealth::Healthy;
|
||||
}
|
||||
}
|
||||
ProbeOutcome::Failure => {
|
||||
self.consecutive_successes = 0;
|
||||
self.consecutive_failures = self.consecutive_failures.saturating_add(1);
|
||||
self.state = self
|
||||
.state
|
||||
.worse_of(failure_state(self.consecutive_failures));
|
||||
}
|
||||
ProbeOutcome::AuthFailure => {
|
||||
self.consecutive_successes = 0;
|
||||
self.consecutive_failures = self.consecutive_failures.saturating_add(1);
|
||||
self.state = ServerHealth::Quarantined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn may_probe(&self, now: Millis) -> bool {
|
||||
match (self.state.probe_interval_ms(), self.last_probe_at) {
|
||||
(None, _) => true,
|
||||
(Some(_), None) => true,
|
||||
(Some(interval), Some(last)) => now.saturating_since(last) >= interval,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn last_probe_at(&self) -> Option<Millis> {
|
||||
self.last_probe_at
|
||||
}
|
||||
}
|
||||
|
||||
const fn failure_state(consecutive_failures: u32) -> ServerHealth {
|
||||
if consecutive_failures >= QUARANTINED_AT_FAILURES {
|
||||
ServerHealth::Quarantined
|
||||
} else if consecutive_failures >= UNREACHABLE_AT_FAILURES {
|
||||
ServerHealth::Unreachable
|
||||
} else if consecutive_failures >= DEGRADED_AT_FAILURES {
|
||||
ServerHealth::Degraded
|
||||
} else {
|
||||
ServerHealth::Healthy
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct ServerHealthMap {
|
||||
servers: BTreeMap<Location, ServerHealthTracker>,
|
||||
}
|
||||
|
||||
impl ServerHealthMap {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn record(&mut self, location: &Location, outcome: ProbeOutcome, now: Millis) {
|
||||
self.servers
|
||||
.entry(location.clone())
|
||||
.or_default()
|
||||
.record(outcome, now);
|
||||
}
|
||||
|
||||
pub fn health(&self, location: &Location) -> ServerHealth {
|
||||
self.servers
|
||||
.get(location)
|
||||
.map_or(ServerHealth::Healthy, ServerHealthTracker::state)
|
||||
}
|
||||
|
||||
pub fn ever_answered(&self, location: &Location) -> bool {
|
||||
self.servers
|
||||
.get(location)
|
||||
.is_some_and(ServerHealthTracker::ever_answered)
|
||||
}
|
||||
|
||||
pub fn may_probe(&self, location: &Location, now: Millis) -> bool {
|
||||
self.servers
|
||||
.get(location)
|
||||
.is_none_or(|tracker| tracker.may_probe(now))
|
||||
}
|
||||
|
||||
pub fn tracked(&self) -> impl Iterator<Item = (&Location, &ServerHealthTracker)> {
|
||||
self.servers.iter()
|
||||
}
|
||||
|
||||
pub fn forget(&mut self, location: &Location) {
|
||||
self.servers.remove(location);
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.servers.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.servers.is_empty()
|
||||
}
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::fmt;
|
||||
|
||||
const REDACTED: &str = "***";
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum IdError {
|
||||
#[error("identifier is empty")]
|
||||
Empty,
|
||||
#[error("identifier contains whitespace")]
|
||||
ContainsWhitespace,
|
||||
#[error("identifier contains an underscore")]
|
||||
ContainsUnderscore,
|
||||
}
|
||||
|
||||
macro_rules! u64_newtype {
|
||||
($name:ident) => {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct $name(u64);
|
||||
|
||||
impl $name {
|
||||
pub const fn new(value: u64) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
pub const fn get(self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for $name {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
u64_newtype!(GuildId);
|
||||
u64_newtype!(ChannelId);
|
||||
u64_newtype!(UserId);
|
||||
u64_newtype!(TurnId);
|
||||
u64_newtype!(Epoch);
|
||||
u64_newtype!(Millis);
|
||||
u64_newtype!(WallMillis);
|
||||
|
||||
impl TurnId {
|
||||
pub const FIRST: Self = Self(0);
|
||||
|
||||
pub const fn next(self) -> Self {
|
||||
Self(self.0.saturating_add(1))
|
||||
}
|
||||
}
|
||||
|
||||
impl Epoch {
|
||||
pub const FIRST: Self = Self(0);
|
||||
|
||||
pub const fn next(self) -> Self {
|
||||
Self(self.0.saturating_add(1))
|
||||
}
|
||||
}
|
||||
|
||||
impl Millis {
|
||||
pub const ZERO: Self = Self(0);
|
||||
|
||||
pub const fn saturating_since(self, earlier: Self) -> u64 {
|
||||
self.0.saturating_sub(earlier.0)
|
||||
}
|
||||
|
||||
pub const fn saturating_add_millis(self, delta: u64) -> Self {
|
||||
Self(self.0.saturating_add(delta))
|
||||
}
|
||||
}
|
||||
|
||||
impl WallMillis {
|
||||
pub const ZERO: Self = Self(0);
|
||||
|
||||
pub const fn saturating_since(self, earlier: Self) -> u64 {
|
||||
self.0.saturating_sub(earlier.0)
|
||||
}
|
||||
|
||||
pub const fn saturating_add_millis(self, delta: u64) -> Self {
|
||||
Self(self.0.saturating_add(delta))
|
||||
}
|
||||
}
|
||||
|
||||
fn checked_text(value: &str) -> Result<Box<str>, IdError> {
|
||||
if value.is_empty() {
|
||||
return Err(IdError::Empty);
|
||||
}
|
||||
if value.contains(char::is_whitespace) {
|
||||
return Err(IdError::ContainsWhitespace);
|
||||
}
|
||||
Ok(Box::from(value))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ConnectionId(Box<str>);
|
||||
|
||||
impl ConnectionId {
|
||||
pub fn new(value: &str) -> Result<Self, IdError> {
|
||||
let text = checked_text(value)?;
|
||||
if text.contains('_') {
|
||||
return Err(IdError::ContainsUnderscore);
|
||||
}
|
||||
Ok(Self(text))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ConnectionId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct RegionId(Box<str>);
|
||||
|
||||
impl RegionId {
|
||||
pub fn new(value: &str) -> Result<Self, IdError> {
|
||||
checked_text(value).map(Self)
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RegionId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ServerId(Box<str>);
|
||||
|
||||
impl ServerId {
|
||||
pub fn new(value: &str) -> Result<Self, IdError> {
|
||||
checked_text(value).map(Self)
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ServerId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct ApiSecret(Box<str>);
|
||||
|
||||
impl ApiSecret {
|
||||
pub fn new(value: &str) -> Self {
|
||||
Self(Box::from(value))
|
||||
}
|
||||
|
||||
pub fn expose(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ApiSecret {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(REDACTED)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ApiSecret {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(REDACTED)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum Scope {
|
||||
Guild,
|
||||
Dm,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum RoomKey {
|
||||
Guild {
|
||||
guild_id: GuildId,
|
||||
channel_id: ChannelId,
|
||||
},
|
||||
Dm {
|
||||
channel_id: ChannelId,
|
||||
},
|
||||
}
|
||||
|
||||
impl RoomKey {
|
||||
pub const fn scope(self) -> Scope {
|
||||
match self {
|
||||
Self::Guild { .. } => Scope::Guild,
|
||||
Self::Dm { .. } => Scope::Dm,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn channel_id(self) -> ChannelId {
|
||||
match self {
|
||||
Self::Guild { channel_id, .. } => channel_id,
|
||||
Self::Dm { channel_id } => channel_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn guild_id(self) -> Option<GuildId> {
|
||||
match self {
|
||||
Self::Guild { guild_id, .. } => Some(guild_id),
|
||||
Self::Dm { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Location {
|
||||
pub region: RegionId,
|
||||
pub server: ServerId,
|
||||
}
|
||||
|
||||
impl Location {
|
||||
pub const fn new(region: RegionId, server: ServerId) -> Self {
|
||||
Self { region, server }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Location {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}/{}", self.region, self.server)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ConnectionKey {
|
||||
pub room: RoomKey,
|
||||
pub connection: ConnectionId,
|
||||
}
|
||||
|
||||
impl ConnectionKey {
|
||||
pub const fn new(room: RoomKey, connection: ConnectionId) -> Self {
|
||||
Self { room, connection }
|
||||
}
|
||||
}
|
||||
@@ -1,978 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::mem::size_of;
|
||||
|
||||
use crate::decide::DecideParams;
|
||||
use crate::evidence::{CliffWindow, PriorConnection};
|
||||
use crate::gateway::Nonce;
|
||||
use crate::ids::{ConnectionId, ConnectionKey, Epoch, Location, Millis, RoomKey, TurnId, UserId};
|
||||
|
||||
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
|
||||
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
|
||||
const CONNECTION_ID_HEAP_BYTES: usize = 40;
|
||||
pub const MAX_ROSTER_STAMPS: usize = 16;
|
||||
const ROSTER_STAMP_BYTES: usize = 16;
|
||||
const ROOM_OVERHEAD_BYTES: usize = 256 + MAX_ROSTER_STAMPS * ROSTER_STAMP_BYTES;
|
||||
|
||||
pub const MAP_ENTRY_OVERHEAD_BYTES: usize = 48;
|
||||
pub const LOCATION_MAP_BYTES: u64 = 640;
|
||||
pub const LOCATION_ENTRY_BYTES: u64 = 64;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Fingerprint(u64);
|
||||
|
||||
impl Fingerprint {
|
||||
pub const fn get(self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct Hasher(u64);
|
||||
|
||||
impl Hasher {
|
||||
const fn new() -> Self {
|
||||
Self(FNV_OFFSET)
|
||||
}
|
||||
|
||||
const fn byte(&mut self, value: u8) {
|
||||
self.0 ^= value as u64;
|
||||
self.0 = self.0.wrapping_mul(FNV_PRIME);
|
||||
}
|
||||
|
||||
const fn u64(&mut self, value: u64) {
|
||||
let bytes = value.to_le_bytes();
|
||||
let mut index = 0;
|
||||
while index < bytes.len() {
|
||||
self.byte(bytes[index]);
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn text(&mut self, value: &str) {
|
||||
for byte in value.as_bytes() {
|
||||
self.byte(*byte);
|
||||
}
|
||||
self.byte(0);
|
||||
}
|
||||
|
||||
const fn finish(self) -> Fingerprint {
|
||||
Fingerprint(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct FingerprintInput<'a> {
|
||||
pub room: RoomKey,
|
||||
pub connection: &'a ConnectionId,
|
||||
pub user_id: UserId,
|
||||
pub gateway_siblings: &'a [ConnectionId],
|
||||
pub media_locations: &'a [Location],
|
||||
pub participant_joined_at: Option<Millis>,
|
||||
pub census_epoch: Epoch,
|
||||
pub topology_epoch: Epoch,
|
||||
}
|
||||
|
||||
pub fn fingerprint(input: &FingerprintInput<'_>) -> Fingerprint {
|
||||
let mut hasher = Hasher::new();
|
||||
|
||||
match input.room {
|
||||
RoomKey::Guild {
|
||||
guild_id,
|
||||
channel_id,
|
||||
} => {
|
||||
hasher.byte(1);
|
||||
hasher.u64(guild_id.get());
|
||||
hasher.u64(channel_id.get());
|
||||
}
|
||||
RoomKey::Dm { channel_id } => {
|
||||
hasher.byte(2);
|
||||
hasher.u64(channel_id.get());
|
||||
}
|
||||
}
|
||||
|
||||
hasher.text(input.connection.as_str());
|
||||
hasher.u64(input.user_id.get());
|
||||
|
||||
hasher.u64(input.gateway_siblings.len() as u64);
|
||||
for sibling in input.gateway_siblings {
|
||||
hasher.text(sibling.as_str());
|
||||
}
|
||||
|
||||
hasher.u64(input.media_locations.len() as u64);
|
||||
for location in input.media_locations {
|
||||
hasher.text(location.region.as_str());
|
||||
hasher.text(location.server.as_str());
|
||||
}
|
||||
|
||||
match input.participant_joined_at {
|
||||
None => hasher.byte(0),
|
||||
Some(joined_at) => {
|
||||
hasher.byte(1);
|
||||
hasher.u64(joined_at.get());
|
||||
}
|
||||
}
|
||||
|
||||
hasher.u64(input.census_epoch.get());
|
||||
hasher.u64(input.topology_epoch.get());
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct LocationIndex(u16);
|
||||
|
||||
impl LocationIndex {
|
||||
pub const fn get(self) -> u16 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct LocationTable {
|
||||
entries: Vec<Location>,
|
||||
}
|
||||
|
||||
impl LocationTable {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
entries: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intern(&mut self, location: &Location) -> Option<LocationIndex> {
|
||||
if let Some(position) = self.entries.iter().position(|entry| entry == location) {
|
||||
return u16::try_from(position).ok().map(LocationIndex);
|
||||
}
|
||||
let index = u16::try_from(self.entries.len()).ok()?;
|
||||
self.entries.push(location.clone());
|
||||
Some(LocationIndex(index))
|
||||
}
|
||||
|
||||
pub fn get(&self, index: LocationIndex) -> Option<&Location> {
|
||||
self.entries.get(usize::from(index.0))
|
||||
}
|
||||
|
||||
pub fn index_of(&self, location: &Location) -> Option<LocationIndex> {
|
||||
self.entries
|
||||
.iter()
|
||||
.position(|entry| entry == location)
|
||||
.and_then(|position| u16::try_from(position).ok())
|
||||
.map(LocationIndex)
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entries.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum RepairVerdict {
|
||||
Repaired,
|
||||
NoChange,
|
||||
NotRepairable,
|
||||
}
|
||||
|
||||
impl RepairVerdict {
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Repaired => "repaired",
|
||||
Self::NoChange => "no_change",
|
||||
Self::NotRepairable => "not_repairable",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ActionKind {
|
||||
RemoveGatewayState,
|
||||
RemoveParticipant,
|
||||
}
|
||||
|
||||
impl ActionKind {
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::RemoveGatewayState => "remove_gateway_state",
|
||||
Self::RemoveParticipant => "remove_participant",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PriorDivergence {
|
||||
GatewayOnly,
|
||||
MediaOnly,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum WedgedReason {
|
||||
RepairExhausted,
|
||||
ActionIneffective,
|
||||
}
|
||||
|
||||
impl WedgedReason {
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::RepairExhausted => "repair_exhausted",
|
||||
Self::ActionIneffective => "action_ineffective",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ConnectionState {
|
||||
Nascent,
|
||||
Consistent,
|
||||
PendingJoin {
|
||||
expires_at: crate::ids::WallMillis,
|
||||
nonce: Nonce,
|
||||
last_confirm: Millis,
|
||||
},
|
||||
GatewayOnly {
|
||||
since: Millis,
|
||||
corroborations: u32,
|
||||
fingerprint: Fingerprint,
|
||||
last_corroboration: Millis,
|
||||
},
|
||||
MediaOnly {
|
||||
since: Millis,
|
||||
corroborations: u32,
|
||||
fingerprint: Fingerprint,
|
||||
last_corroboration: Millis,
|
||||
location: LocationIndex,
|
||||
participant_joined_at: Millis,
|
||||
repair_verdict: Option<RepairVerdict>,
|
||||
},
|
||||
Repairing {
|
||||
since: Millis,
|
||||
last_attempt: Millis,
|
||||
},
|
||||
ActionTaken {
|
||||
kind: ActionKind,
|
||||
at: Millis,
|
||||
authorizing_turn: TurnId,
|
||||
prior: PriorDivergence,
|
||||
since: Millis,
|
||||
},
|
||||
Wedged {
|
||||
reason: WedgedReason,
|
||||
at: Millis,
|
||||
fingerprint: Fingerprint,
|
||||
},
|
||||
Retired {
|
||||
at: Millis,
|
||||
},
|
||||
}
|
||||
|
||||
impl ConnectionState {
|
||||
pub const fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Nascent => "nascent",
|
||||
Self::Consistent => "consistent",
|
||||
Self::PendingJoin { .. } => "pending_join",
|
||||
Self::GatewayOnly { .. } => "gateway_only",
|
||||
Self::MediaOnly { .. } => "media_only",
|
||||
Self::Repairing { .. } => "repairing",
|
||||
Self::ActionTaken { .. } => "action_taken",
|
||||
Self::Wedged { .. } => "wedged",
|
||||
Self::Retired { .. } => "retired",
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn stored_fingerprint(&self) -> Option<Fingerprint> {
|
||||
match self {
|
||||
Self::GatewayOnly { fingerprint, .. }
|
||||
| Self::MediaOnly { fingerprint, .. }
|
||||
| Self::Wedged { fingerprint, .. } => Some(*fingerprint),
|
||||
Self::Nascent
|
||||
| Self::Consistent
|
||||
| Self::PendingJoin { .. }
|
||||
| Self::Repairing { .. }
|
||||
| Self::ActionTaken { .. }
|
||||
| Self::Retired { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn corroborations(&self) -> u32 {
|
||||
match self {
|
||||
Self::GatewayOnly { corroborations, .. } | Self::MediaOnly { corroborations, .. } => {
|
||||
*corroborations
|
||||
}
|
||||
Self::Nascent
|
||||
| Self::Consistent
|
||||
| Self::PendingJoin { .. }
|
||||
| Self::Repairing { .. }
|
||||
| Self::ActionTaken { .. }
|
||||
| Self::Wedged { .. }
|
||||
| Self::Retired { .. } => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn divergence_since(&self) -> Option<Millis> {
|
||||
match self {
|
||||
Self::GatewayOnly { since, .. }
|
||||
| Self::MediaOnly { since, .. }
|
||||
| Self::Repairing { since, .. }
|
||||
| Self::ActionTaken { since, .. } => Some(*since),
|
||||
Self::Nascent
|
||||
| Self::Consistent
|
||||
| Self::PendingJoin { .. }
|
||||
| Self::Wedged { .. }
|
||||
| Self::Retired { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_divergent(&self) -> bool {
|
||||
match self {
|
||||
Self::GatewayOnly { .. }
|
||||
| Self::MediaOnly { .. }
|
||||
| Self::Repairing { .. }
|
||||
| Self::ActionTaken { .. }
|
||||
| Self::Wedged { .. } => true,
|
||||
Self::Nascent | Self::Consistent | Self::PendingJoin { .. } | Self::Retired { .. } => {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ConnectionLedger {
|
||||
pub connection: ConnectionId,
|
||||
pub user_id: UserId,
|
||||
pub state: ConnectionState,
|
||||
pub last_seen: Millis,
|
||||
pub action_attempts: u8,
|
||||
pub repair_attempts: u8,
|
||||
pub last_action_at: Option<Millis>,
|
||||
pub last_repair_verdict: Option<RepairVerdict>,
|
||||
pub last_location: Option<LocationIndex>,
|
||||
}
|
||||
|
||||
impl ConnectionLedger {
|
||||
pub fn new(connection: ConnectionId, user_id: UserId, now: Millis) -> Self {
|
||||
Self {
|
||||
connection,
|
||||
user_id,
|
||||
state: ConnectionState::Nascent,
|
||||
last_seen: now,
|
||||
action_attempts: 0,
|
||||
repair_attempts: 0,
|
||||
last_action_at: None,
|
||||
last_repair_verdict: None,
|
||||
last_location: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_divergent(&self) -> bool {
|
||||
self.state.is_divergent()
|
||||
}
|
||||
|
||||
pub const fn believed_home(&self) -> Option<LocationIndex> {
|
||||
match self.state {
|
||||
ConnectionState::Retired { .. } => None,
|
||||
ConnectionState::Nascent
|
||||
| ConnectionState::Consistent
|
||||
| ConnectionState::PendingJoin { .. }
|
||||
| ConnectionState::GatewayOnly { .. }
|
||||
| ConnectionState::MediaOnly { .. }
|
||||
| ConnectionState::Repairing { .. }
|
||||
| ConnectionState::ActionTaken { .. }
|
||||
| ConnectionState::Wedged { .. } => self.last_location,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_evidence(&mut self) {
|
||||
self.state = ConnectionState::Nascent;
|
||||
self.last_repair_verdict = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RoomLedger {
|
||||
room: RoomKey,
|
||||
connections: Vec<ConnectionLedger>,
|
||||
last_seen: Millis,
|
||||
last_turn: TurnId,
|
||||
gateway_cliff: CliffWindow,
|
||||
media_cliff: CliffWindow,
|
||||
partially_unreadable: bool,
|
||||
no_candidates: bool,
|
||||
last_known_locations: Vec<LocationIndex>,
|
||||
roster_stamps: Vec<(LocationIndex, Millis)>,
|
||||
}
|
||||
|
||||
impl RoomLedger {
|
||||
fn new(room: RoomKey, now: Millis) -> Self {
|
||||
Self {
|
||||
room,
|
||||
connections: Vec::new(),
|
||||
last_seen: now,
|
||||
last_turn: TurnId::FIRST,
|
||||
gateway_cliff: CliffWindow::clear(),
|
||||
media_cliff: CliffWindow::clear(),
|
||||
partially_unreadable: false,
|
||||
no_candidates: false,
|
||||
last_known_locations: Vec::new(),
|
||||
roster_stamps: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn room(&self) -> RoomKey {
|
||||
self.room
|
||||
}
|
||||
|
||||
pub fn connections(&self) -> &[ConnectionLedger] {
|
||||
&self.connections
|
||||
}
|
||||
|
||||
pub fn connection(&self, connection: &ConnectionId) -> Option<&ConnectionLedger> {
|
||||
self.connections
|
||||
.iter()
|
||||
.find(|entry| &entry.connection == connection)
|
||||
}
|
||||
|
||||
pub fn connection_mut(&mut self, connection: &ConnectionId) -> Option<&mut ConnectionLedger> {
|
||||
self.connections
|
||||
.iter_mut()
|
||||
.find(|entry| &entry.connection == connection)
|
||||
}
|
||||
|
||||
pub const fn last_seen(&self) -> Millis {
|
||||
self.last_seen
|
||||
}
|
||||
|
||||
pub const fn last_turn(&self) -> TurnId {
|
||||
self.last_turn
|
||||
}
|
||||
|
||||
pub const fn note_turn(&mut self, turn: TurnId, now: Millis) {
|
||||
self.last_turn = turn;
|
||||
self.last_seen = now;
|
||||
}
|
||||
|
||||
pub const fn gateway_cliff(&self) -> CliffWindow {
|
||||
self.gateway_cliff
|
||||
}
|
||||
|
||||
pub const fn gateway_cliff_mut(&mut self) -> &mut CliffWindow {
|
||||
&mut self.gateway_cliff
|
||||
}
|
||||
|
||||
pub const fn media_cliff(&self) -> CliffWindow {
|
||||
self.media_cliff
|
||||
}
|
||||
|
||||
pub const fn media_cliff_mut(&mut self) -> &mut CliffWindow {
|
||||
&mut self.media_cliff
|
||||
}
|
||||
|
||||
pub const fn partially_unreadable(&self) -> bool {
|
||||
self.partially_unreadable
|
||||
}
|
||||
|
||||
pub const fn set_partially_unreadable(&mut self, value: bool) {
|
||||
self.partially_unreadable = value;
|
||||
}
|
||||
|
||||
pub const fn no_candidates(&self) -> bool {
|
||||
self.no_candidates
|
||||
}
|
||||
|
||||
pub const fn set_no_candidates(&mut self, value: bool) {
|
||||
self.no_candidates = value;
|
||||
}
|
||||
|
||||
pub fn last_known_locations(&self) -> &[LocationIndex] {
|
||||
&self.last_known_locations
|
||||
}
|
||||
|
||||
pub fn note_location(&mut self, index: LocationIndex) {
|
||||
if !self.last_known_locations.contains(&index) {
|
||||
self.last_known_locations.push(index);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn note_roster_read(&mut self, index: LocationIndex, at: Millis) {
|
||||
if let Some(stamp) = self
|
||||
.roster_stamps
|
||||
.iter_mut()
|
||||
.find(|(stamped, _)| *stamped == index)
|
||||
{
|
||||
stamp.1 = at;
|
||||
return;
|
||||
}
|
||||
if self.roster_stamps.len() >= MAX_ROSTER_STAMPS {
|
||||
let oldest = self
|
||||
.roster_stamps
|
||||
.iter()
|
||||
.enumerate()
|
||||
.min_by_key(|(_, (_, at))| at.get())
|
||||
.map(|(position, _)| position);
|
||||
if let Some(position) = oldest {
|
||||
self.roster_stamps.remove(position);
|
||||
}
|
||||
}
|
||||
self.roster_stamps.push((index, at));
|
||||
}
|
||||
|
||||
pub fn roster_read_at(&self, index: LocationIndex) -> Option<Millis> {
|
||||
self.roster_stamps
|
||||
.iter()
|
||||
.find(|(stamped, _)| *stamped == index)
|
||||
.map(|(_, at)| *at)
|
||||
}
|
||||
|
||||
pub fn retain_roster_stamps(&mut self, live: &[LocationIndex]) {
|
||||
self.roster_stamps.retain(|(index, _)| live.contains(index));
|
||||
}
|
||||
|
||||
pub fn roster_stamps(&self) -> &[(LocationIndex, Millis)] {
|
||||
&self.roster_stamps
|
||||
}
|
||||
|
||||
pub fn believed_homes(&self) -> Vec<LocationIndex> {
|
||||
let mut homes: Vec<LocationIndex> = self
|
||||
.connections
|
||||
.iter()
|
||||
.filter_map(ConnectionLedger::believed_home)
|
||||
.collect();
|
||||
homes.sort_by_key(|index| index.get());
|
||||
homes.dedup();
|
||||
homes
|
||||
}
|
||||
|
||||
pub fn believed_home_counts(&self) -> Vec<(LocationIndex, u32)> {
|
||||
let mut counts: Vec<(LocationIndex, u32)> = Vec::new();
|
||||
for home in self
|
||||
.connections
|
||||
.iter()
|
||||
.filter_map(ConnectionLedger::believed_home)
|
||||
{
|
||||
match counts.iter_mut().find(|(index, _)| *index == home) {
|
||||
Some((_, count)) => *count = count.saturating_add(1),
|
||||
None => counts.push((home, 1)),
|
||||
}
|
||||
}
|
||||
counts.sort_by_key(|(index, _)| index.get());
|
||||
counts
|
||||
}
|
||||
|
||||
pub fn prior_connections(&self) -> Vec<PriorConnection> {
|
||||
self.connections
|
||||
.iter()
|
||||
.map(|entry| PriorConnection {
|
||||
connection: entry.connection.clone(),
|
||||
was_consistent: matches!(entry.state, ConnectionState::Consistent),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn reset_corroborations(&mut self) {
|
||||
for entry in &mut self.connections {
|
||||
if entry.is_divergent() {
|
||||
entry.clear_evidence();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn divergent_connections(&self) -> usize {
|
||||
self.connections
|
||||
.iter()
|
||||
.filter(|entry| entry.is_divergent())
|
||||
.count()
|
||||
}
|
||||
|
||||
fn is_cold(&self) -> bool {
|
||||
self.connections.iter().all(|entry| {
|
||||
matches!(
|
||||
entry.state,
|
||||
ConnectionState::Consistent
|
||||
| ConnectionState::Nascent
|
||||
| ConnectionState::Retired { .. }
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct LedgerLimits {
|
||||
pub max_tracked_rooms: usize,
|
||||
pub max_tracked_connections: usize,
|
||||
pub max_connections_per_room: usize,
|
||||
pub memory_budget_bytes: u64,
|
||||
pub retired_grace_ms: u64,
|
||||
}
|
||||
|
||||
impl Default for LedgerLimits {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_tracked_rooms: 8_192,
|
||||
max_tracked_connections: 65_536,
|
||||
max_connections_per_room: 512,
|
||||
memory_budget_bytes: 16_777_216,
|
||||
retired_grace_ms: 60_000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const CONNECTION_ENTRY_BYTES: u64 =
|
||||
(size_of::<ConnectionLedger>() + CONNECTION_ID_HEAP_BYTES) as u64;
|
||||
pub const ROOM_ENTRY_BYTES: u64 = (size_of::<RoomLedger>() + ROOM_OVERHEAD_BYTES) as u64;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum AdmissionRefusal {
|
||||
RoomCapacity,
|
||||
ConnectionCapacity,
|
||||
RoomConnectionCapacity,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Ledger {
|
||||
rooms: BTreeMap<RoomKey, RoomLedger>,
|
||||
locations: LocationTable,
|
||||
limits: LedgerLimits,
|
||||
params: DecideParams,
|
||||
tracked_connections: usize,
|
||||
evicted_rooms_total: u64,
|
||||
evicted_connections_total: u64,
|
||||
}
|
||||
|
||||
impl Ledger {
|
||||
pub fn new(limits: LedgerLimits, params: DecideParams) -> Self {
|
||||
Self {
|
||||
rooms: BTreeMap::new(),
|
||||
locations: LocationTable::new(),
|
||||
limits,
|
||||
params,
|
||||
tracked_connections: 0,
|
||||
evicted_rooms_total: 0,
|
||||
evicted_connections_total: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn limits(&self) -> LedgerLimits {
|
||||
self.limits
|
||||
}
|
||||
|
||||
pub const fn params(&self) -> &DecideParams {
|
||||
&self.params
|
||||
}
|
||||
|
||||
pub const fn locations(&self) -> &LocationTable {
|
||||
&self.locations
|
||||
}
|
||||
|
||||
pub fn intern_location(&mut self, location: &Location) -> Option<LocationIndex> {
|
||||
self.locations.intern(location)
|
||||
}
|
||||
|
||||
pub fn location(&self, index: LocationIndex) -> Option<&Location> {
|
||||
self.locations.get(index)
|
||||
}
|
||||
|
||||
pub fn location_index(&self, location: &Location) -> Option<LocationIndex> {
|
||||
self.locations.index_of(location)
|
||||
}
|
||||
|
||||
pub fn rooms(&self) -> impl Iterator<Item = (&RoomKey, &RoomLedger)> {
|
||||
self.rooms.iter()
|
||||
}
|
||||
|
||||
pub fn room(&self, room: &RoomKey) -> Option<&RoomLedger> {
|
||||
self.rooms.get(room)
|
||||
}
|
||||
|
||||
pub fn room_mut(&mut self, room: &RoomKey) -> Option<&mut RoomLedger> {
|
||||
self.rooms.get_mut(room)
|
||||
}
|
||||
|
||||
pub fn connection(&self, key: &ConnectionKey) -> Option<&ConnectionLedger> {
|
||||
self.rooms
|
||||
.get(&key.room)
|
||||
.and_then(|room| room.connection(&key.connection))
|
||||
}
|
||||
|
||||
pub fn connection_mut(&mut self, key: &ConnectionKey) -> Option<&mut ConnectionLedger> {
|
||||
self.rooms
|
||||
.get_mut(&key.room)
|
||||
.and_then(|room| room.connection_mut(&key.connection))
|
||||
}
|
||||
|
||||
pub fn ensure_room(
|
||||
&mut self,
|
||||
room: RoomKey,
|
||||
now: Millis,
|
||||
) -> Result<&mut RoomLedger, AdmissionRefusal> {
|
||||
if !self.rooms.contains_key(&room) && self.rooms.len() >= self.limits.max_tracked_rooms {
|
||||
return Err(AdmissionRefusal::RoomCapacity);
|
||||
}
|
||||
Ok(self
|
||||
.rooms
|
||||
.entry(room)
|
||||
.or_insert_with(|| RoomLedger::new(room, now)))
|
||||
}
|
||||
|
||||
pub fn upsert_connection(
|
||||
&mut self,
|
||||
room: RoomKey,
|
||||
connection: &ConnectionId,
|
||||
user_id: UserId,
|
||||
now: Millis,
|
||||
) -> Result<&mut ConnectionLedger, AdmissionRefusal> {
|
||||
let limits = self.limits;
|
||||
let tracked = self.tracked_connections;
|
||||
let room_entry = self.ensure_room(room, now)?;
|
||||
|
||||
let existing = room_entry
|
||||
.connections
|
||||
.iter()
|
||||
.position(|entry| &entry.connection == connection);
|
||||
|
||||
let index = match existing {
|
||||
Some(index) => index,
|
||||
None => {
|
||||
if room_entry.connections.len() >= limits.max_connections_per_room {
|
||||
room_entry.partially_unreadable = true;
|
||||
return Err(AdmissionRefusal::RoomConnectionCapacity);
|
||||
}
|
||||
if tracked >= limits.max_tracked_connections {
|
||||
return Err(AdmissionRefusal::ConnectionCapacity);
|
||||
}
|
||||
room_entry.connections.push(ConnectionLedger::new(
|
||||
connection.clone(),
|
||||
user_id,
|
||||
now,
|
||||
));
|
||||
self.tracked_connections = tracked.saturating_add(1);
|
||||
self.rooms
|
||||
.get(&room)
|
||||
.map(|entry| entry.connections.len().saturating_sub(1))
|
||||
.unwrap_or(0)
|
||||
}
|
||||
};
|
||||
|
||||
let entry = self
|
||||
.rooms
|
||||
.get_mut(&room)
|
||||
.and_then(|room_ledger| room_ledger.connections.get_mut(index))
|
||||
.expect("the connection was just admitted");
|
||||
entry.user_id = user_id;
|
||||
entry.last_seen = now;
|
||||
Ok(entry)
|
||||
}
|
||||
|
||||
pub fn tracked_rooms(&self) -> usize {
|
||||
self.rooms.len()
|
||||
}
|
||||
|
||||
pub const fn tracked_connections(&self) -> usize {
|
||||
self.tracked_connections
|
||||
}
|
||||
|
||||
pub fn tracked_bytes(&self) -> u64 {
|
||||
(self.rooms.len() as u64)
|
||||
.saturating_mul(ROOM_ENTRY_BYTES)
|
||||
.saturating_add(
|
||||
(self.tracked_connections as u64).saturating_mul(CONNECTION_ENTRY_BYTES),
|
||||
)
|
||||
}
|
||||
|
||||
pub const fn evicted_rooms_total(&self) -> u64 {
|
||||
self.evicted_rooms_total
|
||||
}
|
||||
|
||||
pub const fn evicted_connections_total(&self) -> u64 {
|
||||
self.evicted_connections_total
|
||||
}
|
||||
|
||||
pub fn divergent_connections(&self) -> usize {
|
||||
self.rooms
|
||||
.values()
|
||||
.map(RoomLedger::divergent_connections)
|
||||
.sum()
|
||||
}
|
||||
|
||||
pub fn divergent_fraction(&self) -> f64 {
|
||||
if self.tracked_connections == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
self.divergent_connections() as f64 / self.tracked_connections as f64
|
||||
}
|
||||
|
||||
pub fn reset_all_corroborations(&mut self) {
|
||||
for room in self.rooms.values_mut() {
|
||||
room.reset_corroborations();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prune_retired(&mut self, now: Millis) -> usize {
|
||||
let grace = self.limits.retired_grace_ms;
|
||||
let mut dropped = 0;
|
||||
|
||||
for room in self.rooms.values_mut() {
|
||||
room.connections.retain(|entry| match entry.state {
|
||||
ConnectionState::Retired { at } => {
|
||||
let expired = now.saturating_since(at) >= grace;
|
||||
if expired {
|
||||
dropped += 1;
|
||||
}
|
||||
!expired
|
||||
}
|
||||
ConnectionState::Nascent
|
||||
| ConnectionState::Consistent
|
||||
| ConnectionState::PendingJoin { .. }
|
||||
| ConnectionState::GatewayOnly { .. }
|
||||
| ConnectionState::MediaOnly { .. }
|
||||
| ConnectionState::Repairing { .. }
|
||||
| ConnectionState::ActionTaken { .. }
|
||||
| ConnectionState::Wedged { .. } => true,
|
||||
});
|
||||
}
|
||||
|
||||
self.tracked_connections = self.tracked_connections.saturating_sub(dropped);
|
||||
self.evicted_connections_total = self
|
||||
.evicted_connections_total
|
||||
.saturating_add(dropped as u64);
|
||||
dropped
|
||||
}
|
||||
|
||||
pub fn drop_empty_rooms(&mut self) -> usize {
|
||||
let before = self.rooms.len();
|
||||
self.rooms.retain(|_, room| !room.connections.is_empty());
|
||||
before.saturating_sub(self.rooms.len())
|
||||
}
|
||||
|
||||
pub fn evict_to_budget(&mut self) -> usize {
|
||||
let mut evicted = 0;
|
||||
|
||||
while self.tracked_bytes() > self.limits.memory_budget_bytes
|
||||
|| self.rooms.len() > self.limits.max_tracked_rooms
|
||||
|| self.tracked_connections > self.limits.max_tracked_connections
|
||||
{
|
||||
let Some(victim) = self.coldest_room() else {
|
||||
break;
|
||||
};
|
||||
if self.evict_room(&victim) {
|
||||
evicted += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
evicted
|
||||
}
|
||||
|
||||
pub fn evict_room(&mut self, room: &RoomKey) -> bool {
|
||||
match self.rooms.remove(room) {
|
||||
None => false,
|
||||
Some(entry) => {
|
||||
self.tracked_connections = self
|
||||
.tracked_connections
|
||||
.saturating_sub(entry.connections.len());
|
||||
self.evicted_connections_total = self
|
||||
.evicted_connections_total
|
||||
.saturating_add(entry.connections.len() as u64);
|
||||
self.evicted_rooms_total = self.evicted_rooms_total.saturating_add(1);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn coldest_room(&self) -> Option<RoomKey> {
|
||||
let cold = self
|
||||
.rooms
|
||||
.values()
|
||||
.filter(|room| room.is_cold())
|
||||
.min_by_key(|room| (room.last_seen(), room.room()))
|
||||
.map(RoomLedger::room);
|
||||
|
||||
cold.or_else(|| {
|
||||
self.rooms
|
||||
.values()
|
||||
.min_by_key(|room| (room.last_seen(), room.room()))
|
||||
.map(RoomLedger::room)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn record_action_issued(
|
||||
&mut self,
|
||||
key: &ConnectionKey,
|
||||
kind: ActionKind,
|
||||
at: Millis,
|
||||
authorizing_turn: TurnId,
|
||||
) -> bool {
|
||||
let Some(entry) = self.connection_mut(key) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let (prior, since) = match &entry.state {
|
||||
ConnectionState::GatewayOnly { since, .. } => (PriorDivergence::GatewayOnly, *since),
|
||||
ConnectionState::MediaOnly { since, .. } => (PriorDivergence::MediaOnly, *since),
|
||||
ConnectionState::Nascent
|
||||
| ConnectionState::Consistent
|
||||
| ConnectionState::PendingJoin { .. }
|
||||
| ConnectionState::Repairing { .. }
|
||||
| ConnectionState::ActionTaken { .. }
|
||||
| ConnectionState::Wedged { .. }
|
||||
| ConnectionState::Retired { .. } => return false,
|
||||
};
|
||||
|
||||
entry.action_attempts = entry.action_attempts.saturating_add(1);
|
||||
entry.last_action_at = Some(at);
|
||||
entry.state = ConnectionState::ActionTaken {
|
||||
kind,
|
||||
at,
|
||||
authorizing_turn,
|
||||
prior,
|
||||
since,
|
||||
};
|
||||
true
|
||||
}
|
||||
|
||||
pub fn refund_action_attempt(&mut self, key: &ConnectionKey) -> bool {
|
||||
let Some(entry) = self.connection_mut(key) else {
|
||||
return false;
|
||||
};
|
||||
if entry.action_attempts == 0 {
|
||||
return false;
|
||||
}
|
||||
entry.action_attempts = entry.action_attempts.saturating_sub(1);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn record_repair_issued(&mut self, key: &ConnectionKey, at: Millis) -> bool {
|
||||
let Some(entry) = self.connection_mut(key) else {
|
||||
return false;
|
||||
};
|
||||
entry.repair_attempts = entry.repair_attempts.saturating_add(1);
|
||||
entry.last_action_at = Some(at);
|
||||
if let ConnectionState::Repairing { last_attempt, .. } = &mut entry.state {
|
||||
*last_attempt = at;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn record_repair_verdict(&mut self, key: &ConnectionKey, verdict: RepairVerdict) -> bool {
|
||||
let Some(entry) = self.connection_mut(key) else {
|
||||
return false;
|
||||
};
|
||||
entry.last_repair_verdict = Some(verdict);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn record_confirm_issued(&mut self, key: &ConnectionKey, at: Millis) -> bool {
|
||||
let Some(entry) = self.connection_mut(key) else {
|
||||
return false;
|
||||
};
|
||||
entry.last_action_at = Some(at);
|
||||
if let ConnectionState::PendingJoin { last_confirm, .. } = &mut entry.state {
|
||||
*last_confirm = at;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
pub mod actuate;
|
||||
pub mod budget;
|
||||
pub mod census;
|
||||
pub mod clock;
|
||||
pub mod config;
|
||||
pub mod control;
|
||||
pub mod decide;
|
||||
pub mod discovery;
|
||||
pub mod evidence;
|
||||
pub mod gateway;
|
||||
pub mod guards;
|
||||
pub mod health;
|
||||
pub mod ids;
|
||||
pub mod ledger;
|
||||
pub mod livekit;
|
||||
pub mod metrics;
|
||||
pub mod names;
|
||||
pub mod observe;
|
||||
pub mod runtime;
|
||||
pub mod schedule;
|
||||
pub mod service;
|
||||
pub mod singleton;
|
||||
pub mod suspicion;
|
||||
pub mod topology;
|
||||
pub mod turn;
|
||||
pub mod webhook;
|
||||
@@ -1,170 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use base64::prelude::*;
|
||||
use hmac::{Hmac, KeyInit, Mac};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
|
||||
use crate::ids::ApiSecret;
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
pub const ADMIN_TOKEN_TTL_SECONDS: u64 = 600;
|
||||
|
||||
const HEADER_JSON: &str = r#"{"alg":"HS256","typ":"JWT"}"#;
|
||||
const HEADER_ALG: &str = "HS256";
|
||||
const HEADER_TYP: &str = "JWT";
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum TokenError {
|
||||
#[error("token is not three dot-separated parts")]
|
||||
Malformed,
|
||||
#[error("token header is not an HS256 JWT header")]
|
||||
UnsupportedHeader,
|
||||
#[error("token carries invalid base64url")]
|
||||
BadBase64,
|
||||
#[error("token carries invalid json")]
|
||||
BadJson,
|
||||
#[error("token signature does not verify")]
|
||||
BadSignature,
|
||||
#[error("token issuer does not match the api key")]
|
||||
IssuerMismatch,
|
||||
#[error("token has expired")]
|
||||
Expired,
|
||||
#[error("token is not valid yet")]
|
||||
NotYetValid,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum VideoGrant {
|
||||
RoomList,
|
||||
RoomAdmin { room: String },
|
||||
}
|
||||
|
||||
impl VideoGrant {
|
||||
pub fn claim(&self) -> VideoGrantClaim {
|
||||
match self {
|
||||
Self::RoomList => VideoGrantClaim {
|
||||
room_list: Some(true),
|
||||
room_admin: None,
|
||||
room: None,
|
||||
},
|
||||
Self::RoomAdmin { room } => VideoGrantClaim {
|
||||
room_list: None,
|
||||
room_admin: Some(true),
|
||||
room: Some(room.clone()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct VideoGrantClaim {
|
||||
#[serde(rename = "roomList", default, skip_serializing_if = "Option::is_none")]
|
||||
pub room_list: Option<bool>,
|
||||
#[serde(rename = "roomAdmin", default, skip_serializing_if = "Option::is_none")]
|
||||
pub room_admin: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub room: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AdminClaims {
|
||||
pub iss: String,
|
||||
pub exp: u64,
|
||||
pub nbf: u64,
|
||||
pub video: VideoGrantClaim,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||
struct TokenHeader {
|
||||
alg: String,
|
||||
typ: String,
|
||||
}
|
||||
|
||||
pub fn admin_claims(api_key: &str, grant: &VideoGrant, now_unix_seconds: u64) -> AdminClaims {
|
||||
AdminClaims {
|
||||
iss: api_key.to_owned(),
|
||||
exp: now_unix_seconds.saturating_add(ADMIN_TOKEN_TTL_SECONDS),
|
||||
nbf: 0,
|
||||
video: grant.claim(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mint_admin_token(
|
||||
api_key: &str,
|
||||
api_secret: &ApiSecret,
|
||||
grant: &VideoGrant,
|
||||
now_unix_seconds: u64,
|
||||
) -> String {
|
||||
encode_admin_token(&admin_claims(api_key, grant, now_unix_seconds), api_secret)
|
||||
}
|
||||
|
||||
pub fn encode_admin_token(claims: &AdminClaims, api_secret: &ApiSecret) -> String {
|
||||
let header = BASE64_URL_SAFE_NO_PAD.encode(HEADER_JSON.as_bytes());
|
||||
let payload = BASE64_URL_SAFE_NO_PAD
|
||||
.encode(serde_json::to_vec(claims).expect("admin claims serialise to json"));
|
||||
let signing_input = format!("{header}.{payload}");
|
||||
let signature = sign(signing_input.as_bytes(), api_secret);
|
||||
format!(
|
||||
"{signing_input}.{}",
|
||||
BASE64_URL_SAFE_NO_PAD.encode(signature)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn verify_admin_token(
|
||||
token: &str,
|
||||
api_key: &str,
|
||||
api_secret: &ApiSecret,
|
||||
now_unix_seconds: u64,
|
||||
) -> Result<AdminClaims, TokenError> {
|
||||
let mut parts = token.split('.');
|
||||
let (Some(header_b64), Some(payload_b64), Some(signature_b64), None) =
|
||||
(parts.next(), parts.next(), parts.next(), parts.next())
|
||||
else {
|
||||
return Err(TokenError::Malformed);
|
||||
};
|
||||
|
||||
let header_bytes = decode_part(header_b64)?;
|
||||
let header: TokenHeader =
|
||||
serde_json::from_slice(&header_bytes).map_err(|_| TokenError::BadJson)?;
|
||||
if header.alg != HEADER_ALG || header.typ != HEADER_TYP {
|
||||
return Err(TokenError::UnsupportedHeader);
|
||||
}
|
||||
|
||||
let signature = decode_part(signature_b64)?;
|
||||
let mut mac = HmacSha256::new_from_slice(api_secret.expose().as_bytes())
|
||||
.expect("hmac accepts any key length");
|
||||
mac.update(header_b64.as_bytes());
|
||||
mac.update(b".");
|
||||
mac.update(payload_b64.as_bytes());
|
||||
mac.verify_slice(&signature)
|
||||
.map_err(|_| TokenError::BadSignature)?;
|
||||
|
||||
let payload_bytes = decode_part(payload_b64)?;
|
||||
let claims: AdminClaims =
|
||||
serde_json::from_slice(&payload_bytes).map_err(|_| TokenError::BadJson)?;
|
||||
if claims.iss != api_key {
|
||||
return Err(TokenError::IssuerMismatch);
|
||||
}
|
||||
if now_unix_seconds >= claims.exp {
|
||||
return Err(TokenError::Expired);
|
||||
}
|
||||
if now_unix_seconds < claims.nbf {
|
||||
return Err(TokenError::NotYetValid);
|
||||
}
|
||||
Ok(claims)
|
||||
}
|
||||
|
||||
fn decode_part(part: &str) -> Result<Vec<u8>, TokenError> {
|
||||
BASE64_URL_SAFE_NO_PAD
|
||||
.decode(part.as_bytes())
|
||||
.map_err(|_| TokenError::BadBase64)
|
||||
}
|
||||
|
||||
fn sign(input: &[u8], api_secret: &ApiSecret) -> Vec<u8> {
|
||||
let mut mac = HmacSha256::new_from_slice(api_secret.expose().as_bytes())
|
||||
.expect("hmac accepts any key length");
|
||||
mac.update(input);
|
||||
mac.finalize().into_bytes().to_vec()
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user