Compare commits

...
Author SHA1 Message Date
HampusandGitHub 1ec8f31253 refactor: simplify account standing and verification levels (#3130) 2026-10-02 18:17:41 +02:00
HampusandGitHub 1abde06824 feat(admin): accept domain entries in the email blocklist (#3129) 2026-10-02 18:00:38 +02:00
HampusandGitHub b54016653b fix(app): show active incidents on the reconnecting banner (#3128) 2026-10-02 17:27:21 +02:00
HampusandGitHub ee74d61f27 fix(channel): track the member list width with its divider (#3127) 2026-10-02 17:26:52 +02:00
HampusandGitHub 1f18d3262d fix(composer): keep emoji autocomplete open on tilde names (#3126) 2026-10-02 17:26:30 +02:00
HampusandGitHub 8ea7707b37 fix(guild): clear guild header menu highlight on pointer leave (#3125) 2026-10-02 17:26:09 +02:00
HampusandGitHub 7b40df5d6c fix(messages): keep spoilers on forwarded link embeds (#3124) 2026-10-02 17:25:33 +02:00
HampusandGitHub 6f98de33f7 fix(ui): portal combobox menus into the fullscreen call host (#3123) 2026-10-02 17:25:08 +02:00
HampusandGitHub efe94ed094 fix(voice): stop offering h264 to firefox on linux (#3122) 2026-10-02 17:24:45 +02:00
HampusandGitHub 603b936536 fix(installer): point Fedora at podman with docker-compose (#3121) 2026-10-02 17:24:20 +02:00
HampusandGitHub d87351eefe fix(privacy): let minors block media in DMs from others (#3120) 2026-10-02 17:23:50 +02:00
HampusandGitHub 76e6891f5b fix(api): credit self-hosted gift codes to the issuing admin (#3119) 2026-10-02 17:23:26 +02:00
HampusandGitHub 5040ae2c10 fix(sso): join provisioned users to the single community (#3118) 2026-10-02 17:23:03 +02:00
HampusandGitHub 87df92e2c2 fix(gifs): fetch featured category previews concurrently (#3117) 2026-10-02 17:22:30 +02:00
HampusandGitHub 237aff666d perf(app-proxy): skip disk reads for absent static prefixes (#3115) 2026-10-02 16:09:34 +02:00
HampusandGitHub 11645cbf28 fix(ci): keep published source maps when a rebuild differs (#3113) 2026-10-02 15:25:05 +02:00
HampusandGitHub e297a6a653 fix(app-proxy): make the SPA shell identical for every visitor (#3112) 2026-10-02 15:13:36 +02:00
HampusandGitHub 1eed347ffb fix(premium): follow the light theme on the Plutonium page (#3111) 2026-10-02 14:15:24 +02:00
HampusandGitHub 4aa7a3e181 fix(voice): allow stereo mics at 64 kbps and in the mic test (#3110) 2026-10-02 14:11:19 +02:00
HampusandGitHub 69786d3b49 fix(voice): prefer vp8 for automatic screen shares in firefox (#3109) 2026-10-02 14:09:55 +02:00
HampusandGitHub 2fb5fb1abb fix(voice): allow av1 and vp9 screen shares in firefox (#3108) 2026-10-02 14:08:41 +02:00
HampusandGitHub a9265cbb39 perf(gateway): speed up reconnects and pin guilds to nodes (#3107) 2026-10-02 14:02:22 +02:00
HampusandGitHub ee2d11ee0a fix(voice): prefer vp9 over software h264 for screen shares (#3106) 2026-10-02 13:29:29 +02:00
TarekandGitHub 632067b552 feat(gateway,admin): Expand stats for metrics (#3064) 2026-10-02 12:58:16 +02:00
HampusandGitHub 840dc3dfa5 feat(premium): match the Plutonium page to the new site look (#3104) 2026-10-02 12:20:44 +02:00
HampusandGitHub 4e6f9b539c fix(voice): make RNNoise the default noise suppression (#3103) 2026-10-02 11:31:40 +02:00
HampusandGitHub 98cce4815d feat(users): add temporary new conversation limits (#3100) 2026-10-02 01:28:35 +02:00
HampusandGitHub b375abc20a feat(desktop): live-reload linked css theme files (#3099) 2026-10-02 01:02:30 +02:00
HampusandGitHub 21cb7ba69c feat(premium): show App Store and Google Play subs on web (#3098) 2026-10-01 22:51:14 +02:00
HampusandGitHub be69333eaf feat(premium): add the Plutonium page behind an experiment (#3097) 2026-10-01 21:45:44 +02:00
HampusandGitHub 9a074adb11 fix(app): make disabling built-in shortcuts take effect live (#3096) 2026-10-01 20:37:28 +02:00
HampusandGitHub 2df82b2b5e fix(guild): treat very high as high without phone verification (#3095) 2026-10-01 20:33:02 +02:00
HampusandGitHub d691047884 feat(desktop): add start minimized option for launch at login (#3094) 2026-10-01 19:51:43 +02:00
HampusandGitHub c2e7fde5bc test(api): isolate crosspost tests that mock constants (#3091) 2026-10-01 17:13:49 +02:00
HampusandGitHub 7e4d5137f8 feat: add announcement channels, publishing and following (#3090) 2026-10-01 16:57:21 +02:00
HampusandGitHub 376afd2ad6 fix(voice): keep mic publish state in sync with voice state (#3088) 2026-10-01 14:03:04 +02:00
HampusandGitHub e3fcedbec5 fix(voice): stabilize voice input and noise suppression (#3087) 2026-10-01 14:02:12 +02:00
HampusandGitHub 7c9564bcad feat(deploy): add helm charts for the fluxer services (#3082) 2026-10-01 04:11:30 +02:00
HampusandGitHub cfed6cc4e0 perf(media-proxy): gzip static assets on the fly (#3079) 2026-09-30 23:41:16 +02:00
HampusandGitHub c7bd1be3e4 fix(auth): offer every transport for passkeys stored without any (#3077) 2026-09-30 23:01:37 +02:00
HampusandGitHub 2161d84701 fix(self-hosting): grow seaweedfs one volume at a time (#3076) 2026-09-30 22:55:12 +02:00
HampusandGitHub eaeeb3b502 fix(api): report final system DM progress (#3074) 2026-09-30 22:11:34 +02:00
HampusandGitHub dc32a7c70e feat(admin): allow system DMs to all users (#3073) 2026-09-30 21:29:05 +02:00
HampusandGitHub ab0b483fbe perf(gateway): speed up presence and harden guild queries (#3072) 2026-09-30 21:12:13 +02:00
1089 changed files with 102754 additions and 71327 deletions
+2
View File
@@ -2,3 +2,5 @@
fluxer_static/** -text -diff
fluxer_static/**/*.md text diff
packages/fonts/files/** -text -diff
fluxer_app/src/features/voice/utils/noise_suppression/deepfilternet3/*.wasm -text -diff
fluxer_app/src/features/voice/utils/noise_suppression/deepfilternet3/*.tar.gz -text -diff
Generated
+5
View File
@@ -1785,8 +1785,10 @@ name = "fluxer-gifs"
version = "0.1.0"
dependencies = [
"anyhow",
"axum",
"fluxer-svc",
"fluxer_common",
"futures",
"hmac 0.13.0",
"moka",
"reqwest",
@@ -1823,6 +1825,7 @@ dependencies = [
"cc",
"clap",
"criterion",
"flate2",
"fluxer_common",
"futures-util",
"hex",
@@ -1850,6 +1853,7 @@ dependencies = [
"tokio",
"tokio-util",
"tower",
"tower-http 0.7.1",
"tracing",
"tracing-subscriber",
"url",
@@ -2038,6 +2042,7 @@ dependencies = [
"reqwest",
"serde",
"serde_json",
"sha2 0.11.0",
"tokio",
"tokio-util",
"tower",
+4
View File
@@ -143,6 +143,10 @@
],
"linter": {"rules": {"style": {"noRestrictedImports": "off"}}}
},
{
"includes": ["fluxer_app/src/**/*.worklet.js"],
"javascript": {"globals": ["AudioWorkletProcessor", "registerProcessor", "sampleRate", "currentTime"]}
},
{
"includes": ["**/*.astro"],
"linter": {"rules": {"correctness": {"noUnusedImports": "off", "noUnusedVariables": "off"}}},
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: fluxer-api
description: Fluxer HTTP API and background job workers
type: application
version: 0.1.0
appVersion: "v1"
@@ -0,0 +1,244 @@
{{- define "fluxer-api.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end }}
{{- define "fluxer-api.selectorLabels" -}}
app.kubernetes.io/name: {{ .name }}
app.kubernetes.io/instance: {{ .root.Release.Name }}
{{- end }}
{{- define "fluxer-api.labels" -}}
{{ include "fluxer-api.selectorLabels" . }}
app.kubernetes.io/component: {{ .component }}
app.kubernetes.io/part-of: fluxer
app.kubernetes.io/managed-by: {{ .root.Release.Service }}
helm.sh/chart: {{ include "fluxer-api.chart" .root }}
{{- end }}
{{- define "fluxer-api.image" -}}
{{- $g := .root.Values.image | default dict -}}
{{- $i := .w.image | default dict -}}
{{- $repo := $i.repository -}}
{{- if not $repo -}}
{{- $repo = printf "%s/%s" (required "image.registry is required" $g.registry) ($i.name | default "fluxer-api") -}}
{{- end -}}
{{- $tag := required "image.tag is required" ($i.tag | default $g.tag) -}}
{{- if $i.digest -}}
{{- printf "%s:%s@%s" $repo $tag $i.digest | quote -}}
{{- else -}}
{{- printf "%s:%s" $repo $tag | quote -}}
{{- end -}}
{{- end }}
{{- define "fluxer-api.pick" -}}
{{- $v := ternary (get .w .key) (get .root.Values .key) (hasKey .w .key) -}}
{{- if $v }}
{{- toYaml $v }}
{{- end }}
{{- end }}
{{- define "fluxer-api.str" -}}
{{- if and (kindIs "float64" .) (eq . (floor .)) -}}
{{- int64 . | toString | quote -}}
{{- else -}}
{{- toString . | quote -}}
{{- end -}}
{{- end }}
{{- define "fluxer-api.env" -}}
{{- $env := dict -}}
{{- range $k, $val := .root.Values.env | default dict }}
{{- $_ := set $env $k $val }}
{{- end }}
{{- range $k, $val := .w.env | default dict }}
{{- $_ := set $env $k $val }}
{{- end }}
{{- range $k, $val := $env }}
{{- if not (kindIs "invalid" $val) }}
- name: {{ $k }}
value: {{ include "fluxer-api.str" $val }}
{{- end }}
{{- end }}
{{- with .w.buildVersion }}
- name: BUILD_VERSION
value: {{ include "fluxer-api.str" . }}
{{- end }}
{{- with concat (.root.Values.extraEnv | default list) (.w.extraEnv | default list) }}
{{ toYaml . }}
{{- end }}
{{- end }}
{{- define "fluxer-api.topologySpread" -}}
{{- $tscs := ternary .w.topologySpreadConstraints .root.Values.topologySpreadConstraints (hasKey .w "topologySpreadConstraints") -}}
{{- range $tscs }}
{{- $c := deepCopy . }}
{{- if not $c.labelSelector }}
{{- $_ := set $c "labelSelector" (dict "matchLabels" (include "fluxer-api.selectorLabels" $ | fromYaml)) }}
{{- end }}
- {{- toYaml $c | nindent 2 }}
{{- end }}
{{- end }}
{{- define "fluxer-api.pdb" -}}
{{- with .w.pdb }}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $.name }}-pdb
namespace: {{ $.root.Release.Namespace }}
labels:
{{- include "fluxer-api.labels" $ | nindent 4 }}
spec:
{{- toYaml . | nindent 2 }}
selector:
matchLabels:
{{- include "fluxer-api.selectorLabels" $ | nindent 6 }}
{{- end }}
{{- end }}
{{- define "fluxer-api.hpa" -}}
{{- with .w.hpa }}
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ $.name }}
namespace: {{ $.root.Release.Namespace }}
labels:
{{- include "fluxer-api.labels" $ | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ $.name }}
minReplicas: {{ required (printf "%s.hpa.minReplicas is required" $.name) .minReplicas }}
maxReplicas: {{ required (printf "%s.hpa.maxReplicas is required" $.name) .maxReplicas }}
{{- with .targetCPUUtilizationPercentage }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ . }}
{{- end }}
{{- with .behavior }}
behavior:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
{{- end }}
{{- define "fluxer-api.deployment" -}}
{{- $root := .root -}}
{{- $v := $root.Values -}}
{{- $w := .w -}}
{{- $envFrom := concat ($v.envFrom | default list) ($w.envFrom | default list) -}}
{{- $podAnnotations := merge (dict) ($w.podAnnotations | default dict) ($v.podAnnotations | default dict) -}}
{{- $wProbes := $w.probes | default dict -}}
{{- $gProbes := .probes | default dict -}}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .name }}
namespace: {{ $root.Release.Namespace }}
labels:
{{- include "fluxer-api.labels" . | nindent 4 }}
spec:
{{- if not $w.hpa }}
replicas: {{ if kindIs "invalid" $w.replicas }}1{{ else }}{{ int $w.replicas }}{{ end }}
{{- end }}
{{- if not (kindIs "invalid" $w.minReadySeconds) }}
minReadySeconds: {{ int $w.minReadySeconds }}
{{- end }}
selector:
matchLabels:
{{- include "fluxer-api.selectorLabels" . | nindent 6 }}
{{- with include "fluxer-api.pick" (dict "root" $root "w" $w "key" "strategy") }}
strategy:
{{- . | nindent 4 }}
{{- end }}
template:
metadata:
labels:
{{- include "fluxer-api.labels" . | nindent 8 }}
{{- with $podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with include "fluxer-api.pick" (dict "root" $root "w" $w "key" "imagePullSecrets") }}
imagePullSecrets:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-api.pick" (dict "root" $root "w" $w "key" "podSecurityContext") }}
securityContext:
{{- . | nindent 8 }}
{{- end }}
{{- if not (kindIs "invalid" $w.terminationGracePeriodSeconds) }}
terminationGracePeriodSeconds: {{ int $w.terminationGracePeriodSeconds }}
{{- end }}
{{- with include "fluxer-api.pick" (dict "root" $root "w" $w "key" "nodeSelector") }}
nodeSelector:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-api.pick" (dict "root" $root "w" $w "key" "affinity") }}
affinity:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-api.pick" (dict "root" $root "w" $w "key" "tolerations") }}
tolerations:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-api.topologySpread" . | trim }}
topologySpreadConstraints:
{{- . | nindent 8 }}
{{- end }}
containers:
- name: {{ .name }}
image: {{ include "fluxer-api.image" . }}
imagePullPolicy: {{ ($w.image | default dict).pullPolicy | default ($v.image | default dict).pullPolicy | default "IfNotPresent" }}
{{- with .command }}
command:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with include "fluxer-api.env" . | trim }}
env:
{{- . | nindent 12 }}
{{- end }}
{{- with $envFrom }}
envFrom:
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- name: http
containerPort: 8080
{{- with $w.lifecycle }}
lifecycle:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- range $probe := list "startup" "liveness" "readiness" }}
{{- with hasKey $wProbes $probe | ternary (get $wProbes $probe) (get $gProbes $probe) }}
{{ $probe }}Probe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
{{- with $w.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with include "fluxer-api.pick" (dict "root" $root "w" $w "key" "securityContext") }}
securityContext:
{{- . | nindent 12 }}
{{- end }}
{{- with $w.extraVolumeMounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $w.extraVolumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
+24
View File
@@ -0,0 +1,24 @@
{{- range $name, $w := .Values.api }}
{{- if not (kindIs "invalid" $w) }}
{{- $ctx := dict "root" $ "name" $name "w" $w "component" "api" "probes" ($.Values.probes | default dict) }}
{{ include "fluxer-api.deployment" $ctx }}
{{ include "fluxer-api.hpa" $ctx }}
{{ include "fluxer-api.pdb" $ctx }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-api.labels" $ctx | nindent 4 }}
spec:
type: ClusterIP
selector:
{{- include "fluxer-api.selectorLabels" $ctx | nindent 4 }}
ports:
- name: http
port: 8080
targetPort: http
{{- end }}
{{- end }}
@@ -0,0 +1,8 @@
{{- range $name, $w := .Values.workers }}
{{- if not (kindIs "invalid" $w) }}
{{- $ctx := dict "root" $ "name" $name "w" $w "component" "worker" "command" (list "node" "dist/WorkerEntrypoint.js") "probes" (dict) }}
{{ include "fluxer-api.deployment" $ctx }}
{{ include "fluxer-api.hpa" $ctx }}
{{ include "fluxer-api.pdb" $ctx }}
{{- end }}
{{- end }}
+86
View File
@@ -0,0 +1,86 @@
image:
registry: ghcr.io/fluxerapp
tag: v1
pullPolicy: IfNotPresent
imagePullSecrets: []
env:
NODE_ENV: production
FLUXER_ENV: production
FLUXER_PUBLIC_ORIGIN: https://web.example.com
FLUXER_API_ENDPOINT: https://api.example.com
FLUXER_GATEWAY_ENDPOINT: wss://gateway.example.com
FLUXER_MEDIA_ENDPOINT: https://media.example.com
FLUXER_ADMIN_ENDPOINT: https://admin.example.com
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_ENDPOINT: https://uploads.example.com
FLUXER_INTERNAL_MEDIA_PROXY_ENDPOINT: http://media-proxy:8080
FLUXER_KV_URL: redis://valkey:6379/0
FLUXER_NATS_URL: nats://nats:4222
FLUXER_NATS_JETSTREAM_URL: nats://nats:4222
extraEnv: []
envFrom:
- secretRef:
name: fluxer-env
podAnnotations: {}
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
probes:
startup:
httpGet:
path: /_health
port: http
periodSeconds: 10
failureThreshold: 30
liveness:
httpGet:
path: /_health
port: http
readiness:
httpGet:
path: /_health
port: http
strategy:
type: RollingUpdate
topologySpreadConstraints: []
nodeSelector: {}
tolerations: []
affinity: {}
api:
api:
replicas: 1
resources:
requests:
cpu: 250m
memory: 1Gi
limits:
memory: 2560Mi
workers:
worker:
replicas: 1
env:
FLUXER_API_WORKER_MODE: all_lanes
FLUXER_API_WORKER_ENABLE_CRON_SCHEDULER: "true"
resources:
requests:
cpu: 250m
memory: 1Gi
limits:
memory: 2560Mi
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: fluxer-gateway
description: A Helm chart for the Fluxer realtime gateway.
type: application
version: 0.1.0
appVersion: "v1"
@@ -0,0 +1,280 @@
{{- define "gateway.selectorLabels" -}}
app.kubernetes.io/name: {{ .name }}
app.kubernetes.io/instance: {{ .root.Release.Name }}
{{- end }}
{{- define "gateway.labels" -}}
{{ include "gateway.selectorLabels" . }}
{{- with .component }}
app.kubernetes.io/component: {{ . }}
{{- end }}
app.kubernetes.io/part-of: fluxer
app.kubernetes.io/managed-by: {{ .root.Release.Service }}
helm.sh/chart: {{ printf "%s-%s" .root.Chart.Name .root.Chart.Version | replace "+" "_" }}
{{- end }}
{{- define "gateway.headlessName" -}}
{{ printf "%s-headless" .Release.Name }}
{{- end }}
{{- define "gateway.pick" -}}
{{- $v := get .root.Values .key }}
{{- if hasKey .w .key }}
{{- $v = get .w .key }}
{{- end }}
{{- with $v }}
{{- toYaml . }}
{{- end }}
{{- end }}
{{- define "gateway.string" -}}
{{- if and (kindIs "float64" .) (eq . (float64 (int64 .))) }}
{{- int64 . | toString }}
{{- else }}
{{- toString . }}
{{- end }}
{{- end }}
{{- define "gateway.envList" -}}
{{- $env := deepCopy (.root.Values.env | default dict) }}
{{- range $k, $v := .w.env | default dict }}
{{- if kindIs "invalid" $v }}
{{- $_ := unset $env $k }}
{{- else }}
{{- $_ := set $env $k $v }}
{{- end }}
{{- end }}
{{- range $k, $v := $env }}
{{- if not (kindIs "invalid" $v) }}
- name: {{ $k }}
value: {{ include "gateway.string" $v | quote }}
{{- end }}
{{- end }}
{{- with concat (.root.Values.extraEnv | default list) (.w.extraEnv | default list) }}
{{ toYaml . }}
{{- end }}
{{- end }}
{{- define "gateway.envFrom" -}}
{{- with concat (.root.Values.envFrom | default list) (.w.envFrom | default list) }}
{{- toYaml . }}
{{- end }}
{{- end }}
{{- define "gateway.podAnnotations" -}}
{{- with merge (deepCopy (.w.podAnnotations | default dict)) (deepCopy (.root.Values.podAnnotations | default dict)) }}
{{- toYaml . }}
{{- end }}
{{- end }}
{{- define "gateway.probes" -}}
{{- $global := .root.Values.probes | default dict }}
{{- $own := .w.probes | default dict }}
{{- range $probe := list "startup" "liveness" "readiness" }}
{{- $p := get $global $probe }}
{{- if hasKey $own $probe }}
{{- $p = get $own $probe }}
{{- end }}
{{- with $p }}
{{ $probe }}Probe:
{{- toYaml . | nindent 2 }}
{{- end }}
{{- end }}
{{- end }}
{{- define "gateway.topologySpreadConstraints" -}}
{{- $out := list }}
{{- range include "gateway.pick" (dict "root" .root "w" .w "key" "topologySpreadConstraints") | fromYamlArray }}
{{- $c := deepCopy . }}
{{- if not (hasKey $c "labelSelector") }}
{{- $_ := set $c "labelSelector" (dict "matchLabels" (include "gateway.selectorLabels" $ | fromYaml)) }}
{{- end }}
{{- $out = append $out $c }}
{{- end }}
{{- with $out }}
{{- toYaml . }}
{{- end }}
{{- end }}
{{- define "gateway.image" -}}
{{- $img := .w.image | default dict }}
{{- $v := .root.Values.image }}
{{- $repo := $img.repository | default (printf "%s/%s" $v.registry ($img.name | default "fluxer-gateway")) }}
{{- $ref := printf "%s:%s" $repo ($img.tag | default $v.tag) }}
{{- with $img.digest }}
{{- $ref = printf "%s@%s" $ref . }}
{{- end }}
{{- $ref | quote }}
{{- end }}
{{- define "gateway.replicas" -}}
{{- if kindIs "invalid" .w.replicas }}1{{ else }}{{ .w.replicas }}{{ end }}
{{- end }}
{{- define "gateway.env" -}}
{{- $root := .root }}
{{- $w := .w -}}
{{- with $w.role }}
- name: FLUXER_GATEWAY_ROLE
value: {{ . | quote }}
{{- end }}
{{- if not (kindIs "invalid" $w.buildVersion) }}
- name: BUILD_VERSION
value: {{ include "gateway.string" $w.buildVersion | quote }}
{{- end }}
- name: POD_IP
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.podIP
- name: FLUXER_ERLANG_NODE_NAME
value: fluxer_gateway@$(POD_IP)
- name: FLUXER_ERLANG_DIST_PORT
value: "8081"
- name: FLUXER_GATEWAY_CLUSTER_ENABLED
value: "true"
- name: FLUXER_GATEWAY_CLUSTER_DISCOVERY_DNS_NAME
value: {{ printf "%s.%s.svc.%s" (include "gateway.headlessName" $root) $root.Release.Namespace $root.Values.clusterDomain | quote }}
- name: FLUXER_GATEWAY_CLUSTER_DISCOVERY_NODE_BASENAME
value: fluxer_gateway
{{- include "gateway.envList" . }}
{{- end }}
{{- define "gateway.pod" -}}
{{- $root := .root }}
{{- $w := .w -}}
metadata:
labels:
{{- include "gateway.labels" . | nindent 4 }}
{{- with include "gateway.podAnnotations" . }}
annotations:
{{- . | nindent 4 }}
{{- end }}
spec:
{{- with include "gateway.pick" (dict "root" $root "w" $w "key" "affinity") }}
affinity:
{{- . | nindent 4 }}
{{- end }}
{{- with include "gateway.pick" (dict "root" $root "w" $w "key" "imagePullSecrets") }}
imagePullSecrets:
{{- . | nindent 4 }}
{{- end }}
{{- with include "gateway.pick" (dict "root" $root "w" $w "key" "nodeSelector") }}
nodeSelector:
{{- . | nindent 4 }}
{{- end }}
{{- with include "gateway.pick" (dict "root" $root "w" $w "key" "tolerations") }}
tolerations:
{{- . | nindent 4 }}
{{- end }}
{{- with include "gateway.topologySpreadConstraints" . }}
topologySpreadConstraints:
{{- . | nindent 4 }}
{{- end }}
{{- with include "gateway.pick" (dict "root" $root "w" $w "key" "podSecurityContext") }}
securityContext:
{{- . | nindent 4 }}
{{- end }}
{{- if not (kindIs "invalid" $w.terminationGracePeriodSeconds) }}
terminationGracePeriodSeconds: {{ $w.terminationGracePeriodSeconds }}
{{- end }}
containers:
- name: gateway
image: {{ include "gateway.image" . }}
imagePullPolicy: {{ ($w.image | default dict).pullPolicy | default $root.Values.image.pullPolicy }}
env:
{{- include "gateway.env" . | trim | nindent 6 }}
{{- with include "gateway.envFrom" . }}
envFrom:
{{- . | nindent 6 }}
{{- end }}
{{- with $w.lifecycle }}
lifecycle:
{{- toYaml . | nindent 6 }}
{{- end }}
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: epmd
containerPort: 4369
protocol: TCP
- name: erl-dist
containerPort: 8081
protocol: TCP
{{- with include "gateway.probes" . | trim }}
{{- . | nindent 4 }}
{{- end }}
{{- with $w.resources }}
resources:
{{- toYaml . | nindent 6 }}
{{- end }}
{{- with include "gateway.pick" (dict "root" $root "w" $w "key" "securityContext") }}
securityContext:
{{- . | nindent 6 }}
{{- end }}
{{- with $w.extraVolumeMounts }}
volumeMounts:
{{- toYaml . | nindent 6 }}
{{- end }}
{{- with $w.extraVolumes }}
volumes:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
{{- define "gateway.pdb" -}}
{{- with .w.pdb }}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $.name }}-pdb
namespace: {{ $.root.Release.Namespace }}
labels:
{{- include "gateway.labels" $ | nindent 4 }}
spec:
{{- if not (kindIs "invalid" .minAvailable) }}
minAvailable: {{ .minAvailable }}
{{- end }}
{{- if not (kindIs "invalid" .maxUnavailable) }}
maxUnavailable: {{ .maxUnavailable }}
{{- end }}
selector:
matchLabels:
{{- include "gateway.selectorLabels" $ | nindent 6 }}
{{- end }}
{{- end }}
{{- define "gateway.hpa" -}}
{{- with .w.hpa }}
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ $.name }}
namespace: {{ $.root.Release.Namespace }}
labels:
{{- include "gateway.labels" $ | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ $.name }}
minReplicas: {{ required (printf "%s.hpa.minReplicas is required" $.name) .minReplicas }}
maxReplicas: {{ required (printf "%s.hpa.maxReplicas is required" $.name) .maxReplicas }}
{{- if not (kindIs "invalid" .targetCPUUtilizationPercentage) }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .targetCPUUtilizationPercentage }}
{{- end }}
{{- with .behavior }}
behavior:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
{{- end }}
@@ -0,0 +1,48 @@
{{- range $name, $w := .Values.deployments }}
{{- if not (kindIs "invalid" $w) }}
{{- $ctx := dict "root" $ "name" $name "component" $w.role "w" $w }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "gateway.labels" $ctx | nindent 4 }}
spec:
{{- if not $w.hpa }}
replicas: {{ include "gateway.replicas" $ctx }}
{{- end }}
{{- if not (kindIs "invalid" $w.minReadySeconds) }}
minReadySeconds: {{ $w.minReadySeconds }}
{{- end }}
selector:
matchLabels:
{{- include "gateway.selectorLabels" $ctx | nindent 6 }}
{{- with include "gateway.pick" (dict "root" $ "w" $w "key" "strategy") }}
strategy:
{{- . | nindent 4 }}
{{- end }}
template:
{{- include "gateway.pod" $ctx | nindent 4 }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "gateway.labels" $ctx | nindent 4 }}
spec:
type: ClusterIP
ports:
- name: http
port: 8080
protocol: TCP
targetPort: http
selector:
{{- include "gateway.selectorLabels" $ctx | nindent 4 }}
{{- include "gateway.hpa" $ctx }}
{{- include "gateway.pdb" $ctx }}
{{- end }}
{{- end }}
@@ -0,0 +1,26 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "gateway.headlessName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "gateway.labels" (dict "root" . "name" "gateway" "component" "discovery") | nindent 4 }}
spec:
type: ClusterIP
clusterIP: None
ports:
- name: http
port: 8080
protocol: TCP
targetPort: http
- name: epmd
port: 4369
protocol: TCP
targetPort: epmd
- name: erl-dist
port: 8081
protocol: TCP
targetPort: erl-dist
selector:
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/part-of: fluxer
@@ -0,0 +1,53 @@
{{- $np := .Values.networkPolicy | default dict }}
{{- if $np.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: gateway
namespace: {{ .Release.Namespace }}
labels:
{{- include "gateway.labels" (dict "root" . "name" "gateway") | nindent 4 }}
spec:
podSelector:
matchLabels:
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/part-of: fluxer
policyTypes:
- Ingress
- Egress
egress:
- {}
ingress:
{{- with $np.ingressNamespace }}
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ . }}
ports:
- port: 8080
protocol: TCP
{{- end }}
{{- with $np.clients }}
- from:
{{- range . }}
- podSelector:
matchLabels:
{{- toYaml . | nindent 10 }}
{{- end }}
ports:
- port: 8080
protocol: TCP
{{- end }}
- from:
- podSelector:
matchLabels:
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/part-of: fluxer
ports:
- port: 8080
protocol: TCP
- port: 4369
protocol: TCP
- port: 8081
protocol: TCP
{{- end }}
@@ -0,0 +1,29 @@
{{- range $name, $w := .Values.statefulsets }}
{{- if not (kindIs "invalid" $w) }}
{{- $ctx := dict "root" $ "name" $name "component" $w.role "w" $w }}
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "gateway.labels" $ctx | nindent 4 }}
spec:
replicas: {{ include "gateway.replicas" $ctx }}
{{- if not (kindIs "invalid" $w.minReadySeconds) }}
minReadySeconds: {{ $w.minReadySeconds }}
{{- end }}
serviceName: {{ include "gateway.headlessName" $ }}
selector:
matchLabels:
{{- include "gateway.selectorLabels" $ctx | nindent 6 }}
{{- with include "gateway.pick" (dict "root" $ "w" $w "key" "updateStrategy") }}
updateStrategy:
{{- . | nindent 4 }}
{{- end }}
template:
{{- include "gateway.pod" $ctx | nindent 4 }}
{{- include "gateway.pdb" $ctx }}
{{- end }}
{{- end }}
+86
View File
@@ -0,0 +1,86 @@
image:
registry: ghcr.io/fluxerapp
tag: v1
pullPolicy: IfNotPresent
imagePullSecrets: []
clusterDomain: cluster.local
env:
FLUXER_ENV: production
FLUXER_GATEWAY_PORT: "8080"
FLUXER_GATEWAY_MEDIA_PROXY_ENDPOINT: https://media.example.com
FLUXER_INTERNAL_API_ENDPOINT: http://api:8080
extraEnv: []
envFrom:
- secretRef:
name: fluxer-env
podAnnotations: {}
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
probes:
startup:
httpGet:
path: /_health
port: http
failureThreshold: 30
liveness:
httpGet:
path: /_health
port: http
readiness:
exec:
command:
- curl
- -fsS
- -o
- /dev/null
- --max-time
- "2"
- http://127.0.0.1:8080/_health/ready
timeoutSeconds: 3
strategy: {}
updateStrategy: {}
topologySpreadConstraints: []
nodeSelector: {}
tolerations: []
affinity: {}
networkPolicy:
enabled: false
ingressNamespace: ingress-nginx
clients:
- app.kubernetes.io/part-of: fluxer
deployments:
gateway:
role: all
replicas: 1
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- curl -fsS -o /dev/null --max-time 2 http://127.0.0.1:8080/_health/drain; sleep 5
resources:
requests:
cpu: 100m
memory: 384Mi
limits:
memory: 1Gi
statefulsets: {}
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: fluxer-infra
description: NATS and Valkey for a Fluxer installation.
type: application
version: 0.1.0
appVersion: "v1"
@@ -0,0 +1,282 @@
{{- define "fluxer-infra.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end }}
{{- define "fluxer-infra.selectorLabels" -}}
app.kubernetes.io/name: {{ .name }}
app.kubernetes.io/instance: {{ .root.Release.Name }}
{{- end }}
{{- define "fluxer-infra.labels" -}}
{{ include "fluxer-infra.selectorLabels" . }}
app.kubernetes.io/component: {{ .component }}
app.kubernetes.io/part-of: fluxer
app.kubernetes.io/managed-by: {{ .root.Release.Service }}
helm.sh/chart: {{ include "fluxer-infra.chart" .root }}
{{- end }}
{{- define "fluxer-infra.pick" -}}
{{- $v := get .root.Values .key }}
{{- if hasKey .w .key }}
{{- $v = get .w .key }}
{{- end }}
{{- with $v }}
{{- toYaml . }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.string" -}}
{{- if and (kindIs "float64" .) (eq . (float64 (int64 .))) }}
{{- int64 . | toString }}
{{- else }}
{{- toString . }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.envList" -}}
{{- $env := deepCopy (.root.Values.env | default dict) }}
{{- range $k, $v := .w.env | default dict }}
{{- if kindIs "invalid" $v }}
{{- $_ := unset $env $k }}
{{- else }}
{{- $_ := set $env $k $v }}
{{- end }}
{{- end }}
{{- range $k, $v := $env }}
{{- if not (kindIs "invalid" $v) }}
- name: {{ $k }}
value: {{ include "fluxer-infra.string" $v | quote }}
{{- end }}
{{- end }}
{{- with concat (.root.Values.extraEnv | default list) (.w.extraEnv | default list) }}
{{ toYaml . }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.envFrom" -}}
{{- with concat (.root.Values.envFrom | default list) (.w.envFrom | default list) }}
{{- toYaml . }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.probes" -}}
{{- $global := .root.Values.probes | default dict }}
{{- $own := .w.probes | default dict }}
{{- range $probe := list "startup" "liveness" "readiness" }}
{{- $p := get $global $probe }}
{{- if hasKey $own $probe }}
{{- $p = get $own $probe }}
{{- end }}
{{- with $p }}
{{ $probe }}Probe:
{{- toYaml . | nindent 2 }}
{{- end }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.topologySpreadConstraints" -}}
{{- $out := list }}
{{- range include "fluxer-infra.pick" (dict "root" .root "w" .w "key" "topologySpreadConstraints") | fromYamlArray }}
{{- $c := deepCopy . }}
{{- if not (hasKey $c "labelSelector") }}
{{- $_ := set $c "labelSelector" (dict "matchLabels" (include "fluxer-infra.selectorLabels" $ | fromYaml)) }}
{{- end }}
{{- $out = append $out $c }}
{{- end }}
{{- with $out }}
{{- toYaml . }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.replicas" -}}
{{- if kindIs "invalid" .w.replicas }}1{{ else }}{{ .w.replicas }}{{ end }}
{{- end }}
{{- define "fluxer-infra.image" -}}
{{- $ref := printf "%s:%s" .repository .tag }}
{{- with .digest }}
{{- $ref = printf "%s@%s" $ref . }}
{{- end }}
{{- $ref | quote }}
{{- end }}
{{- define "fluxer-infra.podAnnotations" -}}
{{- with merge (deepCopy (.extra | default dict)) (deepCopy (.w.podAnnotations | default dict)) (deepCopy (.root.Values.podAnnotations | default dict)) }}
annotations:
{{- toYaml . | nindent 2 }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.podSpec" -}}
{{- $root := .root }}
{{- $w := .w }}
{{- with include "fluxer-infra.pick" (dict "root" $root "w" $w "key" "affinity") }}
affinity:
{{- . | nindent 2 }}
{{- end }}
{{- with include "fluxer-infra.pick" (dict "root" $root "w" $w "key" "imagePullSecrets") }}
imagePullSecrets:
{{- . | nindent 2 }}
{{- end }}
{{- with include "fluxer-infra.pick" (dict "root" $root "w" $w "key" "nodeSelector") }}
nodeSelector:
{{- . | nindent 2 }}
{{- end }}
{{- with include "fluxer-infra.pick" (dict "root" $root "w" $w "key" "tolerations") }}
tolerations:
{{- . | nindent 2 }}
{{- end }}
{{- with include "fluxer-infra.topologySpreadConstraints" . }}
topologySpreadConstraints:
{{- . | nindent 2 }}
{{- end }}
{{- with include "fluxer-infra.pick" (dict "root" $root "w" $w "key" "podSecurityContext") }}
securityContext:
{{- . | nindent 2 }}
{{- end }}
{{- if not (kindIs "invalid" $w.terminationGracePeriodSeconds) }}
terminationGracePeriodSeconds: {{ $w.terminationGracePeriodSeconds }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.containerCommon" -}}
{{- $root := .root }}
{{- $w := .w }}
{{- $img := $w.image | default dict }}
image: {{ include "fluxer-infra.image" $img }}
imagePullPolicy: {{ $img.pullPolicy }}
{{- $env := include "fluxer-infra.envList" . | trim }}
{{- if or .env $env }}
env:
{{- with .env }}
{{- toYaml . | nindent 2 }}
{{- end }}
{{- with $env }}
{{- . | nindent 2 }}
{{- end }}
{{- end }}
{{- with include "fluxer-infra.envFrom" . }}
envFrom:
{{- . | nindent 2 }}
{{- end }}
{{- with $w.lifecycle }}
lifecycle:
{{- toYaml . | nindent 2 }}
{{- end }}
{{- include "fluxer-infra.probes" . }}
{{- with $w.resources }}
resources:
{{- toYaml . | nindent 2 }}
{{- end }}
{{- with include "fluxer-infra.pick" (dict "root" $root "w" $w "key" "securityContext") }}
securityContext:
{{- . | nindent 2 }}
{{- end }}
{{- with concat .mounts ($w.extraVolumeMounts | default list) }}
volumeMounts:
{{- toYaml . | nindent 2 }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.statefulSetSpec" -}}
{{- $w := .w }}
{{- with include "fluxer-infra.pick" (dict "root" .root "w" $w "key" "updateStrategy") }}
updateStrategy:
{{- . | nindent 2 }}
{{- end }}
{{- if not (kindIs "invalid" $w.minReadySeconds) }}
minReadySeconds: {{ $w.minReadySeconds }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.volumeClaim" -}}
- metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
{{- with .storageClassName }}
storageClassName: {{ . | quote }}
{{- end }}
resources:
requests:
storage: {{ .size }}
{{- end }}
{{- define "fluxer-infra.pdb" -}}
{{- with .w.pdb }}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $.name }}-pdb
namespace: {{ $.root.Release.Namespace }}
labels:
{{- include "fluxer-infra.labels" $ | nindent 4 }}
spec:
{{- if not (kindIs "invalid" .minAvailable) }}
minAvailable: {{ .minAvailable }}
{{- end }}
{{- if not (kindIs "invalid" .maxUnavailable) }}
maxUnavailable: {{ .maxUnavailable }}
{{- end }}
selector:
matchLabels:
{{- include "fluxer-infra.selectorLabels" $ | nindent 6 }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.service" }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ .svcName }}
namespace: {{ .root.Release.Namespace }}
labels:
{{- include "fluxer-infra.labels" . | nindent 4 }}
spec:
{{- if .headless }}
clusterIP: None
{{- end }}
{{- if .publishNotReady }}
publishNotReadyAddresses: true
{{- end }}
selector:
{{- include "fluxer-infra.selectorLabels" . | nindent 4 }}
ports:
{{- range .ports }}
- name: {{ index . 0 }}
port: {{ index . 1 }}
targetPort: {{ index . 0 }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.natsConf" -}}
{{- $w := .Values.nats -}}
{{- with $w.config -}}
listen: 0.0.0.0:4222
http: 0.0.0.0:8222
max_payload: {{ .maxPayload }}
max_pending: {{ .maxPending }}
max_connections: {{ .maxConnections }}
{{- if $w.jetstream.enabled }}
server_name: $POD_NAME
jetstream {
store_dir: /data
}
{{- end }}
cluster {
name: {{ .clusterName }}
listen: 0.0.0.0:6222
routes = [
{{- range $i := until (int (include "fluxer-infra.replicas" (dict "w" $w))) }}
nats-route://nats-{{ $i }}.nats-headless.{{ $.Release.Namespace }}.svc.{{ $.Values.clusterDomain }}:6222
{{- end }}
]
}
{{ end }}
{{- end }}
@@ -0,0 +1,71 @@
{{- with .Values.nats }}
{{- $ctx := dict "root" $ "w" . "name" "nats" "component" "messaging" }}
apiVersion: v1
kind: ConfigMap
metadata:
name: nats-config
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-infra.labels" $ctx | nindent 4 }}
data:
nats.conf: {{ include "fluxer-infra.natsConf" $ | toJson }}
{{- include "fluxer-infra.pdb" $ctx }}
{{- include "fluxer-infra.service" (merge (dict "svcName" "nats" "ports" (list (list "client" 4222))) $ctx) }}
{{- include "fluxer-infra.service" (merge (dict "svcName" "nats-headless" "headless" true "ports" (list (list "client" 4222) (list "cluster" 6222) (list "monitor" 8222))) $ctx) }}
{{- $mounts := list (dict "name" "config" "mountPath" "/etc/nats") }}
{{- $env := list }}
{{- if .jetstream.enabled }}
{{- $mounts = append $mounts (dict "name" "data" "mountPath" "/data") }}
{{- $env = append $env (dict "name" "POD_NAME" "valueFrom" (dict "fieldRef" (dict "fieldPath" "metadata.name"))) }}
{{- end }}
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: nats
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-infra.labels" $ctx | nindent 4 }}
spec:
replicas: {{ include "fluxer-infra.replicas" $ctx }}
serviceName: nats-headless
{{- with include "fluxer-infra.statefulSetSpec" $ctx | trim }}
{{- . | nindent 2 }}
{{- end }}
selector:
matchLabels:
{{- include "fluxer-infra.selectorLabels" $ctx | nindent 6 }}
template:
metadata:
labels:
{{- include "fluxer-infra.labels" $ctx | nindent 8 }}
{{- with include "fluxer-infra.podAnnotations" (merge (dict "extra" (dict "checksum/config" (include "fluxer-infra.natsConf" $ | sha256sum))) $ctx) | trim }}
{{- . | nindent 6 }}
{{- end }}
spec:
{{- include "fluxer-infra.podSpec" $ctx | trim | nindent 6 }}
containers:
- name: nats
{{- include "fluxer-infra.containerCommon" (merge (dict "env" $env "mounts" $mounts) $ctx) | trim | nindent 10 }}
args:
- -c
- /etc/nats/nats.conf
ports:
- name: client
containerPort: 4222
- name: cluster
containerPort: 6222
- name: monitor
containerPort: 8222
volumes:
- name: config
configMap:
name: nats-config
{{- with .extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .jetstream.enabled }}
volumeClaimTemplates:
{{- include "fluxer-infra.volumeClaim" .jetstream.storage | nindent 4 }}
{{- end }}
{{- end }}
@@ -0,0 +1,67 @@
{{- with .Values.valkey }}
{{- $ctx := dict "root" $ "w" . "name" "valkey" "component" "cache" }}
{{- include "fluxer-infra.pdb" $ctx }}
{{- include "fluxer-infra.service" (merge (dict "svcName" "valkey" "ports" (list (list "valkey" 6379))) $ctx) }}
{{- include "fluxer-infra.service" (merge (dict "svcName" "valkey-headless" "headless" true "publishNotReady" true "ports" (list (list "valkey" 6379))) $ctx) }}
{{- $mounts := list }}
{{- if .persistence.enabled }}
{{- $mounts = append $mounts (dict "name" "data" "mountPath" "/data") }}
{{- end }}
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: valkey
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-infra.labels" $ctx | nindent 4 }}
spec:
replicas: 1
serviceName: valkey-headless
{{- with include "fluxer-infra.statefulSetSpec" $ctx | trim }}
{{- . | nindent 2 }}
{{- end }}
selector:
matchLabels:
{{- include "fluxer-infra.selectorLabels" $ctx | nindent 6 }}
template:
metadata:
labels:
{{- include "fluxer-infra.labels" $ctx | nindent 8 }}
{{- with include "fluxer-infra.podAnnotations" $ctx | trim }}
{{- . | nindent 6 }}
{{- end }}
spec:
{{- include "fluxer-infra.podSpec" $ctx | trim | nindent 6 }}
containers:
- name: valkey
{{- include "fluxer-infra.containerCommon" (merge (dict "env" list "mounts" $mounts) $ctx) | trim | nindent 10 }}
command:
- valkey-server
{{- if .persistence.enabled }}
- --appendonly
- "yes"
- --dir
- /data
{{- else }}
- --save
- ""
- --appendonly
- "no"
{{- end }}
- --maxmemory
- {{ .maxmemory | quote }}
- --maxmemory-policy
- {{ .maxmemoryPolicy | quote }}
ports:
- name: valkey
containerPort: 6379
{{- with .extraVolumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .persistence.enabled }}
volumeClaimTemplates:
{{- include "fluxer-infra.volumeClaim" .persistence | nindent 4 }}
{{- end }}
{{- end }}
+108
View File
@@ -0,0 +1,108 @@
imagePullSecrets: []
clusterDomain: cluster.local
env: {}
extraEnv: []
envFrom: []
podAnnotations: {}
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
probes: {}
updateStrategy: {}
topologySpreadConstraints: []
nodeSelector: {}
tolerations: []
affinity: {}
nats:
image:
repository: nats
tag: 2.14-alpine
pullPolicy: IfNotPresent
replicas: 3
config:
clusterName: nats
maxPayload: 1MB
maxPending: 64MB
maxConnections: 65536
jetstream:
enabled: true
storage:
size: 10Gi
storageClassName: ""
podSecurityContext:
fsGroup: 65534
runAsGroup: 65534
runAsNonRoot: true
runAsUser: 65534
seccompProfile:
type: RuntimeDefault
probes:
liveness:
httpGet:
path: /healthz
port: monitor
initialDelaySeconds: 10
readiness:
httpGet:
path: /healthz?js-enabled-only=true
port: monitor
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 512Mi
valkey:
image:
repository: valkey/valkey
tag: 9.1-alpine
pullPolicy: IfNotPresent
maxmemory: 192mb
maxmemoryPolicy: noeviction
persistence:
enabled: true
size: 1Gi
storageClassName: ""
podSecurityContext:
fsGroup: 999
runAsGroup: 999
runAsNonRoot: true
runAsUser: 999
seccompProfile:
type: RuntimeDefault
probes:
liveness:
exec:
command:
- valkey-cli
- ping
initialDelaySeconds: 10
readiness:
exec:
command:
- valkey-cli
- ping
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 256Mi
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: fluxer-ingress
description: Ingress routing for the public Fluxer endpoints.
type: application
version: 0.1.0
appVersion: "v1"
@@ -0,0 +1,27 @@
{{- define "fluxer-ingress.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end }}
{{- define "fluxer-ingress.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/part-of: fluxer
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ include "fluxer-ingress.chart" . }}
{{- end }}
{{- define "fluxer-ingress.annotationKey" -}}
{{- if or (contains "/" .key) (not .prefix) -}}
{{- .key -}}
{{- else -}}
{{- printf "%s/%s" .prefix .key -}}
{{- end -}}
{{- end }}
{{- define "fluxer-ingress.string" -}}
{{- if and (kindIs "float64" .) (eq . (floor .)) -}}
{{- . | int64 | toString -}}
{{- else -}}
{{- . | toString -}}
{{- end -}}
{{- end }}
@@ -0,0 +1,20 @@
{{- with .Values.clusterIssuer }}
{{- if .enabled }}
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: {{ required "clusterIssuer.name is required" .name }}
labels:
{{- include "fluxer-ingress.labels" $ | nindent 4 }}
spec:
acme:
email: {{ required "clusterIssuer.email is required" .email | quote }}
privateKeySecretRef:
name: {{ required "clusterIssuer.privateKeySecretName is required" .privateKeySecretName }}
server: {{ required "clusterIssuer.server is required" .server }}
solvers:
- http01:
ingress:
class: {{ required "clusterIssuer.solverIngressClass is required" .solverIngressClass }}
{{- end }}
{{- end }}
@@ -0,0 +1,58 @@
{{- $v := .Values }}
{{- $presets := $v.annotationPresets | default dict }}
{{- $issuer := $v.clusterIssuer | default dict }}
{{- range $name, $spec := ($v.ingresses | default dict) }}
{{- if not (kindIs "invalid" $spec) }}
{{- $ann := deepCopy ($v.commonAnnotations | default dict) }}
{{- range ($spec.presets | default list) }}
{{- $ann = mergeOverwrite $ann (deepCopy (required (printf "unknown annotation preset %s" .) (index $presets .))) }}
{{- end }}
{{- if and $spec.tls $issuer.enabled }}
{{- $_ := set $ann "cert-manager.io/cluster-issuer" (required "clusterIssuer.name is required" $issuer.name) }}
{{- end }}
{{- $ann = mergeOverwrite $ann (deepCopy ($spec.annotations | default dict)) }}
{{- range $k, $val := $ann }}
{{- if kindIs "invalid" $val }}
{{- $_ := unset $ann $k }}
{{- end }}
{{- end }}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-ingress.labels" $ | nindent 4 }}
{{- with $ann }}
annotations:
{{- range $k, $val := . }}
{{ include "fluxer-ingress.annotationKey" (dict "key" $k "prefix" $v.annotationPrefix) }}: {{ include "fluxer-ingress.string" $val | quote }}
{{- end }}
{{- end }}
spec:
{{- with $spec.ingressClassName | default $v.ingressClassName }}
ingressClassName: {{ . }}
{{- end }}
{{- with $spec.tls }}
tls:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
{{- range $rule := required (printf "ingress %s needs rules" $name) $spec.rules }}
- host: {{ required (printf "ingress %s has a rule without a host" $name) $rule.host | quote }}
http:
paths:
{{- range $p := $rule.paths | default (list dict) }}
{{- $p = $p | default dict }}
- path: {{ $p.path | default "/" | quote }}
pathType: {{ $p.pathType | default "Prefix" }}
backend:
service:
name: {{ required (printf "ingress %s host %s needs a service" $name $rule.host) ($p.service | default $rule.service) }}
port:
number: {{ required (printf "ingress %s host %s needs a port or servicePort" $name $rule.host) ($p.port | default $rule.port | default $v.servicePort) | int64 }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
+53
View File
@@ -0,0 +1,53 @@
ingressClassName: nginx
annotationPrefix: nginx.ingress.kubernetes.io
servicePort: 8080
commonAnnotations: {}
annotationPresets:
websocket:
proxy-read-timeout: "3600"
proxy-send-timeout: "3600"
stripPrefix:
use-regex: "true"
rewrite-target: /$2
ingresses:
fluxer:
rules:
- host: web.example.com
service: app-proxy
- host: api.example.com
service: api
- host: admin.example.com
service: admin
- host: media.example.com
service: media-proxy
fluxer-web-api:
presets: [stripPrefix]
rules:
- host: web.example.com
service: api
paths:
- path: /api(/(.*))?$
pathType: ImplementationSpecific
fluxer-gateway:
presets: [websocket]
rules:
- host: gateway.example.com
service: gateway
fluxer-uploads:
annotations:
proxy-body-size: 100m
proxy-request-buffering: "off"
rules:
- host: uploads.example.com
service: uploads
clusterIssuer:
enabled: false
name: letsencrypt
email: ""
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretName: letsencrypt-account-key
solverIngressClass: nginx
@@ -0,0 +1,6 @@
apiVersion: v2
name: fluxer-media-proxy
description: Fluxer media proxy and upload relay workloads.
type: application
version: 0.1.0
appVersion: "v1"
@@ -0,0 +1,87 @@
{{- define "fluxer-media-proxy.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end }}
{{- define "fluxer-media-proxy.selectorLabels" -}}
app.kubernetes.io/name: {{ .name }}
app.kubernetes.io/instance: {{ .root.Release.Name }}
{{- end }}
{{- define "fluxer-media-proxy.labels" -}}
{{ include "fluxer-media-proxy.selectorLabels" . }}
app.kubernetes.io/component: {{ include "fluxer-media-proxy.mode" . }}
app.kubernetes.io/part-of: fluxer
app.kubernetes.io/managed-by: {{ .root.Release.Service }}
helm.sh/chart: {{ include "fluxer-media-proxy.chart" .root }}
{{- end }}
{{- define "fluxer-media-proxy.image" -}}
{{- $g := .root.Values.image -}}
{{- $i := .w.image | default dict -}}
{{- $repo := $i.repository | default (printf "%s/%s" $g.registry ($i.name | default "fluxer-media-proxy")) -}}
{{- $tag := $i.tag | default $g.tag -}}
{{- if $i.digest -}}
{{- printf "%s:%s@%s" $repo $tag $i.digest | quote -}}
{{- else -}}
{{- printf "%s:%s" $repo $tag | quote -}}
{{- end -}}
{{- end }}
{{- define "fluxer-media-proxy.pick" -}}
{{- $v := ternary (get .w .key) (get .root.Values .key) (hasKey .w .key) -}}
{{- if $v }}
{{- toYaml $v }}
{{- end }}
{{- end }}
{{- define "fluxer-media-proxy.mode" -}}
{{- $mode := required (printf "workloads.%s.mode is required" .name) .w.mode -}}
{{- if not (has $mode (list "mp" "static" "upload" "relay")) -}}
{{- fail (printf "workloads.%s.mode must be mp, static, upload or relay" .name) -}}
{{- end -}}
{{- $mode -}}
{{- end }}
{{- define "fluxer-media-proxy.envValue" -}}
{{- if and (kindIs "float64" .) (eq . (float64 (int64 .))) -}}
{{- int64 . | toString -}}
{{- else -}}
{{- toString . -}}
{{- end -}}
{{- end }}
{{- define "fluxer-media-proxy.mergeEnv" -}}
{{- $out := dict -}}
{{- range $layer := . -}}
{{- range $k, $v := ($layer | default dict) -}}
{{- if kindIs "invalid" $v -}}
{{- $_ := unset $out $k -}}
{{- else -}}
{{- $_ := set $out $k $v -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- toYaml $out -}}
{{- end }}
{{- define "fluxer-media-proxy.topologySpreadConstraints" -}}
{{- $out := list -}}
{{- range .constraints -}}
{{- if .labelSelector -}}
{{- $out = append $out . -}}
{{- else -}}
{{- $out = append $out (merge (dict "labelSelector" (dict "matchLabels" $.selector)) .) -}}
{{- end -}}
{{- end -}}
{{- toYaml $out -}}
{{- end }}
{{- define "fluxer-media-proxy.pdb" -}}
{{- $out := dict -}}
{{- range $k := list "minAvailable" "maxUnavailable" -}}
{{- if and (hasKey $ $k) (not (kindIs "invalid" (index $ $k))) -}}
{{- $_ := set $out $k (index $ $k) -}}
{{- end -}}
{{- end -}}
{{- toYaml $out -}}
{{- end }}
@@ -0,0 +1,191 @@
{{- range $name, $w := .Values.workloads }}
{{- if not (kindIs "invalid" $w) }}
{{- $ctx := dict "root" $ "name" $name "w" $w }}
{{- $mode := include "fluxer-media-proxy.mode" $ctx }}
{{- $sel := include "fluxer-media-proxy.selectorLabels" $ctx | fromYaml }}
{{- $env := include "fluxer-media-proxy.mergeEnv" (list $.Values.env $w.env) | fromYaml }}
{{- $extraEnv := concat ($.Values.extraEnv | default list) ($w.extraEnv | default list) }}
{{- $envFrom := concat ($.Values.envFrom | default list) ($w.envFrom | default list) }}
{{- $podAnnotations := merge (dict) ($w.podAnnotations | default dict) ($.Values.podAnnotations | default dict) }}
{{- $probes := dict }}
{{- range $k, $v := ($.Values.probes | default dict) }}
{{- $_ := set $probes $k $v }}
{{- end }}
{{- range $k, $v := ($w.probes | default dict) }}
{{- $_ := set $probes $k $v }}
{{- end }}
{{- $pick := dict "root" $ "w" $w }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-media-proxy.labels" $ctx | nindent 4 }}
spec:
{{- if not $w.hpa }}
replicas: {{ ternary $w.replicas 1 (hasKey $w "replicas") | int64 }}
{{- end }}
{{- if not (kindIs "invalid" $w.minReadySeconds) }}
minReadySeconds: {{ $w.minReadySeconds | int64 }}
{{- end }}
selector:
matchLabels:
{{- toYaml $sel | nindent 6 }}
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "strategy") }}
strategy:
{{- . | nindent 4 }}
{{- end }}
template:
metadata:
{{- with $podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "fluxer-media-proxy.labels" $ctx | nindent 8 }}
spec:
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "imagePullSecrets") }}
imagePullSecrets:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "podSecurityContext") }}
securityContext:
{{- . | nindent 8 }}
{{- end }}
{{- if not (kindIs "invalid" $w.terminationGracePeriodSeconds) }}
terminationGracePeriodSeconds: {{ $w.terminationGracePeriodSeconds | int64 }}
{{- end }}
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "nodeSelector") }}
nodeSelector:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "tolerations") }}
tolerations:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "affinity") }}
affinity:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "topologySpreadConstraints") | fromYamlArray }}
topologySpreadConstraints:
{{- include "fluxer-media-proxy.topologySpreadConstraints" (dict "constraints" . "selector" $sel) | nindent 8 }}
{{- end }}
containers:
- name: {{ $name }}
image: {{ include "fluxer-media-proxy.image" $ctx }}
imagePullPolicy: {{ ($w.image | default dict).pullPolicy | default $.Values.image.pullPolicy }}
env:
{{- if not (kindIs "invalid" $w.buildVersion) }}
- name: BUILD_VERSION
value: {{ include "fluxer-media-proxy.envValue" $w.buildVersion | quote }}
{{- end }}
- name: FLUXER_MEDIA_PROXY_MODE
value: {{ $mode | quote }}
{{- range $k, $v := $env }}
- name: {{ $k }}
value: {{ include "fluxer-media-proxy.envValue" $v | quote }}
{{- end }}
{{- with $extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $envFrom }}
envFrom:
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- name: http
containerPort: 8080
protocol: TCP
{{- with $w.lifecycle }}
lifecycle:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- range $k := list "startup" "liveness" "readiness" }}
{{- with get $probes $k }}
{{ $k }}Probe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
{{- with $w.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "securityContext") }}
securityContext:
{{- . | nindent 12 }}
{{- end }}
{{- with $w.extraVolumeMounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $w.extraVolumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-media-proxy.labels" $ctx | nindent 4 }}
spec:
type: ClusterIP
selector:
{{- toYaml $sel | nindent 4 }}
ports:
- name: http
port: 8080
targetPort: http
protocol: TCP
{{- with include "fluxer-media-proxy.pdb" ($w.pdb | default dict) | fromYaml }}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $name }}-pdb
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-media-proxy.labels" $ctx | nindent 4 }}
spec:
{{- toYaml . | nindent 2 }}
selector:
matchLabels:
{{- toYaml $sel | nindent 6 }}
{{- end }}
{{- with $w.hpa }}
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-media-proxy.labels" $ctx | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ $name }}
minReplicas: {{ required (printf "workloads.%s.hpa.minReplicas is required" $name) .minReplicas | int64 }}
maxReplicas: {{ required (printf "workloads.%s.hpa.maxReplicas is required" $name) .maxReplicas | int64 }}
{{- if not (kindIs "invalid" .targetCPUUtilizationPercentage) }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .targetCPUUtilizationPercentage | int64 }}
{{- end }}
{{- with .behavior }}
behavior:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
@@ -0,0 +1,72 @@
image:
registry: ghcr.io/fluxerapp
tag: v1
pullPolicy: IfNotPresent
imagePullSecrets: []
env: {}
extraEnv: []
envFrom:
- secretRef:
name: fluxer-env
podAnnotations: {}
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
probes:
liveness:
httpGet:
path: /_health
port: http
readiness:
httpGet:
path: /_health
port: http
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
topologySpreadConstraints: []
nodeSelector: {}
tolerations: []
affinity: {}
workloads:
media-proxy:
mode: mp
replicas: 1
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 1Gi
uploads:
mode: relay
replicas: 1
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 512Mi
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: fluxer-push
description: Fluxer push notification delivery service
type: application
version: 0.1.0
appVersion: "v1"
@@ -0,0 +1,71 @@
{{- define "fluxer-push.selectorLabels" -}}
app.kubernetes.io/name: {{ .name }}
app.kubernetes.io/instance: {{ .root.Release.Name }}
{{- end }}
{{- define "fluxer-push.labels" -}}
{{ include "fluxer-push.selectorLabels" . }}
app.kubernetes.io/component: {{ include "fluxer-push.mode" . }}
app.kubernetes.io/part-of: fluxer
app.kubernetes.io/managed-by: {{ .root.Release.Service }}
helm.sh/chart: {{ printf "%s-%s" .root.Chart.Name .root.Chart.Version | replace "+" "_" }}
{{- end }}
{{- define "fluxer-push.mode" -}}
{{- $mode := .w.mode | default "delivery" -}}
{{- if not (has $mode (list "delivery" "relay")) -}}
{{- fail (printf "workloads.%s.mode must be delivery or relay" .name) -}}
{{- end -}}
{{- $mode -}}
{{- end }}
{{- define "fluxer-push.port" -}}
{{- .w.port | default (ternary 8127 8126 (eq (include "fluxer-push.mode" .) "relay")) -}}
{{- end }}
{{- define "fluxer-push.image" -}}
{{- $global := .root.Values.image | default dict -}}
{{- $img := .w.image | default dict -}}
{{- $repo := $img.repository -}}
{{- if not $repo -}}
{{- $repo = printf "%s/%s" (required "image.registry is required" $global.registry) ($img.name | default "fluxer-push") -}}
{{- end -}}
{{- $ref := printf "%s:%s" $repo (include "fluxer-push.string" (required "image.tag is required" ($img.tag | default $global.tag))) -}}
{{- with $img.digest }}{{ $ref = printf "%s@%s" $ref . }}{{ end -}}
{{- $ref -}}
{{- end }}
{{- define "fluxer-push.string" -}}
{{- if and (kindIs "float64" .) (eq . (floor .)) -}}
{{- . | int64 | toString -}}
{{- else -}}
{{- . | toString -}}
{{- end -}}
{{- end }}
{{- define "fluxer-push.env" -}}
{{- $env := deepCopy (.root.Values.env | default dict) -}}
{{- range $k, $v := (.w.env | default dict) -}}
{{- if kindIs "invalid" $v -}}
{{- $_ := unset $env $k -}}
{{- else -}}
{{- $_ := set $env $k $v -}}
{{- end -}}
{{- end -}}
{{- if not (kindIs "invalid" .w.port) -}}
{{- $_ := set $env "FLUXER_PUSH_SERVICE_PORT" .w.port -}}
{{- end -}}
{{- if not (kindIs "invalid" .w.buildVersion) }}
- name: BUILD_VERSION
value: {{ include "fluxer-push.string" .w.buildVersion | quote }}
{{- end }}
{{- range $k, $v := $env }}
{{- if not (kindIs "invalid" $v) }}
- name: {{ $k }}
value: {{ include "fluxer-push.string" $v | quote }}
{{- end }}
{{- end }}
{{- with concat (.root.Values.extraEnv | default list) (.w.extraEnv | default list) }}
{{ toYaml . }}
{{- end }}
{{- end }}
@@ -0,0 +1,205 @@
{{- range $name, $w := .Values.workloads }}
{{- if not (kindIs "invalid" $w) }}
{{- $ctx := dict "root" $ "name" $name "w" $w }}
{{- $mode := include "fluxer-push.mode" $ctx }}
{{- $port := include "fluxer-push.port" $ctx | int }}
{{- $globalProbes := $.Values.probes | default dict }}
{{- $workloadProbes := $w.probes | default dict }}
{{- $probes := dict }}
{{- range $probe := list "startup" "liveness" "readiness" }}
{{- $_ := set $probes $probe (ternary (index $workloadProbes $probe) (index $globalProbes $probe) (hasKey $workloadProbes $probe)) }}
{{- end }}
{{- $annotations := mergeOverwrite (deepCopy ($.Values.podAnnotations | default dict)) (deepCopy ($w.podAnnotations | default dict)) }}
{{- $pullSecrets := ternary $w.imagePullSecrets $.Values.imagePullSecrets (hasKey $w "imagePullSecrets") }}
{{- $podSecurityContext := ternary $w.podSecurityContext $.Values.podSecurityContext (hasKey $w "podSecurityContext") }}
{{- $securityContext := ternary $w.securityContext $.Values.securityContext (hasKey $w "securityContext") }}
{{- $strategy := ternary $w.strategy $.Values.strategy (hasKey $w "strategy") }}
{{- $tsc := ternary $w.topologySpreadConstraints $.Values.topologySpreadConstraints (hasKey $w "topologySpreadConstraints") }}
{{- $nodeSelector := ternary $w.nodeSelector $.Values.nodeSelector (hasKey $w "nodeSelector") }}
{{- $tolerations := ternary $w.tolerations $.Values.tolerations (hasKey $w "tolerations") }}
{{- $affinity := ternary $w.affinity $.Values.affinity (hasKey $w "affinity") }}
{{- $envFrom := concat ($.Values.envFrom | default list) ($w.envFrom | default list) }}
{{- $env := include "fluxer-push.env" $ctx }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-push.labels" $ctx | nindent 4 }}
spec:
{{- if not $w.hpa }}
replicas: {{ ternary $w.replicas 1 (hasKey $w "replicas") | int }}
{{- end }}
{{- if hasKey $w "minReadySeconds" }}
minReadySeconds: {{ $w.minReadySeconds | int }}
{{- end }}
selector:
matchLabels:
{{- include "fluxer-push.selectorLabels" $ctx | nindent 6 }}
{{- with $strategy }}
strategy:
{{- toYaml . | nindent 4 }}
{{- end }}
template:
metadata:
{{- with $annotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "fluxer-push.labels" $ctx | nindent 8 }}
spec:
{{- with $pullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if hasKey $w "terminationGracePeriodSeconds" }}
terminationGracePeriodSeconds: {{ $w.terminationGracePeriodSeconds | int }}
{{- end }}
{{- with $nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $tsc }}
topologySpreadConstraints:
{{- range . }}
{{- $c := deepCopy . }}
{{- if not $c.labelSelector }}
{{- $_ := set $c "labelSelector" (dict "matchLabels" (include "fluxer-push.selectorLabels" $ctx | fromYaml)) }}
{{- end }}
{{- toYaml (list $c) | nindent 8 }}
{{- end }}
{{- end }}
containers:
- name: {{ $name }}
image: {{ include "fluxer-push.image" $ctx | quote }}
imagePullPolicy: {{ ($w.image | default dict).pullPolicy | default ($.Values.image | default dict).pullPolicy | default "IfNotPresent" }}
command:
- /usr/local/bin/fluxer-push
{{- if eq $mode "relay" }}
args:
- --mode
- relay
{{- end }}
{{- with trim $env }}
env:
{{- . | nindent 12 }}
{{- end }}
{{- with $envFrom }}
envFrom:
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- name: http
containerPort: {{ $port }}
protocol: TCP
{{- with $probes.startup }}
startupProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $probes.liveness }}
livenessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $probes.readiness }}
readinessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $w.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $w.lifecycle }}
lifecycle:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $w.extraVolumeMounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $w.extraVolumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-push.labels" $ctx | nindent 4 }}
spec:
type: ClusterIP
selector:
{{- include "fluxer-push.selectorLabels" $ctx | nindent 4 }}
ports:
- name: http
port: {{ $port }}
protocol: TCP
targetPort: http
{{- with $w.pdb }}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $name }}-pdb
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-push.labels" $ctx | nindent 4 }}
spec:
{{- toYaml . | nindent 2 }}
selector:
matchLabels:
{{- include "fluxer-push.selectorLabels" $ctx | nindent 6 }}
{{- end }}
{{- with $w.hpa }}
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-push.labels" $ctx | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ $name }}
minReplicas: {{ required (printf "workloads.%s.hpa.minReplicas is required" $name) .minReplicas | int }}
maxReplicas: {{ required (printf "workloads.%s.hpa.maxReplicas is required" $name) .maxReplicas | int }}
{{- if not (kindIs "invalid" .targetCPUUtilizationPercentage) }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .targetCPUUtilizationPercentage | int }}
{{- end }}
{{- with .behavior }}
behavior:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
+65
View File
@@ -0,0 +1,65 @@
image:
registry: ghcr.io/fluxerapp
tag: v1
pullPolicy: IfNotPresent
imagePullSecrets: []
env: {}
extraEnv: []
envFrom:
- secretRef:
name: fluxer-env
podAnnotations: {}
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
probes:
liveness:
httpGet:
path: /_healthz
port: http
readiness:
httpGet:
path: /_healthz
port: http
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
topologySpreadConstraints: []
nodeSelector: {}
tolerations: []
affinity: {}
workloads:
push:
mode: delivery
replicas: 1
env:
FLUXER_INTERNAL_API_ENDPOINT: http://api:8080
FLUXER_SVC_NATS_URL: nats://nats:4222
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 256Mi
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: fluxer-svc
description: Fluxer internal services, each a router Deployment and a shard StatefulSet
type: application
version: 0.1.0
appVersion: v1
@@ -0,0 +1,203 @@
{{- define "fluxer-svc.chart" -}}
{{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
{{- end }}
{{- define "fluxer-svc.selectorLabels" -}}
app.kubernetes.io/name: {{ .name }}
app.kubernetes.io/instance: {{ .root.Release.Name }}
{{- end }}
{{- define "fluxer-svc.labels" -}}
{{ include "fluxer-svc.selectorLabels" . }}
app.kubernetes.io/component: {{ .mode }}
app.kubernetes.io/part-of: fluxer
app.kubernetes.io/managed-by: {{ .root.Release.Service }}
helm.sh/chart: {{ include "fluxer-svc.chart" .root }}
{{- end }}
{{- define "fluxer-svc.envValue" -}}
{{- if and (kindIs "float64" .) (eq . (float64 (int64 .))) -}}
{{- int64 . | toString -}}
{{- else -}}
{{- toString . -}}
{{- end -}}
{{- end }}
{{- define "fluxer-svc.mergeEnv" -}}
{{- $out := dict -}}
{{- range $layer := . -}}
{{- range $k, $v := ($layer | default dict) -}}
{{- if kindIs "invalid" $v -}}
{{- $_ := unset $out $k -}}
{{- else -}}
{{- $_ := set $out $k $v -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- toYaml $out -}}
{{- end }}
{{- define "fluxer-svc.topologySpreadConstraints" -}}
{{- $out := list -}}
{{- range .constraints -}}
{{- if .labelSelector -}}
{{- $out = append $out . -}}
{{- else -}}
{{- $out = append $out (merge (dict "labelSelector" (dict "matchLabels" $.selector)) .) -}}
{{- end -}}
{{- end -}}
{{- toYaml $out -}}
{{- end }}
{{- define "fluxer-svc.pdb" -}}
{{- $out := dict -}}
{{- range $k := list "minAvailable" "maxUnavailable" -}}
{{- if and (hasKey $ $k) (not (kindIs "invalid" (index $ $k))) -}}
{{- $_ := set $out $k (index $ $k) -}}
{{- end -}}
{{- end -}}
{{- toYaml $out -}}
{{- end }}
{{- define "fluxer-svc.config" -}}
{{- $v := .root.Values -}}
{{- $levels := list (index $v .mode) (index .svc .mode) -}}
{{- $c := dict "extraEnv" ($v.extraEnv | default list) "envFrom" ($v.envFrom | default list) "podAnnotations" (deepCopy ($v.podAnnotations | default dict)) "probes" (deepCopy ($v.probes | default dict)) "image" (deepCopy (.svc.image | default dict)) -}}
{{- range $k := list "imagePullSecrets" "podSecurityContext" "securityContext" "topologySpreadConstraints" "nodeSelector" "tolerations" "affinity" (ternary "updateStrategy" "strategy" (eq .mode "shard")) -}}
{{- $_ := set $c $k (index $v $k) -}}
{{- end -}}
{{- $envLayers := list $v.env -}}
{{- range $level := $levels -}}
{{- range $k, $x := ($level | default dict) -}}
{{- if eq $k "env" -}}
{{- $envLayers = append $envLayers $x -}}
{{- else if has $k (list "podAnnotations" "image") -}}
{{- $_ := set $c $k (mergeOverwrite (index $c $k) (deepCopy ($x | default dict))) -}}
{{- else if has $k (list "extraEnv" "envFrom") -}}
{{- $_ := set $c $k (concat (index $c $k) ($x | default list)) -}}
{{- else if eq $k "probes" -}}
{{- range $name, $p := ($x | default dict) -}}
{{- $_ := set $c.probes $name $p -}}
{{- end -}}
{{- else -}}
{{- $_ := set $c $k $x -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $_ := set $c "env" (include "fluxer-svc.mergeEnv" $envLayers | fromYaml) -}}
{{- toYaml $c }}
{{- end }}
{{- define "fluxer-svc.image" -}}
{{- $g := .root.Values.image -}}
{{- $i := .c.image -}}
{{- $repo := $i.repository | default (printf "%s/%s" $g.registry ($i.name | default (printf "fluxer-%s" .service))) -}}
{{- $ref := printf "%s:%s" $repo ($i.tag | default $g.tag) -}}
{{- with $i.digest }}{{ $ref = printf "%s@%s" $ref . }}{{ end -}}
{{- $ref -}}
{{- end }}
{{- define "fluxer-svc.pod" -}}
{{- $v := .root.Values -}}
{{- $c := .c -}}
metadata:
{{- with $c.podAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
labels:
{{- include "fluxer-svc.labels" . | nindent 4 }}
spec:
{{- with $c.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with $c.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- if not (kindIs "invalid" $c.terminationGracePeriodSeconds) }}
terminationGracePeriodSeconds: {{ $c.terminationGracePeriodSeconds | int64 }}
{{- end }}
{{- with $c.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with $c.tolerations }}
tolerations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with $c.affinity }}
affinity:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with $c.topologySpreadConstraints }}
topologySpreadConstraints:
{{- include "fluxer-svc.topologySpreadConstraints" (dict "constraints" . "selector" (include "fluxer-svc.selectorLabels" $ | fromYaml)) | nindent 4 }}
{{- end }}
containers:
- name: {{ .mode }}
image: {{ include "fluxer-svc.image" . | quote }}
imagePullPolicy: {{ $c.image.pullPolicy | default $v.image.pullPolicy }}
env:
- name: FLUXER_SVC_MODE
value: {{ .mode | quote }}
- name: FLUXER_SVC_NAME
value: {{ .service | quote }}
- name: FLUXER_SVC_SHARD_COUNT
value: {{ .shardCount | quote }}
- name: FLUXER_SVC_PORT
value: {{ include "fluxer-svc.envValue" $v.port | quote }}
{{- if not (kindIs "invalid" $c.buildVersion) }}
- name: BUILD_VERSION
value: {{ include "fluxer-svc.envValue" $c.buildVersion | quote }}
{{- end }}
{{- if eq .mode "shard" }}
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
{{- end }}
{{- range $name, $value := $c.env }}
- name: {{ $name }}
value: {{ include "fluxer-svc.envValue" $value | quote }}
{{- end }}
{{- with $c.extraEnv }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $c.envFrom }}
envFrom:
{{- toYaml . | nindent 8 }}
{{- end }}
ports:
- name: http
containerPort: {{ $v.port }}
protocol: TCP
{{- with $c.lifecycle }}
lifecycle:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- range $name := list "startup" "liveness" "readiness" }}
{{- with index $c.probes $name }}
{{ $name }}Probe:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- with $c.resources }}
resources:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $c.securityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $c.extraVolumeMounts }}
volumeMounts:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $c.extraVolumes }}
volumes:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
@@ -0,0 +1,145 @@
{{- range $service, $svc := .Values.services }}
{{- if not (kindIs "invalid" $svc) }}
{{- $svc = $svc | default dict }}
{{- $rc := fromYaml (include "fluxer-svc.config" (dict "root" $ "svc" $svc "mode" "router")) }}
{{- $sc := fromYaml (include "fluxer-svc.config" (dict "root" $ "svc" $svc "mode" "shard")) }}
{{- $routerReplicas := ternary $rc.replicas 1 (hasKey $rc "replicas") | int64 }}
{{- $shardCount := ternary $sc.replicas 1 (hasKey $sc "replicas") | int64 }}
{{- if lt $shardCount 1 }}
{{- fail (printf "services.%s shard replicas must be at least 1" $service) }}
{{- end }}
{{- $router := dict "root" $ "service" $service "svc" $svc "mode" "router" "name" $service "c" $rc "shardCount" (toString $shardCount) }}
{{- $shard := dict "root" $ "service" $service "svc" $svc "mode" "shard" "name" (printf "%s-shard" $service) "c" $sc "shardCount" (toString $shardCount) }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ $service }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-svc.labels" $router | nindent 4 }}
spec:
{{- if not $rc.hpa }}
replicas: {{ $routerReplicas }}
{{- end }}
{{- if not (kindIs "invalid" $rc.minReadySeconds) }}
minReadySeconds: {{ $rc.minReadySeconds | int64 }}
{{- end }}
selector:
matchLabels:
{{- include "fluxer-svc.selectorLabels" $router | nindent 6 }}
{{- with $rc.strategy }}
strategy:
{{- toYaml . | nindent 4 }}
{{- end }}
template:
{{- include "fluxer-svc.pod" $router | nindent 4 }}
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ $service }}-shard
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-svc.labels" $shard | nindent 4 }}
spec:
replicas: {{ $shardCount }}
{{- if not (kindIs "invalid" $sc.minReadySeconds) }}
minReadySeconds: {{ $sc.minReadySeconds | int64 }}
{{- end }}
podManagementPolicy: Parallel
serviceName: {{ $service }}-shard-headless
selector:
matchLabels:
{{- include "fluxer-svc.selectorLabels" $shard | nindent 6 }}
{{- with $sc.updateStrategy }}
updateStrategy:
{{- toYaml . | nindent 4 }}
{{- end }}
template:
{{- include "fluxer-svc.pod" $shard | nindent 4 }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ $service }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-svc.labels" $router | nindent 4 }}
spec:
type: ClusterIP
selector:
{{- include "fluxer-svc.selectorLabels" $router | nindent 4 }}
ports:
- name: http
port: {{ $.Values.port }}
targetPort: {{ $.Values.port }}
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: {{ $service }}-shard-headless
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-svc.labels" $shard | nindent 4 }}
spec:
type: ClusterIP
clusterIP: None
publishNotReadyAddresses: true
selector:
{{- include "fluxer-svc.selectorLabels" $shard | nindent 4 }}
ports:
- name: http
port: {{ $.Values.port }}
targetPort: {{ $.Values.port }}
protocol: TCP
{{- with $rc.hpa }}
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ $service }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-svc.labels" $router | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ $service }}
minReplicas: {{ required (printf "services.%s router hpa.minReplicas is required" $service) .minReplicas | int64 }}
maxReplicas: {{ required (printf "services.%s router hpa.maxReplicas is required" $service) .maxReplicas | int64 }}
{{- if not (kindIs "invalid" .targetCPUUtilizationPercentage) }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .targetCPUUtilizationPercentage | int64 }}
{{- end }}
{{- with .behavior }}
behavior:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
{{- range $ctx := list $router $shard }}
{{- with include "fluxer-svc.pdb" ($ctx.c.pdb | default dict) | fromYaml }}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $ctx.name }}-pdb
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-svc.labels" $ctx | nindent 4 }}
spec:
{{- toYaml . | nindent 2 }}
selector:
matchLabels:
{{- include "fluxer-svc.selectorLabels" $ctx | nindent 6 }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
+89
View File
@@ -0,0 +1,89 @@
image:
registry: ghcr.io/fluxerapp
tag: v1
pullPolicy: IfNotPresent
imagePullSecrets: []
env:
FLUXER_SVC_NATS_URL: nats://nats:4222
extraEnv: []
envFrom:
- secretRef:
name: fluxer-env
podAnnotations: {}
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
probes:
liveness:
httpGet:
path: /_healthz
port: http
readiness:
httpGet:
path: /_health
port: http
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
updateStrategy:
type: RollingUpdate
topologySpreadConstraints: []
nodeSelector: {}
tolerations: []
affinity: {}
port: 8090
router:
replicas: 1
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 192Mi
shard:
replicas: 2
probes:
startup:
httpGet:
path: /_healthz
port: http
periodSeconds: 10
failureThreshold: 30
resources:
requests:
cpu: 50m
memory: 96Mi
limits:
memory: 384Mi
services:
gifs:
shard:
env:
FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT: https://media.example.com
messages: {}
snowflakes: {}
unfurl:
shard:
env:
FLUXER_MEDIA_PROXY_ENDPOINT: http://media-proxy:8080
users: {}
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: fluxer-web
description: Fluxer web app proxy and admin dashboard.
type: application
version: 0.1.0
appVersion: "v1"
@@ -0,0 +1,80 @@
{{- define "fluxer-web.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end }}
{{- define "fluxer-web.selectorLabels" -}}
app.kubernetes.io/name: {{ .name }}
app.kubernetes.io/instance: {{ .root.Release.Name }}
{{- end }}
{{- define "fluxer-web.labels" -}}
{{ include "fluxer-web.selectorLabels" . }}
app.kubernetes.io/component: web
app.kubernetes.io/part-of: fluxer
app.kubernetes.io/managed-by: {{ .root.Release.Service }}
helm.sh/chart: {{ include "fluxer-web.chart" .root }}
{{- end }}
{{- define "fluxer-web.image" -}}
{{- $g := .root.Values.image | default dict -}}
{{- $i := .w.image | default dict -}}
{{- $repo := $i.repository -}}
{{- if not $repo -}}
{{- $repo = printf "%s/%s" (required "image.registry is required" $g.registry) ($i.name | default (printf "fluxer-%s" .name)) -}}
{{- end -}}
{{- $tag := required "image.tag is required" ($i.tag | default $g.tag) -}}
{{- if $i.digest -}}
{{- printf "%s:%s@%s" $repo $tag $i.digest | quote -}}
{{- else -}}
{{- printf "%s:%s" $repo $tag | quote -}}
{{- end -}}
{{- end }}
{{- define "fluxer-web.pick" -}}
{{- $v := ternary (get .w .key) (get .root.Values .key) (hasKey .w .key) -}}
{{- if $v }}
{{- toYaml $v }}
{{- end }}
{{- end }}
{{- define "fluxer-web.str" -}}
{{- if and (kindIs "float64" .) (eq . (floor .)) -}}
{{- int64 . | toString | quote -}}
{{- else -}}
{{- toString . | quote -}}
{{- end -}}
{{- end }}
{{- define "fluxer-web.env" -}}
{{- $env := dict -}}
{{- range $k, $val := .root.Values.env | default dict }}
{{- $_ := set $env $k $val }}
{{- end }}
{{- range $k, $val := .w.env | default dict }}
{{- $_ := set $env $k $val }}
{{- end }}
{{- range $k, $val := $env }}
{{- if not (kindIs "invalid" $val) }}
- name: {{ $k }}
value: {{ include "fluxer-web.str" $val }}
{{- end }}
{{- end }}
{{- with .w.buildVersion }}
- name: BUILD_VERSION
value: {{ include "fluxer-web.str" . }}
{{- end }}
{{- with concat (.root.Values.extraEnv | default list) (.w.extraEnv | default list) }}
{{ toYaml . }}
{{- end }}
{{- end }}
{{- define "fluxer-web.topologySpread" -}}
{{- $tscs := ternary .w.topologySpreadConstraints .root.Values.topologySpreadConstraints (hasKey .w "topologySpreadConstraints") -}}
{{- range $tscs }}
{{- $c := deepCopy . }}
{{- if not $c.labelSelector }}
{{- $_ := set $c "labelSelector" (dict "matchLabels" (include "fluxer-web.selectorLabels" $ | fromYaml)) }}
{{- end }}
- {{- toYaml $c | nindent 2 }}
{{- end }}
{{- end }}
@@ -0,0 +1,172 @@
{{- $v := .Values }}
{{- range $name, $w := .Values.workloads }}
{{- if not (kindIs "invalid" $w) }}
{{- $ctx := dict "root" $ "name" $name "w" $w }}
{{- $envFrom := concat ($v.envFrom | default list) ($w.envFrom | default list) }}
{{- $podAnnotations := merge (dict) ($w.podAnnotations | default dict) ($v.podAnnotations | default dict) }}
{{- $wProbes := $w.probes | default dict }}
{{- $gProbes := $v.probes | default dict }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-web.labels" $ctx | nindent 4 }}
spec:
{{- if not $w.hpa }}
replicas: {{ if kindIs "invalid" $w.replicas }}1{{ else }}{{ int $w.replicas }}{{ end }}
{{- end }}
{{- if not (kindIs "invalid" $w.minReadySeconds) }}
minReadySeconds: {{ int $w.minReadySeconds }}
{{- end }}
selector:
matchLabels:
{{- include "fluxer-web.selectorLabels" $ctx | nindent 6 }}
{{- with include "fluxer-web.pick" (dict "root" $ "w" $w "key" "strategy") }}
strategy:
{{- . | nindent 4 }}
{{- end }}
template:
metadata:
labels:
{{- include "fluxer-web.labels" $ctx | nindent 8 }}
{{- with $podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with include "fluxer-web.pick" (dict "root" $ "w" $w "key" "imagePullSecrets") }}
imagePullSecrets:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-web.pick" (dict "root" $ "w" $w "key" "podSecurityContext") }}
securityContext:
{{- . | nindent 8 }}
{{- end }}
{{- if not (kindIs "invalid" $w.terminationGracePeriodSeconds) }}
terminationGracePeriodSeconds: {{ int $w.terminationGracePeriodSeconds }}
{{- end }}
{{- with include "fluxer-web.pick" (dict "root" $ "w" $w "key" "nodeSelector") }}
nodeSelector:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-web.pick" (dict "root" $ "w" $w "key" "affinity") }}
affinity:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-web.pick" (dict "root" $ "w" $w "key" "tolerations") }}
tolerations:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-web.topologySpread" $ctx | trim }}
topologySpreadConstraints:
{{- . | nindent 8 }}
{{- end }}
containers:
- name: {{ $name }}
image: {{ include "fluxer-web.image" $ctx }}
imagePullPolicy: {{ ($w.image | default dict).pullPolicy | default ($v.image | default dict).pullPolicy | default "IfNotPresent" }}
{{- with include "fluxer-web.env" $ctx | trim }}
env:
{{- . | nindent 12 }}
{{- end }}
{{- with $envFrom }}
envFrom:
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- name: http
containerPort: 8080
protocol: TCP
{{- with $w.lifecycle }}
lifecycle:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- range $probe := list "startup" "liveness" "readiness" }}
{{- with hasKey $wProbes $probe | ternary (get $wProbes $probe) (get $gProbes $probe) }}
{{ $probe }}Probe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
{{- with $w.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with include "fluxer-web.pick" (dict "root" $ "w" $w "key" "securityContext") }}
securityContext:
{{- . | nindent 12 }}
{{- end }}
{{- with $w.extraVolumeMounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $w.extraVolumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-web.labels" $ctx | nindent 4 }}
spec:
type: ClusterIP
selector:
{{- include "fluxer-web.selectorLabels" $ctx | nindent 4 }}
ports:
- name: http
port: 8080
targetPort: http
protocol: TCP
{{- with $w.hpa }}
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-web.labels" $ctx | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ $name }}
minReplicas: {{ required (printf "%s.hpa.minReplicas is required" $name) .minReplicas }}
maxReplicas: {{ required (printf "%s.hpa.maxReplicas is required" $name) .maxReplicas }}
{{- with .targetCPUUtilizationPercentage }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ . }}
{{- end }}
{{- with .behavior }}
behavior:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
{{- with $w.pdb }}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $name }}-pdb
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-web.labels" $ctx | nindent 4 }}
spec:
{{- toYaml . | nindent 2 }}
selector:
matchLabels:
{{- include "fluxer-web.selectorLabels" $ctx | nindent 6 }}
{{- end }}
{{- end }}
{{- end }}
+83
View File
@@ -0,0 +1,83 @@
image:
registry: ghcr.io/fluxerapp
tag: v1
pullPolicy: IfNotPresent
imagePullSecrets: []
env: {}
extraEnv: []
envFrom:
- secretRef:
name: fluxer-env
podAnnotations: {}
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
probes:
startup:
httpGet:
path: /_health
port: http
periodSeconds: 10
failureThreshold: 30
liveness:
httpGet:
path: /_health
port: http
readiness:
httpGet:
path: /_health
port: http
strategy:
type: RollingUpdate
topologySpreadConstraints: []
nodeSelector: {}
tolerations: []
affinity: {}
workloads:
admin:
image:
name: fluxer-admin
replicas: 1
env:
FLUXER_ENV: production
FLUXER_API_ENDPOINT: https://api.example.com
FLUXER_ADMIN_ENDPOINT: https://admin.example.com
FLUXER_MEDIA_ENDPOINT: https://media.example.com
FLUXER_APP_ENDPOINT: https://web.example.com
resources:
requests:
cpu: 50m
memory: 96Mi
limits:
memory: 384Mi
app-proxy:
image:
name: fluxer-app-proxy-self-hosted
replicas: 1
env:
RELEASE_CHANNEL: stable
PUBLIC_BOOTSTRAP_API_ENDPOINT: /api
PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT: https://web.example.com/api
resources:
requests:
cpu: 50m
memory: 96Mi
limits:
memory: 384Mi
+5
View File
@@ -448,6 +448,11 @@ FLUXER_DISCOVERY_ENABLED=true
#FLUXER_SEAWEEDFS_GOMEMLIMIT=1536MiB
#FLUXER_SEAWEEDFS_TELEMETRY=false
# Volumes SeaweedFS creates at once when a bucket needs space. Each reserves 1 GB
# of free disk from the start, and SeaweedFS's own default of 7 fills a small
# disk before every bucket has one, so uploads fail with no free volumes left.
#FLUXER_SEAWEEDFS_VOLUME_GROWTH=1
# Node sizes its heap from the container limit by default. Leave these unset
# unless you need to pin it. A heap ceiling above the container limit gets the
# container OOM-killed instead of reporting a heap error. The values below are
+1
View File
@@ -353,6 +353,7 @@ services:
memory: ${FLUXER_SEAWEEDFS_MEMORY_LIMIT:-2gb}
environment:
GOMEMLIMIT: ${FLUXER_SEAWEEDFS_GOMEMLIMIT:-1536MiB}
WEED_MASTER_VOLUME_GROWTH_COPY_1: ${FLUXER_SEAWEEDFS_VOLUME_GROWTH:-1}
command: ["server", "-s3", "-dir=/data", "-master.telemetry=${FLUXER_SEAWEEDFS_TELEMETRY:-false}"]
volumes:
- seaweedfs-data:/data
+18
View File
@@ -40,6 +40,7 @@ fn generate_admin_api(manifest_dir: &Path, out_dir: &Path) {
adapt_progenitor_throttled_errors(&mut spec);
relax_guild_audit_log_schemas(&mut spec);
relax_progenitor_schema_strictness(&mut spec);
relax_integer_enums(&mut spec);
let mut settings = progenitor::GenerationSettings::new();
settings.with_interface(progenitor::InterfaceStyle::Positional);
@@ -174,6 +175,23 @@ fn relax_guild_audit_log_schemas(spec: &mut openapiv3::OpenAPI) {
}
}
const OPEN_INTEGER_ENUMS: &[&str] = &["ChannelType", "MessageType", "WebhookType"];
fn relax_integer_enums(spec: &mut openapiv3::OpenAPI) {
let components = spec.components.as_mut().expect("missing API components");
for name in OPEN_INTEGER_ENUMS {
let Some(openapiv3::ReferenceOr::Item(schema)) = components.schemas.get_mut(*name) else {
panic!("missing inline {name} schema");
};
let openapiv3::SchemaKind::Type(openapiv3::Type::Integer(integer)) =
&mut schema.schema_kind
else {
panic!("{name} must be an integer schema");
};
integer.enumeration.clear();
}
}
fn object_schema_mut<'a>(
components: &'a mut openapiv3::Components,
name: &str,
+167 -365
View File
@@ -1683,7 +1683,7 @@
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
}
},
"description": "Enqueue one background administrative job. The `task` discriminator selects both the body variant and the ACL evaluated for the request: `update_user_flags` needs bulk:update:user_flags, `update_suspicious_activity_flags` needs bulk:update:suspicious_activity, `update_guild_features` needs bulk:update:guild_features, `add_guild_members` needs bulk:add:guild_members, `schedule_user_deletion` needs bulk:delete:users, and `delete_user_messages` needs bulk:delete:user_messages. Returns a job_id immediately; observe progress at /admin/jobs/:job_id.",
"description": "Enqueue one background administrative job. The `task` discriminator selects both the body variant and the ACL evaluated for the request: `update_user_flags` needs bulk:update:user_flags, `update_guild_features` needs bulk:update:guild_features, `add_guild_members` needs bulk:add:guild_members, `schedule_user_deletion` needs bulk:delete:users, and `delete_user_messages` needs bulk:delete:user_messages. Returns a job_id immediately; observe progress at /admin/jobs/:job_id.",
"security": [{"adminApiKey": []}],
"requestBody": {
"required": true,
@@ -5435,7 +5435,7 @@
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
}
},
"description": "Queue a worker job that delivers the same content to every listed user as a direct message from the system account. Progress is observable through the Jobs admin resource (task_type=sendSystemDm), and an in-flight broadcast is stopped by cancelling that job. Requires SYSTEM_DM_SEND permission.",
"description": "Queue a worker job that delivers the same content to every listed user, or to every user when all_users is set, as a direct message from the system account. Progress is observable through the Jobs admin resource (task_type=sendSystemDm), and an in-flight broadcast is stopped by cancelling that job. Requires SYSTEM_DM_SEND permission.",
"security": [{"adminApiKey": []}],
"requestBody": {
"required": true,
@@ -7190,74 +7190,6 @@
]
}
},
"/admin/users/{user_id}/phone-verification": {
"put": {
"operationId": "update_admin_user_phone_verification",
"summary": "Update user phone verification flag",
"tags": ["Admin"],
"responses": {
"200": {
"description": "Success",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserMutationResponse"}}}
},
"400": {
"description": "Bad Request - The request was malformed or contained invalid data",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
},
"401": {
"description": "Unauthorized - Authentication is required or the token is invalid",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
},
"403": {
"description": "Forbidden - You do not have permission to perform this action",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
},
"429": {
"description": "Too Many Requests - You are being rate limited",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ThrottledError"}}},
"headers": {
"Retry-After": {
"description": "Number of seconds to wait before retrying (only on 429)",
"schema": {"type": "integer"}
},
"X-RateLimit-Limit": {
"description": "The number of requests that can be made in the current window",
"schema": {"type": "integer"}
},
"X-RateLimit-Remaining": {
"description": "The number of remaining requests that can be made",
"schema": {"type": "integer"}
},
"X-RateLimit-Reset": {
"description": "Unix timestamp when the rate limit resets",
"schema": {"type": "integer"}
}
}
},
"500": {
"description": "Internal Server Error - An unexpected error occurred",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
}
},
"description": "Set whether a user is treated as having completed phone verification. This is the only supported path for clearing the irreversible user-facing phone verification flag. Requires USER_UPDATE_PHONE permission.",
"security": [{"adminApiKey": []}],
"parameters": [
{
"name": "user_id",
"in": "path",
"required": true,
"schema": {"description": "The ID of the user", "allOf": [{"$ref": "#/components/schemas/SnowflakeType"}]},
"description": "The ID of the user"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {"schema": {"$ref": "#/components/schemas/AdminUserPhoneVerificationRequest"}}
}
}
}
},
"/admin/users/{user_id}/premium-flags": {
"patch": {
"operationId": "update_admin_user_premium_flags",
@@ -7851,142 +7783,6 @@
]
}
},
"/admin/users/{user_id}/suspicious-activity-disablement": {
"put": {
"operationId": "disable_admin_user_suspicious",
"summary": "Disable user for suspicious activity",
"tags": ["Admin"],
"responses": {
"200": {
"description": "Success",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserMutationResponse"}}}
},
"400": {
"description": "Bad Request - The request was malformed or contained invalid data",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
},
"401": {
"description": "Unauthorized - Authentication is required or the token is invalid",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
},
"403": {
"description": "Forbidden - You do not have permission to perform this action",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
},
"429": {
"description": "Too Many Requests - You are being rate limited",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ThrottledError"}}},
"headers": {
"Retry-After": {
"description": "Number of seconds to wait before retrying (only on 429)",
"schema": {"type": "integer"}
},
"X-RateLimit-Limit": {
"description": "The number of requests that can be made in the current window",
"schema": {"type": "integer"}
},
"X-RateLimit-Remaining": {
"description": "The number of remaining requests that can be made",
"schema": {"type": "integer"}
},
"X-RateLimit-Reset": {
"description": "Unix timestamp when the rate limit resets",
"schema": {"type": "integer"}
}
}
},
"500": {
"description": "Internal Server Error - An unexpected error occurred",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
}
},
"description": "Disable user account due to suspicious activity or abuse. Account is locked pending review. User cannot access services. Emails the user unless notify_user is false. Creates audit log entry. Requires USER_DISABLE_SUSPICIOUS permission.",
"security": [{"adminApiKey": []}],
"parameters": [
{
"name": "user_id",
"in": "path",
"required": true,
"schema": {"description": "The ID of the user", "allOf": [{"$ref": "#/components/schemas/SnowflakeType"}]},
"description": "The ID of the user"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {"schema": {"$ref": "#/components/schemas/AdminUserSuspiciousDisableRequest"}}
}
}
}
},
"/admin/users/{user_id}/suspicious-activity-flags": {
"put": {
"operationId": "update_admin_user_suspicious_activity_flags",
"summary": "Update suspicious activity flags",
"tags": ["Admin"],
"responses": {
"200": {
"description": "Success",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserMutationResponse"}}}
},
"400": {
"description": "Bad Request - The request was malformed or contained invalid data",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
},
"401": {
"description": "Unauthorized - Authentication is required or the token is invalid",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
},
"403": {
"description": "Forbidden - You do not have permission to perform this action",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
},
"429": {
"description": "Too Many Requests - You are being rate limited",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ThrottledError"}}},
"headers": {
"Retry-After": {
"description": "Number of seconds to wait before retrying (only on 429)",
"schema": {"type": "integer"}
},
"X-RateLimit-Limit": {
"description": "The number of requests that can be made in the current window",
"schema": {"type": "integer"}
},
"X-RateLimit-Remaining": {
"description": "The number of remaining requests that can be made",
"schema": {"type": "integer"}
},
"X-RateLimit-Reset": {
"description": "Unix timestamp when the rate limit resets",
"schema": {"type": "integer"}
}
}
},
"500": {
"description": "Internal Server Error - An unexpected error occurred",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
}
},
"description": "Flag user as suspicious for account abuse, fraud, or policy violations. Enables enforcement actions and rate limiting. Creates audit log entry. Requires USER_UPDATE_SUSPICIOUS_ACTIVITY permission.",
"security": [{"adminApiKey": []}],
"parameters": [
{
"name": "user_id",
"in": "path",
"required": true,
"schema": {"description": "The ID of the user", "allOf": [{"$ref": "#/components/schemas/SnowflakeType"}]},
"description": "The ID of the user"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {"schema": {"$ref": "#/components/schemas/AdminUserSuspiciousActivityFlagsRequest"}}
}
}
}
},
"/admin/users/{user_id}/system-status": {
"put": {
"operationId": "set_admin_user_system_status",
@@ -9506,31 +9302,6 @@
},
"required": ["system"]
},
"AdminUserSuspiciousActivityFlagsRequest": {
"type": "object",
"properties": {
"flags": {
"description": "Bitmask of suspicious activity flags",
"allOf": [{"$ref": "#/components/schemas/SuspiciousActivityFlags"}]
}
},
"required": ["flags"]
},
"AdminUserSuspiciousDisableRequest": {
"type": "object",
"properties": {
"flags": {
"description": "Bitmask of suspicious activity flags that triggered the disable",
"allOf": [{"$ref": "#/components/schemas/SuspiciousActivityFlags"}]
},
"notify_user": {
"default": true,
"description": "Whether to email the user that the account was disabled",
"type": "boolean"
}
},
"required": ["flags"]
},
"AdminStorePurchaseListResponse": {
"type": "object",
"properties": {
@@ -9685,16 +9456,6 @@
}
}
},
"AdminUserPhoneVerificationRequest": {
"type": "object",
"properties": {
"has_verified_phone": {
"type": "boolean",
"description": "Whether the user should be treated as having completed phone verification"
}
},
"required": ["has_verified_phone"]
},
"DeleteAllUserMessagesResponse": {
"type": "object",
"properties": {
@@ -10110,7 +9871,7 @@
"type": "object",
"properties": {
"acls": {
"maxItems": 108,
"maxItems": 104,
"type": "array",
"items": {"$ref": "#/components/schemas/AdminAclType"},
"description": "List of access control permissions to assign"
@@ -10150,20 +9911,25 @@
"description": "Message content to send to each recipient"
},
"user_ids": {
"description": "Recipient user IDs. Each receives the same content as a system DM.",
"minItems": 1,
"maxItems": 10000,
"type": "array",
"items": {"$ref": "#/components/schemas/SnowflakeType"},
"description": "Recipient user IDs. Each receives the same content as a system DM."
"items": {"$ref": "#/components/schemas/SnowflakeType"}
},
"all_users": {
"description": "Send to every user account, skipping bots, system accounts, and deleted or disabled accounts",
"type": "boolean"
}
},
"required": ["content", "user_ids"]
"required": ["content"]
},
"SendSystemDmResponse": {
"type": "object",
"properties": {
"recipient_count": {
"description": "Number of recipients the worker job was queued to deliver to",
"nullable": true,
"description": "Number of recipients the worker job was queued to deliver to, or null when sending to all users",
"allOf": [{"$ref": "#/components/schemas/Int32Type"}]
}
},
@@ -10804,6 +10570,7 @@
"gateway_rollout": {"$ref": "#/components/schemas/GatewayRolloutConfigResponse"},
"push_relay": {"$ref": "#/components/schemas/PushRelayConfigResponse"},
"domain_migration": {"$ref": "#/components/schemas/DomainMigrationConfigResponse"},
"plutonium_page": {"$ref": "#/components/schemas/PlutoniumPageConfigResponse"},
"captcha": {"$ref": "#/components/schemas/CaptchaConfigResponse"},
"experiment_delivery": {"$ref": "#/components/schemas/ExperimentDeliveryConfigResponse"},
"registration": {
@@ -11204,6 +10971,7 @@
"gateway_rollout",
"push_relay",
"domain_migration",
"plutonium_page",
"captcha",
"experiment_delivery",
"registration",
@@ -11341,6 +11109,10 @@
"nullable": true,
"allOf": [{"$ref": "#/components/schemas/DomainMigrationConfigUpdateRequest"}]
},
"plutonium_page": {
"nullable": true,
"allOf": [{"$ref": "#/components/schemas/PlutoniumPageConfigUpdateRequest"}]
},
"captcha": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/CaptchaConfigUpdateRequest"}]},
"experiment_delivery": {
"nullable": true,
@@ -12048,6 +11820,9 @@
"properties": {
"status": {"type": "string", "minLength": 1, "maxLength": 256},
"sessions": {"$ref": "#/components/schemas/Int32Type"},
"session_resumes_total": {"type": "integer", "minimum": 0, "maximum": 9007199254740991},
"websocket_dispatches_total": {"type": "integer", "minimum": 0, "maximum": 9007199254740991},
"websocket_dispatch_drops_total": {"type": "integer", "minimum": 0, "maximum": 9007199254740991},
"guilds": {"$ref": "#/components/schemas/Int32Type"},
"presences": {"$ref": "#/components/schemas/Int32Type"},
"calls": {"$ref": "#/components/schemas/Int32Type"},
@@ -12074,6 +11849,24 @@
"node_id": {"type": "string", "minLength": 1, "maxLength": 256},
"status": {"type": "string", "minLength": 1, "maxLength": 256},
"sessions": {"$ref": "#/components/schemas/Int32Type"},
"session_resumes_total": {
"nullable": true,
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"websocket_dispatches_total": {
"nullable": true,
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"websocket_dispatch_drops_total": {
"nullable": true,
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"guilds": {"$ref": "#/components/schemas/Int32Type"},
"presences": {"$ref": "#/components/schemas/Int32Type"},
"calls": {"$ref": "#/components/schemas/Int32Type"},
@@ -12127,6 +11920,9 @@
"node_id",
"status",
"sessions",
"session_resumes_total",
"websocket_dispatches_total",
"websocket_dispatch_drops_total",
"guilds",
"presences",
"calls",
@@ -12142,6 +11938,9 @@
"required": [
"status",
"sessions",
"session_resumes_total",
"websocket_dispatches_total",
"websocket_dispatch_drops_total",
"guilds",
"presences",
"calls",
@@ -12398,37 +12197,6 @@
},
"required": ["user_ids", "task"]
},
{
"type": "object",
"properties": {
"user_ids": {
"maxItems": 1000,
"type": "array",
"items": {"$ref": "#/components/schemas/SnowflakeType"},
"description": "List of user IDs to update"
},
"add_flags": {
"default": [],
"description": "Suspicious activity flag names to add to all specified users",
"maxItems": 32,
"type": "array",
"items": {"type": "string"}
},
"remove_flags": {
"default": [],
"description": "Suspicious activity flag names to remove from all specified users",
"maxItems": 32,
"type": "array",
"items": {"type": "string"}
},
"task": {
"type": "string",
"enum": ["update_suspicious_activity_flags"],
"description": "Adds and removes verification requirements on every targeted user"
}
},
"required": ["user_ids", "task"]
},
{
"type": "object",
"properties": {
@@ -12961,7 +12729,7 @@
},
"acls": {
"description": "Replacement list of access control permissions for the key",
"maxItems": 108,
"maxItems": 104,
"type": "array",
"items": {"$ref": "#/components/schemas/AdminAclType"}
}
@@ -12979,7 +12747,7 @@
"type": "string"
},
"acls": {
"maxItems": 108,
"maxItems": 104,
"type": "array",
"items": {"type": "string"},
"description": "List of access control permissions for the key"
@@ -13009,7 +12777,7 @@
"maximum": 365
},
"acls": {
"maxItems": 108,
"maxItems": 104,
"type": "array",
"items": {"$ref": "#/components/schemas/AdminAclType"},
"description": "List of access control permissions for the key"
@@ -13030,7 +12798,7 @@
"type": "string"
},
"acls": {
"maxItems": 108,
"maxItems": 104,
"type": "array",
"items": {"type": "string"},
"description": "List of access control permissions for the key"
@@ -13043,7 +12811,7 @@
"type": "object",
"properties": {
"acls": {
"maxItems": 108,
"maxItems": 104,
"type": "array",
"items": {"type": "string", "minLength": 1, "maxLength": 64},
"description": "Every admin access control permission the admin API recognises"
@@ -13097,7 +12865,6 @@
"bulk:delete:users",
"bulk:delete:user_messages",
"bulk:update:guild_features",
"bulk:update:suspicious_activity",
"bulk:update:user_flags",
"csam:submit_ncmec",
"discovery:remove",
@@ -13133,7 +12900,6 @@
"system_dm:send",
"user:cancel:bulk_message_deletion",
"user:delete",
"user:disable:suspicious",
"user:list:dm_channels",
"user:list:guilds",
"user:list:relationships",
@@ -13150,9 +12916,7 @@
"user:update:email",
"user:update:flags",
"user:update:mfa",
"user:update:phone",
"user:update:profile",
"user:update:suspicious_activity",
"user:update:traits",
"user:update:username",
"voice:region:create",
@@ -13282,7 +13046,7 @@
"ChannelType": {
"description": "The type of the channel",
"type": "integer",
"enum": [0, 1, 2, 3, 4, 998, 999],
"enum": [0, 1, 2, 3, 4, 5, 998, 999],
"format": "int32",
"x-enumNames": [
"GUILD_TEXT",
@@ -13290,6 +13054,7 @@
"GUILD_VOICE",
"GROUP_DM",
"GUILD_CATEGORY",
"GUILD_ANNOUNCEMENT",
"GUILD_LINK",
"DM_PERSONAL_NOTES"
],
@@ -13299,6 +13064,7 @@
"A voice channel within a guild",
"A group direct message between users",
"A category that contains channels",
"A guild channel whose messages can be published to channels that follow it",
"A link channel for external resources",
"Personal notes DM channel"
]
@@ -13387,7 +13153,10 @@
"BanEmailRequest": {
"type": "object",
"properties": {
"email": {"description": "Email address to ban", "allOf": [{"$ref": "#/components/schemas/EmailType"}]}
"email": {
"description": "Email address to ban, or a domain written as @example.com to ban every address at it and its subdomains",
"allOf": [{"$ref": "#/components/schemas/EmailBlocklistEntryType"}]
}
},
"required": ["email"]
},
@@ -13396,7 +13165,7 @@
"properties": {"ip": {"description": "IPv4/IPv6 address or CIDR range to ban", "type": "string"}},
"required": ["ip"]
},
"EmailType": {"type": "string"},
"EmailBlocklistEntryType": {"type": "string"},
"CheckAvatarHashRequest": {
"type": "object",
"properties": {"hashes": {"minItems": 1, "maxItems": 1000, "type": "array", "items": {"type": "string"}}},
@@ -13497,7 +13266,7 @@
"enum": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]
},
"GuildFeatureSchema": {
"description": "A guild feature flag Known values: ANIMATED_ICON, ANIMATED_BANNER, AUDIO_BITRATE_128_KBPS, AUDIO_BITRATE_256_KBPS, AUDIO_BITRATE_384_KBPS, BANNER, CLONE_EMOJI_DISABLED, CLONE_EMOJI_ENABLED, CLONE_STICKER_DISABLED, CLONE_STICKER_ENABLED, DETACHED_BANNER, INVITE_SPLASH, INVITES_DISABLED, RAID_DETECTED, TEXT_CHANNEL_FLEXIBLE_NAMES, HIDE_OWNER_CROWN, MORE_EMOJI, MORE_STICKERS, UNLIMITED_EMOJI, UNLIMITED_STICKERS, EXPRESSION_PURGE_ALLOWED, VANITY_URL, DISCOVERABLE, PARTNERED, VERIFIED, VIP_VOICE, VOICE_E2EE, UNAVAILABLE_FOR_EVERYONE, UNAVAILABLE_FOR_EVERYONE_BUT_STAFF, UNAVAILABLE_HIDDEN, VISIONARY, LARGE_GUILD_OVERRIDE, VERY_LARGE_GUILD (other values allowed)",
"description": "A guild feature flag Known values: ANIMATED_ICON, ANIMATED_BANNER, AUDIO_BITRATE_128_KBPS, AUDIO_BITRATE_256_KBPS, AUDIO_BITRATE_384_KBPS, BANNER, CLONE_EMOJI_DISABLED, CLONE_EMOJI_ENABLED, CLONE_STICKER_DISABLED, CLONE_STICKER_ENABLED, DETACHED_BANNER, INVITE_SPLASH, INVITES_DISABLED, RAID_DETECTED, TEXT_CHANNEL_FLEXIBLE_NAMES, HIDE_OWNER_CROWN, MORE_EMOJI, MORE_STICKERS, UNLIMITED_EMOJI, UNLIMITED_STICKERS, EXPRESSION_PURGE_ALLOWED, VANITY_URL, DISCOVERABLE, PARTNERED, VERIFIED, VIP_VOICE, VOICE_E2EE, UNAVAILABLE_FOR_EVERYONE, UNAVAILABLE_FOR_EVERYONE_BUT_STAFF, UNAVAILABLE_HIDDEN, VISIONARY, LARGE_GUILD_OVERRIDE, VERY_LARGE_GUILD, ANNOUNCEMENT_CHANNELS_DISABLED (other values allowed)",
"x-enumNames": [
"ANIMATED_ICON",
"ANIMATED_BANNER",
@@ -13531,7 +13300,8 @@
"UNAVAILABLE_HIDDEN",
"VISIONARY",
"LARGE_GUILD_OVERRIDE",
"VERY_LARGE_GUILD"
"VERY_LARGE_GUILD",
"ANNOUNCEMENT_CHANNELS_DISABLED"
],
"x-enumDescriptions": [
"Guild can have an animated icon",
@@ -13566,7 +13336,8 @@
"Guild is hidden when it is force unavailable",
"Guild is a visionary guild",
"Guild has large guild overrides enabled",
"Guild has increased member capacity enabled"
"Guild has increased member capacity enabled",
"Guild cannot publish announcement messages or gain new followers"
],
"type": "string"
},
@@ -13592,11 +13363,6 @@
},
{"name": "SPAMMER", "value": "64", "description": "User is flagged as a spammer"},
{"name": "DELETED", "value": "17179869184", "description": "User account has been deleted"},
{
"name": "DISABLED_SUSPICIOUS_ACTIVITY",
"value": "34359738368",
"description": "User account disabled due to suspicious activity"
},
{"name": "SELF_DELETED", "value": "68719476736", "description": "User account was self-deleted"},
{"name": "DISABLED", "value": "274877906944", "description": "User account is disabled"},
{"name": "HAS_SESSION_STARTED", "value": "549755813888", "description": "User has started a session"},
@@ -13607,6 +13373,7 @@
"value": "562949953421312",
"description": "User is verified as not underage"
},
{"name": "ACCOUNT_LIMITED", "value": "1125899906842624", "description": "User account is limited"},
{
"name": "HAS_DISMISSED_PREMIUM_ONBOARDING",
"value": "2251799813685248",
@@ -13624,14 +13391,9 @@
"description": "User has verified their age as an adult via credit card verification"
},
{
"name": "FORCE_INBOUND_PHONE_VERIFICATION",
"value": "2305843009213693952",
"description": "User is forced through inbound phone verification, for debugging"
},
{
"name": "NOT_SUSPICIOUS",
"name": "LIMIT_EXEMPT",
"value": "4611686018427387904",
"description": "User is permanently exempt from automatic suspicious-activity flagging"
"description": "User is permanently exempt from account limitation"
}
]
},
@@ -13733,7 +13495,8 @@
"allOf": [{"$ref": "#/components/schemas/SnowflakeStringType"}]
},
"message_id": {
"description": "The ID of the referenced message",
"description": "The ID of the referenced message, absent on a channel follow system message",
"nullable": true,
"allOf": [{"$ref": "#/components/schemas/SnowflakeStringType"}]
},
"guild_id": {
@@ -13743,7 +13506,7 @@
},
"type": {"allOf": [{"$ref": "#/components/schemas/MessageReferenceType"}]}
},
"required": ["channel_id", "message_id", "type"],
"required": ["channel_id", "type"],
"additionalProperties": false
},
"message_snapshots": {
@@ -13878,7 +13641,8 @@
"allOf": [{"$ref": "#/components/schemas/SnowflakeStringType"}]
},
"message_id": {
"description": "The ID of the referenced message",
"description": "The ID of the referenced message, absent on a channel follow system message",
"nullable": true,
"allOf": [{"$ref": "#/components/schemas/SnowflakeStringType"}]
},
"guild_id": {
@@ -13888,7 +13652,7 @@
},
"type": {"allOf": [{"$ref": "#/components/schemas/MessageReferenceType"}]}
},
"required": ["channel_id", "message_id", "type"],
"required": ["channel_id", "type"],
"additionalProperties": false
},
"message_snapshots": {
@@ -14379,11 +14143,26 @@
"description": "The bitwise flags of the original message",
"format": "int32",
"x-bitflagValues": [
{
"name": "CROSSPOSTED",
"value": "1",
"description": "This message has been published to channels that follow this announcement channel"
},
{
"name": "IS_CROSSPOST",
"value": "2",
"description": "This message was delivered from an announcement channel this channel follows"
},
{
"name": "SUPPRESS_EMBEDS",
"value": "4",
"description": "Do not include embeds when serialising this message"
},
{
"name": "SOURCE_MESSAGE_DELETED",
"value": "8",
"description": "The published message this copy came from has been deleted"
},
{
"name": "SUPPRESS_NOTIFICATIONS",
"value": "4096",
@@ -14395,7 +14174,7 @@
"MessageType": {
"description": "The type of message",
"type": "integer",
"enum": [0, 1, 2, 3, 4, 5, 6, 7, 19],
"enum": [0, 1, 2, 3, 4, 5, 6, 7, 12, 19],
"format": "int32",
"x-enumNames": [
"DEFAULT",
@@ -14406,6 +14185,7 @@
"CHANNEL_ICON_CHANGE",
"CHANNEL_PINNED_MESSAGE",
"USER_JOIN",
"CHANNEL_FOLLOW_ADD",
"REPLY"
],
"x-enumDescriptions": [
@@ -14417,6 +14197,7 @@
"A system message indicating the channel icon changed",
"A system message indicating a message was pinned",
"A system message indicating a user joined",
"System message posted when a channel starts following an announcement channel",
"A reply message"
]
},
@@ -14955,15 +14736,14 @@
"GuildVerificationLevel": {
"description": "Required verification level for members",
"type": "integer",
"enum": [0, 1, 2, 3, 4],
"enum": [0, 1, 2, 3],
"format": "int32",
"x-enumNames": ["NONE", "LOW", "MEDIUM", "HIGH", "VERY_HIGH"],
"x-enumNames": ["NONE", "LOW", "MEDIUM", "HIGH"],
"x-enumDescriptions": [
"Unrestricted",
"Must have verified email",
"Registered for more than 5 minutes",
"Member of the server for more than 10 minutes",
"Must have a verified phone number"
"Member of the server for more than 10 minutes"
]
},
"GuildOperations": {
@@ -15045,15 +14825,14 @@
"minimum": 0,
"maximum": 2147483647,
"format": "int32",
"x-enumNames": ["NONE", "LOW", "MEDIUM", "HIGH", "VERY_HIGH"],
"x-enumNames": ["NONE", "LOW", "MEDIUM", "HIGH"],
"x-enumDescriptions": [
"Unrestricted",
"Must have verified email",
"Registered for more than 5 minutes",
"Member of the server for more than 10 minutes",
"Must have a verified phone number"
"Member of the server for more than 10 minutes"
],
"enum": [0, 1, 2, 3, 4]
"enum": [0, 1, 2, 3]
},
"PurgeGuildAssetErrorSchema": {
"type": "object",
@@ -15423,6 +15202,30 @@
"max_counter": {"type": "integer", "minimum": 100, "maximum": 20000}
}
},
"PlutoniumPageConfigUpdateRequest": {
"type": "object",
"properties": {
"enabled": {"type": "boolean"},
"rollout_basis_points": {"type": "integer", "minimum": 0, "maximum": 10000},
"rollout_salt": {"type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[\\x20-\\x7e]+$"},
"included_user_ids": {
"maxItems": 1000,
"type": "array",
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
},
"included_guild_ids": {
"maxItems": 1000,
"type": "array",
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
},
"include_premium_users": {"type": "boolean"},
"excluded_user_ids": {
"maxItems": 1000,
"type": "array",
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
}
}
},
"DomainMigrationConfigUpdateRequest": {
"type": "object",
"properties": {
@@ -15562,6 +15365,51 @@
"required": ["enabled", "cost", "max_counter"],
"additionalProperties": false
},
"PlutoniumPageConfigResponse": {
"type": "object",
"properties": {
"enabled": {"default": false, "type": "boolean"},
"config_version": {"default": 0, "type": "integer", "minimum": 0, "maximum": 9007199254740991},
"rollout_basis_points": {"default": 0, "type": "integer", "minimum": 0, "maximum": 10000},
"rollout_salt": {
"default": "plutonium-page-v1",
"type": "string",
"minLength": 1,
"maxLength": 64,
"pattern": "^[\\x20-\\x7e]+$"
},
"included_user_ids": {
"default": [],
"maxItems": 1000,
"type": "array",
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
},
"included_guild_ids": {
"default": [],
"maxItems": 1000,
"type": "array",
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
},
"include_premium_users": {"default": false, "type": "boolean"},
"excluded_user_ids": {
"default": [],
"maxItems": 1000,
"type": "array",
"items": {"type": "string", "pattern": "^\\d{1,20}$"}
}
},
"required": [
"enabled",
"config_version",
"rollout_basis_points",
"rollout_salt",
"included_user_ids",
"included_guild_ids",
"include_premium_users",
"excluded_user_ids"
],
"additionalProperties": false
},
"DomainMigrationConfigResponse": {
"type": "object",
"properties": {
@@ -15754,7 +15602,6 @@
"email": {"nullable": true, "type": "string"},
"email_verified": {"type": "boolean"},
"email_bounced": {"type": "boolean"},
"has_verified_phone": {"type": "boolean"},
"date_of_birth": {"nullable": true, "type": "string"},
"locale": {"nullable": true, "type": "string"},
"premium_type": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/Int32Type"}]},
@@ -15762,11 +15609,6 @@
"premium_until": {"nullable": true, "type": "string"},
"premium_grace_ends_at": {"nullable": true, "type": "string"},
"premium_lifetime_sequence": {"nullable": true, "allOf": [{"$ref": "#/components/schemas/Int32Type"}]},
"suspicious_activity_flags": {"allOf": [{"$ref": "#/components/schemas/SuspiciousActivityFlags"}]},
"phone_verification_deferred": {
"type": "boolean",
"description": "Whether a stored phone requirement is deferred and not enforced"
},
"temp_banned_until": {"nullable": true, "type": "string"},
"pending_deletion_at": {"nullable": true, "type": "string"},
"pending_bulk_message_deletion_at": {"nullable": true, "type": "string"},
@@ -15787,7 +15629,7 @@
"description": "ISO 8601 timestamp when the pending deletion was scheduled",
"type": "string"
},
"acls": {"maxItems": 108, "type": "array", "items": {"type": "string"}},
"acls": {"maxItems": 104, "type": "array", "items": {"type": "string"}},
"traits": {"maxItems": 100, "type": "array", "items": {"type": "string"}},
"has_totp": {"type": "boolean"},
"authenticator_types": {"maxItems": 10, "type": "array", "items": {"$ref": "#/components/schemas/Int32Type"}},
@@ -15813,7 +15655,6 @@
"email",
"email_verified",
"email_bounced",
"has_verified_phone",
"date_of_birth",
"locale",
"premium_type",
@@ -15821,8 +15662,6 @@
"premium_until",
"premium_grace_ends_at",
"premium_lifetime_sequence",
"suspicious_activity_flags",
"phone_verification_deferred",
"temp_banned_until",
"pending_deletion_at",
"pending_bulk_message_deletion_at",
@@ -15842,44 +15681,6 @@
],
"additionalProperties": false
},
"SuspiciousActivityFlags": {
"type": "integer",
"minimum": 0,
"maximum": 2147483647,
"description": "Suspicious activity indicators",
"format": "int32",
"x-bitflagValues": [
{"name": "REQUIRE_VERIFIED_EMAIL", "value": "1", "description": "Requires verified email address"},
{"name": "REQUIRE_REVERIFIED_EMAIL", "value": "2", "description": "Requires re-verified email address"},
{"name": "REQUIRE_VERIFIED_PHONE", "value": "4", "description": "Requires verified phone number"},
{"name": "REQUIRE_REVERIFIED_PHONE", "value": "8", "description": "Requires re-verified phone number"},
{
"name": "REQUIRE_VERIFIED_EMAIL_OR_VERIFIED_PHONE",
"value": "16",
"description": "Requires verified email or verified phone"
},
{
"name": "REQUIRE_REVERIFIED_EMAIL_OR_VERIFIED_PHONE",
"value": "32",
"description": "Requires re-verified email or re-verified phone"
},
{
"name": "REQUIRE_VERIFIED_EMAIL_OR_REVERIFIED_PHONE",
"value": "64",
"description": "Requires verified email or re-verified phone"
},
{
"name": "REQUIRE_REVERIFIED_EMAIL_OR_REVERIFIED_PHONE",
"value": "128",
"description": "Requires re-verified email or re-verified phone"
},
{
"name": "REQUIRE_INBOUND_PHONE_VERIFICATION",
"value": "256",
"description": "Requires inbound SMS verification (user must text code to platform number)"
}
]
},
"PremiumFlags": {
"type": "integer",
"minimum": 0,
@@ -15906,6 +15707,7 @@
{"name": "PERKS_DISABLED", "value": "256", "description": "User has temporarily disabled premium perks"}
]
},
"EmailType": {"type": "string"},
"AdminRelationshipEntrySchema": {
"type": "object",
"properties": {
-8
View File
@@ -42,7 +42,6 @@ pub const BULK_ADD_GUILD_MEMBERS: &str = "bulk:add:guild_members";
pub const BULK_DELETE_USERS: &str = "bulk:delete:users";
pub const BULK_DELETE_USER_MESSAGES: &str = "bulk:delete:user_messages";
pub const BULK_UPDATE_GUILD_FEATURES: &str = "bulk:update:guild_features";
pub const BULK_UPDATE_SUSPICIOUS_ACTIVITY: &str = "bulk:update:suspicious_activity";
pub const BULK_UPDATE_USER_FLAGS: &str = "bulk:update:user_flags";
pub const CSAM_SUBMIT_NCMEC: &str = "csam:submit_ncmec";
pub const DISCOVERY_REMOVE: &str = "discovery:remove";
@@ -78,7 +77,6 @@ pub const REPORT_VIEW_REPORTER_PII: &str = "report:view:reporter_pii";
pub const SYSTEM_DM_SEND: &str = "system_dm:send";
pub const USER_CANCEL_BULK_MESSAGE_DELETION: &str = "user:cancel:bulk_message_deletion";
pub const USER_DELETE: &str = "user:delete";
pub const USER_DISABLE_SUSPICIOUS: &str = "user:disable:suspicious";
pub const USER_LIST_DM_CHANNELS: &str = "user:list:dm_channels";
pub const USER_LIST_GUILDS: &str = "user:list:guilds";
pub const USER_LIST_RELATIONSHIPS: &str = "user:list:relationships";
@@ -95,9 +93,7 @@ pub const USER_UPDATE_DOB: &str = "user:update:dob";
pub const USER_UPDATE_EMAIL: &str = "user:update:email";
pub const USER_UPDATE_FLAGS: &str = "user:update:flags";
pub const USER_UPDATE_MFA: &str = "user:update:mfa";
pub const USER_UPDATE_PHONE: &str = "user:update:phone";
pub const USER_UPDATE_PROFILE: &str = "user:update:profile";
pub const USER_UPDATE_SUSPICIOUS_ACTIVITY: &str = "user:update:suspicious_activity";
pub const USER_UPDATE_TRAITS: &str = "user:update:traits";
pub const USER_UPDATE_USERNAME: &str = "user:update:username";
pub const VOICE_REGION_CREATE: &str = "voice:region:create";
@@ -151,7 +147,6 @@ pub const ALL_ACLS: &[&str] = &[
BULK_DELETE_USERS,
BULK_DELETE_USER_MESSAGES,
BULK_UPDATE_GUILD_FEATURES,
BULK_UPDATE_SUSPICIOUS_ACTIVITY,
BULK_UPDATE_USER_FLAGS,
CSAM_SUBMIT_NCMEC,
DISCOVERY_REMOVE,
@@ -187,7 +182,6 @@ pub const ALL_ACLS: &[&str] = &[
SYSTEM_DM_SEND,
USER_CANCEL_BULK_MESSAGE_DELETION,
USER_DELETE,
USER_DISABLE_SUSPICIOUS,
USER_LIST_DM_CHANNELS,
USER_LIST_GUILDS,
USER_LIST_RELATIONSHIPS,
@@ -204,9 +198,7 @@ pub const ALL_ACLS: &[&str] = &[
USER_UPDATE_EMAIL,
USER_UPDATE_FLAGS,
USER_UPDATE_MFA,
USER_UPDATE_PHONE,
USER_UPDATE_PROFILE,
USER_UPDATE_SUSPICIOUS_ACTIVITY,
USER_UPDATE_TRAITS,
USER_UPDATE_USERNAME,
VOICE_REGION_CREATE,
+8 -52
View File
@@ -19,19 +19,18 @@ pub mod user_flag_bits {
pub const SPAMMER: u64 = 1 << 6;
pub const HIGH_GLOBAL_RATE_LIMIT: u64 = 1 << 33;
pub const DELETED: u64 = 1 << 34;
pub const DISABLED_SUSPICIOUS_ACTIVITY: u64 = 1 << 35;
pub const SELF_DELETED: u64 = 1 << 36;
pub const DISABLED: u64 = 1 << 38;
pub const HAS_SESSION_STARTED: u64 = 1 << 39;
pub const RATE_LIMIT_BYPASS: u64 = 1 << 47;
pub const REPORT_BANNED: u64 = 1 << 48;
pub const VERIFIED_NOT_UNDERAGE: u64 = 1 << 49;
pub const ACCOUNT_LIMITED: u64 = 1 << 50;
pub const HAS_DISMISSED_PREMIUM_ONBOARDING: u64 = 1 << 51;
pub const APP_STORE_REVIEWER: u64 = 1 << 53;
pub const STAFF_HIDDEN: u64 = 1 << 57;
pub const AGE_VERIFIED_ADULT: u64 = 1 << 60;
pub const FORCE_INBOUND_PHONE_VERIFICATION: u64 = 1 << 61;
pub const NOT_SUSPICIOUS: u64 = 1 << 62;
pub const LIMIT_EXEMPT: u64 = 1 << 62;
}
pub const USER_FLAGS: &[U64Flag] = &[
@@ -67,10 +66,6 @@ pub const USER_FLAGS: &[U64Flag] = &[
name: "DELETED",
value: user_flag_bits::DELETED,
},
U64Flag {
name: "DISABLED_SUSPICIOUS_ACTIVITY",
value: user_flag_bits::DISABLED_SUSPICIOUS_ACTIVITY,
},
U64Flag {
name: "SELF_DELETED",
value: user_flag_bits::SELF_DELETED,
@@ -95,6 +90,10 @@ pub const USER_FLAGS: &[U64Flag] = &[
name: "VERIFIED_NOT_UNDERAGE",
value: user_flag_bits::VERIFIED_NOT_UNDERAGE,
},
U64Flag {
name: "ACCOUNT_LIMITED",
value: user_flag_bits::ACCOUNT_LIMITED,
},
U64Flag {
name: "HAS_DISMISSED_PREMIUM_ONBOARDING",
value: user_flag_bits::HAS_DISMISSED_PREMIUM_ONBOARDING,
@@ -112,12 +111,8 @@ pub const USER_FLAGS: &[U64Flag] = &[
value: user_flag_bits::AGE_VERIFIED_ADULT,
},
U64Flag {
name: "FORCE_INBOUND_PHONE_VERIFICATION",
value: user_flag_bits::FORCE_INBOUND_PHONE_VERIFICATION,
},
U64Flag {
name: "NOT_SUSPICIOUS",
value: user_flag_bits::NOT_SUSPICIOUS,
name: "LIMIT_EXEMPT",
value: user_flag_bits::LIMIT_EXEMPT,
},
];
@@ -159,42 +154,3 @@ pub const PREMIUM_FLAGS: &[I32Flag] = &[
value: 1 << 8,
},
];
pub const SUSPICIOUS_ACTIVITY_FLAGS: &[I32Flag] = &[
I32Flag {
name: "REQUIRE_VERIFIED_EMAIL",
value: 1 << 0,
},
I32Flag {
name: "REQUIRE_REVERIFIED_EMAIL",
value: 1 << 1,
},
I32Flag {
name: "REQUIRE_VERIFIED_PHONE",
value: 1 << 2,
},
I32Flag {
name: "REQUIRE_REVERIFIED_PHONE",
value: 1 << 3,
},
I32Flag {
name: "REQUIRE_VERIFIED_EMAIL_OR_VERIFIED_PHONE",
value: 1 << 4,
},
I32Flag {
name: "REQUIRE_REVERIFIED_EMAIL_OR_VERIFIED_PHONE",
value: 1 << 5,
},
I32Flag {
name: "REQUIRE_VERIFIED_EMAIL_OR_REVERIFIED_PHONE",
value: 1 << 6,
},
I32Flag {
name: "REQUIRE_REVERIFIED_EMAIL_OR_REVERIFIED_PHONE",
value: 1 << 7,
},
I32Flag {
name: "REQUIRE_INBOUND_PHONE_VERIFICATION",
value: 1 << 8,
},
];
+3 -6
View File
@@ -12,7 +12,7 @@ impl AdminApiClient {
acls: &[String],
) -> ApiResult<CreateAdminApiKeyResponse> {
let body = generated_types::CreateAdminApiKeyRequest {
acls: parse_acls(acls)?,
acls: parse_acls(acls),
expires_in_days: None,
name: generated_types::CreateAdminApiKeyRequestName::try_from(name)
.map_err(|e| ApiError::Parse(e.to_string()))?,
@@ -44,11 +44,8 @@ impl AdminApiClient {
}
}
pub(super) fn parse_acls(acls: &[String]) -> ApiResult<Vec<generated_types::AdminAclType>> {
pub(super) fn parse_acls(acls: &[String]) -> Vec<generated_types::AdminAclType> {
acls.iter()
.map(|acl| {
generated_types::AdminAclType::try_from(acl.as_str())
.map_err(|e| ApiError::Parse(e.to_string()))
})
.filter_map(|acl| generated_types::AdminAclType::try_from(acl.as_str()).ok())
.collect()
}
+1 -1
View File
@@ -11,7 +11,7 @@ impl AdminApiClient {
"email",
generated_types::AdminBlocklistEntryCreateRequest::from(
generated_types::BanEmailRequest {
email: generated_types::EmailType::from(email.to_owned()),
email: generated_types::EmailBlocklistEntryType::from(email.to_owned()),
},
),
audit_log_reason,
-16
View File
@@ -22,22 +22,6 @@ impl AdminApiClient {
.await
}
pub async fn bulk_update_suspicious_activity_flags(
&self,
user_ids: &[String],
add_flags: &[String],
remove_flags: &[String],
audit_log_reason: Option<&str>,
) -> ApiResult<BulkJobResponse> {
let body = generated_types::AdminBulkJobCreateRequest::UpdateSuspiciousActivityFlags {
add_flags: add_flags.to_vec(),
remove_flags: remove_flags.to_vec(),
user_ids: snowflakes(user_ids),
};
self.post_typed_with_reason("/admin/bulk-jobs", &body, audit_log_reason)
.await
}
pub async fn bulk_update_guild_features(
&self,
guild_ids: &[String],
-3
View File
@@ -85,7 +85,6 @@ mod tests {
"email": "[email protected]",
"email_verified": true,
"email_bounced": false,
"has_verified_phone": false,
"date_of_birth": "2000-01-15",
"locale": "en-US",
"premium_type": 2,
@@ -93,8 +92,6 @@ mod tests {
"premium_until": null,
"premium_grace_ends_at": null,
"premium_lifetime_sequence": null,
"suspicious_activity_flags": 0,
"phone_verification_deferred": false,
"temp_banned_until": null,
"pending_deletion_at": null,
"pending_bulk_message_deletion_at": null,
+7 -2
View File
@@ -8,13 +8,18 @@ use super::types::SendSystemDmResponse;
impl AdminApiClient {
pub async fn send_system_dm(
&self,
user_ids: &[String],
user_ids: Option<&[String]>,
content: &str,
) -> ApiResult<SendSystemDmResponse> {
let body = generated_types::SendSystemDmRequest {
content: generated_types::SendSystemDmRequestContent::try_from(content)
.map_err(|e| ApiError::Parse(e.to_string()))?,
user_ids: user_ids.iter().map(|id| snowflake(id)).collect(),
user_ids: user_ids
.unwrap_or_default()
.iter()
.map(|id| snowflake(id))
.collect(),
all_users: user_ids.is_none().then_some(true),
};
let response = self
.generated()
-6
View File
@@ -108,15 +108,9 @@ pub struct AdminUser {
pub premium_grace_ends_at: Option<String>,
pub premium_lifetime_sequence: Option<i32>,
#[serde(default)]
pub suspicious_activity_flags: i32,
#[serde(default)]
pub phone_verification_deferred: bool,
#[serde(default)]
pub has_totp: bool,
#[serde(default)]
pub authenticator_types: Vec<i32>,
#[serde(default)]
pub has_verified_phone: bool,
pub temp_banned_until: Option<String>,
pub pending_deletion_at: Option<String>,
pub pending_bulk_message_deletion_at: Option<String>,
@@ -25,6 +25,8 @@ pub struct InstanceConfigResponse {
#[serde(default)]
pub domain_migration: DomainMigrationConfigResponse,
#[serde(default)]
pub plutonium_page: PlutoniumPageConfigResponse,
#[serde(default)]
pub captcha: CaptchaConfigResponse,
#[serde(default)]
pub experiment_delivery: ExperimentDeliveryConfigResponse,
@@ -430,6 +432,7 @@ impl VoiceE2eeScope {
pub const EXPERIMENT_MAX_TARGETED_USERS: usize = 1_000;
pub const DOMAIN_MIGRATION_DEFAULT_SALT: &str = "domain-migration-v1";
pub const PLUTONIUM_PAGE_DEFAULT_SALT: &str = "plutonium-page-v1";
pub const CAPTCHA_COST_RANGE: std::ops::RangeInclusive<u32> = 1_000..=20_000;
pub const CAPTCHA_MAX_COUNTER_RANGE: std::ops::RangeInclusive<u32> = 100..=20_000;
@@ -501,6 +504,52 @@ pub struct DomainMigrationConfigUpdateRequest {
pub standalone_forwarding: Option<bool>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct PlutoniumPageConfigResponse {
pub enabled: bool,
pub config_version: u64,
pub rollout_basis_points: u32,
pub rollout_salt: String,
pub included_user_ids: Vec<String>,
pub included_guild_ids: Vec<String>,
pub include_premium_users: bool,
pub excluded_user_ids: Vec<String>,
}
impl Default for PlutoniumPageConfigResponse {
fn default() -> Self {
Self {
enabled: false,
config_version: 0,
rollout_basis_points: 0,
rollout_salt: PLUTONIUM_PAGE_DEFAULT_SALT.to_owned(),
included_user_ids: Vec::new(),
included_guild_ids: Vec::new(),
include_premium_users: false,
excluded_user_ids: Vec::new(),
}
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct PlutoniumPageConfigUpdateRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rollout_basis_points: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rollout_salt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub included_user_ids: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub included_guild_ids: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_premium_users: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub excluded_user_ids: Option<Vec<String>>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct CaptchaConfigResponse {
@@ -647,6 +696,8 @@ pub struct InstanceConfigUpdateRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub domain_migration: Option<DomainMigrationConfigUpdateRequest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub plutonium_page: Option<PlutoniumPageConfigUpdateRequest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub captcha: Option<CaptchaConfigUpdateRequest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub experiment_delivery: Option<ExperimentDeliveryConfigUpdateRequest>,
@@ -949,17 +1000,24 @@ mod tests {
.expect("admin schema");
let domain_migration = serde_json::from_value::<DomainMigrationConfigResponse>(json!({}))
.expect("default domain migration config");
let plutonium_page = serde_json::from_value::<PlutoniumPageConfigResponse>(json!({}))
.expect("default plutonium page config");
let captcha = serde_json::from_value::<CaptchaConfigResponse>(json!({}))
.expect("default captcha config");
let delivery = serde_json::from_value::<ExperimentDeliveryConfigResponse>(json!({}))
.expect("default delivery config");
let domain_migration =
serde_json::to_value(domain_migration).expect("serializable domain migration config");
let plutonium_page =
serde_json::to_value(plutonium_page).expect("serializable plutonium page config");
let captcha = serde_json::to_value(captcha).expect("serializable captcha config");
let delivery = serde_json::to_value(delivery).expect("serializable delivery config");
let generated_domain_migration: generated_types::DomainMigrationConfigResponse =
serde_json::from_value(domain_migration.clone())
.expect("generated domain migration config contract");
let generated_plutonium_page: generated_types::PlutoniumPageConfigResponse =
serde_json::from_value(plutonium_page.clone())
.expect("generated plutonium page config contract");
let generated_captcha: generated_types::CaptchaConfigResponse =
serde_json::from_value(captcha.clone()).expect("generated captcha config contract");
let generated_delivery: generated_types::ExperimentDeliveryConfigResponse =
@@ -969,6 +1027,11 @@ mod tests {
.expect("serializable generated domain migration config"),
domain_migration
);
assert_eq!(
serde_json::to_value(generated_plutonium_page)
.expect("serializable generated plutonium page config"),
plutonium_page
);
assert_eq!(
serde_json::to_value(generated_captcha).expect("serializable generated captcha config"),
captcha
@@ -980,6 +1043,7 @@ mod tests {
);
for (name, value) in [
("DomainMigrationConfigResponse", domain_migration),
("PlutoniumPageConfigResponse", plutonium_page),
("CaptchaConfigResponse", captcha),
("ExperimentDeliveryConfigResponse", delivery),
] {
@@ -1014,4 +1078,25 @@ mod tests {
json!({})
);
}
#[test]
fn plutonium_page_update_preserves_empty_lists_and_omitted_fields() {
let update = PlutoniumPageConfigUpdateRequest {
included_user_ids: Some(Vec::new()),
excluded_user_ids: Some(Vec::new()),
..Default::default()
};
let value = serde_json::to_value(update).expect("serializable update");
serde_json::from_value::<generated_types::PlutoniumPageConfigUpdateRequest>(value.clone())
.expect("generated update contract");
assert_eq!(
value,
json!({"included_user_ids": [], "excluded_user_ids": []})
);
assert_eq!(
serde_json::to_value(PlutoniumPageConfigUpdateRequest::default())
.expect("serializable update"),
json!({})
);
}
}
+1 -1
View File
@@ -4,5 +4,5 @@ use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SendSystemDmResponse {
pub recipient_count: i64,
pub recipient_count: Option<i64>,
}
+1 -29
View File
@@ -231,22 +231,9 @@ impl AdminApiClient {
Ok(resp.user)
}
pub async fn update_suspicious_flags(&self, user_id: &str, flags: i32) -> ApiResult<AdminUser> {
let body = generated_types::AdminUserSuspiciousActivityFlagsRequest {
flags: generated_types::SuspiciousActivityFlags::from(flags),
};
let response = self
.generated()
.update_admin_user_suspicious_activity_flags(&snowflake(user_id), &body)
.await
.map_err(|e| self.generated_error(e))?;
let resp: UserMutationResponse = self.generated_value(response.into_inner())?;
Ok(resp.user)
}
pub async fn set_user_acls(&self, user_id: &str, acls: &[String]) -> ApiResult<AdminUser> {
let body = generated_types::AdminUserAclsRequest {
acls: super::admin_api_keys::parse_acls(acls)?,
acls: super::admin_api_keys::parse_acls(acls),
};
let response = self
.generated()
@@ -296,21 +283,6 @@ impl AdminApiClient {
Ok(resp.user)
}
pub async fn update_has_verified_phone(
&self,
user_id: &str,
has_verified_phone: bool,
) -> ApiResult<AdminUser> {
let body = generated_types::AdminUserPhoneVerificationRequest { has_verified_phone };
let response = self
.generated()
.update_admin_user_phone_verification(&snowflake(user_id), &body)
.await
.map_err(|e| self.generated_error(e))?;
let resp: UserMutationResponse = self.generated_value(response.into_inner())?;
Ok(resp.user)
}
pub async fn clear_user_fields(
&self,
user_id: &str,
+2 -14
View File
@@ -171,7 +171,8 @@ pub(crate) async fn system_dms_post(
let flash = if let Some(content) = content.as_deref()
&& !user_ids.is_empty()
{
match client.send_system_dm(&user_ids, content).await {
let recipients = (user_ids != ["*"]).then_some(user_ids.as_slice());
match client.send_system_dm(recipients, content).await {
Ok(_) => FlashData::success("System DM sent"),
Err(error) => {
tracing::warn!(%error, "admin API request failed: send system DM");
@@ -218,19 +219,6 @@ pub(crate) async fn bulk_actions_post(
.bulk_update_user_flags(&user_ids, &add, &remove, audit_log_reason.as_deref())
.await
}
"bulk-update-suspicious-activity-flags" => {
let user_ids = form.list_values_any(&["user_ids[]", "user_ids"]);
let add = form.list_values_any(&["add_flags[]", "add_flags"]);
let remove = form.list_values_any(&["remove_flags[]", "remove_flags"]);
client
.bulk_update_suspicious_activity_flags(
&user_ids,
&add,
&remove,
audit_log_reason.as_deref(),
)
.await
}
"bulk-update-guild-features" => {
let guild_ids = form.list_values_any(&["guild_ids[]", "guild_ids"]);
let mut add = form.list_values_any(&["add_features[]", "add_features"]);
+107 -2
View File
@@ -18,8 +18,8 @@ use crate::{
InstanceMediaUpdateRequest, InstancePolicyUpdateRequest,
InstanceRegistrationConfigUpdateRequest, InstanceServicesUpdateRequest,
InstanceYoutubeIntegrationUpdateRequest, LimitConfigUpdateRequest, LimitRule,
LimitRuleFilters, PremiumMode, PushRelayConfigUpdateRequest, RegistrationMode,
SsoConfigUpdateRequest, VoiceE2eeScope,
LimitRuleFilters, PlutoniumPageConfigUpdateRequest, PremiumMode,
PushRelayConfigUpdateRequest, RegistrationMode, SsoConfigUpdateRequest, VoiceE2eeScope,
},
},
config::AdminConfig,
@@ -220,6 +220,10 @@ pub async fn instance_config_post(
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
Err(message) => FlashData::error(message),
},
"update_plutonium_page" => match build_plutonium_page_update(&form) {
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
Err(message) => FlashData::error(message),
},
"update_captcha" => match build_captcha_update(&form) {
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
Err(message) => FlashData::error(message),
@@ -609,6 +613,41 @@ fn build_domain_migration_update(
})
}
fn build_plutonium_page_update(
form: &MultiValueForm,
) -> Result<InstanceConfigUpdateRequest, String> {
Ok(InstanceConfigUpdateRequest {
plutonium_page: Some(PlutoniumPageConfigUpdateRequest {
enabled: Some(form.bool_value("plutonium_page_enabled")),
rollout_basis_points: parse_form_number(
form,
"plutonium_page_rollout_basis_points",
"Rollout basis points",
0,
EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX,
)?,
rollout_salt: parse_experiment_rollout_salt(form, "plutonium_page_rollout_salt")?,
included_user_ids: Some(parse_experiment_user_ids(
form.first("plutonium_page_included_user_ids")
.unwrap_or_default(),
"Included user IDs",
)?),
included_guild_ids: Some(parse_experiment_user_ids(
form.first("plutonium_page_included_guild_ids")
.unwrap_or_default(),
"Included guild IDs",
)?),
include_premium_users: Some(form.bool_value("plutonium_page_include_premium_users")),
excluded_user_ids: Some(parse_experiment_user_ids(
form.first("plutonium_page_excluded_user_ids")
.unwrap_or_default(),
"Excluded user IDs",
)?),
}),
..Default::default()
})
}
fn build_captcha_update(form: &MultiValueForm) -> Result<InstanceConfigUpdateRequest, String> {
Ok(InstanceConfigUpdateRequest {
captcha: Some(CaptchaConfigUpdateRequest {
@@ -1444,6 +1483,72 @@ mod tests {
);
}
#[test]
fn build_plutonium_page_update_reads_the_rollout_fields() {
let form = MultiValueForm::parse(
b"plutonium_page_enabled=true&plutonium_page_rollout_basis_points=%20500%20&plutonium_page_rollout_salt=%20plutonium-page-v2%20&plutonium_page_included_user_ids=1500000000000000001&plutonium_page_excluded_user_ids=1500000000000000002&plutonium_page_included_guild_ids=1500000000000000005%0A1500000000000000006%2C1500000000000000005&plutonium_page_include_premium_users=true",
);
let update = build_plutonium_page_update(&form)
.expect("valid form")
.plutonium_page
.expect("plutonium page update");
assert_eq!(update.enabled, Some(true));
assert_eq!(update.rollout_basis_points, Some(500));
assert_eq!(update.rollout_salt, Some("plutonium-page-v2".to_owned()));
assert_eq!(update.include_premium_users, Some(true));
assert_eq!(
update.included_guild_ids,
Some(vec![
"1500000000000000005".to_owned(),
"1500000000000000006".to_owned()
])
);
assert_eq!(
update.included_user_ids,
Some(vec!["1500000000000000001".to_owned()])
);
assert_eq!(
update.excluded_user_ids,
Some(vec!["1500000000000000002".to_owned()])
);
}
#[test]
fn build_plutonium_page_update_leaves_the_feature_inert_when_nothing_is_submitted() {
let form = MultiValueForm::parse(b"_csrf=token");
let request = build_plutonium_page_update(&form).expect("valid form");
assert_eq!(
serde_json::to_value(request).expect("serializable update"),
serde_json::json!({"plutonium_page": {
"enabled": false,
"included_user_ids": [],
"included_guild_ids": [],
"include_premium_users": false,
"excluded_user_ids": [],
}})
);
}
#[test]
fn build_plutonium_page_update_rejects_invalid_rollout_fields() {
for (form, message) in [
(
"plutonium_page_rollout_basis_points=10001",
"Rollout basis points must be a whole number between 0 and 10000",
),
(
"plutonium_page_included_guild_ids=1500000000000000005%0Anot-a-guild",
"Included guild IDs entry 2 must contain 1 to 20 decimal digits",
),
] {
let form = MultiValueForm::parse(form.as_bytes());
assert_eq!(
build_plutonium_page_update(&form).expect_err("invalid field"),
message
);
}
}
#[test]
fn build_experiment_delivery_update_leaves_both_fields_unchanged_when_absent() {
let form = MultiValueForm::parse(b"_csrf=token");
-21
View File
@@ -134,19 +134,6 @@ pub async fn dispatch(
"Failed to update premium flags",
)
}
"update_suspicious_flags" => {
let Ok(submitted) =
form.parse_list_values::<i32>(&["suspicious_flags[]", "suspicious_flags"])
else {
return DispatchOutcome::error("Invalid suspicious activity flag value");
};
let flags = submitted.into_iter().fold(0, |acc, flag| acc | flag);
DispatchOutcome::from_result(
client.update_suspicious_flags(user_id, flags).await,
"Suspicious activity flags updated successfully",
"Failed to update suspicious activity flags",
)
}
"update_acls" => {
let acls = form.list_values_any(&["acls[]", "acls"]);
DispatchOutcome::from_result(
@@ -178,14 +165,6 @@ pub async fn dispatch(
"Email verified successfully",
"Failed to verify email",
),
"update_has_verified_phone" => {
let val = form.bool_value("has_verified_phone");
DispatchOutcome::from_result(
client.update_has_verified_phone(user_id, val).await,
"Phone verification status updated successfully",
"Failed to update phone verification status",
)
}
"terminate_sessions" => DispatchOutcome::from_result(
client.terminate_user_sessions(user_id).await,
"User sessions terminated successfully",
@@ -54,7 +54,6 @@ pub const NAV_SECTIONS: &[NavSection] = &[
"bulk-actions",
[
acl::BULK_UPDATE_USER_FLAGS,
acl::BULK_UPDATE_SUSPICIOUS_ACTIVITY,
acl::BULK_UPDATE_GUILD_FEATURES,
acl::BULK_ADD_GUILD_MEMBERS,
acl::BULK_DELETE_USERS,
@@ -268,7 +267,6 @@ mod tests {
.expect("bulk actions nav item");
for required in [
acl::BULK_UPDATE_USER_FLAGS,
acl::BULK_UPDATE_SUSPICIOUS_ACTIVITY,
acl::BULK_UPDATE_GUILD_FEATURES,
acl::BULK_ADD_GUILD_MEMBERS,
acl::BULK_DELETE_USERS,
@@ -19,11 +19,7 @@ pub fn format_action(action: &str) -> String {
pub fn action_badge_variant(action: &str) -> BadgeVariant {
match action {
"temp_ban"
| "disable_suspicious_activity"
| "schedule_deletion"
| "ban_ip"
| "ban_email" => BadgeVariant::Danger,
"temp_ban" | "schedule_deletion" | "ban_ip" | "ban_email" => BadgeVariant::Danger,
"unban" | "cancel_deletion" | "unban_ip" | "unban_email" => BadgeVariant::Success,
"update_flags" | "update_features" | "set_acls" | "update_settings" | "annotate_ban" => {
BadgeVariant::Info
@@ -361,4 +357,12 @@ mod tests {
assert!(!markup.contains("<a "));
assert!(markup.contains("Email domain"));
}
#[test]
fn retired_action_names_still_render() {
let mut retired = entry("user", "1500000000000000002");
retired.action = "update_retired_toggle".to_string();
let markup = audit_log_table_body("/admin", &[retired]).into_string();
assert!(markup.contains("Update retired toggle"));
}
}
+3 -3
View File
@@ -37,10 +37,10 @@ pub const BAN_CONFIGS: &[BanConfig] = &[
BanConfig {
title: "Email Bans",
route: "/email-bans",
input_label: "Email Address",
input_label: "Email Address or Domain",
input_name: "email",
input_type: "email",
placeholder: "[email protected]",
input_type: "text",
placeholder: "[email protected] or @example.com",
entity_name: "Email",
active_page: "email-bans",
show_bulk_tools: false,
@@ -80,10 +80,6 @@ const PATCHABLE_USER_FLAGS: &[UserFlag] = &[
name: "DELETED",
value: 1 << 34,
},
UserFlag {
name: "DISABLED_SUSPICIOUS_ACTIVITY",
value: 1 << 35,
},
UserFlag {
name: "SELF_DELETED",
value: 1 << 36,
@@ -108,6 +104,10 @@ const PATCHABLE_USER_FLAGS: &[UserFlag] = &[
name: "VERIFIED_NOT_UNDERAGE",
value: 1 << 49,
},
UserFlag {
name: "ACCOUNT_LIMITED",
value: 1 << 50,
},
UserFlag {
name: "HAS_DISMISSED_PREMIUM_ONBOARDING",
value: 1 << 51,
@@ -125,26 +125,11 @@ const PATCHABLE_USER_FLAGS: &[UserFlag] = &[
value: 1 << 60,
},
UserFlag {
name: "FORCE_INBOUND_PHONE_VERIFICATION",
value: 1 << 61,
},
UserFlag {
name: "NOT_SUSPICIOUS",
name: "LIMIT_EXEMPT",
value: 1 << 62,
},
];
const SUSPICIOUS_ACTIVITY_FLAGS: &[&str] = &[
"REQUIRE_VERIFIED_EMAIL",
"REQUIRE_REVERIFIED_EMAIL",
"REQUIRE_VERIFIED_PHONE",
"REQUIRE_REVERIFIED_PHONE",
"REQUIRE_VERIFIED_EMAIL_OR_VERIFIED_PHONE",
"REQUIRE_REVERIFIED_EMAIL_OR_VERIFIED_PHONE",
"REQUIRE_VERIFIED_EMAIL_OR_REVERIFIED_PHONE",
"REQUIRE_REVERIFIED_EMAIL_OR_REVERIFIED_PHONE",
"REQUIRE_INBOUND_PHONE_VERIFICATION",
];
const GUILD_FEATURES: &[&str] = &[
"ANIMATED_ICON",
"ANIMATED_BANNER",
@@ -179,6 +164,7 @@ const GUILD_FEATURES: &[&str] = &[
"VISIONARY",
"LARGE_GUILD_OVERRIDE",
"VERY_LARGE_GUILD",
"ANNOUNCEMENT_CHANNELS_DISABLED",
];
const DEPRECATED_GUILD_FEATURES: &[&str] = &["CLONE_EMOJI_DISABLED", "CLONE_STICKER_DISABLED"];
@@ -205,9 +191,6 @@ pub fn bulk_actions_page(config: &AdminConfig, auth: &AuthContext, csrf_token: &
@if acl::has_permission(admin_acls, acl::BULK_UPDATE_USER_FLAGS) {
(bulk_update_user_flags_section(base, csrf_token))
}
@if acl::has_permission(admin_acls, acl::BULK_UPDATE_SUSPICIOUS_ACTIVITY) {
(bulk_update_suspicious_activity_section(base, csrf_token))
}
@if acl::has_permission(admin_acls, acl::BULK_UPDATE_GUILD_FEATURES) {
(bulk_update_guild_features_section(base, csrf_token))
}
@@ -225,16 +208,6 @@ pub fn bulk_actions_page(config: &AdminConfig, auth: &AuthContext, csrf_token: &
admin_layout(config, auth, "Bulk Actions", "bulk-actions", None, content)
}
fn flag_checkbox_grid(prefix: &str, flags: &[&str]) -> Markup {
html! {
div class="grid grid-cols-1 gap-3 sm:grid-cols-2" {
@for flag in flags {
(checkbox(prefix, flag, flag, false, true))
}
}
}
}
fn guild_feature_checkbox_grid(prefix: &str, include_deprecated: bool) -> Markup {
html! {
div class="grid grid-cols-1 gap-3 sm:grid-cols-2" {
@@ -287,36 +260,6 @@ fn bulk_update_user_flags_section(base: &str, csrf_token: &str) -> Markup {
)
}
fn bulk_update_suspicious_activity_section(base: &str, csrf_token: &str) -> Markup {
section_card_simple(
"Bulk Update Suspicious Activity Flags",
html! {
form method="post" action={(base) "/bulk-actions?action=bulk-update-suspicious-activity-flags"} {
(csrf_input(csrf_token))
div class="space-y-4" {
(textarea_input("user_ids", "User IDs (one per line)", "123456789\n987654321", "", 5, true))
div {
p class="font-semibold text-neutral-500 text-xs uppercase tracking-wide mb-2" {
"Flags to Add"
}
(flag_checkbox_grid("add_flags[]", SUSPICIOUS_ACTIVITY_FLAGS))
}
div {
p class="font-semibold text-neutral-500 text-xs uppercase tracking-wide mb-2" {
"Flags to Remove"
}
(flag_checkbox_grid("remove_flags[]", SUSPICIOUS_ACTIVITY_FLAGS))
}
(text_input("audit_log_reason", "Audit Log Reason (optional)", "", "Reason for this bulk operation"))
(form_actions(html! {
(submit_button("Update Suspicious Activity Flags"))
}))
}
}
},
)
}
fn bulk_update_guild_features_section(base: &str, csrf_token: &str) -> Markup {
section_card_simple(
"Bulk Update Guild Features",
@@ -25,6 +25,10 @@ pub(crate) fn stat_card(label: &str, value: &str) -> Markup {
pub(crate) fn node_stats_section(data: &serde_json::Value, expanded: bool, base: &str) -> Markup {
let sessions = data.get("sessions").and_then(|v| v.as_u64()).unwrap_or(0);
let reconnects: u64 = data
.get("session_resumes_total")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let guilds = data.get("guilds").and_then(|v| v.as_u64()).unwrap_or(0);
let presences = data.get("presences").and_then(|v| v.as_u64()).unwrap_or(0);
let calls = data.get("calls").and_then(|v| v.as_u64()).unwrap_or(0);
@@ -64,6 +68,7 @@ pub(crate) fn node_stats_section(data: &serde_json::Value, expanded: bool, base:
div class="grid grid-cols-2 gap-3 sm:gap-4 md:grid-cols-3 lg:grid-cols-6" {
(stat_card("Nodes", &node_count.to_string()))
(stat_card("Sessions", &sessions.to_string()))
(stat_card("Reconnects", &reconnects.to_string()))
(stat_card("Guilds", &guilds.to_string()))
(stat_card("Presences", &presences.to_string()))
(stat_card("Calls", &calls.to_string()))
@@ -83,6 +88,7 @@ pub(crate) fn node_stats_table(nodes: &[serde_json::Value]) -> Markup {
tr {
th class="px-6 py-3 text-left text-neutral-600 text-xs uppercase" { "Node" }
th class="px-6 py-3 text-right text-neutral-600 text-xs uppercase" { "Sessions" }
th class="px-6 py-3 text-right text-neutral-600 text-xs uppercase" { "Session Resumes" }
th class="px-6 py-3 text-right text-neutral-600 text-xs uppercase" { "Guilds" }
th class="px-6 py-3 text-right text-neutral-600 text-xs uppercase" { "Presences" }
th class="px-6 py-3 text-right text-neutral-600 text-xs uppercase" { "Calls" }
@@ -95,6 +101,7 @@ pub(crate) fn node_stats_table(nodes: &[serde_json::Value]) -> Markup {
@let label = format_node_id(node_id, i);
@let status = node.get("status").and_then(|v| v.as_str()).unwrap_or("-");
@let ns = node.get("sessions").and_then(|v| v.as_u64()).unwrap_or(0);
@let nsr = node.get("session_resumes_total").and_then(|v| v.as_u64()).unwrap_or(0);
@let ng = node.get("guilds").and_then(|v| v.as_u64()).unwrap_or(0);
@let np = node.get("presences").and_then(|v| v.as_u64()).unwrap_or(0);
@let nc = node.get("calls").and_then(|v| v.as_u64()).unwrap_or(0);
@@ -105,6 +112,7 @@ pub(crate) fn node_stats_table(nodes: &[serde_json::Value]) -> Markup {
div class="text-neutral-500 text-xs" { (status) }
}
td class="whitespace-nowrap px-6 py-4 text-right text-sm" { (ns) }
td class="whitespace-nowrap px-6 py-4 text-right text-sm" { (nsr) }
td class="whitespace-nowrap px-6 py-4 text-right text-sm" { (ng) }
td class="whitespace-nowrap px-6 py-4 text-right text-sm" { (np) }
td class="whitespace-nowrap px-6 py-4 text-right text-sm" { (nc) }
@@ -45,6 +45,7 @@ const GUILD_FEATURES: &[&str] = &[
"VISIONARY",
"LARGE_GUILD_OVERRIDE",
"VERY_LARGE_GUILD",
"ANNOUNCEMENT_CHANNELS_DISABLED",
];
const HOSTED_ONLY: &[&str] = &["VISIONARY", "VIP_VOICE"];
@@ -32,6 +32,7 @@ fn channel_type_label(channel_type: i32) -> &'static str {
0 => "Text",
2 => "Voice",
4 => "Category",
5 => "Announcement",
13 => "Link",
_ => "Unknown",
}
@@ -52,13 +52,12 @@ pub fn settings_tab(
"guild-verification-level",
"verification_level",
"Verification Level",
guild.verification_level.unwrap_or(0),
guild.verification_level.unwrap_or(0).min(3),
&[
(0, "None"),
(1, "Low (verified email)"),
(2, "Medium (5+ minutes)"),
(3, "High (10+ minutes)"),
(4, "Very High (verified phone)"),
],
))
(select_field(
@@ -225,12 +224,11 @@ fn select_field(
}
fn settings_tab_readonly(guild: &GuildDetailInfo) -> Markup {
let verification_label = match guild.verification_level.unwrap_or(0) {
let verification_label = match guild.verification_level.unwrap_or(0).min(3) {
0 => "None",
1 => "Low (verified email)",
2 => "Medium (5+ minutes)",
3 => "High (10+ minutes)",
4 => "Very High (verified phone)",
_ => "Unknown",
};
let mfa_label = match guild.mfa_level.unwrap_or(0) {
@@ -7,8 +7,9 @@ use crate::{
EXPERIMENT_MAX_TARGETED_USERS, ExperimentDeliveryConfigResponse,
GatewayRolloutConfigResponse, InstanceConfigResponse, InstanceIntegrationsResponse,
InstanceMediaResponse, InstancePolicyResponse, InstanceRegistrationResponse,
LimitConfigResponse, PendingRegistrationResponse, PushRelayConfigResponse,
RegistrationUrlResponse, SsoConfigResponse,
LimitConfigResponse, PLUTONIUM_PAGE_DEFAULT_SALT, PendingRegistrationResponse,
PlutoniumPageConfigResponse, PushRelayConfigResponse, RegistrationUrlResponse,
SsoConfigResponse,
},
config::AdminConfig,
middleware::auth::AuthContext,
@@ -181,6 +182,7 @@ pub fn instance_config_page(
html! {
(gateway_rollout_section(base, csrf_token, &instance_config.gateway_rollout))
(domain_migration_section(base, csrf_token, &instance_config.domain_migration))
(plutonium_page_section(base, csrf_token, &instance_config.plutonium_page))
(experiment_delivery_section(base, csrf_token, &instance_config.experiment_delivery))
@if let Some(limit_config) = limit_config {
(limit_config_section(base, limit_config))
@@ -1207,6 +1209,147 @@ fn domain_migration_section(
)
}
fn plutonium_page_section(
base: &str,
csrf_token: &str,
plutonium_page: &PlutoniumPageConfigResponse,
) -> Markup {
let status = if plutonium_page.enabled {
("Live", BadgeVariant::Success)
} else {
("Inert", BadgeVariant::Default)
};
let included_user_ids = plutonium_page.included_user_ids.join("\n");
let excluded_user_ids = plutonium_page.excluded_user_ids.join("\n");
section_card_with_description(
"Plutonium page",
"Replaces the Plutonium settings tab with a full Plutonium page, makes app pages linkable \
in chat, and uses a minimal gift purchase modal.",
html! {
form method="post" action={(base) "/instance-config?action=update_plutonium_page"} {
(csrf_input(csrf_token))
div class="space-y-6" {
div class="flex flex-wrap items-center gap-2" {
h3 class="text-sm font-semibold text-neutral-900" { "Master switch" }
(badge(status.0, status.1))
span class="text-xs text-neutral-500" {
"Config version " (plutonium_page.config_version)
}
}
(checkbox(
"plutonium_page_enabled",
"true",
"Serve the Plutonium page to the selected users",
plutonium_page.enabled,
true,
))
p class="text-xs text-neutral-500" {
"Off is the safe state and the kill switch. With this unchecked every \
client keeps the Plutonium settings tab, so the rollout and targeting \
fields below have no effect at all."
}
h3 class="text-sm font-semibold text-neutral-900" { "Rollout" }
(number_field(
"plutonium_page_rollout_basis_points",
"Rollout (basis points)",
&plutonium_page.rollout_basis_points.to_string(),
Some(0), Some(10000), "1",
Some("Share of users bucketed into the Plutonium page, in basis points: 0 is nobody, 100 is 1%, 10000 is everybody."),
))
div class="flex flex-col gap-2" {
(text_input(
"plutonium_page_rollout_salt",
"Rollout Salt",
&plutonium_page.rollout_salt,
PLUTONIUM_PAGE_DEFAULT_SALT,
))
p class="text-xs text-neutral-500" {
"Seeds the bucketing hash. Changing it reshuffles which users fall \
inside the percentage above. Leave it alone to keep the current \
cohort stable."
}
}
div class="flex flex-col gap-2" {
(textarea_input(
"plutonium_page_included_user_ids",
"Always-on User IDs",
"1500000000000000001\n1500000000000000002",
&included_user_ids,
4,
false,
))
(entry_count_hint(
plutonium_page.included_user_ids.len(),
EXPERIMENT_MAX_TARGETED_USERS,
))
p class="text-xs text-neutral-500" {
"One snowflake per line, or comma separated. These users are targeted \
regardless of the percentage above. IDs must contain 1 to 20 decimal \
digits. Invalid entries prevent the save. Blank entries and duplicate \
IDs are ignored."
}
}
div class="flex flex-col gap-2" {
(checkbox(
"plutonium_page_include_premium_users",
"true",
"Include premium users",
plutonium_page.include_premium_users,
true,
))
p class="text-xs text-neutral-500" {
"Includes every account with active premium perks, regardless of the \
percentage above. The never-on list still wins."
}
}
div class="flex flex-col gap-2" {
(textarea_input(
"plutonium_page_included_guild_ids",
"Always-on Guild IDs",
"1500000000000000005\n1500000000000000006",
&plutonium_page.included_guild_ids.join("\n"),
4,
false,
))
(entry_count_hint(
plutonium_page.included_guild_ids.len(),
EXPERIMENT_MAX_TARGETED_USERS,
))
p class="text-xs text-neutral-500" {
"Same format, with guild IDs. Every member of a listed guild is \
included regardless of the percentage above, unless the user is \
in the never-on list."
}
}
div class="flex flex-col gap-2" {
(textarea_input(
"plutonium_page_excluded_user_ids",
"Never-on User IDs",
"1500000000000000003\n1500000000000000004",
&excluded_user_ids,
4,
false,
))
(entry_count_hint(
plutonium_page.excluded_user_ids.len(),
EXPERIMENT_MAX_TARGETED_USERS,
))
p class="text-xs text-neutral-500" {
"Same format. Exclusion wins over both the always-on list and the \
percentage."
}
}
(form_actions(html! {
(submit_button("Save Plutonium Page Configuration"))
}))
}
}
},
)
}
fn estimate_low_end_solve_seconds(cost: u32, max_counter: u32) -> f64 {
0.75 * f64::from(cost) * f64::from(max_counter) / 1_050_000.0
}
@@ -1260,7 +1403,7 @@ fn captcha_section(base: &str, csrf_token: &str, captcha: &CaptchaConfigResponse
))
p class="text-sm text-neutral-700" {
(format!(
"Average solve: about {estimate:.1} s on a low-end Android phone, \
"Average solve: about {estimate:.1} s on a low-end Android device, \
well under a second in desktop browsers."
))
}
@@ -1928,6 +2071,34 @@ mod tests {
assert!(!markup.contains("at the cap"));
}
#[test]
fn plutonium_page_section_shows_the_rollout_and_list_counts() {
let plutonium_page = PlutoniumPageConfigResponse {
enabled: true,
config_version: 3,
rollout_basis_points: 250,
included_user_ids: vec!["1500000000000000001".to_owned()],
excluded_user_ids: vec![
"1500000000000000002".to_owned(),
"1500000000000000003".to_owned(),
],
..PlutoniumPageConfigResponse::default()
};
let markup = plutonium_page_section("/admin", "csrf", &plutonium_page).into_string();
assert!(markup.contains("Plutonium page"));
assert!(markup.contains("action=update_plutonium_page"));
assert!(markup.contains("name=\"plutonium_page_enabled\""));
assert!(markup.contains("name=\"plutonium_page_rollout_basis_points\""));
assert!(markup.contains("value=\"250\""));
assert!(markup.contains("name=\"plutonium_page_include_premium_users\""));
assert!(markup.contains("name=\"plutonium_page_included_guild_ids\""));
assert!(markup.contains("Config version 3"));
assert!(markup.contains("1 of 1000 stored"));
assert!(markup.contains("2 of 1000 stored"));
assert!(!markup.contains("anonymous_rollout_basis_points"));
assert!(!markup.contains("standalone_forwarding"));
}
#[test]
fn push_relay_section_shows_the_consent_toggle() {
let accepted = PushRelayConfigResponse {
@@ -58,7 +58,7 @@ pub fn system_dm_page(
(form_field_group(
"Recipient user IDs", "system-dm-user-ids",
true, None,
Some("One per line. Snowflake IDs only."),
Some("One per line. Snowflake IDs only, or a single * to send to every user."),
html! {
textarea id="system-dm-user-ids" name="user_ids"
required rows="10"
@@ -154,10 +154,6 @@ fn session_entry(base: &str, s: &UserSession, is_tombstone: bool) -> Markup {
}
fn quick_actions_card(base: &str, user: &AdminUser, csrf_token: &str) -> Markup {
let phone_action = format!(
"{base}/users/{}?action=update_has_verified_phone&tab=account",
user.id
);
html! {
(card_with_header("Quick Actions", html! {
div class="flex flex-wrap gap-3" {
@@ -165,20 +161,6 @@ fn quick_actions_card(base: &str, user: &AdminUser, csrf_token: &str) -> Markup
(action_form(base, &user.id, "verify_email", "account", None,
"Verify Email", csrf_token))
}
form method="post"
action=(&phone_action)
hx-post=(&phone_action)
hx-target="#flash-container"
hx-swap="none"
hx-push-url="false" {
(csrf_input(csrf_token))
input type="hidden" name="has_verified_phone"
value=@if user.has_verified_phone { "false" } @else { "true" };
button type="submit" class=(BTN_CLS) {
@if user.has_verified_phone { "Clear Phone Verified" }
@else { "Mark Phone Verified" }
}
}
(action_form(base, &user.id, "send_password_reset", "account", None,
"Send Password Reset", csrf_token))
}
@@ -139,13 +139,6 @@ fn render_overview_tab(
}
}))
}
(detail_row("Phone", html! {
@if user.has_verified_phone {
span class="text-green-700" { "Verified" }
} @else {
span class="text-neutral-400" { "Not verified" }
}
}))
@if acl::has_permission(admin_acls, acl::USER_VIEW_DOB) {
(detail_row("Date of Birth", html! {
(user.date_of_birth.as_deref().unwrap_or("Not set"))
@@ -270,8 +263,6 @@ fn flags_card(
csrf_token: &str,
) -> Markup {
let can_update_flags = acl::has_permission(admin_acls, acl::USER_UPDATE_FLAGS);
let can_update_suspicious =
acl::has_permission(admin_acls, acl::USER_UPDATE_SUSPICIOUS_ACTIVITY);
html! {
div class="space-y-6" {
(u64_flag_form(
@@ -298,23 +289,6 @@ fn flags_card(
can_update_flags,
Some(acl::USER_UPDATE_FLAGS),
))
(i32_flag_form(
config,
&user.id,
"Suspicious Activity Flags",
"update_suspicious_flags",
"suspicious_flags[]",
user.suspicious_activity_flags,
admin_flags::SUSPICIOUS_ACTIVITY_FLAGS,
csrf_token,
can_update_suspicious,
Some(acl::USER_UPDATE_SUSPICIOUS_ACTIVITY),
))
@if user.phone_verification_deferred {
p class="text-sm text-amber-700 dark:text-amber-400" {
"Phone verification is deferred: the requirement above is stored but not enforced."
}
}
}
}
}
+25 -12
View File
@@ -23,7 +23,6 @@ fn deserialize_admin_users_me_response() {
"email": "[email protected]",
"email_verified": true,
"email_bounced": false,
"has_verified_phone": true,
"date_of_birth": "2003-02-25",
"locale": "en-US",
"premium_type": 2,
@@ -31,7 +30,6 @@ fn deserialize_admin_users_me_response() {
"premium_until": null,
"premium_grace_ends_at": null,
"premium_lifetime_sequence": 1,
"suspicious_activity_flags": 0,
"temp_banned_until": null,
"pending_deletion_at": null,
"pending_bulk_message_deletion_at": null,
@@ -64,9 +62,7 @@ fn deserialize_admin_users_me_response() {
assert_eq!(user.acls, vec!["super_admin"]);
assert_eq!(user.traits, vec!["beta_tester"]);
assert_eq!(user.premium_type, Some(2));
assert_eq!(user.suspicious_activity_flags, 0);
assert!(user.has_totp);
assert!(user.has_verified_phone);
assert_eq!(user.last_active_ip.as_deref(), Some("1.2.3.4"));
}
@@ -79,10 +75,10 @@ fn deserialize_flags_as_string_and_number() {
"premium_flags": 0, "avatar": null, "banner": null, "bio": null,
"pronouns": null, "accent_color": null, "email": null,
"email_verified": false, "email_bounced": false,
"has_verified_phone": false, "date_of_birth": null, "locale": null,
"date_of_birth": null, "locale": null,
"premium_type": null, "premium_since": null, "premium_until": null,
"premium_grace_ends_at": null, "premium_lifetime_sequence": null,
"suspicious_activity_flags": 0, "temp_banned_until": null,
"temp_banned_until": null,
"pending_deletion_at": null, "pending_bulk_message_deletion_at": null,
"deletion_reason_code": null, "deletion_public_reason": null, "deletion_audit_log_reason": null,
"deletion_scheduled_by": null, "deletion_scheduled_at": null,
@@ -111,10 +107,10 @@ fn deserialize_discriminator_int_and_string() {
"bot": true, "system": false, "flags": "0", "premium_flags": 0,
"avatar": null, "banner": null, "bio": null, "pronouns": null,
"accent_color": null, "email": null, "email_verified": false,
"email_bounced": false, "has_verified_phone": false, "date_of_birth": null,
"email_bounced": false, "date_of_birth": null,
"locale": null, "premium_type": null, "premium_since": null,
"premium_until": null, "premium_grace_ends_at": null,
"premium_lifetime_sequence": null, "suspicious_activity_flags": 0,
"premium_lifetime_sequence": null,
"temp_banned_until": null, "pending_deletion_at": null,
"pending_bulk_message_deletion_at": null, "deletion_reason_code": null,
"deletion_public_reason": null, "deletion_audit_log_reason": null,
@@ -161,7 +157,6 @@ fn deserialize_search_users_response() {
"email": null,
"email_verified": false,
"email_bounced": false,
"has_verified_phone": false,
"date_of_birth": null,
"locale": null,
"premium_type": null,
@@ -169,7 +164,6 @@ fn deserialize_search_users_response() {
"premium_until": null,
"premium_grace_ends_at": null,
"premium_lifetime_sequence": null,
"suspicious_activity_flags": 0,
"temp_banned_until": null,
"pending_deletion_at": null,
"pending_bulk_message_deletion_at": null,
@@ -422,6 +416,17 @@ fn deserialize_instance_config_response_with_unknown_keys() {
"anonymous_rollout_basis_points": 100,
"standalone_forwarding": true
},
"plutonium_page": {
"enabled": true,
"config_version": 3,
"rollout_basis_points": 500,
"rollout_salt": "plutonium-page-v1",
"included_user_ids": ["1500000000000000001"],
"excluded_user_ids": ["1500000000000000002"],
"included_guild_ids": ["1500000000000000005"],
"include_premium_users": true,
"future_plutonium_page_knob": true
},
"captcha": {
"enabled": true,
"cost": 5000,
@@ -578,6 +583,14 @@ fn deserialize_instance_config_response_with_unknown_keys() {
assert_eq!(resp.domain_migration.included_user_ids.len(), 1);
assert_eq!(resp.domain_migration.anonymous_rollout_basis_points, 100);
assert!(resp.domain_migration.standalone_forwarding);
assert!(resp.plutonium_page.enabled);
assert_eq!(resp.plutonium_page.config_version, 3);
assert_eq!(resp.plutonium_page.rollout_basis_points, 500);
assert_eq!(*resp.plutonium_page.rollout_salt, "plutonium-page-v1");
assert_eq!(resp.plutonium_page.included_user_ids.len(), 1);
assert_eq!(resp.plutonium_page.excluded_user_ids.len(), 1);
assert_eq!(resp.plutonium_page.included_guild_ids.len(), 1);
assert!(resp.plutonium_page.include_premium_users);
assert!(resp.push_relay.relay_consent_accepted);
assert!(resp.captcha.enabled);
assert_eq!(resp.captcha.max_counter, 1000);
@@ -862,10 +875,10 @@ fn deserialize_user_mutation_response() {
"flags": "1", "premium_flags": 0, "avatar": null, "banner": null,
"bio": null, "pronouns": null, "accent_color": null, "email": null,
"email_verified": false, "email_bounced": false,
"has_verified_phone": false, "date_of_birth": null, "locale": null,
"date_of_birth": null, "locale": null,
"premium_type": null, "premium_since": null, "premium_until": null,
"premium_grace_ends_at": null, "premium_lifetime_sequence": null,
"suspicious_activity_flags": 0, "temp_banned_until": null,
"temp_banned_until": null,
"pending_deletion_at": null, "pending_bulk_message_deletion_at": null,
"deletion_reason_code": null, "deletion_public_reason": null, "deletion_audit_log_reason": null,
"deletion_scheduled_by": null, "deletion_scheduled_at": null,
-3
View File
@@ -263,11 +263,8 @@ fn admin_user() -> Value {
"premium_until": null,
"premium_grace_ends_at": null,
"premium_lifetime_sequence": null,
"suspicious_activity_flags": 0,
"phone_verification_deferred": false,
"has_totp": false,
"authenticator_types": [],
"has_verified_phone": false,
"temp_banned_until": null,
"pending_deletion_at": null,
"pending_bulk_message_deletion_at": null,
+17 -9
View File
@@ -363,7 +363,9 @@ async fn user_account_actions_use_no_swap_htmx_toasts() {
assert!(body.contains("__fluxerAdminActionForms"), "{body}");
assert!(!body.contains(&native_confirm), "{body}");
assert!(
body.contains(r#"hx-post="/users/1500000000000000001?action=update_has_verified_phone&amp;tab=account""#),
body.contains(
r#"hx-post="/users/1500000000000000001?action=send_password_reset&amp;tab=account""#
),
"{body}"
);
assert!(body.contains(r##"hx-target="#flash-container""##), "{body}");
@@ -374,7 +376,7 @@ async fn user_account_actions_use_no_swap_htmx_toasts() {
.unwrap_or_else(|| panic!("account page did not set csrf_token cookie\n{body}"));
let (status, response_headers, response_body) = post_form_with_headers(
&app,
"/users/1500000000000000001?action=update_has_verified_phone&tab=account",
"/users/1500000000000000001?action=send_password_reset&tab=account",
&[
("HX-Request", "true"),
("HX-Target", "flash-container"),
@@ -383,7 +385,7 @@ async fn user_account_actions_use_no_swap_htmx_toasts() {
&format!("{}; csrf_token={}", app.session_cookie, csrf_token),
),
],
&format!("_csrf={csrf_token}&has_verified_phone=true"),
&format!("_csrf={csrf_token}"),
)
.await;
assert_eq!(status, StatusCode::NO_CONTENT, "{response_body}");
@@ -399,7 +401,7 @@ async fn user_account_actions_use_no_swap_htmx_toasts() {
.unwrap_or_else(|| panic!("missing toast header\n{response_body}"));
assert!(toast.contains("success"), "{toast}");
assert!(
toast.contains("Phone verification status updated successfully"),
toast.contains("Password reset sent successfully"),
"{toast}"
);
}
@@ -467,6 +469,7 @@ async fn mutating_admin_pages_render_usable_csrf_tokens() {
"/instance-config?action=update_gateway_rollout",
"/instance-config?action=update_sso",
"/instance-config?action=update_domain_migration",
"/instance-config?action=update_plutonium_page",
"/instance-config?action=update_experiment_delivery",
][..],
),
@@ -838,8 +841,8 @@ async fn mock_api(method: Method, uri: Uri) -> Response {
(Method::GET, "/admin/users/1500000000000000001") => {
json_response(json!({ "users": [searched_user()] }))
}
(Method::PUT, "/admin/users/1500000000000000001/phone-verification") => {
json_response(json!({ "user": searched_user() }))
(Method::POST, "/admin/users/1500000000000000001/password-reset") => {
StatusCode::NO_CONTENT.into_response()
}
(Method::GET, "/admin/guilds") => {
json_response(json!({ "guilds": [searched_guild()], "total": 1 }))
@@ -946,11 +949,8 @@ fn user(id: &str, username: &str) -> Value {
"premium_until": null,
"premium_grace_ends_at": null,
"premium_lifetime_sequence": null,
"suspicious_activity_flags": 0,
"phone_verification_deferred": false,
"has_totp": false,
"authenticator_types": [],
"has_verified_phone": false,
"temp_banned_until": null,
"pending_deletion_at": null,
"pending_bulk_message_deletion_at": null,
@@ -1193,6 +1193,14 @@ fn instance_config() -> Value {
"anonymous_rollout_basis_points": 0,
"standalone_forwarding": false
},
"plutonium_page": {
"enabled": false,
"config_version": 0,
"rollout_basis_points": 0,
"rollout_salt": "plutonium-page-v1",
"included_user_ids": [],
"excluded_user_ids": []
},
"experiment_delivery": {
"poll_interval_seconds": 300,
"poll_jitter_percent": 15
@@ -322,11 +322,8 @@ fn admin_user() -> Value {
"premium_until": null,
"premium_grace_ends_at": null,
"premium_lifetime_sequence": null,
"suspicious_activity_flags": 0,
"phone_verification_deferred": false,
"has_totp": false,
"authenticator_types": [],
"has_verified_phone": false,
"temp_banned_until": null,
"pending_deletion_at": null,
"pending_bulk_message_deletion_at": null,
@@ -16,7 +16,6 @@
"email": "[email protected]",
"email_verified": true,
"email_bounced": false,
"has_verified_phone": true,
"date_of_birth": "2000-01-01",
"locale": "en-US",
"premium_type": null,
@@ -24,8 +23,6 @@
"premium_until": null,
"premium_grace_ends_at": null,
"premium_lifetime_sequence": null,
"suspicious_activity_flags": 0,
"phone_verification_deferred": false,
"temp_banned_until": null,
"pending_deletion_at": null,
"pending_bulk_message_deletion_at": null,
@@ -17,7 +17,6 @@
"email": "[email protected]",
"email_verified": true,
"email_bounced": false,
"has_verified_phone": false,
"date_of_birth": null,
"locale": "en-US",
"premium_type": null,
@@ -25,8 +24,6 @@
"premium_until": null,
"premium_grace_ends_at": null,
"premium_lifetime_sequence": null,
"suspicious_activity_flags": 0,
"phone_verification_deferred": false,
"temp_banned_until": null,
"pending_deletion_at": null,
"pending_bulk_message_deletion_at": null,
@@ -17,7 +17,6 @@
"email": "[email protected]",
"email_verified": true,
"email_bounced": false,
"has_verified_phone": false,
"date_of_birth": null,
"locale": "en-US",
"premium_type": null,
@@ -25,8 +24,6 @@
"premium_until": null,
"premium_grace_ends_at": null,
"premium_lifetime_sequence": null,
"suspicious_activity_flags": 0,
"phone_verification_deferred": false,
"temp_banned_until": null,
"pending_deletion_at": null,
"pending_bulk_message_deletion_at": null,
@@ -294,11 +294,8 @@ fn admin_user() -> Value {
"premium_until": null,
"premium_grace_ends_at": null,
"premium_lifetime_sequence": null,
"suspicious_activity_flags": 0,
"phone_verification_deferred": false,
"has_totp": false,
"authenticator_types": [],
"has_verified_phone": false,
"temp_banned_until": null,
"pending_deletion_at": null,
"pending_bulk_message_deletion_at": null,
@@ -131,7 +131,6 @@ export const ELASTICSEARCH_INDEX_DEFINITIONS: Record<FluxerSearchIndexName, Elas
id: keyword(),
username: textWithKeyword(),
email: textWithKeyword(),
phone: textWithKeyword(),
discriminator: integer(),
isBot: bool(),
isSystem: bool(),
@@ -139,7 +138,6 @@ export const ELASTICSEARCH_INDEX_DEFINITIONS: Record<FluxerSearchIndexName, Elas
premiumType: integer(),
emailVerified: bool(),
emailBounced: bool(),
suspiciousActivityFlags: integer(),
acls: keyword(),
createdAt: long(),
lastActiveAt: long(),
@@ -36,9 +36,6 @@ function buildUserFilters(filters: UserSearchFilters): Array<ElasticsearchFilter
if (filters.hasAcl && filters.hasAcl.length > 0) {
clauses.push(...esAndTerms('acls', filters.hasAcl));
}
if (filters.minSuspiciousActivityFlags !== undefined) {
clauses.push(esRangeFilter('suspiciousActivityFlags', {gte: filters.minSuspiciousActivityFlags}));
}
if (filters.createdAtGreaterThanOrEqual !== undefined) {
clauses.push(esRangeFilter('createdAt', {gte: filters.createdAtGreaterThanOrEqual}));
}
@@ -65,7 +62,7 @@ export class ElasticsearchUserAdapter extends ElasticsearchIndexAdapter<UserSear
super({
client: options.client,
index: ELASTICSEARCH_INDEX_DEFINITIONS.users,
searchableFields: ['username', 'email', 'phone', 'id'],
searchableFields: ['username', 'email', 'id'],
buildFilters: buildUserFilters,
buildSort: buildUserSort,
lock: options.lock,
-13
View File
@@ -79,19 +79,6 @@ export class EmailService implements IEmailService {
});
}
async sendAccountDisabledForSuspiciousActivityEmail(
email: string,
username: string,
reason: string | null,
locale: string | null = null,
): Promise<boolean> {
return this.sendTemplatedEmail(email, 'account_disabled_suspicious', locale, {
username,
reason: optionalReason(reason),
forgotUrl: `${this.config.appBaseUrl}/forgot`,
});
}
async sendAccountTempBannedEmail(
email: string,
username: string,
@@ -16,12 +16,6 @@ export interface IEmailService {
location: string,
locale?: string | null,
): Promise<boolean>;
sendAccountDisabledForSuspiciousActivityEmail(
email: string,
username: string,
reason: string | null,
locale?: string | null,
): Promise<boolean>;
sendAccountTempBannedEmail(
email: string,
username: string,
@@ -69,16 +69,6 @@ export class TestEmailService implements ITestEmailService {
return this.record(email, 'ip_authorization', {token: authorizationToken, ip: ipAddress, location});
}
async sendAccountDisabledForSuspiciousActivityEmail(
email: string,
username: string,
reason: string | null,
_locale?: string | null,
): Promise<boolean> {
this.logger.info(`Account disabled email sent to ${email} for user ${username}, reason: ${reason ?? 'none'}`);
return this.record(email, 'account_disabled_suspicious', {reason: reason ?? ''});
}
async sendAccountTempBannedEmail(
email: string,
username: string,
@@ -19,7 +19,6 @@ const FIXTURE: {[K in EmailTemplateKey]: EmailTemplateVariables[K]} = {
account_deletion_cancelled: {username: 'testuser'},
account_deletion_scheduled_inactivity: {username: 'testuser', reason: 'Inactive', deletionDate: DATE},
account_deletion_scheduled_requested: {username: 'testuser', reason: 'Requested', deletionDate: DATE},
account_disabled_suspicious: {username: 'testuser', reason: 'Spam', forgotUrl: 'https://example.com/forgot'},
account_scheduled_deletion: {
username: 'testuser',
reason: 'Spam',
@@ -15,10 +15,6 @@ export const EMAIL_I18N_MESSAGES = {
subject: 'Your {product_name} account deletion is scheduled',
body: "Hello {username},\n\nAs you requested, your {product_name} account is scheduled for permanent deletion on:\n\n{deletionDate, date, full} at {deletionDate, time, short}{reason, select, null {} other {\n\nReason: {reason}}}\n\nYour account is locked until then. If you didn't request this, or you want to keep your account, contact {safety_email} from this email address before that date.\n\n– {product_name} Team",
},
account_disabled_suspicious: {
subject: 'Your {product_name} account has been temporarily disabled',
body: "Hello {username},\n\nWe temporarily disabled your {product_name} account because we detected suspicious activity.\n\n{reason, select,\n null {}\n other {Reason: {reason}}\n}\n\nTo regain access to your account, you'll need to reset your password:\n\n{forgotUrl}\n\nAfter you reset your password, you'll be able to log in again.\n\nIf you believe this was done in error, please contact our support team.\n\n– {product_name} Safety Team",
},
account_scheduled_deletion: {
subject: 'Your {product_name} account will be permanently deleted',
body: 'Hello {username},\n\nYour {product_name} account has been scheduled for permanent deletion due to violations of our Terms of Service or Community Guidelines.\n\nScheduled deletion: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Reason: {reason}}\n}\n\nThis is a serious enforcement action. Your account data will be permanently deleted on the scheduled date.\n\nPlease review:\n- Terms of Service: {termsUrl}\n- Community Guidelines: {guidelinesUrl}\n\nAppeals process:\nIf you believe this enforcement decision was incorrect or unjustified, you have 60 days to submit an appeal. Email {appeals_email} from this email address.\n\nIn your appeal:\n- Clearly explain why you believe the enforcement decision was incorrect or unjustified\n- Provide any relevant evidence or context\n\nA member of the {product_name} Safety Team will review your appeal and may pause the pending deletion until a final decision has been reached.\n\n– {product_name} Safety Team',
@@ -49,7 +45,7 @@ export const EMAIL_I18N_MESSAGES = {
},
email_change_revert: {
subject: 'Your {product_name} email was changed',
body: "Hello {username},\n\nThe email address on your {product_name} account was changed to {newEmail}.\n\nIf you made this change, no action is needed. If you didn't, you can revert the change and secure your account using this link:\n\n{revertUrl}\n\nThis will restore your previous email, sign you out everywhere, remove linked phone numbers, disable MFA, and require you to set a new password.\n\n– {product_name} Safety Team",
body: "Hello {username},\n\nThe email address on your {product_name} account was changed to {newEmail}.\n\nIf you made this change, no action is needed. If you didn't, you can revert the change and secure your account using this link:\n\n{revertUrl}\n\nThis will restore your previous email, sign you out everywhere, disable MFA, and require you to set a new password.\n\n– {product_name} Safety Team",
},
email_verification: {
subject: 'Verify your {product_name} email address',
@@ -4,7 +4,6 @@ export type EmailTemplateKey =
| 'account_deletion_cancelled'
| 'account_deletion_scheduled_inactivity'
| 'account_deletion_scheduled_requested'
| 'account_disabled_suspicious'
| 'account_scheduled_deletion'
| 'account_temp_banned'
| 'donation_confirmation'
@@ -14,11 +14,6 @@ export interface EmailTemplateVariables {
reason: string | null;
deletionDate: Date;
};
account_disabled_suspicious: {
username: string;
reason: string | null;
forgotUrl: string;
};
account_scheduled_deletion: {
username: string;
reason: string | null;
@@ -15,10 +15,6 @@ const EMAIL_I18N_AR_MESSAGES = defineEmailI18nLocaleMessages({
"subject": "تمت جدولة حذف حسابك في {product_name}",
"body": "مرحبًا {username}،\n\nبناءً على طلبك، تمت جدولة حسابك في {product_name} للحذف الدائم في:\n\n{deletionDate, date, full} الساعة {deletionDate, time, short}{reason, select, null {} other {\n\nالسبب: {reason}}}\n\nسيظل حسابك مقفلًا حتى ذلك الحين. إذا لم تطلب هذا، أو كنت ترغب في الاحتفاظ بحسابك، فاتصل بـ{safety_email} من عنوان البريد الإلكتروني هذا قبل ذلك التاريخ.\n\n– فريق {product_name}"
},
"account_disabled_suspicious": {
"subject": "تم تعطيل حسابك في {product_name} مؤقتًا",
"body": "مرحبًا {username}،\n\nلقد قمنا بتعطيل حسابك في {product_name} مؤقتًا لأننا اكتشفنا نشاطًا مشبوهًا.\n\n{reason, select,\n null {}\n other {السبب: {reason}}\n}\n\nلاستعادة الوصول إلى حسابك، ستحتاج إلى إعادة تعيين كلمة المرور الخاصة بك:\n\n{forgotUrl}\n\nبعد إعادة تعيين كلمة المرور الخاصة بك، ستتمكن من تسجيل الدخول مرة أخرى.\n\nإذا كنت تعتقد أن هذا حدث عن طريق الخطأ، يرجى الاتصال بفريق الدعم لدينا.\n\n– فريق أمان {product_name}"
},
"account_scheduled_deletion": {
"subject": "سيتم حذف حسابك في {product_name} نهائيًا",
"body": "مرحبًا {username}،\n\nتمت جدولة حسابك في {product_name} للحذف الدائم بسبب انتهاكات لشروط الخدمة أو إرشادات المجتمع الخاصة بنا.\n\nالحذف المجدول: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {السبب: {reason}}\n}\n\nهذا إجراء إنفاذ جاد. سيتم حذف بيانات حسابك نهائيًا في التاريخ المحدد.\n\nيرجى مراجعة:\n- شروط الخدمة: {termsUrl}\n- إرشادات المجتمع: {guidelinesUrl}\n\nعملية الاستئناف:\nإذا كنت تعتقد أن قرار الإنفاذ هذا كان غير صحيح أو غير مبرر، لديك 60 يومًا لتقديم استئناف. أرسل بريدًا إلكترونيًا إلى {appeals_email} من عنوان البريد الإلكتروني هذا.\n\nفي استئنافك:\n- اشرح بوضوح سبب اعتقادك أن قرار الإنفاذ كان غير صحيح أو غير مبرر\n- قدم أي دليل أو سياق ذي صلة\n\nسيقوم عضو من فريق أمان {product_name} بمراجعة استئنافك وقد يوقف الحذف المعلق مؤقتًا إلى حين التوصل إلى قرار نهائي.\n\n– فريق أمان {product_name}"
@@ -49,7 +45,7 @@ const EMAIL_I18N_AR_MESSAGES = defineEmailI18nLocaleMessages({
},
"email_change_revert": {
"subject": "تم تغيير بريدك الإلكتروني في {product_name}",
"body": "مرحبًا {username}،\n\nتم تغيير عنوان البريد الإلكتروني لحسابك في {product_name} إلى {newEmail}.\n\nإذا قمت بهذا التغيير، فلا داعي لاتخاذ أي إجراء. إذا لم تقم بذلك، يمكنك التراجع عن التغيير وتأمين حسابك باستخدام هذا الرابط:\n\n{revertUrl}\n\nسيؤدي هذا إلى استعادة بريدك الإلكتروني السابق، وتسجيل خروجك من جميع الأجهزة، وإزالة أرقام الهواتف المرتبطة، وتعطيل المصادقة متعددة العوامل، وسيُطلب منك تعيين كلمة مرور جديدة.\n\n– فريق أمان {product_name}"
"body": "مرحبًا {username}،\n\nتم تغيير عنوان البريد الإلكتروني لحسابك في {product_name} إلى {newEmail}.\n\nإذا قمت بهذا التغيير، فلا داعي لاتخاذ أي إجراء. إذا لم تقم بذلك، يمكنك التراجع عن التغيير وتأمين حسابك باستخدام هذا الرابط:\n\n{revertUrl}\n\nسيؤدي هذا إلى استعادة بريدك الإلكتروني السابق، وتسجيل خروجك من جميع الأجهزة، وتعطيل المصادقة متعددة العوامل، وسيُطلب منك تعيين كلمة مرور جديدة.\n\n– فريق أمان {product_name}"
},
"email_verification": {
"subject": "تحقق من بريدك الإلكتروني في {product_name}",
@@ -15,10 +15,6 @@ const EMAIL_I18N_BG_MESSAGES = defineEmailI18nLocaleMessages({
"subject": "Изтриването на акаунта ти във {product_name} е насрочено",
"body": "Здравей, {username},\n\nКакто поиска, акаунтът ти във {product_name} е насрочен за постоянно изтриване на:\n\n{deletionDate, date, full} в {deletionDate, time, short}{reason, select, null {} other {\n\nПричина: {reason}}}\n\nДотогава акаунтът ти е заключен. Ако не си поискал това или искаш да запазиш акаунта си, свържи се с {safety_email} от този имейл адрес преди тази дата.\n\n– Екип на {product_name}"
},
"account_disabled_suspicious": {
"subject": "Акаунтът ти във {product_name} е временно деактивиран",
"body": "Здравей, {username},\n\nВременно деактивирахме акаунта ти във {product_name}, защото открихме подозрителна активност.\n\n{reason, select,\n null {}\n other {Причина: {reason}}\n}\n\nЗа да възстановиш достъпа до акаунта си, трябва да нулираш паролата си:\n\n{forgotUrl}\n\nСлед като нулираш паролата си, ще можеш да влезеш отново.\n\nАко смяташ, че това е грешка, свържи се с нашия екип за поддръжка.\n\n– Екип за безопасност на {product_name}"
},
"account_scheduled_deletion": {
"subject": "Акаунтът ти във {product_name} ще бъде изтрит за постоянно",
"body": "Здравей, {username},\n\nАкаунтът ти във {product_name} е насрочен за постоянно изтриване поради нарушения на нашите Общи условия или Насоки на общността.\n\nНасрочено изтриване: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Причина: {reason}}\n}\n\nТова е сериозна мярка. Данните на акаунта ти ще бъдат изтрити за постоянно на насрочената дата.\n\nПрегледай:\n- Общи условия: {termsUrl}\n- Насоки на общността: {guidelinesUrl}\n\nПроцес на обжалване:\nАко смяташ, че това решение е неправилно или необосновано, имаш 60 дни да подадеш обжалване. Изпрати имейл на {appeals_email} от този имейл адрес.\n\nВ обжалването си:\n- Обясни ясно защо смяташ, че решението е неправилно или необосновано\n- Приложи всички релевантни доказателства или контекст\n\nЧлен на екипа за безопасност на {product_name} ще прегледа обжалването ти и може да спре предстоящото изтриване, докато не бъде взето окончателно решение.\n\n– Екип за безопасност на {product_name}"
@@ -49,7 +45,7 @@ const EMAIL_I18N_BG_MESSAGES = defineEmailI18nLocaleMessages({
},
"email_change_revert": {
"subject": "Имейлът ти във {product_name} беше променен",
"body": "Здравей, {username},\n\nИмейл адресът на акаунта ти във {product_name} беше променен на {newEmail}.\n\nАко ти си направил тази промяна, не е необходимо да предприемаш никакви действия. Ако не си, можеш да отмениш промяната и да защитиш акаунта си, като използваш този линк:\n\n{revertUrl}\n\nТова ще възстанови предишния ти имейл, ще прекрати всички активни сесии, ще премахне свързаните телефонни номера, ще деактивира MFA и ще изиска да зададеш нова парола.\n\n– Екип за безопасност на {product_name}"
"body": "Здравей, {username},\n\nИмейл адресът на акаунта ти във {product_name} беше променен на {newEmail}.\n\nАко ти си направил тази промяна, не е необходимо да предприемаш никакви действия. Ако не си, можеш да отмениш промяната и да защитиш акаунта си, като използваш този линк:\n\n{revertUrl}\n\nТова ще възстанови предишния ти имейл, ще прекрати всички активни сесии, ще деактивира MFA и ще изиска да зададеш нова парола.\n\n– Екип за безопасност на {product_name}"
},
"email_verification": {
"subject": "Потвърди имейл адреса си във {product_name}",
@@ -15,10 +15,6 @@ const EMAIL_I18N_CS_MESSAGES = defineEmailI18nLocaleMessages({
"subject": "Smazání vašeho účtu {product_name} je naplánováno",
"body": "Dobrý den, {username},\n\nNa vaši žádost je trvalé smazání vašeho účtu {product_name} naplánováno na:\n\n{deletionDate, date, full} v {deletionDate, time, short}{reason, select, null {} other {\n\nDůvod: {reason}}}\n\nDo té doby je váš účet uzamčen. Pokud jste o smazání nežádali nebo si chcete účet ponechat, kontaktujte před tímto datem {safety_email} z této e-mailové adresy.\n\n– Tým {product_name}"
},
"account_disabled_suspicious": {
"subject": "Váš účet {product_name} byl dočasně deaktivován",
"body": "Dobrý den, {username},\n\nDočasně jsme deaktivovali váš účet {product_name}, protože jsme zaznamenali podezřelou aktivitu.\n\n{reason, select,\n null {}\n other {Důvod: {reason}}\n}\n\nAbyste znovu získali přístup k účtu, musíte si nastavit nové heslo:\n\n{forgotUrl}\n\nPo změně hesla se budete moci znovu přihlásit.\n\nPokud se domníváte, že jde o chybu, kontaktujte náš tým podpory.\n\n– Bezpečnostní tým {product_name}"
},
"account_scheduled_deletion": {
"subject": "Váš účet {product_name} bude trvale smazán",
"body": "Dobrý den, {username},\n\nKvůli porušení našich podmínek služby nebo komunitních pravidel bylo naplánováno trvalé smazání vašeho účtu {product_name}.\n\nNaplánované smazání: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Důvod: {reason}}\n}\n\nJedná se o závažné opatření. Data vašeho účtu budou v naplánovaný den trvale smazána.\n\nPřečtěte si:\n- Podmínky služby: {termsUrl}\n- Komunitní pravidla: {guidelinesUrl}\n\nPostup odvolání:\nPokud se domníváte, že toto rozhodnutí bylo nesprávné nebo neopodstatněné, máte 60 dní na podání odvolání. Pošlete e-mail na {appeals_email} z této e-mailové adresy.\n\nV odvolání:\n- Jasně vysvětlete, proč se domníváte, že rozhodnutí bylo nesprávné nebo neopodstatněné\n- Uveďte všechny relevantní důkazy a souvislosti\n\nČlen bezpečnostního týmu {product_name} vaše odvolání přezkoumá a může pozastavit plánované smazání, dokud nepadne konečné rozhodnutí.\n\n– Bezpečnostní tým {product_name}"
@@ -49,7 +45,7 @@ const EMAIL_I18N_CS_MESSAGES = defineEmailI18nLocaleMessages({
},
"email_change_revert": {
"subject": "Vaše e-mailová adresa pro {product_name} byla změněna",
"body": "Dobrý den, {username},\n\nE-mailová adresa vašeho účtu {product_name} byla změněna na {newEmail}.\n\nPokud jste tuto změnu provedli vy, nemusíte nic dělat. Pokud ne, můžete ji vrátit zpět a zabezpečit svůj účet pomocí tohoto odkazu:\n\n{revertUrl}\n\nTím se obnoví vaše předchozí e-mailová adresa, budete odhlášeni ze všech zařízení, odstraní se propojená telefonní čísla, vypne se vícefaktorové ověřování a budete muset nastavit nové heslo.\n\n– Bezpečnostní tým {product_name}"
"body": "Dobrý den, {username},\n\nE-mailová adresa vašeho účtu {product_name} byla změněna na {newEmail}.\n\nPokud jste tuto změnu provedli vy, nemusíte nic dělat. Pokud ne, můžete ji vrátit zpět a zabezpečit svůj účet pomocí tohoto odkazu:\n\n{revertUrl}\n\nTím se obnoví vaše předchozí e-mailová adresa, budete odhlášeni ze všech zařízení, vypne se vícefaktorové ověřování a budete muset nastavit nové heslo.\n\n– Bezpečnostní tým {product_name}"
},
"email_verification": {
"subject": "Ověřte svou e-mailovou adresu pro {product_name}",
@@ -15,10 +15,6 @@ const EMAIL_I18N_DA_MESSAGES = defineEmailI18nLocaleMessages({
"subject": "Sletningen af din {product_name}-konto er planlagt",
"body": "Hej {username},\n\nSom du har anmodet om, slettes din {product_name}-konto permanent den:\n\n{deletionDate, date, full} kl. {deletionDate, time, short}{reason, select, null {} other {\n\nÅrsag: {reason}}}\n\nDin konto er låst indtil da. Hvis du ikke har anmodet om dette, eller hvis du vil beholde din konto, skal du kontakte {safety_email} fra denne e-mailadresse inden denne dato.\n\n– Teamet bag {product_name}"
},
"account_disabled_suspicious": {
"subject": "Din {product_name}-konto er midlertidigt deaktiveret",
"body": "Hej {username},\n\nVi har midlertidigt deaktiveret din {product_name}-konto, fordi vi har registreret mistænkelig aktivitet.\n\n{reason, select,\n null {}\n other {Årsag: {reason}}\n}\n\nFor at få adgang til din konto igen skal du nulstille din adgangskode:\n\n{forgotUrl}\n\nNår du har nulstillet din adgangskode, kan du logge ind igen.\n\nHvis du mener, at dette er sket ved en fejl, kan du kontakte vores supportteam.\n\n– Sikkerhedsteamet hos {product_name}"
},
"account_scheduled_deletion": {
"subject": "Din {product_name}-konto slettes permanent",
"body": "Hej {username},\n\nVi har planlagt at slette din {product_name}-konto permanent på grund af overtrædelser af vores servicevilkår eller retningslinjer for fællesskabet.\n\nPlanlagt sletning: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Årsag: {reason}}\n}\n\nDette er en alvorlig sanktion. Dine kontodata bliver permanent slettet på den planlagte dato.\n\nGennemgå:\n- Servicevilkår: {termsUrl}\n- Retningslinjer for fællesskabet: {guidelinesUrl}\n\nKlageproces:\nHvis du mener, at denne beslutning om sanktioner var forkert eller uberettiget, har du 60 dage til at indsende en klage. Send en e-mail til {appeals_email} fra denne e-mailadresse.\n\nI din klage:\n- Forklar tydeligt, hvorfor du mener, at beslutning om sanktioneren var forkert eller uberettiget\n- Fremlæg relevant dokumentation eller kontekst\n\nEt medlem af sikkerhedsteamet hos {product_name} vil gennemgå din klage og kan sætte den afventende sletning på pause, indtil en endelig beslutning er truffet.\n\n– Sikkerhedsteamet hos {product_name}"
@@ -49,7 +45,7 @@ const EMAIL_I18N_DA_MESSAGES = defineEmailI18nLocaleMessages({
},
"email_change_revert": {
"subject": "Din e-mailadresse til {product_name} er ændret",
"body": "Hej {username},\n\nE-mailadressen på din {product_name}-konto blev ændret til {newEmail}.\n\nHvis du selv har foretaget ændringen, behøver du ikke gøre noget. Ellers kan du fortryde ændringen og sikre din konto ved at bruge dette link:\n\n{revertUrl}\n\nDette vil gendanne din tidligere e-mailadresse, logge dig ud overalt, fjerne tilknyttede telefonnumre, slå multifaktorgodkendelse fra og kræve, at du vælger en ny adgangskode.\n\n– Sikkerhedsteamet hos {product_name}"
"body": "Hej {username},\n\nE-mailadressen på din {product_name}-konto blev ændret til {newEmail}.\n\nHvis du selv har foretaget ændringen, behøver du ikke gøre noget. Ellers kan du fortryde ændringen og sikre din konto ved at bruge dette link:\n\n{revertUrl}\n\nDette vil gendanne din tidligere e-mailadresse, logge dig ud overalt, slå multifaktorgodkendelse fra og kræve, at du vælger en ny adgangskode.\n\n– Sikkerhedsteamet hos {product_name}"
},
"email_verification": {
"subject": "Bekræft din {product_name}-e-mailadresse",
@@ -15,10 +15,6 @@ const EMAIL_I18N_DE_MESSAGES = defineEmailI18nLocaleMessages({
"subject": "Die Löschung deines {product_name}-Accounts ist geplant",
"body": "Hallo {username},\n\nwie von dir beantragt, wird dein {product_name}-Account an folgendem Termin dauerhaft gelöscht:\n\n{deletionDate, date, full} um {deletionDate, time, short}{reason, select, null {} other {\n\nGrund: {reason}}}\n\nDein Account ist bis dahin gesperrt. Wenn du das nicht beantragt hast oder deinen Account behalten möchtest, kontaktiere vor diesem Termin {safety_email} von dieser E-Mail-Adresse aus.\n\n– {product_name}-Team"
},
"account_disabled_suspicious": {
"subject": "Dein {product_name}-Account wurde vorübergehend deaktiviert",
"body": "Hallo {username},\n\nwir haben deinen {product_name}-Account vorübergehend deaktiviert, da wir verdächtige Aktivitäten festgestellt haben.\n\n{reason, select,\n null {}\n other {Grund: {reason}}\n}\n\nUm wieder Zugriff auf deinen Account zu erhalten, musst du dein Passwort zurücksetzen:\n\n{forgotUrl}\n\nNachdem du dein Passwort zurückgesetzt hast, kannst du dich wieder anmelden.\n\nWenn du glaubst, dass dies ein Fehler war, kontaktiere bitte unser Support-Team.\n\n– {product_name}-Sicherheitsteam"
},
"account_scheduled_deletion": {
"subject": "Dein {product_name}-Account wird dauerhaft gelöscht",
"body": "Hallo {username},\n\ndein {product_name}-Account wurde aufgrund von Verstößen gegen unsere Nutzungsbedingungen oder Community-Richtlinien zur dauerhaften Löschung vorgemerkt.\n\nGeplante Löschung: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Grund: {reason}}\n}\n\nDies ist eine ernsthafte Maßnahme. Deine Accountdaten werden am geplanten Datum dauerhaft gelöscht.\n\nBitte lies dir Folgendes durch:\n- Nutzungsbedingungen: {termsUrl}\n- Community-Richtlinien: {guidelinesUrl}\n\nEinspruchsverfahren:\nWenn du glaubst, dass diese Entscheidung falsch oder ungerechtfertigt war, hast du 60 Tage Zeit, Einspruch einzulegen. Sende eine E-Mail von dieser E-Mail-Adresse an {appeals_email}.\n\nIn deinem Einspruch:\n- Erkläre klar, warum du glaubst, dass die Entscheidung falsch oder ungerechtfertigt war\n- Füge alle relevanten Beweise oder Kontextinformationen bei\n\nEin Mitglied des {product_name}-Sicherheitsteams wird deinen Einspruch prüfen und die geplante Löschung möglicherweise aussetzen, bis eine endgültige Entscheidung getroffen wurde.\n\n– {product_name}-Sicherheitsteam"
@@ -49,7 +45,7 @@ const EMAIL_I18N_DE_MESSAGES = defineEmailI18nLocaleMessages({
},
"email_change_revert": {
"subject": "Deine {product_name}-E-Mail-Adresse wurde geändert",
"body": "Hallo {username},\n\ndie E-Mail-Adresse deines {product_name}-Accounts wurde in {newEmail} geändert.\n\nWenn du diese Änderung vorgenommen hast, ist keine weitere Aktion erforderlich. Wenn nicht, kannst du die Änderung rückgängig machen und deinen Account über diesen Link absichern:\n\n{revertUrl}\n\nDadurch wird deine vorherige E-Mail-Adresse wiederhergestellt, du wirst überall abgemeldet, verknüpfte Telefonnummern werden entfernt, MFA wird deaktiviert und du musst ein neues Passwort festlegen.\n\n– {product_name}-Sicherheitsteam"
"body": "Hallo {username},\n\ndie E-Mail-Adresse deines {product_name}-Accounts wurde in {newEmail} geändert.\n\nWenn du diese Änderung vorgenommen hast, ist keine weitere Aktion erforderlich. Wenn nicht, kannst du die Änderung rückgängig machen und deinen Account über diesen Link absichern:\n\n{revertUrl}\n\nDadurch wird deine vorherige E-Mail-Adresse wiederhergestellt, du wirst überall abgemeldet, MFA wird deaktiviert und du musst ein neues Passwort festlegen.\n\n– {product_name}-Sicherheitsteam"
},
"email_verification": {
"subject": "Bestätige deine {product_name}-E-Mail-Adresse",
@@ -15,10 +15,6 @@ const EMAIL_I18N_EL_MESSAGES = defineEmailI18nLocaleMessages({
"subject": "Η διαγραφή του λογαριασμού σου στο {product_name} είναι προγραμματισμένη",
"body": "Γεια σου {username},\n\nΌπως ζήτησες, ο λογαριασμός σου στο {product_name} έχει προγραμματιστεί για οριστική διαγραφή:\n\n{deletionDate, date, full} και ώρα {deletionDate, time, short}{reason, select, null {} other {\n\nΛόγος: {reason}}}\n\nΜέχρι τότε ο λογαριασμός σου είναι κλειδωμένος. Εάν δεν το ζήτησες ή θέλεις να διατηρήσεις τον λογαριασμό σου, στείλε email στο {safety_email} από αυτή τη διεύθυνση email πριν από αυτή την ημερομηνία.\n\n– Η ομάδα του {product_name}"
},
"account_disabled_suspicious": {
"subject": "Ο λογαριασμός σου στο {product_name} έχει απενεργοποιηθεί προσωρινά",
"body": "Γεια σου {username},\n\nΑπενεργοποιήσαμε προσωρινά τον λογαριασμό σου στο {product_name} επειδή εντοπίσαμε ύποπτη δραστηριότητα.\n\n{reason, select,\n null {}\n other {Λόγος: {reason}}\n}\n\nΓια να αποκτήσεις ξανά πρόσβαση στον λογαριασμό σου, θα πρέπει να επαναφέρεις τον κωδικό πρόσβασής σου:\n\n{forgotUrl}\n\nΑφού επαναφέρεις τον κωδικό πρόσβασής σου, θα μπορείς να συνδεθείς ξανά.\n\nΕάν πιστεύεις ότι αυτό έγινε κατά λάθος, επικοινώνησε με την ομάδα υποστήριξής μας.\n\n– Η ομάδα ασφαλείας του {product_name}"
},
"account_scheduled_deletion": {
"subject": "Ο λογαριασμός σου στο {product_name} θα διαγραφεί οριστικά",
"body": "Γεια σου {username},\n\nΟ λογαριασμός σου στο {product_name} έχει προγραμματιστεί για οριστική διαγραφή λόγω παραβιάσεων των Όρων παροχής υπηρεσιών ή των Οδηγιών κοινότητας.\n\nΠρογραμματισμένη διαγραφή: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Λόγος: {reason}}\n}\n\nΠρόκειται για σοβαρό μέτρο. Τα δεδομένα του λογαριασμού σου θα διαγραφούν οριστικά την προγραμματισμένη ημερομηνία.\n\nΈλεγξε:\n- Όροι παροχής υπηρεσιών: {termsUrl}\n- Οδηγίες κοινότητας: {guidelinesUrl}\n\nΔιαδικασία προσφυγής:\nΕάν πιστεύεις ότι αυτή η απόφαση ήταν λανθασμένη ή αδικαιολόγητη, έχεις 60 ημέρες για να υποβάλεις προσφυγή. Στείλε email στο {appeals_email} από αυτήν τη διεύθυνση email.\n\nΣτην προσφυγή σου:\n- Εξήγησε σαφώς γιατί πιστεύεις ότι η απόφαση ήταν λανθασμένη ή αδικαιολόγητη\n- Παράθεσε τυχόν σχετικά αποδεικτικά στοιχεία ή πλαίσιο\n\nΈνα μέλος της ομάδας ασφαλείας του {product_name} θα εξετάσει την προσφυγή σου και ενδέχεται να αναστείλει την εκκρεμή διαγραφή μέχρι να ληφθεί τελική απόφαση.\n\n– Η ομάδα ασφαλείας του {product_name}"
@@ -49,7 +45,7 @@ const EMAIL_I18N_EL_MESSAGES = defineEmailI18nLocaleMessages({
},
"email_change_revert": {
"subject": "Το email σου στο {product_name} άλλαξε",
"body": "Γεια σου {username},\n\nΗ διεύθυνση email του λογαριασμού σου στο {product_name} άλλαξε σε {newEmail}.\n\nΕάν έκανες εσύ αυτήν την αλλαγή, δεν χρειάζεται να κάνεις τίποτα. Εάν όχι, μπορείς να αναιρέσεις την αλλαγή και να ασφαλίσεις τον λογαριασμό σου με αυτόν τον σύνδεσμο:\n\n{revertUrl}\n\nΑυτό θα επαναφέρει το προηγούμενο email σου, θα σε αποσυνδέσει από παντού, θα αφαιρέσει τους συνδεδεμένους αριθμούς τηλεφώνου, θα απενεργοποιήσει το MFA και θα σου ζητήσει να ορίσεις νέο κωδικό πρόσβασης.\n\n– Η ομάδα ασφαλείας του {product_name}"
"body": "Γεια σου {username},\n\nΗ διεύθυνση email του λογαριασμού σου στο {product_name} άλλαξε σε {newEmail}.\n\nΕάν έκανες εσύ αυτήν την αλλαγή, δεν χρειάζεται να κάνεις τίποτα. Εάν όχι, μπορείς να αναιρέσεις την αλλαγή και να ασφαλίσεις τον λογαριασμό σου με αυτόν τον σύνδεσμο:\n\n{revertUrl}\n\nΑυτό θα επαναφέρει το προηγούμενο email σου, θα σε αποσυνδέσει από παντού, θα απενεργοποιήσει το MFA και θα σου ζητήσει να ορίσεις νέο κωδικό πρόσβασης.\n\n– Η ομάδα ασφαλείας του {product_name}"
},
"email_verification": {
"subject": "Επαλήθευσε τη διεύθυνση email σου στο {product_name}",
@@ -15,10 +15,6 @@ const EMAIL_I18N_EN_GB_MESSAGES = defineEmailI18nLocaleMessages({
"subject": "Your {product_name} account deletion is scheduled",
"body": "Hello {username},\n\nAs you requested, your {product_name} account is scheduled for permanent deletion on:\n\n{deletionDate, date, full} at {deletionDate, time, short}{reason, select, null {} other {\n\nReason: {reason}}}\n\nYour account is locked until then. If you didn't request this, or you want to keep your account, contact {safety_email} from this email address before that date.\n\n– {product_name} Team"
},
"account_disabled_suspicious": {
"subject": "Your {product_name} account has been temporarily disabled",
"body": "Hello {username},\n\nWe temporarily disabled your {product_name} account because we detected suspicious activity.\n\n{reason, select,\n null {}\n other {Reason: {reason}}\n}\n\nTo regain access to your account, you'll need to reset your password:\n\n{forgotUrl}\n\nAfter you reset your password, you'll be able to log in again.\n\nIf you believe this was done in error, please contact our support team.\n\n– {product_name} Safety Team"
},
"account_scheduled_deletion": {
"subject": "Your {product_name} account will be permanently deleted",
"body": "Hello {username},\n\nYour {product_name} account has been scheduled for permanent deletion due to violations of our Terms of Service or Community Guidelines.\n\nScheduled deletion: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Reason: {reason}}\n}\n\nThis is a serious enforcement action. Your account data will be permanently deleted on the scheduled date.\n\nPlease review:\n- Terms of Service: {termsUrl}\n- Community Guidelines: {guidelinesUrl}\n\nAppeals process:\nIf you believe this enforcement decision was incorrect or unjustified, you have 60 days to submit an appeal. Email {appeals_email} from this email address.\n\nIn your appeal:\n- Clearly explain why you believe the enforcement decision was incorrect or unjustified\n- Provide any relevant evidence or context\n\nA member of the {product_name} Safety Team will review your appeal and may pause the pending deletion until a final decision has been reached.\n\n– {product_name} Safety Team"
@@ -49,7 +45,7 @@ const EMAIL_I18N_EN_GB_MESSAGES = defineEmailI18nLocaleMessages({
},
"email_change_revert": {
"subject": "Your {product_name} email was changed",
"body": "Hello {username},\n\nThe email address on your {product_name} account was changed to {newEmail}.\n\nIf you made this change, no action is needed. If you didn't, you can revert the change and secure your account using this link:\n\n{revertUrl}\n\nThis will restore your previous email, sign you out everywhere, remove linked phone numbers, disable MFA, and require you to set a new password.\n\n– {product_name} Safety Team"
"body": "Hello {username},\n\nThe email address on your {product_name} account was changed to {newEmail}.\n\nIf you made this change, no action is needed. If you didn't, you can revert the change and secure your account using this link:\n\n{revertUrl}\n\nThis will restore your previous email, sign you out everywhere, disable MFA, and require you to set a new password.\n\n– {product_name} Safety Team"
},
"email_verification": {
"subject": "Verify your {product_name} email address",
@@ -15,10 +15,6 @@ const EMAIL_I18N_ES_419_MESSAGES = defineEmailI18nLocaleMessages({
"subject": "La eliminación de tu cuenta de {product_name} está programada",
"body": "Hola {username}:\n\nSegún lo que solicitaste, la eliminación permanente de tu cuenta de {product_name} está programada para el:\n\n{deletionDate, date, full} a las {deletionDate, time, short}{reason, select, null {} other {\n\nMotivo: {reason}}}\n\nTu cuenta permanecerá bloqueada hasta entonces. Si no solicitaste esto, o si quieres conservar tu cuenta, comunícate con {safety_email} desde esta dirección de correo electrónico antes de esa fecha.\n\n– Equipo de {product_name}"
},
"account_disabled_suspicious": {
"subject": "Tu cuenta de {product_name} ha sido deshabilitada temporalmente",
"body": "Hola {username}:\n\nDeshabilitamos temporalmente tu cuenta de {product_name} porque detectamos actividad sospechosa.\n\n{reason, select,\n null {}\n other {Motivo: {reason}}\n}\n\nPara recuperar el acceso a tu cuenta, deberás restablecer tu contraseña:\n\n{forgotUrl}\n\nDespués de restablecer tu contraseña, podrás iniciar sesión de nuevo.\n\nSi crees que esto fue un error, comunícate con nuestro equipo de soporte.\n\n– Equipo de seguridad de {product_name}"
},
"account_scheduled_deletion": {
"subject": "Tu cuenta de {product_name} será eliminada permanentemente",
"body": "Hola {username}:\n\nSe ha programado la eliminación permanente de tu cuenta de {product_name} debido a infracciones de nuestros Términos del servicio o Normas de la comunidad.\n\nEliminación programada: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Motivo: {reason}}\n}\n\nEsta es una medida grave. Los datos de tu cuenta se eliminarán permanentemente en la fecha programada.\n\nPor favor, revisa:\n- Términos del servicio: {termsUrl}\n- Normas de la comunidad: {guidelinesUrl}\n\nProceso de apelación:\nSi crees que esta decisión fue incorrecta o injustificada, tienes 60 días para presentar una apelación. Envía un correo electrónico a {appeals_email} desde esta dirección de correo electrónico.\n\nEn tu apelación:\n- Explica claramente por qué crees que la decisión fue incorrecta o injustificada\n- Aporta las pruebas o el contexto que consideres relevantes\n\nUn miembro del Equipo de seguridad de {product_name} revisará tu apelación y podrá pausar la eliminación pendiente hasta que se haya tomado una decisión final.\n\n– Equipo de seguridad de {product_name}"
@@ -49,7 +45,7 @@ const EMAIL_I18N_ES_419_MESSAGES = defineEmailI18nLocaleMessages({
},
"email_change_revert": {
"subject": "Se cambió tu correo electrónico en {product_name}",
"body": "Hola {username}:\n\nLa dirección de correo electrónico de tu cuenta de {product_name} se cambió a {newEmail}.\n\nSi realizaste este cambio, no es necesario que hagas nada. Si no lo hiciste, puedes revertir el cambio y proteger tu cuenta usando este enlace:\n\n{revertUrl}\n\nEsto restaurará tu correo electrónico anterior, cerrará tu sesión en todas partes, eliminará los números de teléfono vinculados, deshabilitará la autenticación multifactor y te pedirá que establezcas una nueva contraseña.\n\n– Equipo de seguridad de {product_name}"
"body": "Hola {username}:\n\nLa dirección de correo electrónico de tu cuenta de {product_name} se cambió a {newEmail}.\n\nSi realizaste este cambio, no es necesario que hagas nada. Si no lo hiciste, puedes revertir el cambio y proteger tu cuenta usando este enlace:\n\n{revertUrl}\n\nEsto restaurará tu correo electrónico anterior, cerrará tu sesión en todas partes, deshabilitará la autenticación multifactor y te pedirá que establezcas una nueva contraseña.\n\n– Equipo de seguridad de {product_name}"
},
"email_verification": {
"subject": "Verifica tu dirección de correo electrónico en {product_name}",
@@ -15,10 +15,6 @@ const EMAIL_I18N_ES_ES_MESSAGES = defineEmailI18nLocaleMessages({
"subject": "La eliminación de tu cuenta de {product_name} está programada",
"body": "Hola {username}:\n\nTal y como solicitaste, la eliminación permanente de tu cuenta de {product_name} está programada para el:\n\n{deletionDate, date, full} a las {deletionDate, time, short}{reason, select, null {} other {\n\nMotivo: {reason}}}\n\nTu cuenta permanecerá bloqueada hasta entonces. Si no solicitaste esto, o si quieres conservar tu cuenta, ponte en contacto con {safety_email} desde esta dirección de correo electrónico antes de esa fecha.\n\n– Equipo de {product_name}"
},
"account_disabled_suspicious": {
"subject": "Tu cuenta de {product_name} ha sido deshabilitada temporalmente",
"body": "Hola {username}:\n\nHemos deshabilitado temporalmente tu cuenta de {product_name} porque hemos detectado actividad sospechosa.\n\n{reason, select,\n null {}\n other {Motivo: {reason}}\n}\n\nPara recuperar el acceso a tu cuenta, tendrás que restablecer tu contraseña:\n\n{forgotUrl}\n\nDespués de restablecer tu contraseña, podrás iniciar sesión de nuevo.\n\nSi crees que esto ha sido un error, ponte en contacto con nuestro equipo de soporte.\n\n– Equipo de seguridad de {product_name}"
},
"account_scheduled_deletion": {
"subject": "Tu cuenta de {product_name} será eliminada permanentemente",
"body": "Hola {username}:\n\nSe ha programado la eliminación permanente de tu cuenta de {product_name} debido a infracciones de nuestras Condiciones del servicio o Normas de la comunidad.\n\nEliminación programada: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Motivo: {reason}}\n}\n\nEsta es una medida grave. Los datos de tu cuenta se eliminarán permanentemente en la fecha programada.\n\nPor favor, revisa:\n- Condiciones del servicio: {termsUrl}\n- Normas de la comunidad: {guidelinesUrl}\n\nProceso de apelación:\nSi crees que esta decisión fue incorrecta o injustificada, tienes 60 días para presentar una apelación. Envía un correo electrónico a {appeals_email} desde esta dirección de correo electrónico.\n\nEn tu apelación:\n- Explica claramente por qué crees que la decisión fue incorrecta o injustificada\n- Aporta las pruebas o el contexto que consideres relevantes\n\nUn miembro del Equipo de seguridad de {product_name} revisará tu apelación y podrá pausar la eliminación pendiente hasta que se haya tomado una decisión final.\n\n– Equipo de seguridad de {product_name}"
@@ -49,7 +45,7 @@ const EMAIL_I18N_ES_ES_MESSAGES = defineEmailI18nLocaleMessages({
},
"email_change_revert": {
"subject": "Se ha cambiado tu correo electrónico en {product_name}",
"body": "Hola {username}:\n\nLa dirección de correo electrónico de tu cuenta de {product_name} ha sido cambiada a {newEmail}.\n\nSi realizaste este cambio, no es necesario que hagas nada. Si no lo hiciste, puedes revertir el cambio y proteger tu cuenta usando este enlace:\n\n{revertUrl}\n\nEsto restaurará tu correo electrónico anterior, cerrará tu sesión en todas partes, eliminará los números de teléfono vinculados, deshabilitará la autenticación multifactor y te pedirá que establezcas una nueva contraseña.\n\n– Equipo de seguridad de {product_name}"
"body": "Hola {username}:\n\nLa dirección de correo electrónico de tu cuenta de {product_name} ha sido cambiada a {newEmail}.\n\nSi realizaste este cambio, no es necesario que hagas nada. Si no lo hiciste, puedes revertir el cambio y proteger tu cuenta usando este enlace:\n\n{revertUrl}\n\nEsto restaurará tu correo electrónico anterior, cerrará tu sesión en todas partes, deshabilitará la autenticación multifactor y te pedirá que establezcas una nueva contraseña.\n\n– Equipo de seguridad de {product_name}"
},
"email_verification": {
"subject": "Verifica tu dirección de correo electrónico en {product_name}",
@@ -15,10 +15,6 @@ const EMAIL_I18N_FI_MESSAGES = defineEmailI18nLocaleMessages({
"subject": "{product_name}-tilisi poistaminen on ajoitettu",
"body": "Hei {username},\n\nPyyntösi mukaisesti {product_name}-tilisi on ajoitettu poistettavaksi pysyvästi:\n\n{deletionDate, date, full} klo {deletionDate, time, short}{reason, select, null {} other {\n\nSyy: {reason}}}\n\nTilisi on lukittu siihen asti. Jos et pyytänyt tätä tai haluat säilyttää tilisi, ota yhteyttä osoitteeseen {safety_email} tästä sähköpostiosoitteesta ennen tätä päivämäärää.\n\n– {product_name}-tiimi"
},
"account_disabled_suspicious": {
"subject": "Käyttäjätilisi {product_name}-palvelussa on tilapäisesti poistettu käytöstä",
"body": "Hei {username},\n\nOlemme poistaneet {product_name}-tilisi tilapäisesti käytöstä, koska havaitsimme epäilyttävää toimintaa.\n\n{reason, select,\n null {}\n other {Syy: {reason}}\n}\n\nPäästäksesi takaisin tilillesi sinun on nollattava salasanasi:\n\n{forgotUrl}\n\nKun olet nollannut salasanasi, voit kirjautua sisään uudelleen.\n\nJos uskot, että tämä oli virhe, ota yhteyttä tukitiimiimme.\n\n– {product_name}-turvallisuustiimi"
},
"account_scheduled_deletion": {
"subject": "Käyttäjätilisi {product_name}-palvelussa poistetaan pysyvästi",
"body": "Hei {username},\n\n{product_name}-käyttäjätilisi on ajoitettu poistettavaksi pysyvästi käyttöehtojemme tai yhteisösääntöjemme rikkomisen vuoksi.\n\nAjoitettu poisto: {deletionDate, date, full} klo {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Syy: {reason}}\n}\n\nTämä on vakava toimenpide. Tilisi tiedot poistetaan pysyvästi ajoitettuna päivänä.\n\nTutustu:\n- Käyttöehdot: {termsUrl}\n- Yhteisön säännöt: {guidelinesUrl}\n\nValitusprosessi:\nJos uskot, että tämä päätös oli virheellinen tai perusteeton, sinulla on 60 päivää aikaa tehdä valitus. Lähetä sähköpostia osoitteeseen {appeals_email} tästä sähköpostiosoitteesta.\n\nValituksessasi:\n- Selitä selkeästi, miksi uskot päätöksen olleen virheellinen tai perusteeton\n- Liitä mukaan asiaankuuluvat todisteet tai taustatiedot\n\n{product_name}-turvallisuustiimin jäsen käsittelee valituksesi ja voi keskeyttää vireillä olevan poiston, kunnes lopullinen päätös on tehty.\n\n– {product_name}-turvallisuustiimi"
@@ -49,7 +45,7 @@ const EMAIL_I18N_FI_MESSAGES = defineEmailI18nLocaleMessages({
},
"email_change_revert": {
"subject": "{product_name}-tilisi sähköpostiosoite on vaihdettu",
"body": "Hei {username},\n\nSähköpostiosoitteesi {product_name}-tilillä on vaihdettu osoitteeseen {newEmail}.\n\nJos teit tämän muutoksen, toimenpiteitä ei tarvita. Jos et tehnyt, voit kumota muutoksen ja turvata tilisi käyttämällä tätä linkkiä:\n\n{revertUrl}\n\nTämä palauttaa edellisen sähköpostiosoitteesi, kirjaa sinut ulos kaikkialta, poistaa linkitetyt puhelinnumerot, poistaa MFA:n käytöstä ja edellyttää uuden salasanan asettamista.\n\n– {product_name}-turvallisuustiimi"
"body": "Hei {username},\n\nSähköpostiosoitteesi {product_name}-tilillä on vaihdettu osoitteeseen {newEmail}.\n\nJos teit tämän muutoksen, toimenpiteitä ei tarvita. Jos et tehnyt, voit kumota muutoksen ja turvata tilisi käyttämällä tätä linkkiä:\n\n{revertUrl}\n\nTämä palauttaa edellisen sähköpostiosoitteesi, kirjaa sinut ulos kaikkialta, poistaa MFA:n käytöstä ja edellyttää uuden salasanan asettamista.\n\n– {product_name}-turvallisuustiimi"
},
"email_verification": {
"subject": "Vahvista sähköpostiosoitteesi {product_name}-tilillä",
@@ -15,10 +15,6 @@ const EMAIL_I18N_FR_MESSAGES = defineEmailI18nLocaleMessages({
"subject": "La suppression de votre compte {product_name} est programmée",
"body": "Bonjour {username},\n\nComme vous l’avez demandé, la suppression définitive de votre compte {product_name} est prévue le :\n\n{deletionDate, date, full} à {deletionDate, time, short}{reason, select, null {} other {\n\nMotif : {reason}}}\n\nVotre compte est verrouillé d’ici là. Si vous n’êtes pas à l’origine de cette demande, ou si vous souhaitez conserver votre compte, contactez {safety_email} depuis cette adresse e-mail avant cette date.\n\n– L’équipe {product_name}"
},
"account_disabled_suspicious": {
"subject": "Votre compte {product_name} a été temporairement désactivé",
"body": "Bonjour {username},\n\nNous avons temporairement désactivé votre compte {product_name} après avoir détecté une activité suspecte.\n\n{reason, select, null {} other {Motif : {reason}}}\n\nPour retrouver l’accès à votre compte, réinitialisez votre mot de passe :\n\n{forgotUrl}\n\nVous pourrez ensuite vous connecter à nouveau.\n\nSi vous pensez qu’il s’agit d’une erreur, contactez notre équipe d’assistance.\n\n– L’équipe de sécurité de {product_name}"
},
"account_scheduled_deletion": {
"subject": "Votre compte {product_name} sera définitivement supprimé",
"body": "Bonjour {username},\n\nLa suppression définitive de votre compte {product_name} a été programmée en raison de violations de nos conditions d’utilisation ou des règles de la communauté.\n\nSuppression prévue : {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select, null {} other {Motif : {reason}}}\n\nIl s’agit d’une sanction importante. Les données de votre compte seront définitivement supprimées à la date prévue.\n\nVeuillez consulter :\n- Conditions d’utilisation : {termsUrl}\n- Règles de la communauté : {guidelinesUrl}\n\nProcédure de recours :\nSi vous estimez que cette décision est incorrecte ou injustifiée, vous disposez de 60 jours pour déposer un recours. Écrivez à {appeals_email} depuis cette adresse e-mail.\n\nDans votre recours :\n- Expliquez clairement pourquoi vous estimez la décision incorrecte ou injustifiée\n- Fournissez tout élément de preuve ou de contexte pertinent\n\nUn membre de l’équipe de sécurité de {product_name} examinera votre recours et pourra suspendre la suppression prévue jusqu’à la décision finale.\n\n– L’équipe de sécurité de {product_name}"
@@ -49,7 +45,7 @@ const EMAIL_I18N_FR_MESSAGES = defineEmailI18nLocaleMessages({
},
"email_change_revert": {
"subject": "Votre adresse e-mail {product_name} a été modifiée",
"body": "Bonjour {username},\n\nL’adresse e-mail de votre compte {product_name} a été remplacée par {newEmail}.\n\nSi vous avez effectué ce changement, vous n’avez rien à faire. Sinon, vous pouvez l’annuler et sécuriser votre compte avec ce lien :\n\n{revertUrl}\n\nVotre ancienne adresse e-mail sera rétablie, toutes vos sessions seront fermées, les numéros de téléphone associés seront supprimés et l’authentification multifacteur sera désactivée. Vous devrez aussi définir un nouveau mot de passe.\n\n– L’équipe de sécurité de {product_name}"
"body": "Bonjour {username},\n\nL’adresse e-mail de votre compte {product_name} a été remplacée par {newEmail}.\n\nSi vous avez effectué ce changement, vous n’avez rien à faire. Sinon, vous pouvez l’annuler et sécuriser votre compte avec ce lien :\n\n{revertUrl}\n\nVotre ancienne adresse e-mail sera rétablie, toutes vos sessions seront fermées et l’authentification multifacteur sera désactivée. Vous devrez aussi définir un nouveau mot de passe.\n\n– L’équipe de sécurité de {product_name}"
},
"email_verification": {
"subject": "Vérifiez votre adresse e-mail {product_name}",
@@ -15,10 +15,6 @@ const EMAIL_I18N_HE_MESSAGES = defineEmailI18nLocaleMessages({
"subject": "מחיקת חשבון ה-{product_name} שלכם מתוזמנת",
"body": "שלום {username},\n\nבהתאם לבקשתכם, חשבון ה-{product_name} שלכם מתוכנן להימחק לצמיתות בתאריך:\n\n{deletionDate, date, full} בשעה {deletionDate, time, short}{reason, select, null {} other {\n\nסיבה: {reason}}}\n\nעד אז החשבון שלכם נעול. אם לא ביקשתם זאת, או שברצונכם לשמור על החשבון, צרו קשר עם {safety_email} מכתובת האימייל הזו לפני תאריך זה.\n\nצוות {product_name}"
},
"account_disabled_suspicious": {
"subject": "חשבון ה-{product_name} שלכם הושבת זמנית",
"body": "שלום {username},\n\nהשבתנו זמנית את חשבון ה-{product_name} שלכם מכיוון שזיהינו פעילות חשודה.\n\n{reason, select,\n null {}\n other {סיבה: {reason}}\n}\n\nכדי לשחזר את הגישה לחשבון, תצטרכו לאפס את הסיסמה:\n\n{forgotUrl}\n\nלאחר שתאפסו את הסיסמה, תוכלו להתחבר שוב.\n\nאם אתם חושבים שזו טעות, צרו קשר עם צוות התמיכה שלנו.\n\nצוות הבטיחות של {product_name}"
},
"account_scheduled_deletion": {
"subject": "חשבון ה-{product_name} שלכם יימחק לצמיתות",
"body": "שלום {username},\n\nחשבון ה-{product_name} שלכם נקבע למחיקה לצמיתות עקב הפרות של תנאי השירות או הנחיות הקהילה שלנו.\n\nמחיקה מתוזמנת: {deletionDate, date, full} בשעה {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {סיבה: {reason}}\n}\n\nזוהי פעולת אכיפה חמורה. נתוני החשבון שלכם יימחקו לצמיתות בתאריך המתוכנן.\n\nאנא עיינו במסמכים הבאים:\n- תנאי השירות: {termsUrl}\n- הנחיות הקהילה: {guidelinesUrl}\n\nתהליך ערעור:\nאם אתם חושבים שהחלטת האכיפה שגויה או בלתי מוצדקת, יש לכם 60 יום להגיש ערעור. שלחו אימייל לכתובת {appeals_email} מכתובת האימייל הזו.\n\nבערעור:\n- הסבירו בבירור מדוע לדעתכם החלטת האכיפה שגויה או בלתי מוצדקת\n- צרפו כל ראיה או הקשר רלוונטיים\n\nחבר בצוות הבטיחות של {product_name} יבדוק את הערעור שלכם ועשוי להשהות את המחיקה הממתינה עד לקבלת החלטה סופית.\n\nצוות הבטיחות של {product_name}"
@@ -49,7 +45,7 @@ const EMAIL_I18N_HE_MESSAGES = defineEmailI18nLocaleMessages({
},
"email_change_revert": {
"subject": "האימייל שלכם ב-{product_name} שונה",
"body": "שלום {username},\n\nכתובת האימייל בחשבון ה-{product_name} שלכם שונתה ל-{newEmail}.\n\nאם אתם ביצעתם שינוי זה, אין צורך בפעולה. אם לא, אתם יכולים לבטל את השינוי ולאבטח את חשבונכם באמצעות קישור זה:\n\n{revertUrl}\n\nפעולה זו תשחזר את האימייל הקודם שלכם, תנתק אתכם מכל המכשירים, תסיר מספרי טלפון מקושרים, תבטל את האימות הרב-שלבי (MFA), ותדרוש מכם להגדיר סיסמה חדשה.\n\nצוות הבטיחות של {product_name}"
"body": "שלום {username},\n\nכתובת האימייל בחשבון ה-{product_name} שלכם שונתה ל-{newEmail}.\n\nאם אתם ביצעתם שינוי זה, אין צורך בפעולה. אם לא, אתם יכולים לבטל את השינוי ולאבטח את חשבונכם באמצעות קישור זה:\n\n{revertUrl}\n\nפעולה זו תשחזר את האימייל הקודם שלכם, תנתק אתכם מכל המכשירים, תבטל את האימות הרב-שלבי (MFA), ותדרוש מכם להגדיר סיסמה חדשה.\n\nצוות הבטיחות של {product_name}"
},
"email_verification": {
"subject": "אימות האימייל שלכם ב-{product_name}",

Some files were not shown because too many files have changed in this diff Show More