mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-22 07:24:30 +09:00
Compare commits
48
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9e52da7b8 | ||
|
|
8a7c791e4f | ||
|
|
0eb4e1733e | ||
|
|
527b9fadba | ||
|
|
f2d146c9e4 | ||
|
|
bf6ac57c6d | ||
|
|
3740c418ec | ||
|
|
ab7feb42e6 | ||
|
|
13af47c00d | ||
|
|
92321aa330 | ||
|
|
455f2d576b | ||
|
|
589f33e74d | ||
|
|
1f47e70ebe | ||
|
|
739d215a98 | ||
|
|
51ef7393c0 | ||
|
|
7e4892301e | ||
|
|
49a1ffabbe | ||
|
|
2d8d130be1 | ||
|
|
d564e01518 | ||
|
|
557efe1c7d | ||
|
|
0b1ddc2695 | ||
|
|
c195e2b5de | ||
|
|
9e15194d6f | ||
|
|
275cc73f7a | ||
|
|
3224f2113a | ||
|
|
b9ea837c75 | ||
|
|
41da6b5985 | ||
|
|
9000a9f4c7 | ||
|
|
61a4e3895a | ||
|
|
86c78427d8 | ||
|
|
8beccdfe54 | ||
|
|
8f4c966d3a | ||
|
|
c424656bad | ||
|
|
4b8ce8bf63 | ||
|
|
4f88872a3c | ||
|
|
f5c30fdf8f | ||
|
|
da1842ecab | ||
|
|
8c38e06a15 | ||
|
|
c454add5c8 | ||
|
|
1f4f3318dc | ||
|
|
105bd49c9e | ||
|
|
9213a765b1 | ||
|
|
7c4180c2be | ||
|
|
fe4bb4386e | ||
|
|
674b4d26d1 | ||
|
|
3a74a6f97a | ||
|
|
a7ba56b140 | ||
|
|
a39410623e |
No files matched your search
@@ -1,5 +1,6 @@
|
||||
self-hosted-runner:
|
||||
labels:
|
||||
- fluxer-desktop-macos-arm64
|
||||
- fluxer-electron-build
|
||||
|
||||
config-variables: null
|
||||
@@ -0,0 +1,108 @@
|
||||
name: build desktop Electron
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
prebuilt_release_tag:
|
||||
description: Draft release containing prebuilt Electron distributions. Leave empty to compile Electron on the build runner instead.
|
||||
required: false
|
||||
default: desktop-electron-runtime-44.4.1-fluxer.1
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: desktop-electron-build
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build patched Electron (linux-${{ matrix.arch }})
|
||||
if: inputs.prebuilt_release_tag == ''
|
||||
runs-on: [self-hosted, Linux, X64, fluxer-electron-build]
|
||||
timeout-minutes: 1440
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 1
|
||||
matrix:
|
||||
arch: [x64, arm64]
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Build patched Electron distribution
|
||||
env:
|
||||
CONTAINER_ENGINE: podman
|
||||
CONTAINER_CPUS: "8"
|
||||
CONTAINER_MEMORY: 48g
|
||||
BUILD_JOBS: "8"
|
||||
run: >-
|
||||
bash fluxer_desktop/electron/build-linux.sh
|
||||
--arch "${{ matrix.arch }}"
|
||||
--build-root /srv/fluxer-electron-build/work
|
||||
--output-dir "${{ runner.temp }}/fluxer-electron-artifacts"
|
||||
|
||||
- name: Upload patched Electron distribution
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: desktop-electron-linux-${{ matrix.arch }}
|
||||
path: |
|
||||
${{ runner.temp }}/fluxer-electron-artifacts/electron-v44.4.1-linux-${{ matrix.arch }}.zip
|
||||
${{ runner.temp }}/fluxer-electron-artifacts/electron-v44.4.1-linux-${{ matrix.arch }}.fluxer.json
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
compression-level: 0
|
||||
|
||||
import:
|
||||
name: Import patched Electron (linux-${{ matrix.arch }})
|
||||
if: inputs.prebuilt_release_tag != ''
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [x64, arm64]
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||
with:
|
||||
node-version: 26
|
||||
|
||||
- name: Download and verify build artifacts
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PREBUILT_RELEASE_TAG: ${{ inputs.prebuilt_release_tag }}
|
||||
ELECTRON_ARCH: ${{ matrix.arch }}
|
||||
FLUXER_ELECTRON_DIST: ${{ runner.temp }}/fluxer-electron-artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh release view "$PREBUILT_RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \
|
||||
--json isDraft --jq '.isDraft' | grep -qx true
|
||||
gh release download "$PREBUILT_RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \
|
||||
--pattern "electron-v44.4.1-linux-$ELECTRON_ARCH.zip" \
|
||||
--pattern "electron-v44.4.1-linux-$ELECTRON_ARCH.fluxer.json" \
|
||||
--dir "$FLUXER_ELECTRON_DIST"
|
||||
node <<'NODE'
|
||||
const {verifyLinuxElectronArchive} = require('./fluxer_desktop/scripts/custom-electron.cjs');
|
||||
verifyLinuxElectronArchive(process.env.FLUXER_ELECTRON_DIST, process.env.ELECTRON_ARCH)
|
||||
.then(() => console.log('Patched Electron artifact verified.'))
|
||||
.catch((error) => { console.error(error); process.exitCode = 1; });
|
||||
NODE
|
||||
|
||||
- name: Upload patched Electron distribution
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: desktop-electron-linux-${{ matrix.arch }}
|
||||
path: |
|
||||
${{ runner.temp }}/fluxer-electron-artifacts/electron-v44.4.1-linux-${{ matrix.arch }}.zip
|
||||
${{ runner.temp }}/fluxer-electron-artifacts/electron-v44.4.1-linux-${{ matrix.arch }}.fluxer.json
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
compression-level: 0
|
||||
@@ -21,6 +21,11 @@ on:
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
electron_build_run_id:
|
||||
description: Successful manual main-branch build desktop Electron workflow run supplying the patched Linux Electron distributions. Required when building Linux.
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
shell_contract:
|
||||
description: "Did the shell contract change since the oldest shell these modules still reach? unchanged refuses if it did. compatible keeps older shells on these modules because the renderer still works on them. raise stops sending these modules to shells without the new contract: Linux and managed shells keep their current renderer, Windows and macOS update their shell first."
|
||||
required: true
|
||||
@@ -51,6 +56,7 @@ jobs:
|
||||
timeout-minutes: 25
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
outputs:
|
||||
version: ${{ steps.meta.outputs.version }}
|
||||
pub_date: ${{ steps.meta.outputs.pub_date }}
|
||||
@@ -64,6 +70,30 @@ jobs:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate patched Electron build
|
||||
env:
|
||||
ELECTRON_BUILD_RUN_ID: ${{ inputs.electron_build_run_id }}
|
||||
SKIP_TARGETS: ${{ inputs.skip_targets }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
skipped=",$(printf '%s' "$SKIP_TARGETS" | tr '[:space:]' ','),"
|
||||
if [[ "$skipped" == *,linux,* ]] || \
|
||||
{ [[ "$skipped" == *,linux-x64,* ]] && [[ "$skipped" == *,linux-arm64,* ]]; }; then
|
||||
exit 0
|
||||
fi
|
||||
if ! [[ "$ELECTRON_BUILD_RUN_ID" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo 'Provide electron_build_run_id from a successful build desktop Electron workflow.' >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! gh api "repos/$GITHUB_REPOSITORY/actions/runs/$ELECTRON_BUILD_RUN_ID" \
|
||||
--jq 'select(.status == "completed" and .conclusion == "success" and .event == "workflow_dispatch" and .head_branch == "main" and .repository.full_name != null and .head_repository.full_name == .repository.full_name and .path == ".github/workflows/build-desktop-electron.yaml") | .id' \
|
||||
| grep -qx "$ELECTRON_BUILD_RUN_ID"; then
|
||||
echo 'Use a successful manual build desktop Electron run from this repository on main.' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Set up Rust toolchain (CI helpers)
|
||||
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
|
||||
with:
|
||||
@@ -613,12 +643,22 @@ jobs:
|
||||
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||
--step analyse_velopack_paths
|
||||
|
||||
- name: Download patched Electron distribution (Linux)
|
||||
if: matrix.platform == 'linux'
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
run-id: ${{ inputs.electron_build_run_id }}
|
||||
name: desktop-electron-linux-${{ matrix.arch }}
|
||||
path: ${{ runner.temp }}/fluxer-electron-dist
|
||||
|
||||
- name: Build Electron app (Linux)
|
||||
if: matrix.platform == 'linux'
|
||||
working-directory: ${{ env.WORKDIR }}/fluxer_desktop
|
||||
env:
|
||||
BUILD_CHANNEL: ${{ env.BUILD_CHANNEL }}
|
||||
USE_SYSTEM_FPM: true
|
||||
FLUXER_ELECTRON_DIST: ${{ runner.temp }}/fluxer-electron-dist
|
||||
run: >-
|
||||
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
|
||||
--step build_app_linux
|
||||
|
||||
Generated
+18
-5
@@ -49,9 +49,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "ammonia"
|
||||
version = "4.2.3"
|
||||
version = "4.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "061e83b03c2681c18a6787d956e355c74e0b98ba7ba3d69b0822ade1e6f1d716"
|
||||
checksum = "1f45dfa685e1ec33b1bf1a3b93b8047cbdbef41b6040deebbe6bea7720a45574"
|
||||
dependencies = [
|
||||
"cssparser 0.38.0",
|
||||
"html5ever 0.40.1",
|
||||
@@ -2986,6 +2986,18 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nkeys"
|
||||
version = "0.4.5"
|
||||
@@ -4613,7 +4625,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.4",
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -5763,14 +5775,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "8.6.0"
|
||||
version = "9.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b"
|
||||
checksum = "a954114b47a54df49f83d2722814e86b23d4bb1b54a1b90fb7d92b41619ddc22"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
"indexmap",
|
||||
"memchr",
|
||||
"nix",
|
||||
"typed-path",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
@@ -130,6 +130,8 @@ secrets:
|
||||
# FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64: ""
|
||||
# SMTP password for outgoing email.
|
||||
# FLUXER_EMAIL_SMTP_PASSWORD: ""
|
||||
# SMTP password of the email fallback provider.
|
||||
# FLUXER_EMAIL_FALLBACK_SMTP_PASSWORD: ""
|
||||
# Secret that authenticates email provider webhooks.
|
||||
# FLUXER_EMAIL_WEBHOOK_SECRET: ""
|
||||
# Stripe secret key.
|
||||
|
||||
@@ -325,6 +325,18 @@ FLUXER_EMAIL_SMTP_TLS_MODE=
|
||||
# The older switch, read only while the mode above is empty. true is implicit
|
||||
# and false is opportunistic.
|
||||
#FLUXER_EMAIL_SMTP_SECURE=false
|
||||
# A second SMTP provider for mail to the listed recipient domains, comma
|
||||
# separated and matched exactly. Everything else goes through the provider
|
||||
# above. The port falls back to 587 and the mode works like the one above.
|
||||
#FLUXER_EMAIL_FALLBACK_DOMAINS=gmail.com,googlemail.com
|
||||
#FLUXER_EMAIL_FALLBACK_SMTP_HOST=
|
||||
#FLUXER_EMAIL_FALLBACK_SMTP_PORT=587
|
||||
#FLUXER_EMAIL_FALLBACK_SMTP_USERNAME=
|
||||
#FLUXER_EMAIL_FALLBACK_SMTP_PASSWORD=
|
||||
#FLUXER_EMAIL_FALLBACK_SMTP_TLS_MODE=
|
||||
# Extra headers on every fallback message, as a JSON object of header name to
|
||||
# string value. Use single quotes so the shell keeps the JSON intact.
|
||||
#FLUXER_EMAIL_FALLBACK_SMTP_HEADERS='{"X-Example-Tracking":"off"}'
|
||||
#FLUXER_EMAIL_WEBHOOK_SECRET=
|
||||
|
||||
FLUXER_DISCOVERY_ENABLED=true
|
||||
@@ -353,12 +365,10 @@ FLUXER_DISCOVERY_ENABLED=true
|
||||
#FLUXER_AUTO_JOIN_INVITE_CODE=
|
||||
#FLUXER_DELETION_GRACE_PERIOD_HOURS=336
|
||||
|
||||
# Report retention. A daily job deletes each report this many days after it was
|
||||
# filed, with its evidence copies and search entry, unless a legal hold is set.
|
||||
# Report retention. A daily job deletes each resolved report this many days
|
||||
# after it was resolved, with its evidence copies and search entry, unless a
|
||||
# legal hold is set. Pending reports are kept until they are resolved.
|
||||
#FLUXER_REPORT_RETENTION_DAYS=365
|
||||
# Delete resolved reports this many days after they were resolved, when that
|
||||
# comes first. Unset leaves them to FLUXER_REPORT_RETENTION_DAYS.
|
||||
#FLUXER_RESOLVED_REPORT_RETENTION_DAYS=
|
||||
# true only logs what the job would delete. Deleted evidence cannot be
|
||||
# restored, so check the "Processed report retention" log line of the worker
|
||||
# in a dry run first if you are unsure.
|
||||
|
||||
@@ -126,6 +126,13 @@ x-fluxer-env: &fluxer-env
|
||||
FLUXER_EMAIL_SMTP_PASSWORD: ${FLUXER_EMAIL_SMTP_PASSWORD:-}
|
||||
FLUXER_EMAIL_SMTP_TLS_MODE: ${FLUXER_EMAIL_SMTP_TLS_MODE:-}
|
||||
FLUXER_EMAIL_SMTP_SECURE: ${FLUXER_EMAIL_SMTP_SECURE:-}
|
||||
FLUXER_EMAIL_FALLBACK_DOMAINS: ${FLUXER_EMAIL_FALLBACK_DOMAINS:-}
|
||||
FLUXER_EMAIL_FALLBACK_SMTP_HOST: ${FLUXER_EMAIL_FALLBACK_SMTP_HOST:-}
|
||||
FLUXER_EMAIL_FALLBACK_SMTP_PORT: ${FLUXER_EMAIL_FALLBACK_SMTP_PORT:-}
|
||||
FLUXER_EMAIL_FALLBACK_SMTP_USERNAME: ${FLUXER_EMAIL_FALLBACK_SMTP_USERNAME:-}
|
||||
FLUXER_EMAIL_FALLBACK_SMTP_PASSWORD: ${FLUXER_EMAIL_FALLBACK_SMTP_PASSWORD:-}
|
||||
FLUXER_EMAIL_FALLBACK_SMTP_TLS_MODE: ${FLUXER_EMAIL_FALLBACK_SMTP_TLS_MODE:-}
|
||||
FLUXER_EMAIL_FALLBACK_SMTP_HEADERS: ${FLUXER_EMAIL_FALLBACK_SMTP_HEADERS:-}
|
||||
|
||||
FLUXER_STRIPE_ENABLED: ${FLUXER_STRIPE_ENABLED:-}
|
||||
FLUXER_STRIPE_SECRET_KEY: ${FLUXER_STRIPE_SECRET_KEY:-}
|
||||
@@ -159,7 +166,6 @@ x-fluxer-env: &fluxer-env
|
||||
FLUXER_DISCOVERY_MIN_MEMBER_COUNT: ${FLUXER_DISCOVERY_MIN_MEMBER_COUNT:-}
|
||||
FLUXER_DELETION_GRACE_PERIOD_HOURS: ${FLUXER_DELETION_GRACE_PERIOD_HOURS:-}
|
||||
FLUXER_REPORT_RETENTION_DAYS: ${FLUXER_REPORT_RETENTION_DAYS:-}
|
||||
FLUXER_RESOLVED_REPORT_RETENTION_DAYS: ${FLUXER_RESOLVED_REPORT_RETENTION_DAYS:-}
|
||||
FLUXER_REPORT_RETENTION_DRY_RUN: ${FLUXER_REPORT_RETENTION_DRY_RUN:-}
|
||||
FLUXER_API_WORKER_LANE_CONCURRENCY_OVERRIDES: ${FLUXER_API_WORKER_LANE_CONCURRENCY_OVERRIDES:-}
|
||||
|
||||
|
||||
Generated
+1670
-211
File diff suppressed because it is too large.
Load diff
@@ -45,6 +45,8 @@ pub const BULK_UPDATE_GUILD_FEATURES: &str = "bulk:update:guild_features";
|
||||
pub const BULK_UPDATE_USER_FLAGS: &str = "bulk:update:user_flags";
|
||||
pub const DISCOVERY_REMOVE: &str = "discovery:remove";
|
||||
pub const DISCOVERY_REVIEW: &str = "discovery:review";
|
||||
pub const ENFORCEMENT_ANNOTATE: &str = "enforcement:annotate";
|
||||
pub const ENFORCEMENT_VIEW: &str = "enforcement:view";
|
||||
pub const GATEWAY_MEMORY_STATS: &str = "gateway:memory_stats";
|
||||
pub const GATEWAY_RELOAD_ALL: &str = "gateway:reload_all";
|
||||
pub const GIFT_CODES_GENERATE: &str = "gift_codes:generate";
|
||||
@@ -153,6 +155,8 @@ pub const ALL_ACLS: &[&str] = &[
|
||||
BULK_UPDATE_USER_FLAGS,
|
||||
DISCOVERY_REMOVE,
|
||||
DISCOVERY_REVIEW,
|
||||
ENFORCEMENT_ANNOTATE,
|
||||
ENFORCEMENT_VIEW,
|
||||
GATEWAY_MEMORY_STATS,
|
||||
GATEWAY_RELOAD_ALL,
|
||||
GIFT_CODES_GENERATE,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::api::generated::types::EnforcementKindType;
|
||||
|
||||
pub struct U64Flag {
|
||||
pub name: &'static str,
|
||||
pub value: u64,
|
||||
@@ -121,6 +123,21 @@ pub const USER_FLAGS: &[U64Flag] = &[
|
||||
},
|
||||
];
|
||||
|
||||
const RESTRICTION_USER_FLAGS: u64 = user_flag_bits::DISABLED
|
||||
| user_flag_bits::DELETED
|
||||
| user_flag_bits::SPAMMER
|
||||
| user_flag_bits::REPORT_BANNED;
|
||||
const PURCHASE_DISABLED: i32 = 1 << 6;
|
||||
|
||||
pub fn user_flag_is_editable(flag: u64, current_flags: u64, suspended: bool) -> bool {
|
||||
let self_disabled = flag == user_flag_bits::DISABLED && current_flags & flag != 0 && !suspended;
|
||||
flag & RESTRICTION_USER_FLAGS == 0 || self_disabled
|
||||
}
|
||||
|
||||
pub fn premium_flag_is_editable(flag: i32) -> bool {
|
||||
flag & PURCHASE_DISABLED == 0
|
||||
}
|
||||
|
||||
pub const PREMIUM_FLAGS: &[I32Flag] = &[
|
||||
I32Flag {
|
||||
name: "DISCRIMINATOR",
|
||||
@@ -148,7 +165,7 @@ pub const PREMIUM_FLAGS: &[I32Flag] = &[
|
||||
},
|
||||
I32Flag {
|
||||
name: "PURCHASE_DISABLED",
|
||||
value: 1 << 6,
|
||||
value: PURCHASE_DISABLED,
|
||||
},
|
||||
I32Flag {
|
||||
name: "ENABLED_OVERRIDE",
|
||||
@@ -166,7 +183,7 @@ pub const DELETION_REASONS: &[(&str, &str)] = &[
|
||||
("3", "Spam"),
|
||||
("4", "Cheating or exploitation"),
|
||||
("5", "Coordinated raiding or manipulation"),
|
||||
("6", "Automation or self-bot usage"),
|
||||
("6", "Disruptive automation"),
|
||||
("7", "Nonconsensual sexual content"),
|
||||
("8", "Scam or social engineering"),
|
||||
("9", "Child sexual content"),
|
||||
@@ -183,8 +200,157 @@ pub const DELETION_REASONS: &[(&str, &str)] = &[
|
||||
("20", "Hate speech or extremist content"),
|
||||
("21", "Malicious links or malware distribution"),
|
||||
("22", "Impersonation or fake identity"),
|
||||
("23", "Threats or violent speech"),
|
||||
("24", "Violent extremism"),
|
||||
("25", "Self-harm promotion"),
|
||||
("26", "Sexual content outside 18+ spaces"),
|
||||
("27", "Illegal goods or activity"),
|
||||
("28", "Harmful deception"),
|
||||
("29", "Reporting abuse"),
|
||||
("30", "Under the minimum age"),
|
||||
("31", "Copyright"),
|
||||
];
|
||||
|
||||
pub fn deletion_reason_label(code: i32) -> String {
|
||||
let value = code.to_string();
|
||||
DELETION_REASONS
|
||||
.iter()
|
||||
.find(|(candidate, _)| *candidate == value)
|
||||
.map_or_else(
|
||||
|| format!("Reason {code}"),
|
||||
|(_, label)| (*label).to_owned(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum EnforcementHandling {
|
||||
Standard,
|
||||
Protected,
|
||||
Restricted,
|
||||
}
|
||||
|
||||
pub struct EnforcementReason {
|
||||
pub code: i32,
|
||||
pub handling: EnforcementHandling,
|
||||
pub kinds: Option<&'static [EnforcementKindType]>,
|
||||
}
|
||||
|
||||
impl EnforcementReason {
|
||||
pub fn allows(&self, kind: EnforcementKindType) -> bool {
|
||||
self.kinds.is_none_or(|kinds| kinds.contains(&kind))
|
||||
&& (kind != EnforcementKindType::FeatureLimit
|
||||
|| ENFORCEMENT_LIMITS
|
||||
.iter()
|
||||
.any(|limit| limit.codes.contains(&self.code)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EnforcementLimitOption {
|
||||
pub value: &'static str,
|
||||
pub label: &'static str,
|
||||
pub codes: &'static [i32],
|
||||
}
|
||||
|
||||
pub const ENFORCEMENT_LIMITS: &[EnforcementLimitOption] = &[
|
||||
EnforcementLimitOption {
|
||||
value: "direct_messages",
|
||||
label: "Direct messages",
|
||||
codes: &[3, 6, 8, 18, 21],
|
||||
},
|
||||
EnforcementLimitOption {
|
||||
value: "reports",
|
||||
label: "Reports",
|
||||
codes: &[29],
|
||||
},
|
||||
EnforcementLimitOption {
|
||||
value: "purchases",
|
||||
label: "Purchases",
|
||||
codes: &[12, 14],
|
||||
},
|
||||
];
|
||||
|
||||
pub const COPYRIGHT_REASON_CODE: i32 = 31;
|
||||
|
||||
const RESTRICTED_KINDS: &[EnforcementKindType] = &[
|
||||
EnforcementKindType::Suspension,
|
||||
EnforcementKindType::ContentRemoval,
|
||||
];
|
||||
const PAYMENT_KINDS: &[EnforcementKindType] = &[
|
||||
EnforcementKindType::Warning,
|
||||
EnforcementKindType::Suspension,
|
||||
EnforcementKindType::Closure,
|
||||
EnforcementKindType::FeatureLimit,
|
||||
];
|
||||
|
||||
const fn reason(
|
||||
code: i32,
|
||||
handling: EnforcementHandling,
|
||||
kinds: Option<&'static [EnforcementKindType]>,
|
||||
) -> EnforcementReason {
|
||||
EnforcementReason {
|
||||
code,
|
||||
handling,
|
||||
kinds,
|
||||
}
|
||||
}
|
||||
|
||||
pub const ENFORCEMENT_REASONS: &[EnforcementReason] = {
|
||||
use EnforcementHandling::{Protected, Restricted, Standard};
|
||||
use EnforcementKindType::{
|
||||
Closure, CommunityRemoval, ContentRemoval, FeatureLimit, ProfileReset, Suspension, Warning,
|
||||
};
|
||||
&[
|
||||
reason(3, Standard, None),
|
||||
reason(4, Standard, None),
|
||||
reason(5, Standard, None),
|
||||
reason(6, Standard, None),
|
||||
reason(7, Protected, None),
|
||||
reason(8, Standard, None),
|
||||
reason(9, Restricted, Some(RESTRICTED_KINDS)),
|
||||
reason(10, Protected, None),
|
||||
reason(11, Protected, None),
|
||||
reason(12, Standard, Some(PAYMENT_KINDS)),
|
||||
reason(13, Restricted, Some(RESTRICTED_KINDS)),
|
||||
reason(14, Standard, Some(PAYMENT_KINDS)),
|
||||
reason(15, Protected, None),
|
||||
reason(16, Standard, None),
|
||||
reason(17, Standard, Some(&[Warning, Suspension, Closure])),
|
||||
reason(18, Standard, None),
|
||||
reason(20, Standard, None),
|
||||
reason(21, Standard, None),
|
||||
reason(22, Standard, None),
|
||||
reason(23, Protected, None),
|
||||
reason(24, Restricted, Some(RESTRICTED_KINDS)),
|
||||
reason(
|
||||
25,
|
||||
Standard,
|
||||
Some(&[
|
||||
Warning,
|
||||
Suspension,
|
||||
ContentRemoval,
|
||||
ProfileReset,
|
||||
CommunityRemoval,
|
||||
]),
|
||||
),
|
||||
reason(26, Standard, None),
|
||||
reason(27, Standard, None),
|
||||
reason(28, Standard, None),
|
||||
reason(29, Standard, Some(&[Warning, FeatureLimit, Suspension])),
|
||||
reason(30, Standard, Some(&[Closure])),
|
||||
reason(
|
||||
31,
|
||||
Standard,
|
||||
Some(&[Warning, ContentRemoval, ProfileReset, CommunityRemoval]),
|
||||
),
|
||||
]
|
||||
};
|
||||
|
||||
pub fn enforcement_reason(code: i32) -> Option<&'static EnforcementReason> {
|
||||
ENFORCEMENT_REASONS
|
||||
.iter()
|
||||
.find(|reason| reason.code == code)
|
||||
}
|
||||
|
||||
pub const GUILD_FEATURES: &[&str] = &[
|
||||
"ANIMATED_ICON",
|
||||
"ANIMATED_BANNER",
|
||||
@@ -226,7 +392,196 @@ pub const DEPRECATED_GUILD_FEATURES: &[&str] = &["CLONE_EMOJI_DISABLED", "CLONE_
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::USER_FLAGS;
|
||||
use super::{
|
||||
DELETION_REASONS, ENFORCEMENT_LIMITS, ENFORCEMENT_REASONS, EnforcementHandling, USER_FLAGS,
|
||||
};
|
||||
use crate::api::generated::types::DeletionReasonCode;
|
||||
use std::collections::HashMap;
|
||||
|
||||
const CORE_TS: &str = include_str!("../../packages/constants/src/Core.ts");
|
||||
const ENFORCEMENT_REASONS_TS: &str =
|
||||
include_str!("../../packages/constants/src/EnforcementReasons.ts");
|
||||
|
||||
fn quoted_values(list: &str) -> Vec<String> {
|
||||
list.split('\'')
|
||||
.skip(1)
|
||||
.step_by(2)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn deletion_reason_codes() -> HashMap<String, i32> {
|
||||
let block = CORE_TS
|
||||
.split("export const DeletionReasons = {")
|
||||
.nth(1)
|
||||
.and_then(|rest| rest.split('}').next())
|
||||
.expect("DeletionReasons block");
|
||||
block
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let (name, code) = line.trim().trim_end_matches(',').split_once(": ")?;
|
||||
Some((name.to_owned(), code.parse().ok()?))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn kind_lists() -> HashMap<String, Vec<String>> {
|
||||
ENFORCEMENT_REASONS_TS
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let rest = line.strip_prefix("const ")?;
|
||||
let (name, value) = rest.split_once(": ReadonlyArray<EnforcementKind> = ")?;
|
||||
Some((name.to_owned(), quoted_values(value)))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn typescript_reasons() -> Vec<(i32, String, Option<Vec<String>>)> {
|
||||
let codes = deletion_reason_codes();
|
||||
let lists = kind_lists();
|
||||
let block = ENFORCEMENT_REASONS_TS
|
||||
.split("export const ENFORCEMENT_REASONS")
|
||||
.nth(1)
|
||||
.and_then(|rest| rest.split("\n};").next())
|
||||
.expect("ENFORCEMENT_REASONS block");
|
||||
block
|
||||
.split("[DeletionReasons.")
|
||||
.skip(1)
|
||||
.map(|entry| {
|
||||
let name = entry.split(']').next().expect("reason name");
|
||||
let handling = entry
|
||||
.split("handling: '")
|
||||
.nth(1)
|
||||
.and_then(|rest| rest.split('\'').next())
|
||||
.expect("handling");
|
||||
let kinds = entry.split("kinds: ").nth(1).expect("kinds").trim_start();
|
||||
let kinds = if kinds.starts_with("'any'") {
|
||||
None
|
||||
} else if kinds.starts_with('[') {
|
||||
Some(quoted_values(kinds.split(']').next().expect("kind list")))
|
||||
} else {
|
||||
let list = kinds
|
||||
.split(|c: char| !c.is_ascii_alphanumeric() && c != '_')
|
||||
.next()
|
||||
.expect("kind list name");
|
||||
Some(lists.get(list).expect("named kind list").clone())
|
||||
};
|
||||
(
|
||||
*codes.get(name).expect("reason code"),
|
||||
handling.to_owned(),
|
||||
kinds,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn typescript_reason_rules() -> HashMap<String, Vec<i32>> {
|
||||
let codes = deletion_reason_codes();
|
||||
let block = ENFORCEMENT_REASONS_TS
|
||||
.split("export const ENFORCEMENT_REASONS")
|
||||
.nth(1)
|
||||
.and_then(|rest| rest.split("\n};").next())
|
||||
.expect("ENFORCEMENT_REASONS block");
|
||||
let mut rules: HashMap<String, Vec<i32>> = HashMap::new();
|
||||
for entry in block.split("[DeletionReasons.").skip(1) {
|
||||
let name = entry.split(']').next().expect("reason name");
|
||||
let rule = entry
|
||||
.split("rule: '")
|
||||
.nth(1)
|
||||
.and_then(|rest| rest.split('\'').next())
|
||||
.expect("rule");
|
||||
rules
|
||||
.entry(rule.to_owned())
|
||||
.or_default()
|
||||
.push(*codes.get(name).expect("reason code"));
|
||||
}
|
||||
rules
|
||||
}
|
||||
|
||||
fn typescript_limit_rules(limit: &str) -> Vec<String> {
|
||||
let block = ENFORCEMENT_REASONS_TS
|
||||
.split("const ENFORCEMENT_LIMIT_RULES")
|
||||
.nth(1)
|
||||
.and_then(|rest| rest.split("\n};").next())
|
||||
.expect("ENFORCEMENT_LIMIT_RULES block");
|
||||
let value = block
|
||||
.split(&format!("{limit}: "))
|
||||
.nth(1)
|
||||
.and_then(|rest| rest.split(",\n").next())
|
||||
.expect("limit rules");
|
||||
if value.starts_with("ENFORCEMENT_SPAMMER_LIMIT_RULES") {
|
||||
let spammer = ENFORCEMENT_REASONS_TS
|
||||
.split("const ENFORCEMENT_SPAMMER_LIMIT_RULES: ")
|
||||
.nth(1)
|
||||
.and_then(|rest| rest.split("]);").next())
|
||||
.expect("spammer rules");
|
||||
return quoted_values(spammer);
|
||||
}
|
||||
quoted_values(value)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enforcement_limits_match_the_shared_constants() {
|
||||
let rules = typescript_reason_rules();
|
||||
assert_eq!(ENFORCEMENT_LIMITS.len(), 3);
|
||||
for limit in ENFORCEMENT_LIMITS {
|
||||
let mut expected: Vec<i32> = typescript_limit_rules(limit.value)
|
||||
.iter()
|
||||
.flat_map(|rule| rules.get(rule).cloned().unwrap_or_default())
|
||||
.collect();
|
||||
expected.sort_unstable();
|
||||
let mut actual = limit.codes.to_vec();
|
||||
actual.sort_unstable();
|
||||
assert_eq!(actual, expected, "codes of the {} limit", limit.value);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feature_limits_are_offered_only_for_reasons_with_a_limit() {
|
||||
use crate::api::generated::types::EnforcementKindType::FeatureLimit;
|
||||
let offered: Vec<i32> = ENFORCEMENT_REASONS
|
||||
.iter()
|
||||
.filter(|reason| reason.allows(FeatureLimit))
|
||||
.map(|reason| reason.code)
|
||||
.collect();
|
||||
assert_eq!(offered, vec![3, 6, 8, 12, 14, 18, 21, 29]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restriction_flags_are_read_only_except_a_self_disabled_account() {
|
||||
use super::{premium_flag_is_editable, user_flag_bits as bits, user_flag_is_editable};
|
||||
for flag in [bits::DELETED, bits::SPAMMER, bits::REPORT_BANNED] {
|
||||
assert!(!user_flag_is_editable(flag, 0, false));
|
||||
assert!(!user_flag_is_editable(flag, flag, false));
|
||||
}
|
||||
assert!(!user_flag_is_editable(bits::DISABLED, 0, false));
|
||||
assert!(!user_flag_is_editable(bits::DISABLED, bits::DISABLED, true));
|
||||
assert!(user_flag_is_editable(bits::DISABLED, bits::DISABLED, false));
|
||||
assert!(user_flag_is_editable(bits::ACCOUNT_LIMITED, 0, true));
|
||||
assert!(user_flag_is_editable(bits::STAFF, bits::STAFF, false));
|
||||
assert!(!premium_flag_is_editable(1 << 6));
|
||||
assert!(premium_flag_is_editable(1 << 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enforcement_reasons_match_the_shared_constants() {
|
||||
let expected = typescript_reasons();
|
||||
assert_eq!(expected.len(), ENFORCEMENT_REASONS.len());
|
||||
for (code, handling, kinds) in expected {
|
||||
let reason = super::enforcement_reason(code)
|
||||
.unwrap_or_else(|| panic!("{code} is missing from ENFORCEMENT_REASONS"));
|
||||
let rust_handling = match reason.handling {
|
||||
EnforcementHandling::Standard => "standard",
|
||||
EnforcementHandling::Protected => "protected",
|
||||
EnforcementHandling::Restricted => "restricted",
|
||||
};
|
||||
assert_eq!(rust_handling, handling, "handling of {code}");
|
||||
let rust_kinds = reason
|
||||
.kinds
|
||||
.map(|kinds| kinds.iter().map(ToString::to_string).collect::<Vec<_>>());
|
||||
assert_eq!(rust_kinds, kinds, "kinds of {code}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_flags_cover_every_flag_in_the_admin_spec() {
|
||||
@@ -251,4 +606,31 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deletion_reasons_match_the_generated_reason_codes() {
|
||||
let spec: serde_json::Value =
|
||||
serde_json::from_str(include_str!("../openapi-admin.json")).expect("admin spec");
|
||||
let mut spec_codes: Vec<i32> = spec["components"]["schemas"]["DeletionReasonCode"]["enum"]
|
||||
.as_array()
|
||||
.expect("DeletionReasonCode values")
|
||||
.iter()
|
||||
.map(|value| {
|
||||
i32::try_from(value.as_i64().expect("numeric reason code")).expect("i32 code")
|
||||
})
|
||||
.collect();
|
||||
spec_codes.sort_unstable();
|
||||
let mut label_codes: Vec<i32> = DELETION_REASONS
|
||||
.iter()
|
||||
.map(|(code, _)| code.parse().expect("numeric reason code"))
|
||||
.collect();
|
||||
label_codes.sort_unstable();
|
||||
assert_eq!(label_codes, spec_codes);
|
||||
for code in label_codes {
|
||||
assert!(
|
||||
DeletionReasonCode::try_from(code).is_ok(),
|
||||
"{code} is not a generated DeletionReasonCode"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,9 +74,11 @@ impl AdminApiClient {
|
||||
pub async fn bulk_delete_user_messages(
|
||||
&self,
|
||||
user_ids: &[String],
|
||||
basis: generated_types::BulkModerationBasis,
|
||||
audit_log_reason: Option<&str>,
|
||||
) -> ApiResult<BulkJobResponse> {
|
||||
let body = generated_types::AdminBulkJobCreateRequest::DeleteUserMessages {
|
||||
basis,
|
||||
user_ids: snowflakes(user_ids),
|
||||
};
|
||||
self.send(
|
||||
@@ -91,26 +93,18 @@ impl AdminApiClient {
|
||||
pub async fn bulk_schedule_user_deletion(
|
||||
&self,
|
||||
user_ids: &[String],
|
||||
reason_code: u32,
|
||||
days_until_deletion: u32,
|
||||
public_reason: Option<&str>,
|
||||
notify_user: bool,
|
||||
basis: generated_types::BulkModerationBasis,
|
||||
audit_log_reason: Option<&str>,
|
||||
) -> ApiResult<BulkJobResponse> {
|
||||
let body = generated_types::AdminBulkJobCreateRequest::ScheduleUserDeletion {
|
||||
basis,
|
||||
days_until_deletion: crate::api::generated::nonzero_u32(
|
||||
days_until_deletion,
|
||||
"days_until_deletion",
|
||||
)
|
||||
.map_err(ApiError::Parse)?
|
||||
.into(),
|
||||
notify_user,
|
||||
public_reason: public_reason.map(std::borrow::ToOwned::to_owned),
|
||||
reason_code: crate::api::generated::deletion_reason_code(
|
||||
i32::try_from(reason_code).map_err(|e| ApiError::Parse(e.to_string()))?,
|
||||
"reason_code",
|
||||
)
|
||||
.map_err(ApiError::Parse)?,
|
||||
user_ids: snowflakes(user_ids),
|
||||
};
|
||||
self.send(
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use reqwest::Method;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::api::generated::types as generated_types;
|
||||
|
||||
use super::client::{AdminApiClient, ApiError, ApiResult};
|
||||
use super::types::{AdminUser, UserMutationResponse};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RestrictionResponse {
|
||||
pub user: AdminUser,
|
||||
pub skipped_ips: Vec<generated_types::SkippedNetworkBlock>,
|
||||
}
|
||||
|
||||
pub struct DecisionSubmission {
|
||||
pub decision: generated_types::EnforcementDecisionInput,
|
||||
pub internal_note: String,
|
||||
}
|
||||
|
||||
pub struct ClosureRequest {
|
||||
pub basis: generated_types::ModerationBasis,
|
||||
pub days_until_deletion: u32,
|
||||
pub remove_evidence: bool,
|
||||
pub block_networks: Option<generated_types::NetworkBlockRequest>,
|
||||
pub replace_pending_deletion_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub internal_note: Option<String>,
|
||||
}
|
||||
|
||||
impl AdminApiClient {
|
||||
pub async fn suspend_user(
|
||||
&self,
|
||||
user_id: &str,
|
||||
duration_hours: u32,
|
||||
remove_evidence: bool,
|
||||
block_networks: Option<generated_types::NetworkBlockRequest>,
|
||||
submission: &DecisionSubmission,
|
||||
) -> ApiResult<RestrictionResponse> {
|
||||
let body = generated_types::AdminUserBanRequest {
|
||||
block_networks,
|
||||
decision: submission.decision.clone(),
|
||||
duration_hours: i64::from(duration_hours),
|
||||
remove_evidence: remove_evidence.then_some(true),
|
||||
};
|
||||
self.send(
|
||||
Method::PUT,
|
||||
&format!("/admin/users/{}/ban", urlencoding::encode(user_id)),
|
||||
Some(&body),
|
||||
Some(&submission.internal_note),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_network_block_candidates(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> ApiResult<generated_types::NetworkBlockCandidatesResponse> {
|
||||
self.get(
|
||||
&format!(
|
||||
"/admin/users/{}/network-block-candidates",
|
||||
urlencoding::encode(user_id)
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn warn_user(
|
||||
&self,
|
||||
user_id: &str,
|
||||
submission: &DecisionSubmission,
|
||||
) -> ApiResult<generated_types::AdminUserWarningResponse> {
|
||||
let body = generated_types::AdminUserWarningRequest {
|
||||
decision: submission.decision.clone(),
|
||||
};
|
||||
self.send(
|
||||
Method::POST,
|
||||
&format!("/admin/users/{}/warnings", urlencoding::encode(user_id)),
|
||||
Some(&body),
|
||||
Some(&submission.internal_note),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn close_account(
|
||||
&self,
|
||||
user_id: &str,
|
||||
request: &ClosureRequest,
|
||||
) -> ApiResult<RestrictionResponse> {
|
||||
let body = generated_types::AdminUserDeletionScheduleRequest {
|
||||
basis: request.basis.clone(),
|
||||
block_networks: request.block_networks.clone(),
|
||||
days_until_deletion: crate::api::generated::nonzero_u32(
|
||||
request.days_until_deletion,
|
||||
"days_until_deletion",
|
||||
)
|
||||
.map_err(ApiError::Parse)?
|
||||
.into(),
|
||||
remove_evidence: request.remove_evidence.then_some(true),
|
||||
replace_pending_deletion_at: request.replace_pending_deletion_at,
|
||||
};
|
||||
self.send(
|
||||
Method::PUT,
|
||||
&format!("/admin/users/{}/deletion", urlencoding::encode(user_id)),
|
||||
Some(&body),
|
||||
request.internal_note.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn limit_features(
|
||||
&self,
|
||||
user_id: &str,
|
||||
limit: generated_types::AdminUserFeatureLimitRequestLimit,
|
||||
ends_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
submission: &DecisionSubmission,
|
||||
) -> ApiResult<AdminUser> {
|
||||
let body = generated_types::AdminUserFeatureLimitRequest {
|
||||
decision: submission.decision.clone(),
|
||||
ends_at,
|
||||
limit,
|
||||
};
|
||||
let response: UserMutationResponse = self
|
||||
.send(
|
||||
Method::PUT,
|
||||
&format!(
|
||||
"/admin/users/{}/feature-limits",
|
||||
urlencoding::encode(user_id)
|
||||
),
|
||||
Some(&body),
|
||||
Some(&submission.internal_note),
|
||||
)
|
||||
.await?;
|
||||
Ok(response.user)
|
||||
}
|
||||
|
||||
pub async fn list_review_queue(
|
||||
&self,
|
||||
) -> ApiResult<generated_types::EnforcementDecisionListResponse> {
|
||||
self.get(
|
||||
"/admin/enforcement-decisions",
|
||||
Some(&[("review_queue", "true")]),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_user_decisions(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> ApiResult<generated_types::EnforcementDecisionListResponse> {
|
||||
self.get(
|
||||
"/admin/enforcement-decisions",
|
||||
Some(&[("subject_type", "user"), ("subject_id", user_id)]),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_decision(
|
||||
&self,
|
||||
decision_id: &str,
|
||||
) -> ApiResult<generated_types::EnforcementDecisionResponse> {
|
||||
self.get(
|
||||
&format!(
|
||||
"/admin/enforcement-decisions/{}",
|
||||
urlencoding::encode(decision_id)
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn end_decision(
|
||||
&self,
|
||||
decision_id: &str,
|
||||
outcome: generated_types::EnforcementDecisionEndRequestOutcome,
|
||||
statement: Option<String>,
|
||||
internal_note: &str,
|
||||
) -> ApiResult<generated_types::EnforcementDecisionResponse> {
|
||||
let body = generated_types::EnforcementDecisionEndRequest { outcome, statement };
|
||||
self.send(
|
||||
Method::POST,
|
||||
&format!(
|
||||
"/admin/enforcement-decisions/{}/end",
|
||||
urlencoding::encode(decision_id)
|
||||
),
|
||||
Some(&body),
|
||||
Some(internal_note),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn annotate_decision(
|
||||
&self,
|
||||
decision_id: &str,
|
||||
body: String,
|
||||
) -> ApiResult<generated_types::EnforcementDecisionNoteResponse> {
|
||||
let body = generated_types::EnforcementDecisionNoteRequest { body };
|
||||
self.send(
|
||||
Method::POST,
|
||||
&format!(
|
||||
"/admin/enforcement-decisions/{}/notes",
|
||||
urlencoding::encode(decision_id)
|
||||
),
|
||||
Some(&body),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub fn api_error_message(error: &ApiError) -> Option<String> {
|
||||
let ApiError::Http { status, message } = error else {
|
||||
return None;
|
||||
};
|
||||
if !matches!(status, 400 | 403 | 404 | 409) {
|
||||
return None;
|
||||
}
|
||||
let body: serde_json::Value = serde_json::from_str(message).ok()?;
|
||||
let detail = body["errors"]
|
||||
.as_array()
|
||||
.and_then(|errors| errors.first())
|
||||
.and_then(|error| error["message"].as_str());
|
||||
detail
|
||||
.or_else(|| body["message"].as_str())
|
||||
.map(str::to_owned)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::api_error_message;
|
||||
use crate::api::client::ApiError;
|
||||
|
||||
fn http(status: u16, body: &str) -> ApiError {
|
||||
ApiError::Http {
|
||||
status,
|
||||
message: body.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_error_message_prefers_the_first_field_error() {
|
||||
let body = r#"{"code":"INVALID_FORM_BODY","message":"Invalid form body","errors":[{"path":"decision.statement","code":"ENFORCEMENT_STATEMENT_NOT_ALLOWED","message":"A note to the user is not allowed for this reason"}]}"#;
|
||||
assert_eq!(
|
||||
api_error_message(&http(400, body)).as_deref(),
|
||||
Some("A note to the user is not allowed for this reason")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_error_message_reads_conflicts_and_ignores_server_errors() {
|
||||
let body = r#"{"code":"ENFORCEMENT_DECISION_REPLACED","message":"A later decision replaced this one"}"#;
|
||||
assert_eq!(
|
||||
api_error_message(&http(409, body)).as_deref(),
|
||||
Some("A later decision replaced this one")
|
||||
);
|
||||
assert_eq!(api_error_message(&http(500, body)), None);
|
||||
assert_eq!(api_error_message(&http(409, "not json")), None);
|
||||
assert_eq!(
|
||||
api_error_message(&ApiError::Network("down".to_owned())),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -56,14 +56,6 @@ pub(crate) fn nonzero_u32(value: u32, field: &str) -> Result<std::num::NonZeroU3
|
||||
std::num::NonZeroU32::new(value).ok_or_else(|| format!("{field} must be greater than zero"))
|
||||
}
|
||||
|
||||
pub(crate) fn deletion_reason_code(
|
||||
value: i32,
|
||||
field: &str,
|
||||
) -> Result<types::DeletionReasonCode, String> {
|
||||
types::DeletionReasonCode::try_from(value)
|
||||
.map_err(|_| format!("{field} is not a deletion reason code: {value}"))
|
||||
}
|
||||
|
||||
pub(crate) fn user_flags(values: &[String]) -> ApiResult<Vec<generated_types::UserFlags>> {
|
||||
values
|
||||
.iter()
|
||||
|
||||
@@ -69,13 +69,20 @@ impl AdminApiClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_guild(&self, guild_id: &str) -> ApiResult<SuccessResponse> {
|
||||
let response = self
|
||||
.generated()
|
||||
.delete_admin_guild(&snowflake(guild_id))
|
||||
.await
|
||||
.map_err(|e| self.generated_error(e))?;
|
||||
self.generated_value(response.into_inner())
|
||||
pub async fn delete_guild(
|
||||
&self,
|
||||
guild_id: &str,
|
||||
basis: generated_types::CommunityRemovalBasis,
|
||||
internal_note: Option<&str>,
|
||||
) -> ApiResult<SuccessResponse> {
|
||||
let body = generated_types::AdminGuildDeleteRequest { basis };
|
||||
self.send(
|
||||
Method::DELETE,
|
||||
&format!("/admin/guilds/{}", urlencoding::encode(guild_id)),
|
||||
Some(&body),
|
||||
internal_note,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn transfer_guild_ownership(
|
||||
|
||||
@@ -4,10 +4,12 @@ use reqwest::Method;
|
||||
|
||||
use crate::api::generated::{snowflake, types as generated_types};
|
||||
|
||||
use crate::templates::components::enforcement_fields::BasisSubmission;
|
||||
|
||||
use super::client::{AdminApiClient, ApiError, ApiResult};
|
||||
use super::types::{
|
||||
BrowseChannelResponse, DeleteAllUserMessagesResponse, LookupMessageResponse,
|
||||
MessageShredResponse, MessageShredStatusResponse, SearchChannelMessagesResponse,
|
||||
BrowseChannelResponse, LookupMessageResponse, MessageShredStatusResponse,
|
||||
SearchChannelMessagesResponse,
|
||||
};
|
||||
|
||||
impl AdminApiClient {
|
||||
@@ -15,8 +17,10 @@ impl AdminApiClient {
|
||||
&self,
|
||||
channel_id: &str,
|
||||
message_id: &str,
|
||||
basis: generated_types::MessageDeletionBasis,
|
||||
audit_log_reason: Option<&str>,
|
||||
) -> ApiResult<()> {
|
||||
let body = generated_types::AdminMessageDeleteRequest { basis };
|
||||
let _: serde_json::Value = self
|
||||
.send(
|
||||
Method::DELETE,
|
||||
@@ -25,7 +29,7 @@ impl AdminApiClient {
|
||||
urlencoding::encode(channel_id),
|
||||
urlencoding::encode(message_id)
|
||||
),
|
||||
None::<&serde_json::Value>,
|
||||
Some(&body),
|
||||
audit_log_reason,
|
||||
)
|
||||
.await?;
|
||||
@@ -54,35 +58,44 @@ impl AdminApiClient {
|
||||
pub async fn queue_message_shred(
|
||||
&self,
|
||||
user_id: &str,
|
||||
entries: &[serde_json::Value],
|
||||
) -> ApiResult<MessageShredResponse> {
|
||||
let entries = entries
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(serde_json::from_value::<generated_types::AdminUserMessageShredRequestEntriesItem>)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| ApiError::Parse(e.to_string()))?;
|
||||
let body = generated_types::AdminUserMessageShredRequest { entries };
|
||||
let response = self
|
||||
.generated()
|
||||
.shred_admin_user_messages(&snowflake(user_id), &body)
|
||||
.await
|
||||
.map_err(|e| self.generated_error(e))?;
|
||||
self.generated_value(response.into_inner())
|
||||
entries: Vec<generated_types::AdminUserMessageShredRequestEntriesItem>,
|
||||
basis: &BasisSubmission,
|
||||
) -> ApiResult<generated_types::MessageShredResponse> {
|
||||
let body = generated_types::AdminUserMessageShredRequest {
|
||||
basis: basis.moderation_basis(),
|
||||
entries,
|
||||
};
|
||||
self.send(
|
||||
Method::POST,
|
||||
&format!(
|
||||
"/admin/users/{}/message-shreds",
|
||||
urlencoding::encode(user_id)
|
||||
),
|
||||
Some(&body),
|
||||
basis.internal_note(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_all_user_messages(
|
||||
&self,
|
||||
user_id: &str,
|
||||
dry_run: bool,
|
||||
) -> ApiResult<DeleteAllUserMessagesResponse> {
|
||||
let dry_run = if dry_run { "true" } else { "false" };
|
||||
let response = self
|
||||
.generated()
|
||||
.delete_admin_user_messages(&snowflake(user_id), Some(dry_run))
|
||||
.await
|
||||
.map_err(|e| self.generated_error(e))?;
|
||||
self.generated_value(response.into_inner())
|
||||
basis: Option<&BasisSubmission>,
|
||||
) -> ApiResult<generated_types::DeleteAllUserMessagesResponse> {
|
||||
let body = generated_types::AdminUserMessageDeleteRequest {
|
||||
basis: basis.map(BasisSubmission::moderation_basis),
|
||||
};
|
||||
let dry_run = if basis.is_some() { "false" } else { "true" };
|
||||
self.send(
|
||||
Method::DELETE,
|
||||
&format!(
|
||||
"/admin/users/{}/messages?dry_run={dry_run}",
|
||||
urlencoding::encode(user_id)
|
||||
),
|
||||
Some(&body),
|
||||
basis.and_then(BasisSubmission::internal_note),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_message_shred_status(
|
||||
|
||||
@@ -12,6 +12,7 @@ pub mod bulk;
|
||||
pub mod client;
|
||||
pub mod codes;
|
||||
pub mod discovery;
|
||||
pub mod enforcement;
|
||||
pub mod guild_assets;
|
||||
pub mod guild_threads;
|
||||
pub mod guilds;
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
|
||||
use reqwest::Method;
|
||||
|
||||
use crate::api::generated::{snowflake, types::UpdateReportRequestResolution};
|
||||
use crate::api::generated::{
|
||||
snowflake,
|
||||
types::{
|
||||
ReportNoteRequest, ReportNoteResponse, UpdateReportRequest, UpdateReportRequestOutcome,
|
||||
UpdateReportRequestResolution, UpdateReportRequestStatus,
|
||||
},
|
||||
};
|
||||
|
||||
use super::client::{AdminApiClient, ApiError, ApiResult};
|
||||
use super::types::{
|
||||
@@ -44,20 +50,24 @@ impl AdminApiClient {
|
||||
&self,
|
||||
report_id: &str,
|
||||
resolution: UpdateReportRequestResolution,
|
||||
outcome: Option<UpdateReportRequestOutcome>,
|
||||
duplicate_of: Option<&str>,
|
||||
public_comment: Option<&str>,
|
||||
notify_reporter: bool,
|
||||
audit_log_reason: Option<&str>,
|
||||
) -> ApiResult<ResolveReportResponse> {
|
||||
let mut body = serde_json::json!({"status": "resolved", "resolution": resolution});
|
||||
if let Some(public_comment) = public_comment {
|
||||
body["public_comment"] = serde_json::Value::from(public_comment);
|
||||
}
|
||||
body["notify_reporter"] = serde_json::Value::from(notify_reporter);
|
||||
let body = UpdateReportRequest {
|
||||
duplicate_of: duplicate_of.map(snowflake),
|
||||
notify_reporter,
|
||||
outcome,
|
||||
public_comment: public_comment.map(str::to_owned),
|
||||
resolution,
|
||||
status: UpdateReportRequestStatus::Resolved,
|
||||
};
|
||||
self.send(
|
||||
Method::PATCH,
|
||||
&format!("/admin/reports/{}", urlencoding::encode(report_id)),
|
||||
Some(&body),
|
||||
audit_log_reason,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -84,6 +94,20 @@ impl AdminApiClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn annotate_report(
|
||||
&self,
|
||||
report_id: &str,
|
||||
body: String,
|
||||
) -> ApiResult<ReportNoteResponse> {
|
||||
self.send(
|
||||
Method::POST,
|
||||
&format!("/admin/reports/{}/notes", urlencoding::encode(report_id)),
|
||||
Some(&ReportNoteRequest { body }),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_report(
|
||||
&self,
|
||||
report_id: &str,
|
||||
|
||||
@@ -118,6 +118,8 @@ pub struct AdminUser {
|
||||
pub deletion_scheduled_by: Option<String>,
|
||||
#[serde(default)]
|
||||
pub deletion_scheduled_at: Option<String>,
|
||||
#[serde(default)]
|
||||
pub enforcement_decision_id: Option<String>,
|
||||
pub last_active_at: Option<String>,
|
||||
pub last_active_ip: Option<String>,
|
||||
pub last_active_ip_reverse: Option<String>,
|
||||
@@ -253,6 +255,8 @@ pub struct BanCheckResult {
|
||||
#[serde(default)]
|
||||
pub expires_at: Option<String>,
|
||||
#[serde(default)]
|
||||
pub decision_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub entries: Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
|
||||
@@ -8,32 +8,12 @@ pub struct LookupMessageResponse {
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct MessageShredResponse {
|
||||
pub success: Option<bool>,
|
||||
pub job_id: Option<String>,
|
||||
pub requested: Option<u64>,
|
||||
#[serde(flatten)]
|
||||
pub extra: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct MessageShredStatusResponse {
|
||||
#[serde(flatten)]
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct DeleteAllUserMessagesResponse {
|
||||
pub success: Option<bool>,
|
||||
pub dry_run: bool,
|
||||
pub channel_count: u64,
|
||||
pub message_count: u64,
|
||||
pub job_id: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub extra: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct BrowseChannelResponse {
|
||||
#[serde(flatten)]
|
||||
|
||||
@@ -171,6 +171,16 @@ pub struct ReportEntry {
|
||||
pub legal_hold_until: Option<String>,
|
||||
#[serde(default)]
|
||||
pub legal_hold_reason: Option<String>,
|
||||
#[serde(default)]
|
||||
pub notes: Vec<ReportNoteEntry>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ReportNoteEntry {
|
||||
pub note_id: String,
|
||||
pub author_id: String,
|
||||
pub body: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
|
||||
@@ -6,6 +6,8 @@ use reqwest::Method;
|
||||
|
||||
use crate::api::generated::{snowflake, types as generated_types};
|
||||
|
||||
use crate::templates::components::enforcement_fields::BasisSubmission;
|
||||
|
||||
use super::client::{AdminApiClient, ApiError, ApiResult};
|
||||
use super::types::{
|
||||
AdminUser, AdminUserMeResponse, GuildInfo, ListUserGuildsResponse, LookupUserResponse,
|
||||
@@ -282,6 +284,7 @@ impl AdminApiClient {
|
||||
&self,
|
||||
user_id: &str,
|
||||
fields: &[String],
|
||||
basis: &BasisSubmission,
|
||||
) -> ApiResult<AdminUser> {
|
||||
let fields = fields
|
||||
.iter()
|
||||
@@ -290,14 +293,22 @@ impl AdminApiClient {
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| ApiError::Parse(e.to_string()))?;
|
||||
let body = generated_types::AdminUserClearFieldsRequest { fields };
|
||||
let response = self
|
||||
.generated()
|
||||
.clear_admin_user_profile_fields(&snowflake(user_id), &body)
|
||||
.await
|
||||
.map_err(|e| self.generated_error(e))?;
|
||||
let resp: UserMutationResponse = self.generated_value(response.into_inner())?;
|
||||
Ok(resp.user)
|
||||
let body = generated_types::AdminUserClearFieldsRequest {
|
||||
basis: basis.moderation_basis(),
|
||||
fields,
|
||||
};
|
||||
let response: UserMutationResponse = self
|
||||
.send(
|
||||
Method::DELETE,
|
||||
&format!(
|
||||
"/admin/users/{}/profile-fields",
|
||||
urlencoding::encode(user_id)
|
||||
),
|
||||
Some(&body),
|
||||
basis.internal_note(),
|
||||
)
|
||||
.await?;
|
||||
Ok(response.user)
|
||||
}
|
||||
|
||||
pub async fn set_bot_status(&self, user_id: &str, is_bot: bool) -> ApiResult<AdminUser> {
|
||||
@@ -355,93 +366,28 @@ impl AdminApiClient {
|
||||
Ok(resp.user)
|
||||
}
|
||||
|
||||
pub async fn temp_ban_user(
|
||||
&self,
|
||||
user_id: &str,
|
||||
duration_hours: u32,
|
||||
reason: Option<&str>,
|
||||
notify_user: bool,
|
||||
private_reason: Option<&str>,
|
||||
) -> ApiResult<AdminUser> {
|
||||
let body = generated_types::AdminUserBanRequest {
|
||||
duration_hours: i32::try_from(duration_hours)
|
||||
.map_err(|e| ApiError::Parse(e.to_string()))?
|
||||
.into(),
|
||||
notify_user,
|
||||
reason: reason.map(std::borrow::ToOwned::to_owned),
|
||||
};
|
||||
let resp: UserMutationResponse = self
|
||||
.send(
|
||||
Method::PUT,
|
||||
&format!("/admin/users/{}/ban", urlencoding::encode(user_id)),
|
||||
Some(&body),
|
||||
private_reason,
|
||||
)
|
||||
.await?;
|
||||
Ok(resp.user)
|
||||
}
|
||||
|
||||
pub async fn unban_user(
|
||||
&self,
|
||||
user_id: &str,
|
||||
public_reason: Option<&str>,
|
||||
notify_user: bool,
|
||||
private_reason: Option<&str>,
|
||||
) -> ApiResult<AdminUser> {
|
||||
let body = generated_types::AdminUserUnbanRequest {
|
||||
notify_user,
|
||||
public_reason: public_reason.map(std::borrow::ToOwned::to_owned),
|
||||
};
|
||||
let response = self
|
||||
.generated_with_reason(private_reason)?
|
||||
.unban_admin_user(&snowflake(user_id), &body)
|
||||
.unban_admin_user(&snowflake(user_id))
|
||||
.await
|
||||
.map_err(|e| self.generated_error(e))?;
|
||||
let resp: UserMutationResponse = self.generated_value(response.into_inner())?;
|
||||
Ok(resp.user)
|
||||
}
|
||||
|
||||
pub async fn schedule_deletion(
|
||||
&self,
|
||||
user_id: &str,
|
||||
reason_code: i32,
|
||||
public_reason: Option<&str>,
|
||||
days_until_deletion: u32,
|
||||
notify_user: bool,
|
||||
audit_log_reason: Option<&str>,
|
||||
) -> ApiResult<AdminUser> {
|
||||
let body = generated_types::AdminUserDeletionScheduleRequest {
|
||||
days_until_deletion: crate::api::generated::nonzero_u32(
|
||||
days_until_deletion,
|
||||
"days_until_deletion",
|
||||
)
|
||||
.map_err(ApiError::Parse)?
|
||||
.into(),
|
||||
notify_user,
|
||||
public_reason: public_reason.map(std::borrow::ToOwned::to_owned),
|
||||
reason_code: crate::api::generated::deletion_reason_code(reason_code, "reason_code")
|
||||
.map_err(ApiError::Parse)?,
|
||||
replace_pending_deletion_at: None,
|
||||
};
|
||||
let response = self
|
||||
.generated_with_reason(audit_log_reason)?
|
||||
.schedule_admin_user_deletion(&snowflake(user_id), &body)
|
||||
.await
|
||||
.map_err(|error| self.generated_error(error))?;
|
||||
let resp: UserMutationResponse = self.generated_value(response.into_inner())?;
|
||||
Ok(resp.user)
|
||||
}
|
||||
|
||||
pub async fn cancel_deletion(
|
||||
&self,
|
||||
user_id: &str,
|
||||
expected_pending_deletion_at: &str,
|
||||
notify_user: bool,
|
||||
audit_log_reason: Option<&str>,
|
||||
) -> ApiResult<AdminUser> {
|
||||
let body = serde_json::json!({
|
||||
"expected_pending_deletion_at": expected_pending_deletion_at,
|
||||
"notify_user": notify_user,
|
||||
});
|
||||
let resp: UserMutationResponse = self
|
||||
.send(
|
||||
@@ -454,25 +400,6 @@ impl AdminApiClient {
|
||||
Ok(resp.user)
|
||||
}
|
||||
|
||||
pub async fn annotate_ban(
|
||||
&self,
|
||||
user_id: &str,
|
||||
ban_audit_log_id: &str,
|
||||
note: &str,
|
||||
) -> ApiResult<()> {
|
||||
let body = serde_json::json!({
|
||||
"ban_audit_log_id": ban_audit_log_id,
|
||||
"note": note,
|
||||
});
|
||||
self.send_void(
|
||||
Method::POST,
|
||||
&format!("/admin/users/{}/ban/notes", urlencoding::encode(user_id)),
|
||||
Some(&body),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn change_dob(&self, user_id: &str, dob: &str) -> ApiResult<AdminUser> {
|
||||
let body = generated_types::AdminUserDobUpdateRequest {
|
||||
date_of_birth: dob.to_owned(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::api::client::{AdminApiClient, ApiResult};
|
||||
use crate::api::client::{AdminApiClient, ApiError, ApiResult};
|
||||
use crate::api::types::{FlashLevel, FlashMessage};
|
||||
use crate::middleware::auth::AuthContext;
|
||||
use crate::templates;
|
||||
@@ -201,6 +201,16 @@ async fn execute_check(
|
||||
_ => return ("error", "Unknown ban type".into()),
|
||||
};
|
||||
match result {
|
||||
Ok(r) if r.banned && r.decision_id.is_some() => (
|
||||
"info",
|
||||
format!(
|
||||
"{value} is blocked by decision {}{}. End that decision to remove the block.",
|
||||
r.decision_id.unwrap_or_default(),
|
||||
r.expires_at
|
||||
.map(|expires_at| format!(" until {}", format_admin_timestamp(&expires_at)))
|
||||
.unwrap_or_default()
|
||||
),
|
||||
),
|
||||
Ok(r) if r.banned => match r.expires_at {
|
||||
Some(expires_at) => (
|
||||
"info",
|
||||
@@ -226,6 +236,12 @@ fn ban_action_result(
|
||||
) -> (&'static str, String) {
|
||||
match result {
|
||||
Ok(()) => ("success", success_message),
|
||||
Err(ApiError::Http { status: 409, .. }) => (
|
||||
"error",
|
||||
format!(
|
||||
"{error_message}. It belongs to an enforcement decision, so end that decision instead."
|
||||
),
|
||||
),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "admin API request failed: ban action");
|
||||
("error", error_message)
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::{
|
||||
acl,
|
||||
api::{
|
||||
client::{AdminApiClient, ApiResultExt},
|
||||
enforcement::api_error_message,
|
||||
generated::types::EnforcementDecisionEndRequestOutcome,
|
||||
},
|
||||
middleware::{
|
||||
auth::AuthContext,
|
||||
csrf::CsrfToken,
|
||||
flash::{self, FlashData},
|
||||
htmx,
|
||||
},
|
||||
state::AppState,
|
||||
templates::pages::enforcement_decision,
|
||||
utils::forms::MultiValueForm,
|
||||
};
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{Path, Query, Request, State},
|
||||
http::HeaderMap,
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
routing::get,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DecisionsQuery {
|
||||
id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DecisionActionQuery {
|
||||
action: Option<String>,
|
||||
}
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new().route("/decisions", get(decisions)).route(
|
||||
"/decisions/{decision_id}",
|
||||
get(decision_detail).post(decision_post),
|
||||
)
|
||||
}
|
||||
|
||||
fn is_decision_id(value: &str) -> bool {
|
||||
(1..=20).contains(&value.len()) && value.bytes().all(|byte| byte.is_ascii_digit())
|
||||
}
|
||||
|
||||
async fn decisions(
|
||||
State(state): State<AppState>,
|
||||
auth: axum::Extension<AuthContext>,
|
||||
Query(query): Query<DecisionsQuery>,
|
||||
) -> Response {
|
||||
let config = state.config();
|
||||
let lookup = query
|
||||
.id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|id| !id.is_empty());
|
||||
if let Some(id) = lookup.filter(|id| is_decision_id(id)) {
|
||||
return Redirect::to(&format!("{}/decisions/{id}", config.base_path)).into_response();
|
||||
}
|
||||
let error = lookup.map(|_| "A decision ID is a number.");
|
||||
let admin_acls = auth
|
||||
.0
|
||||
.admin_user
|
||||
.as_ref()
|
||||
.map(|user| user.acls.as_slice())
|
||||
.unwrap_or_default();
|
||||
let review_queue = if acl::has_permission(admin_acls, acl::ENFORCEMENT_VIEW) {
|
||||
let client = AdminApiClient::new(state.http_client(), config, &auth.0.session);
|
||||
let result = client.list_review_queue().await;
|
||||
if let Err(error) = &result {
|
||||
tracing::warn!(%error, "admin API request failed: list the review queue");
|
||||
}
|
||||
Some(result)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let markup =
|
||||
enforcement_decision::decisions_page(config, &auth.0, error, review_queue.as_ref());
|
||||
Html(markup.into_string()).into_response()
|
||||
}
|
||||
|
||||
async fn decision_detail(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
auth: axum::Extension<AuthContext>,
|
||||
csrf: axum::Extension<CsrfToken>,
|
||||
Path(decision_id): Path<String>,
|
||||
) -> Response {
|
||||
let config = state.config();
|
||||
let client = AdminApiClient::new(state.http_client(), config, &auth.0.session);
|
||||
let decision = if is_decision_id(&decision_id) {
|
||||
client
|
||||
.get_decision(&decision_id)
|
||||
.await
|
||||
.log_error("load enforcement decision")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let users = match &decision {
|
||||
Some(decision) => client
|
||||
.lookup_users_by_ids(&enforcement_decision::decision_user_ids(decision))
|
||||
.await
|
||||
.log_error("load users named by an enforcement decision")
|
||||
.unwrap_or_default(),
|
||||
None => Vec::new(),
|
||||
};
|
||||
let markup = enforcement_decision::enforcement_decision_page(
|
||||
config,
|
||||
&auth.0,
|
||||
&decision_id,
|
||||
decision.as_ref(),
|
||||
&users,
|
||||
&csrf.0.0,
|
||||
htmx::targets(&headers, "main-content"),
|
||||
);
|
||||
Html(markup.into_string()).into_response()
|
||||
}
|
||||
|
||||
async fn decision_post(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
auth: axum::Extension<AuthContext>,
|
||||
Path(decision_id): Path<String>,
|
||||
Query(query): Query<DecisionActionQuery>,
|
||||
request: Request,
|
||||
) -> Response {
|
||||
let config = state.config();
|
||||
let redirect = format!("{}/decisions/{decision_id}", config.base_path);
|
||||
let flash = match MultiValueForm::from_request(request).await {
|
||||
Some(form) if is_decision_id(&decision_id) => {
|
||||
let client = AdminApiClient::new(state.http_client(), config, &auth.0.session);
|
||||
dispatch(&client, &decision_id, query.action.as_deref(), &form).await
|
||||
}
|
||||
Some(_) => FlashData::error("A decision ID is a number."),
|
||||
None => FlashData::error("Invalid form data"),
|
||||
};
|
||||
if htmx::is_htmx_request(&headers) && htmx::targets(&headers, "flash-container") {
|
||||
return htmx::toast_response(&flash);
|
||||
}
|
||||
flash::redirect_with_flash(&redirect, flash, config.secure_cookies())
|
||||
}
|
||||
|
||||
fn failure(context: &str, error: &crate::api::client::ApiError) -> FlashData {
|
||||
tracing::warn!(%error, "admin API request failed: {context}");
|
||||
match api_error_message(error) {
|
||||
Some(message) => FlashData::error(format!("{context}: {message}")),
|
||||
None => FlashData::error(context.to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn dispatch(
|
||||
client: &AdminApiClient,
|
||||
decision_id: &str,
|
||||
action: Option<&str>,
|
||||
form: &MultiValueForm,
|
||||
) -> FlashData {
|
||||
match action {
|
||||
Some("end") => {
|
||||
let Some(outcome) = form
|
||||
.first("outcome")
|
||||
.and_then(|value| EnforcementDecisionEndRequestOutcome::try_from(value).ok())
|
||||
else {
|
||||
return FlashData::error("Choose an outcome");
|
||||
};
|
||||
let Some(internal_note) = form.clean("internal_note") else {
|
||||
return FlashData::error("Add an internal note");
|
||||
};
|
||||
match client
|
||||
.end_decision(
|
||||
decision_id,
|
||||
outcome,
|
||||
form.clean("statement"),
|
||||
&internal_note,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => FlashData::success("Decision ended"),
|
||||
Err(error) => failure("Failed to end the decision", &error),
|
||||
}
|
||||
}
|
||||
Some("add_note") => {
|
||||
let Some(body) = form.clean("body") else {
|
||||
return FlashData::error("Write a note first");
|
||||
};
|
||||
match client.annotate_decision(decision_id, body).await {
|
||||
Ok(_) => FlashData::success("Note added"),
|
||||
Err(error) => failure("Failed to add the note", &error),
|
||||
}
|
||||
}
|
||||
_ => FlashData::error("Unknown decision action"),
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::{
|
||||
api::client::{AdminApiClient, ApiResultExt},
|
||||
api::{
|
||||
client::{AdminApiClient, ApiResultExt},
|
||||
enforcement::api_error_message,
|
||||
generated::types::CommunityRemovalBasis,
|
||||
},
|
||||
middleware::{
|
||||
auth::AuthContext,
|
||||
csrf,
|
||||
@@ -10,7 +14,10 @@ use crate::{
|
||||
},
|
||||
routes::guild_tabs,
|
||||
state::AppState,
|
||||
templates,
|
||||
templates::{
|
||||
self,
|
||||
components::enforcement_fields::{BasisSubmission, basis_from_form},
|
||||
},
|
||||
utils::forms::MultiValueForm,
|
||||
};
|
||||
use axum::{
|
||||
@@ -308,11 +315,7 @@ async fn dispatch_guild_action(
|
||||
"Guild shutdown requested",
|
||||
"Failed to shut down guild",
|
||||
),
|
||||
"delete_guild" => action_result(
|
||||
client.delete_guild(guild_id).await,
|
||||
"Guild deleted",
|
||||
"Failed to delete guild",
|
||||
),
|
||||
"delete_guild" => remove_community(client, guild_id, form).await,
|
||||
"update_settings" => {
|
||||
let mut settings = serde_json::Map::new();
|
||||
for key in &[
|
||||
@@ -455,6 +458,37 @@ async fn dispatch_guild_action(
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_community(
|
||||
client: &AdminApiClient,
|
||||
guild_id: &str,
|
||||
form: &MultiValueForm,
|
||||
) -> FlashData {
|
||||
let basis = match basis_from_form(form) {
|
||||
Ok(basis) => basis,
|
||||
Err(message) => return FlashData::error(message),
|
||||
};
|
||||
let community_basis = match &basis {
|
||||
BasisSubmission::Enforcement(submission) => {
|
||||
CommunityRemovalBasis::Enforcement(submission.decision.clone())
|
||||
}
|
||||
BasisSubmission::HolderRequest(_) => CommunityRemovalBasis::HolderRequest,
|
||||
BasisSubmission::Housekeeping(_) => return FlashData::error("Choose a basis"),
|
||||
};
|
||||
match client
|
||||
.delete_guild(guild_id, community_basis, basis.internal_note())
|
||||
.await
|
||||
{
|
||||
Ok(_) => FlashData::success("Community removed"),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, guild_id, "admin API request failed: remove community");
|
||||
FlashData::error(api_error_message(&error).map_or_else(
|
||||
|| "Failed to remove the community".to_owned(),
|
||||
|message| format!("Failed to remove the community: {message}"),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn action_result<T, E: std::fmt::Display>(
|
||||
result: Result<T, E>,
|
||||
success_message: &'static str,
|
||||
|
||||
@@ -2,16 +2,23 @@
|
||||
|
||||
use crate::{
|
||||
acl,
|
||||
api::client::AdminApiClient,
|
||||
api::{
|
||||
client::AdminApiClient,
|
||||
enforcement::api_error_message,
|
||||
generated::types::{BulkModerationBasis, MessageDeletionBasis},
|
||||
},
|
||||
middleware::{
|
||||
auth::AuthContext,
|
||||
flash::{self, FlashData},
|
||||
},
|
||||
state::AppState,
|
||||
templates::{
|
||||
components::enforcement_fields::{bulk_decision_from_form, can_take_action},
|
||||
pages::messages_page::MessageActions,
|
||||
},
|
||||
utils::forms::MultiValueForm,
|
||||
};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Query, Request, State},
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
@@ -93,17 +100,31 @@ pub(crate) async fn messages_post(
|
||||
.into_response()
|
||||
}
|
||||
"delete" => {
|
||||
let (Some(cid), Some(mid)) = (&channel_id, &message_id) else {
|
||||
return json_error(StatusCode::BAD_REQUEST, "Missing channel_id or message_id");
|
||||
let back = format!("{base}/messages?channel_id={}", encode_opt(&channel_id));
|
||||
let basis = form
|
||||
.first("basis")
|
||||
.and_then(|value| MessageDeletionBasis::try_from(value).ok());
|
||||
let flash = match (&channel_id, &message_id, basis) {
|
||||
(Some(cid), Some(mid), Some(basis)) => {
|
||||
let internal_note = form.clean("internal_note");
|
||||
match client
|
||||
.delete_message(cid, mid, basis, internal_note.as_deref())
|
||||
.await
|
||||
{
|
||||
Ok(()) => FlashData::success("Message deleted"),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "admin API request failed: delete message");
|
||||
FlashData::error(api_error_message(&error).map_or_else(
|
||||
|| "Failed to delete the message".to_owned(),
|
||||
|message| format!("Failed to delete the message: {message}"),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
(Some(_), Some(_), None) => FlashData::error("Choose a basis"),
|
||||
_ => FlashData::error("Enter the channel ID and message ID"),
|
||||
};
|
||||
let audit_log_reason = form.clean("audit_log_reason");
|
||||
match client
|
||||
.delete_message(cid, mid, audit_log_reason.as_deref())
|
||||
.await
|
||||
{
|
||||
Ok(()) => Json(serde_json::json!({"success": true})).into_response(),
|
||||
Err(e) => json_error(StatusCode::BAD_REQUEST, &format!("{e}")),
|
||||
}
|
||||
flash::redirect_with_flash(&back, flash, config.secure_cookies())
|
||||
}
|
||||
_ => Redirect::to(&format!("{base}/messages")).into_response(),
|
||||
}
|
||||
@@ -150,6 +171,15 @@ pub(crate) async fn system_dms_post(
|
||||
)
|
||||
}
|
||||
|
||||
fn bulk_basis(form: &MultiValueForm) -> Result<BulkModerationBasis, String> {
|
||||
match form.first("basis") {
|
||||
Some("enforcement") => bulk_decision_from_form(form).map(BulkModerationBasis::Enforcement),
|
||||
Some("holder_request") => Ok(BulkModerationBasis::HolderRequest),
|
||||
Some("housekeeping") => Ok(BulkModerationBasis::Housekeeping),
|
||||
_ => Err("Choose a basis".to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn bulk_actions_post(
|
||||
State(state): State<AppState>,
|
||||
auth: axum::Extension<AuthContext>,
|
||||
@@ -197,35 +227,48 @@ pub(crate) async fn bulk_actions_post(
|
||||
.bulk_add_guild_members(&guild_id, &user_ids, audit_log_reason.as_deref())
|
||||
.await
|
||||
}
|
||||
"bulk-schedule-user-deletion" | "bulk_delete_users" => {
|
||||
"bulk-schedule-user-deletion" => {
|
||||
let user_ids = form.list_values_any(&["user_ids[]", "user_ids"]);
|
||||
let (Ok(reason_code), Ok(days)) = (
|
||||
form.parse_value::<u32>("reason_code"),
|
||||
form.parse_value::<u32>("days_until_deletion"),
|
||||
) else {
|
||||
let basis = match bulk_basis(&form) {
|
||||
Ok(basis) => basis,
|
||||
Err(message) => {
|
||||
return flash::redirect_with_flash(
|
||||
&format!("{base}/bulk-actions"),
|
||||
FlashData::error(message),
|
||||
config.secure_cookies(),
|
||||
);
|
||||
}
|
||||
};
|
||||
let Ok(Some(days)) = form.parse_value::<u32>("days_until_deletion") else {
|
||||
return flash::redirect_with_flash(
|
||||
&format!("{base}/bulk-actions"),
|
||||
FlashData::error("Invalid deletion reason code or delay"),
|
||||
FlashData::error("Enter the days until deletion"),
|
||||
config.secure_cookies(),
|
||||
);
|
||||
};
|
||||
let public_reason = form.clean("public_reason");
|
||||
let notify_user = form.opt_out_value("notify_user");
|
||||
client
|
||||
.bulk_schedule_user_deletion(
|
||||
&user_ids,
|
||||
reason_code.unwrap_or(2),
|
||||
days.unwrap_or(14),
|
||||
public_reason.as_deref(),
|
||||
notify_user,
|
||||
audit_log_reason.as_deref(),
|
||||
days,
|
||||
basis,
|
||||
form.clean("internal_note").as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
"bulk-delete-user-messages" => {
|
||||
let user_ids = form.list_values_any(&["user_ids[]", "user_ids"]);
|
||||
let basis = match bulk_basis(&form) {
|
||||
Ok(basis) => basis,
|
||||
Err(message) => {
|
||||
return flash::redirect_with_flash(
|
||||
&format!("{base}/bulk-actions"),
|
||||
FlashData::error(message),
|
||||
config.secure_cookies(),
|
||||
);
|
||||
}
|
||||
};
|
||||
client
|
||||
.bulk_delete_user_messages(&user_ids, audit_log_reason.as_deref())
|
||||
.bulk_delete_user_messages(&user_ids, basis, form.clean("internal_note").as_deref())
|
||||
.await
|
||||
}
|
||||
_ => {
|
||||
@@ -295,7 +338,13 @@ pub(crate) async fn messages_browse_fragment(
|
||||
match result {
|
||||
Ok(resp) => {
|
||||
let markup = crate::templates::pages::messages_page::browse_messages_fragment(
|
||||
config, &resp.data, can_delete, None,
|
||||
config,
|
||||
&resp.data,
|
||||
MessageActions {
|
||||
delete: can_delete,
|
||||
take_action: can_take_action(admin_acls),
|
||||
},
|
||||
None,
|
||||
);
|
||||
Html(markup.into_string()).into_response()
|
||||
}
|
||||
@@ -325,14 +374,6 @@ fn encode_opt(value: &Option<String>) -> String {
|
||||
urlencoding::encode(value.as_deref().unwrap_or("")).into_owned()
|
||||
}
|
||||
|
||||
fn json_error(status: StatusCode, message: &str) -> Response {
|
||||
(
|
||||
status,
|
||||
Json(serde_json::json!({"success": false, "error": message})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub(crate) async fn archives_download(
|
||||
State(state): State<AppState>,
|
||||
auth: axum::Extension<AuthContext>,
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
api::client::{AdminApiClient, ApiResultExt},
|
||||
middleware::{auth::AuthContext, csrf::CsrfToken},
|
||||
state::AppState,
|
||||
templates,
|
||||
templates::{self, components::enforcement_fields::can_take_action},
|
||||
};
|
||||
use axum::{
|
||||
Router,
|
||||
@@ -137,6 +137,7 @@ async fn messages_page(
|
||||
prefill_channel_id: channel_id,
|
||||
can_lookup,
|
||||
can_delete,
|
||||
can_take_action: can_take_action(admin_acls),
|
||||
lookup_result: lookup_result.as_ref(),
|
||||
browse_result: browse_result.as_ref(),
|
||||
search_result: search_result.as_ref(),
|
||||
@@ -201,13 +202,6 @@ async fn bulk_actions_page(
|
||||
csrf: axum::Extension<CsrfToken>,
|
||||
) -> Response {
|
||||
let config = state.config();
|
||||
let client = AdminApiClient::new(state.http_client(), config, &auth.0.session);
|
||||
let account_identity = state.account_identity(&client).await;
|
||||
let markup = templates::pages::bulk_actions::bulk_actions_page(
|
||||
config,
|
||||
&auth.0,
|
||||
&csrf.0.0,
|
||||
account_identity.is_username(),
|
||||
);
|
||||
let markup = templates::pages::bulk_actions::bulk_actions_page(config, &auth.0, &csrf.0.0);
|
||||
Html(markup.into_string()).into_response()
|
||||
}
|
||||
@@ -8,6 +8,7 @@ mod bans_actions;
|
||||
mod billing_actions;
|
||||
pub mod codes;
|
||||
pub mod discovery;
|
||||
pub mod enforcement;
|
||||
mod guild_tabs;
|
||||
pub mod guilds;
|
||||
pub mod jobs;
|
||||
@@ -64,6 +65,7 @@ pub fn build_router(config: AdminConfig) -> Router {
|
||||
.merge(applications::router())
|
||||
.merge(codes::router())
|
||||
.merge(discovery::router())
|
||||
.merge(enforcement::router())
|
||||
.merge(registrations::router())
|
||||
.merge(jobs::router())
|
||||
.merge(messages::router())
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::{
|
||||
acl,
|
||||
api::{
|
||||
client::{AdminApiClient, ApiError, ApiResultExt},
|
||||
generated::types::{UpdateReportRequestOutcome, UpdateReportRequestResolution},
|
||||
reports::SearchReportsParams,
|
||||
types::ReportEntry,
|
||||
},
|
||||
@@ -15,7 +16,7 @@ use crate::{
|
||||
htmx,
|
||||
},
|
||||
state::AppState,
|
||||
templates,
|
||||
templates::{self, components::enforcement_fields::is_snowflake},
|
||||
utils::{forms::clean_string, timestamps::format_admin_timestamp},
|
||||
};
|
||||
use axum::{
|
||||
@@ -61,6 +62,14 @@ struct LegalHoldForm {
|
||||
clear: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct NoteForm {
|
||||
#[serde(default)]
|
||||
_csrf: Option<String>,
|
||||
#[serde(default)]
|
||||
body: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DeleteForm {
|
||||
#[serde(default)]
|
||||
@@ -78,6 +87,10 @@ struct ResolveForm {
|
||||
#[serde(default)]
|
||||
resolution: Option<String>,
|
||||
#[serde(default)]
|
||||
outcome: Option<String>,
|
||||
#[serde(default)]
|
||||
duplicate_of: Option<String>,
|
||||
#[serde(default)]
|
||||
public_comment: Option<String>,
|
||||
#[serde(default)]
|
||||
notify_reporter: Option<String>,
|
||||
@@ -94,6 +107,10 @@ pub fn router() -> Router<AppState> {
|
||||
"/reports/{report_id}/resolve",
|
||||
axum::routing::post(report_resolve),
|
||||
)
|
||||
.route(
|
||||
"/reports/{report_id}/notes",
|
||||
axum::routing::post(report_note),
|
||||
)
|
||||
.route(
|
||||
"/reports/{report_id}/legal-hold",
|
||||
axum::routing::post(report_legal_hold),
|
||||
@@ -210,12 +227,21 @@ async fn report_detail(
|
||||
.log_error("load report detail");
|
||||
match report {
|
||||
Some(report) => {
|
||||
let live = load_live_profile(&client, &auth.0, &report).await;
|
||||
let note_author_ids = note_author_ids(&report);
|
||||
let (live, note_authors) =
|
||||
tokio::join!(load_live_profile(&client, &auth.0, &report), async {
|
||||
client
|
||||
.lookup_users_by_ids(¬e_author_ids)
|
||||
.await
|
||||
.log_error("load report note authors")
|
||||
.unwrap_or_default()
|
||||
});
|
||||
let markup = templates::pages::report_detail::report_detail_page(
|
||||
config,
|
||||
&auth.0,
|
||||
&report,
|
||||
&live,
|
||||
¬e_authors,
|
||||
&csrf_token,
|
||||
is_detail_fragment,
|
||||
);
|
||||
@@ -228,6 +254,17 @@ async fn report_detail(
|
||||
}
|
||||
}
|
||||
|
||||
fn note_author_ids(report: &ReportEntry) -> Vec<String> {
|
||||
let mut ids: Vec<String> = report
|
||||
.notes
|
||||
.iter()
|
||||
.map(|note| note.author_id.clone())
|
||||
.collect();
|
||||
ids.sort_unstable();
|
||||
ids.dedup();
|
||||
ids
|
||||
}
|
||||
|
||||
async fn load_live_profile(
|
||||
client: &AdminApiClient,
|
||||
auth: &AuthContext,
|
||||
@@ -323,6 +360,35 @@ async fn report_resolve(
|
||||
config.secure_cookies(),
|
||||
);
|
||||
};
|
||||
let outcome = match resolution {
|
||||
UpdateReportRequestResolution::NoViolation => Some(UpdateReportRequestOutcome::NoViolation),
|
||||
UpdateReportRequestResolution::Actioned => form
|
||||
.outcome
|
||||
.as_deref()
|
||||
.and_then(|value| UpdateReportRequestOutcome::try_from(value).ok())
|
||||
.filter(|outcome| *outcome != UpdateReportRequestOutcome::NoViolation),
|
||||
UpdateReportRequestResolution::Duplicate => None,
|
||||
};
|
||||
let duplicate_of = form
|
||||
.duplicate_of
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| is_snowflake(value))
|
||||
.map(str::to_owned);
|
||||
let missing = match resolution {
|
||||
UpdateReportRequestResolution::Actioned if outcome.is_none() => Some(OUTCOME_REQUIRED),
|
||||
UpdateReportRequestResolution::Duplicate if duplicate_of.is_none() => {
|
||||
Some(DUPLICATE_OF_REQUIRED)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(message) = missing {
|
||||
return flash::redirect_with_flash(
|
||||
&back,
|
||||
FlashData::error(message),
|
||||
config.secure_cookies(),
|
||||
);
|
||||
}
|
||||
let client = AdminApiClient::new(state.http_client(), config, &auth.0.session);
|
||||
let public_comment = clean_string(form.public_comment.as_deref().unwrap_or(""));
|
||||
let notify_reporter =
|
||||
@@ -331,9 +397,10 @@ async fn report_resolve(
|
||||
.resolve_report(
|
||||
&report_id,
|
||||
resolution,
|
||||
outcome,
|
||||
duplicate_of.as_deref(),
|
||||
public_comment.as_deref(),
|
||||
notify_reporter,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -346,7 +413,46 @@ async fn report_resolve(
|
||||
flash::redirect_with_flash(&back, flash, config.secure_cookies())
|
||||
}
|
||||
|
||||
async fn report_note(
|
||||
State(state): State<AppState>,
|
||||
auth: axum::Extension<AuthContext>,
|
||||
Path(report_id): Path<String>,
|
||||
request: Request,
|
||||
) -> Response {
|
||||
let config = state.config();
|
||||
let back = format!("{}/reports/{report_id}", config.base_path);
|
||||
let form: NoteForm = match Form::from_request(request, &state).await {
|
||||
Ok(Form(f)) => f,
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, report_id, "failed to parse report note form");
|
||||
return flash::redirect_with_flash(
|
||||
&back,
|
||||
FlashData::error("Invalid form data"),
|
||||
config.secure_cookies(),
|
||||
);
|
||||
}
|
||||
};
|
||||
let Some(body) = clean_string(form.body.as_deref().unwrap_or("")) else {
|
||||
return flash::redirect_with_flash(
|
||||
&back,
|
||||
FlashData::error("Write a note first"),
|
||||
config.secure_cookies(),
|
||||
);
|
||||
};
|
||||
let client = AdminApiClient::new(state.http_client(), config, &auth.0.session);
|
||||
let flash = match client.annotate_report(&report_id, body).await {
|
||||
Ok(_) => FlashData::success("Note added"),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, report_id, "admin API request failed: add report note");
|
||||
FlashData::error("Failed to add the note")
|
||||
}
|
||||
};
|
||||
flash::redirect_with_flash(&back, flash, config.secure_cookies())
|
||||
}
|
||||
|
||||
const RESOLUTION_REQUIRED: &str = "Choose a resolution";
|
||||
const OUTCOME_REQUIRED: &str = "Choose what happened to the reported content or account";
|
||||
const DUPLICATE_OF_REQUIRED: &str = "Enter the ID of the report this one repeats";
|
||||
|
||||
async fn report_legal_hold(
|
||||
State(state): State<AppState>,
|
||||
|
||||
@@ -3,8 +3,19 @@
|
||||
use crate::utils::plural::count_noun;
|
||||
use crate::{
|
||||
admin_flags,
|
||||
api::client::{AdminApiClient, ApiError},
|
||||
api::{
|
||||
client::{AdminApiClient, ApiError},
|
||||
enforcement::{ClosureRequest, api_error_message},
|
||||
generated::{
|
||||
snowflake,
|
||||
types::{AdminUserFeatureLimitRequestLimit, AdminUserMessageShredRequestEntriesItem},
|
||||
},
|
||||
},
|
||||
middleware::flash::FlashData,
|
||||
templates::components::enforcement_fields::{
|
||||
BasisSubmission, basis_from_form, decision_from_form, network_blocks_from_form,
|
||||
skipped_networks_summary,
|
||||
},
|
||||
utils::forms::MultiValueForm,
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
@@ -74,16 +85,21 @@ pub async fn dispatch(
|
||||
return DispatchOutcome::error("User not found");
|
||||
}
|
||||
};
|
||||
let suspended = user.temp_banned_until.is_some();
|
||||
let editable =
|
||||
|flag: &u64| admin_flags::user_flag_is_editable(*flag, user.flags, suspended);
|
||||
let add = submitted
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|flag| user.flags & *flag == 0)
|
||||
.filter(|flag| user.flags & *flag == 0 && editable(flag))
|
||||
.map(|flag| flag.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
let remove = admin_flags::USER_FLAGS
|
||||
.iter()
|
||||
.map(|flag| flag.value)
|
||||
.filter(|flag| user.flags & *flag != 0 && !selected.contains(flag))
|
||||
.filter(|flag| {
|
||||
user.flags & *flag != 0 && !selected.contains(flag) && editable(flag)
|
||||
})
|
||||
.map(|flag| flag.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
DispatchOutcome::from_result(
|
||||
@@ -122,12 +138,18 @@ pub async fn dispatch(
|
||||
let add = submitted
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|flag| current & *flag != *flag)
|
||||
.filter(|flag| {
|
||||
current & *flag != *flag && admin_flags::premium_flag_is_editable(*flag)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let remove = admin_flags::PREMIUM_FLAGS
|
||||
.iter()
|
||||
.map(|flag| flag.value)
|
||||
.filter(|flag| current & *flag == *flag && !selected.contains(flag))
|
||||
.filter(|flag| {
|
||||
current & *flag == *flag
|
||||
&& !selected.contains(flag)
|
||||
&& admin_flags::premium_flag_is_editable(*flag)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
DispatchOutcome::from_result(
|
||||
client.update_premium_flags(user_id, &add, &remove).await,
|
||||
@@ -172,12 +194,20 @@ pub async fn dispatch(
|
||||
"Failed to terminate user sessions",
|
||||
),
|
||||
"clear_fields" => {
|
||||
let f = form.list_values_any(&["fields[]", "fields"]);
|
||||
DispatchOutcome::from_result(
|
||||
client.clear_user_fields(user_id, &f).await,
|
||||
"User fields cleared successfully",
|
||||
"Failed to clear user fields",
|
||||
)
|
||||
let fields = form.list_values_any(&["fields[]", "fields"]);
|
||||
if fields.is_empty() {
|
||||
return DispatchOutcome::error("Choose at least one field");
|
||||
}
|
||||
let basis = match basis_from_form(form) {
|
||||
Ok(basis) => basis,
|
||||
Err(message) => return DispatchOutcome::error(message),
|
||||
};
|
||||
match client.clear_user_fields(user_id, &fields, &basis).await {
|
||||
Ok(_) => DispatchOutcome::success("Profile fields cleared"),
|
||||
Err(error) => {
|
||||
decision_failure("Failed to clear the profile fields", user_id, &error)
|
||||
}
|
||||
}
|
||||
}
|
||||
"set_bot_status" => {
|
||||
let val = form.bool_value("bot");
|
||||
@@ -218,43 +248,61 @@ pub async fn dispatch(
|
||||
"Failed to change email",
|
||||
)
|
||||
}
|
||||
"temp_ban" => {
|
||||
let Ok(duration) = form.parse_value_any::<u32>(&["duration_hours", "duration"]) else {
|
||||
return DispatchOutcome::error("Invalid ban duration");
|
||||
"suspend" => {
|
||||
let Ok(Some(duration_hours)) = form.parse_value::<u32>("duration_hours") else {
|
||||
return DispatchOutcome::error("Choose a duration");
|
||||
};
|
||||
let reason = get("reason");
|
||||
let private = get("private_reason");
|
||||
let notify_user = form.opt_out_value("notify_user");
|
||||
DispatchOutcome::from_result(
|
||||
client
|
||||
.temp_ban_user(
|
||||
user_id,
|
||||
duration.unwrap_or(24),
|
||||
reason.as_deref(),
|
||||
notify_user,
|
||||
private.as_deref(),
|
||||
)
|
||||
.await,
|
||||
"User temporarily banned successfully",
|
||||
"Failed to temporarily ban user",
|
||||
)
|
||||
let submission = match decision_from_form(form) {
|
||||
Ok(submission) => submission,
|
||||
Err(message) => return DispatchOutcome::error(message),
|
||||
};
|
||||
let block_networks = match network_blocks_from_form(form) {
|
||||
Ok(block_networks) => block_networks,
|
||||
Err(message) => return DispatchOutcome::error(message),
|
||||
};
|
||||
let remove_evidence = form.bool_value("remove_evidence");
|
||||
match client
|
||||
.suspend_user(
|
||||
user_id,
|
||||
duration_hours,
|
||||
remove_evidence,
|
||||
block_networks,
|
||||
&submission,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
let done = if duration_hours == 0 {
|
||||
"Account banned."
|
||||
} else {
|
||||
"Account suspended."
|
||||
};
|
||||
DispatchOutcome::success(with_skipped(done, &response.skipped_ips))
|
||||
}
|
||||
Err(error) => decision_failure("Failed to suspend the account", user_id, &error),
|
||||
}
|
||||
}
|
||||
"warn" => {
|
||||
let submission = match decision_from_form(form) {
|
||||
Ok(submission) => submission,
|
||||
Err(message) => return DispatchOutcome::error(message),
|
||||
};
|
||||
match client.warn_user(user_id, &submission).await {
|
||||
Ok(response) => DispatchOutcome::success(format!(
|
||||
"Warning sent as decision {}",
|
||||
response.decision_id.as_str()
|
||||
)),
|
||||
Err(error) => decision_failure("Failed to send the warning", user_id, &error),
|
||||
}
|
||||
}
|
||||
"unban" => {
|
||||
let public_reason = get("public_reason");
|
||||
let private_reason = get("private_reason");
|
||||
let notify_user = form.opt_out_value("notify_user");
|
||||
DispatchOutcome::from_result(
|
||||
client
|
||||
.unban_user(
|
||||
user_id,
|
||||
public_reason.as_deref(),
|
||||
notify_user,
|
||||
private_reason.as_deref(),
|
||||
)
|
||||
.await,
|
||||
"User unbanned successfully",
|
||||
"Failed to unban user",
|
||||
)
|
||||
let Some(internal_note) = get("internal_note") else {
|
||||
return DispatchOutcome::error("Add an internal note");
|
||||
};
|
||||
match client.unban_user(user_id, Some(&internal_note)).await {
|
||||
Ok(_) => DispatchOutcome::success("Suspension lifted"),
|
||||
Err(error) => decision_failure("Failed to lift the suspension", user_id, &error),
|
||||
}
|
||||
}
|
||||
"ban_ip" => {
|
||||
let Some(ip) = get("ip") else {
|
||||
@@ -279,30 +327,33 @@ pub async fn dispatch(
|
||||
"Failed to ban avatar hash",
|
||||
)
|
||||
}
|
||||
"schedule_deletion" => {
|
||||
let Ok(reason_code) = form.parse_value::<i32>("reason_code") else {
|
||||
return DispatchOutcome::error("Invalid deletion reason code");
|
||||
"close" => close_account(client, user_id, form).await,
|
||||
"limit_features" => {
|
||||
let Some(limit) = form
|
||||
.first("limit")
|
||||
.and_then(|value| AdminUserFeatureLimitRequestLimit::try_from(value).ok())
|
||||
else {
|
||||
return DispatchOutcome::error("Choose a limit");
|
||||
};
|
||||
let public_reason = get("public_reason");
|
||||
let private_reason = get("private_reason");
|
||||
let Ok(days) = form.parse_value_any::<u32>(&["days_until_deletion", "days"]) else {
|
||||
return DispatchOutcome::error("Invalid deletion delay");
|
||||
let ends_at = match (limit, form.first("ends_on")) {
|
||||
(AdminUserFeatureLimitRequestLimit::Reports, Some(value)) => match parse_day(value)
|
||||
{
|
||||
Some(ends_at) => Some(ends_at),
|
||||
None => return DispatchOutcome::error("Enter the end date as a date"),
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
let notify_user = form.opt_out_value("notify_user");
|
||||
DispatchOutcome::from_result(
|
||||
client
|
||||
.schedule_deletion(
|
||||
user_id,
|
||||
reason_code.unwrap_or(0),
|
||||
public_reason.as_deref(),
|
||||
days.unwrap_or(60),
|
||||
notify_user,
|
||||
private_reason.as_deref(),
|
||||
)
|
||||
.await,
|
||||
"User deletion scheduled successfully",
|
||||
"Failed to schedule user deletion",
|
||||
)
|
||||
let submission = match decision_from_form(form) {
|
||||
Ok(submission) => submission,
|
||||
Err(message) => return DispatchOutcome::error(message),
|
||||
};
|
||||
match client
|
||||
.limit_features(user_id, limit, ends_at, &submission)
|
||||
.await
|
||||
{
|
||||
Ok(_) => DispatchOutcome::success("Feature limited"),
|
||||
Err(error) => decision_failure("Failed to limit the feature", user_id, &error),
|
||||
}
|
||||
}
|
||||
"cancel_deletion" => {
|
||||
let Some(expected) = get("expected_pending_deletion_at") else {
|
||||
@@ -315,40 +366,15 @@ pub async fn dispatch(
|
||||
"Confirm whose deletion you are canceling before submitting",
|
||||
);
|
||||
}
|
||||
let Some(private_reason) = get("private_reason") else {
|
||||
return DispatchOutcome::error("A private reason is required to cancel a deletion");
|
||||
let Some(internal_note) = get("internal_note") else {
|
||||
return DispatchOutcome::error("Add an internal note");
|
||||
};
|
||||
let notify_user = form.bool_value("notify_user");
|
||||
match client
|
||||
.cancel_deletion(user_id, &expected, notify_user, Some(&private_reason))
|
||||
.cancel_deletion(user_id, &expected, Some(&internal_note))
|
||||
.await
|
||||
{
|
||||
Ok(_) => DispatchOutcome::success("User deletion canceled successfully"),
|
||||
Err(ApiError::Http { status: 409, .. }) => DispatchOutcome::error(
|
||||
"The pending deletion changed since this page loaded. Reload and review.",
|
||||
),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, user_id, "admin API request failed: cancel user deletion");
|
||||
DispatchOutcome::error("Failed to cancel user deletion")
|
||||
}
|
||||
}
|
||||
}
|
||||
"annotate_ban" => {
|
||||
let Some(ban_audit_log_id) = get("ban_audit_log_id") else {
|
||||
return DispatchOutcome::error("The ban audit log entry is missing from the form");
|
||||
};
|
||||
let Some(note) = get("note") else {
|
||||
return DispatchOutcome::error("Note is required");
|
||||
};
|
||||
match client.annotate_ban(user_id, &ban_audit_log_id, ¬e).await {
|
||||
Ok(()) => DispatchOutcome::success("Note added to the ban"),
|
||||
Err(ApiError::Http { status: 409, .. }) => DispatchOutcome::error(
|
||||
"The ban changed since this page loaded. Reload and review.",
|
||||
),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, user_id, "admin API request failed: annotate ban");
|
||||
DispatchOutcome::error("Failed to add the note to the ban")
|
||||
}
|
||||
Ok(_) => DispatchOutcome::success("Deletion cancelled"),
|
||||
Err(error) => decision_failure("Failed to cancel the deletion", user_id, &error),
|
||||
}
|
||||
}
|
||||
"change_dob" => {
|
||||
@@ -431,79 +457,172 @@ pub async fn dispatch(
|
||||
}
|
||||
}
|
||||
}
|
||||
"delete_all_messages" => {
|
||||
let dry_run = parse_dry_run(form.first("dry_run"));
|
||||
match client.delete_all_user_messages(user_id, dry_run).await {
|
||||
Ok(response) if dry_run => DispatchOutcome {
|
||||
flash: FlashData::success(format!(
|
||||
"Dry run found {} across {}. Confirm to delete them permanently.",
|
||||
count_noun(response.message_count, "message", "messages"),
|
||||
count_noun(response.channel_count, "channel", "channels")
|
||||
)),
|
||||
redirect_params: vec![
|
||||
("delete_all_messages_dry_run".to_owned(), "true".to_owned()),
|
||||
(
|
||||
"delete_all_messages_channel_count".to_owned(),
|
||||
response.channel_count.to_string(),
|
||||
),
|
||||
(
|
||||
"delete_all_messages_message_count".to_owned(),
|
||||
response.message_count.to_string(),
|
||||
),
|
||||
],
|
||||
},
|
||||
Ok(response) => {
|
||||
if let Some(job_id) = response.job_id {
|
||||
DispatchOutcome {
|
||||
flash: FlashData::success(
|
||||
"Delete job queued. Monitor progress in the status panel.",
|
||||
),
|
||||
redirect_params: vec![("message_shred_job_id".to_owned(), job_id)],
|
||||
}
|
||||
} else {
|
||||
DispatchOutcome::success("No messages found for deletion.")
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, user_id, "admin API request failed: delete all user messages");
|
||||
DispatchOutcome::error("Failed to delete all user messages")
|
||||
}
|
||||
}
|
||||
}
|
||||
"delete_all_messages" => delete_all_messages(client, user_id, form).await,
|
||||
"cancel_bulk_message_deletion" => DispatchOutcome::from_result(
|
||||
client.cancel_bulk_message_deletion(user_id).await,
|
||||
"Bulk message deletion canceled successfully",
|
||||
"Failed to cancel bulk message deletion",
|
||||
),
|
||||
"message_shred" => {
|
||||
let csv = form.first("csv_data").unwrap_or_default();
|
||||
match parse_message_shred_csv(csv) {
|
||||
Ok(entries) if entries.is_empty() => DispatchOutcome::error(
|
||||
"CSV did not contain any valid channel_id,message_id pairs.",
|
||||
),
|
||||
Ok(entries) => match client.queue_message_shred(user_id, &entries).await {
|
||||
Ok(response) => {
|
||||
if let Some(job_id) = response.job_id {
|
||||
DispatchOutcome {
|
||||
flash: FlashData::success("Message shred job queued"),
|
||||
redirect_params: vec![("message_shred_job_id".to_owned(), job_id)],
|
||||
}
|
||||
} else {
|
||||
DispatchOutcome::success("Message shred job queued")
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, user_id, "admin API request failed: queue message shred job");
|
||||
DispatchOutcome::error("Failed to queue message shred job")
|
||||
}
|
||||
let entries = match parse_message_shred_csv(form.first("csv_data").unwrap_or_default())
|
||||
{
|
||||
Ok(entries) if entries.is_empty() => {
|
||||
return DispatchOutcome::error("The CSV has no channel_id,message_id rows.");
|
||||
}
|
||||
Ok(entries) => entries,
|
||||
Err(error) => return DispatchOutcome::error(error),
|
||||
};
|
||||
let basis = match basis_from_form(form) {
|
||||
Ok(basis) => basis,
|
||||
Err(message) => return DispatchOutcome::error(message),
|
||||
};
|
||||
match client.queue_message_shred(user_id, entries, &basis).await {
|
||||
Ok(response) => DispatchOutcome {
|
||||
flash: FlashData::success(removal_message(
|
||||
"Message shred queued",
|
||||
response.decision_id.as_ref().map(|id| id.as_str()),
|
||||
)),
|
||||
redirect_params: vec![("message_shred_job_id".to_owned(), response.job_id)],
|
||||
},
|
||||
Err(error) => DispatchOutcome::error(error),
|
||||
Err(error) => {
|
||||
decision_failure("Failed to queue the message shred", user_id, &error)
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => DispatchOutcome::error(format!("Unknown user action: {action}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_skipped(
|
||||
done: &str,
|
||||
skipped: &[crate::api::generated::types::SkippedNetworkBlock],
|
||||
) -> String {
|
||||
match skipped_networks_summary(skipped) {
|
||||
Some(summary) => format!("{done} {summary}"),
|
||||
None => done.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn close_account(
|
||||
client: &AdminApiClient,
|
||||
user_id: &str,
|
||||
form: &MultiValueForm,
|
||||
) -> DispatchOutcome {
|
||||
let Ok(Some(days_until_deletion)) = form.parse_value::<u32>("days_until_deletion") else {
|
||||
return DispatchOutcome::error("Enter the days until deletion");
|
||||
};
|
||||
let replace_pending_deletion_at = match form.clean("replace_pending_deletion_at") {
|
||||
Some(value) => match chrono::DateTime::parse_from_rfc3339(&value) {
|
||||
Ok(at) => Some(at.with_timezone(&chrono::Utc)),
|
||||
Err(_) => {
|
||||
return DispatchOutcome::error(
|
||||
"The pending deletion is missing from the form. Reload and review.",
|
||||
);
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let basis = match basis_from_form(form) {
|
||||
Ok(basis) => basis,
|
||||
Err(message) => return DispatchOutcome::error(message),
|
||||
};
|
||||
let enforcement = matches!(basis, BasisSubmission::Enforcement(_));
|
||||
let block_networks = match network_blocks_from_form(form) {
|
||||
Ok(block_networks) => block_networks.filter(|_| enforcement),
|
||||
Err(message) => return DispatchOutcome::error(message),
|
||||
};
|
||||
let request = ClosureRequest {
|
||||
basis: basis.moderation_basis(),
|
||||
days_until_deletion,
|
||||
remove_evidence: enforcement && form.bool_value("remove_evidence"),
|
||||
block_networks,
|
||||
replace_pending_deletion_at,
|
||||
internal_note: basis.internal_note().map(str::to_owned),
|
||||
};
|
||||
match client.close_account(user_id, &request).await {
|
||||
Ok(response) => {
|
||||
DispatchOutcome::success(with_skipped("Account closed.", &response.skipped_ips))
|
||||
}
|
||||
Err(error) => decision_failure("Failed to close the account", user_id, &error),
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_all_messages(
|
||||
client: &AdminApiClient,
|
||||
user_id: &str,
|
||||
form: &MultiValueForm,
|
||||
) -> DispatchOutcome {
|
||||
let basis = if parse_dry_run(form.first("dry_run")) {
|
||||
None
|
||||
} else {
|
||||
match basis_from_form(form) {
|
||||
Ok(basis) => Some(basis),
|
||||
Err(message) => return DispatchOutcome::error(message),
|
||||
}
|
||||
};
|
||||
let response = match client
|
||||
.delete_all_user_messages(user_id, basis.as_ref())
|
||||
.await
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(error) => return decision_failure("Failed to delete the messages", user_id, &error),
|
||||
};
|
||||
let message_count = u64::try_from(*response.message_count).unwrap_or_default();
|
||||
let channel_count = u64::try_from(*response.channel_count).unwrap_or_default();
|
||||
if basis.is_none() {
|
||||
return DispatchOutcome {
|
||||
flash: FlashData::success(format!(
|
||||
"Found {} in {}. Choose a basis to delete them.",
|
||||
count_noun(message_count, "message", "messages"),
|
||||
count_noun(channel_count, "channel", "channels")
|
||||
)),
|
||||
redirect_params: vec![
|
||||
("delete_all_messages_dry_run".to_owned(), "true".to_owned()),
|
||||
(
|
||||
"delete_all_messages_channel_count".to_owned(),
|
||||
channel_count.to_string(),
|
||||
),
|
||||
(
|
||||
"delete_all_messages_message_count".to_owned(),
|
||||
message_count.to_string(),
|
||||
),
|
||||
],
|
||||
};
|
||||
}
|
||||
match response.job_id {
|
||||
Some(job_id) => DispatchOutcome {
|
||||
flash: FlashData::success(removal_message(
|
||||
"Delete job queued",
|
||||
response.decision_id.as_ref().map(|id| id.as_str()),
|
||||
)),
|
||||
redirect_params: vec![("message_shred_job_id".to_owned(), job_id)],
|
||||
},
|
||||
None => DispatchOutcome::success("No messages to delete"),
|
||||
}
|
||||
}
|
||||
|
||||
fn removal_message(queued: &str, decision_id: Option<&str>) -> String {
|
||||
match decision_id {
|
||||
Some(decision_id) => format!("{queued} as decision {decision_id}"),
|
||||
None => queued.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_day(value: &str) -> Option<chrono::DateTime<chrono::Utc>> {
|
||||
chrono::NaiveDate::parse_from_str(value.trim(), "%Y-%m-%d")
|
||||
.ok()?
|
||||
.and_hms_opt(0, 0, 0)
|
||||
.map(|at| at.and_utc())
|
||||
}
|
||||
|
||||
fn decision_failure(context: &str, user_id: &str, error: &ApiError) -> DispatchOutcome {
|
||||
tracing::warn!(%error, user_id, "admin API request failed: {context}");
|
||||
match api_error_message(error) {
|
||||
Some(message) => DispatchOutcome::error(format!("{context}: {message}")),
|
||||
None => DispatchOutcome::error(context.to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
fn has_legacy_flag_delta_fields(form: &MultiValueForm) -> bool {
|
||||
["add_flags[]", "add_flags", "remove_flags[]", "remove_flags"]
|
||||
.iter()
|
||||
@@ -521,7 +640,9 @@ fn parse_dry_run(value: Option<&str>) -> bool {
|
||||
!matches!(value.map(|value| value.trim().to_ascii_lowercase()), Some(value) if value == "false" || value == "0")
|
||||
}
|
||||
|
||||
fn parse_message_shred_csv(csv: &str) -> Result<Vec<serde_json::Value>, String> {
|
||||
fn parse_message_shred_csv(
|
||||
csv: &str,
|
||||
) -> Result<Vec<AdminUserMessageShredRequestEntriesItem>, String> {
|
||||
let mut entries = Vec::new();
|
||||
for line in csv.trim().lines() {
|
||||
let trimmed = line.trim();
|
||||
@@ -534,12 +655,10 @@ fn parse_message_shred_csv(csv: &str) -> Result<Vec<serde_json::Value>, String>
|
||||
"Each row must contain channel_id and message_id separated by a comma".to_owned(),
|
||||
);
|
||||
}
|
||||
let channel_id = parse_csv_snowflake(parts[0], "channel_id", trimmed)?;
|
||||
let message_id = parse_csv_snowflake(parts[1], "message_id", trimmed)?;
|
||||
entries.push(serde_json::json!({
|
||||
"channel_id": channel_id,
|
||||
"message_id": message_id,
|
||||
}));
|
||||
entries.push(AdminUserMessageShredRequestEntriesItem {
|
||||
channel_id: snowflake(&parse_csv_snowflake(parts[0], "channel_id", trimmed)?),
|
||||
message_id: snowflake(&parse_csv_snowflake(parts[1], "message_id", trimmed)?),
|
||||
});
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
@@ -572,8 +691,10 @@ mod tests {
|
||||
let entries = parse_message_shred_csv("channel_id,message_id\n123,456\n789,101112")
|
||||
.expect("valid csv");
|
||||
assert_eq!(entries.len(), 2);
|
||||
assert_eq!(entries[0]["channel_id"], "123");
|
||||
assert_eq!(entries[0]["message_id"], "456");
|
||||
assert_eq!(
|
||||
serde_json::to_value(&entries[0]).expect("serializable entry"),
|
||||
serde_json::json!({"channel_id": "123", "message_id": "456"})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -25,6 +25,8 @@ pub struct TabQuery {
|
||||
pub delete_all_messages_dry_run: Option<String>,
|
||||
pub delete_all_messages_channel_count: Option<u64>,
|
||||
pub delete_all_messages_message_count: Option<u64>,
|
||||
pub report_id: Option<String>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -114,7 +116,8 @@ pub async fn render(
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let ban_logs = if u.temp_banned_until.is_some()
|
||||
let legacy_logs = if u.temp_banned_until.is_some()
|
||||
&& u.enforcement_decision_id.is_none()
|
||||
&& acl::has_permission(admin_acls, acl::AUDIT_LOG_VIEW)
|
||||
{
|
||||
client
|
||||
@@ -130,16 +133,41 @@ pub async fn render(
|
||||
offset: 0,
|
||||
})
|
||||
.await
|
||||
.log_error("load ban audit logs")
|
||||
.log_error("load suspension audit logs")
|
||||
.map(|response| response.logs)
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let decisions = if acl::has_permission(admin_acls, acl::ENFORCEMENT_VIEW) {
|
||||
Some(client.list_user_decisions(user_id).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(Err(error)) = &decisions {
|
||||
tracing::warn!(%error, user_id, "admin API request failed: list user decisions");
|
||||
}
|
||||
let can_block_networks = acl::has_permission(admin_acls, acl::USER_VIEW_IP)
|
||||
&& acl::has_permission(admin_acls, acl::BAN_IP_ADD)
|
||||
&& (acl::has_permission(admin_acls, acl::USER_TEMP_BAN)
|
||||
|| acl::has_permission(admin_acls, acl::USER_DELETE));
|
||||
let network_candidates = if can_block_networks {
|
||||
Some(client.list_network_block_candidates(user_id).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(Err(error)) = &network_candidates {
|
||||
tracing::warn!(%error, user_id, "admin API request failed: list network block candidates");
|
||||
}
|
||||
let context = tabs::moderation::ModerationContext {
|
||||
deletion_scheduler: deletion_scheduler.as_ref(),
|
||||
current_ban: tabs::moderation::find_current_ban(&u, &ban_logs),
|
||||
username_sign_in: account_identity.is_username(),
|
||||
legacy_suspension: tabs::moderation::find_legacy_suspension(&u, &legacy_logs),
|
||||
decisions: decisions.as_ref(),
|
||||
network_candidates: network_candidates.as_ref(),
|
||||
prefill: tabs::moderation::DecisionPrefill {
|
||||
report_ids: query.report_id.iter().cloned().collect(),
|
||||
messages: query.message.iter().cloned().collect(),
|
||||
},
|
||||
};
|
||||
Some(tabs::moderation::moderation_tab(
|
||||
config,
|
||||
|
||||
@@ -53,6 +53,8 @@ struct UserDetailQuery {
|
||||
delete_all_messages_dry_run: Option<String>,
|
||||
delete_all_messages_channel_count: Option<u64>,
|
||||
delete_all_messages_message_count: Option<u64>,
|
||||
report_id: Option<String>,
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -380,6 +382,8 @@ async fn user_tab(
|
||||
delete_all_messages_dry_run: None,
|
||||
delete_all_messages_channel_count: None,
|
||||
delete_all_messages_message_count: None,
|
||||
report_id: None,
|
||||
message: None,
|
||||
};
|
||||
let admin_acls = auth
|
||||
.0
|
||||
@@ -458,6 +462,8 @@ fn to_tab_query(q: &UserDetailQuery) -> user_tabs::TabQuery {
|
||||
delete_all_messages_dry_run: q.delete_all_messages_dry_run.clone(),
|
||||
delete_all_messages_channel_count: q.delete_all_messages_channel_count,
|
||||
delete_all_messages_message_count: q.delete_all_messages_message_count,
|
||||
report_id: q.report_id.clone(),
|
||||
message: q.message.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,923 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::{
|
||||
acl,
|
||||
admin_flags::{
|
||||
self, COPYRIGHT_REASON_CODE, ENFORCEMENT_REASONS, EnforcementHandling, user_flag_bits,
|
||||
},
|
||||
api::{
|
||||
client::ApiResult,
|
||||
enforcement::DecisionSubmission,
|
||||
generated::{snowflake, types as generated_types},
|
||||
},
|
||||
templates::components::form::checkbox,
|
||||
utils::{forms::MultiValueForm, plural::count_noun, timestamps::format_admin_timestamp},
|
||||
};
|
||||
use maud::{Markup, html};
|
||||
|
||||
pub const FIELD_LABEL_CLASS: &str = "block text-sm font-medium text-neutral-700";
|
||||
pub const FIELD_HELP_CLASS: &str = "text-xs text-neutral-500";
|
||||
pub const FIELD_INPUT_CLASS: &str = "block w-full rounded-md border border-neutral-300 \
|
||||
px-3 py-2 text-sm shadow-sm focus:border-brand-primary focus:outline-none \
|
||||
focus:ring-1 focus:ring-brand-primary";
|
||||
pub const INTERNAL_NOTE_MAX_CHARS: usize = 512;
|
||||
const STATEMENT_MAX_CHARS: usize = 500;
|
||||
const ACCOUNT_DELETION_MIN_DAYS: u32 = 14;
|
||||
const ENFORCEMENT_CLOSURE_MIN_DAYS: u32 = 74;
|
||||
const RIGHTS_HOLDER_MAX_CHARS: usize = 200;
|
||||
|
||||
const AUTOMATED_LIMIT_FLAGS: &[(u64, &str)] = &[
|
||||
(user_flag_bits::ACCOUNT_LIMITED, "Account limited"),
|
||||
(user_flag_bits::PROFILE_HIDDEN, "Profile hidden"),
|
||||
];
|
||||
|
||||
const HANDLING_GROUPS: &[(EnforcementHandling, &str)] = &[
|
||||
(EnforcementHandling::Standard, "Standard"),
|
||||
(
|
||||
EnforcementHandling::Protected,
|
||||
"Protected: fewer details in the notice",
|
||||
),
|
||||
(
|
||||
EnforcementHandling::Restricted,
|
||||
"Restricted: no details in the notice",
|
||||
),
|
||||
];
|
||||
|
||||
pub struct DecisionFields<'a> {
|
||||
pub id_prefix: &'a str,
|
||||
pub kind: generated_types::EnforcementKindType,
|
||||
pub report_ids: &'a [String],
|
||||
pub messages: Option<&'a [String]>,
|
||||
pub user_flags: Option<u64>,
|
||||
pub allow_restricted: bool,
|
||||
}
|
||||
|
||||
pub struct BasisFields<'a> {
|
||||
pub decision: DecisionFields<'a>,
|
||||
pub choices: &'a [(&'a str, &'a str)],
|
||||
pub plain_note_help: &'a str,
|
||||
pub enforcement_extra: Markup,
|
||||
}
|
||||
|
||||
pub enum BasisSubmission {
|
||||
Enforcement(DecisionSubmission),
|
||||
HolderRequest(Option<String>),
|
||||
Housekeeping(Option<String>),
|
||||
}
|
||||
|
||||
impl BasisSubmission {
|
||||
pub fn internal_note(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Enforcement(submission) => Some(&submission.internal_note),
|
||||
Self::HolderRequest(note) | Self::Housekeeping(note) => note.as_deref(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn moderation_basis(&self) -> generated_types::ModerationBasis {
|
||||
match self {
|
||||
Self::Enforcement(submission) => {
|
||||
generated_types::ModerationBasis::Enforcement(submission.decision.clone())
|
||||
}
|
||||
Self::HolderRequest(_) => generated_types::ModerationBasis::HolderRequest,
|
||||
Self::Housekeeping(_) => generated_types::ModerationBasis::Housekeeping,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn automated_limits(flags: u64) -> Vec<&'static str> {
|
||||
AUTOMATED_LIMIT_FLAGS
|
||||
.iter()
|
||||
.filter(|(bit, _)| flags & bit != 0)
|
||||
.map(|(_, label)| *label)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn field_label(for_id: &str, text: &str) -> Markup {
|
||||
html! {
|
||||
label for=(for_id) class=(FIELD_LABEL_CLASS) { (text) }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn required_mark() -> Markup {
|
||||
html! {
|
||||
span class="text-red-500 ml-0.5" { "*" }
|
||||
}
|
||||
}
|
||||
|
||||
fn required_label(for_id: &str, text: &str) -> Markup {
|
||||
html! {
|
||||
label for=(for_id) class=(FIELD_LABEL_CLASS) { (text) (required_mark()) }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn internal_note_field(id_prefix: &str, help: &str) -> Markup {
|
||||
let id = format!("{id_prefix}-internal-note");
|
||||
html! {
|
||||
div class="space-y-1" {
|
||||
(required_label(&id, "Internal note"))
|
||||
input type="text" id=(id) name="internal_note" required
|
||||
maxlength=(INTERNAL_NOTE_MAX_CHARS) class=(FIELD_INPUT_CLASS);
|
||||
p class=(FIELD_HELP_CLASS) { (help) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deletion_days_field(id: &str, self_hosted: bool) -> Markup {
|
||||
let (days, help) = if self_hosted {
|
||||
(
|
||||
ACCOUNT_DELETION_MIN_DAYS,
|
||||
format!("At least {ACCOUNT_DELETION_MIN_DAYS} days."),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
ENFORCEMENT_CLOSURE_MIN_DAYS,
|
||||
format!(
|
||||
"At least {ENFORCEMENT_CLOSURE_MIN_DAYS} days when a rule was broken. At least {ACCOUNT_DELETION_MIN_DAYS} days otherwise."
|
||||
),
|
||||
)
|
||||
};
|
||||
html! {
|
||||
div class="space-y-1" {
|
||||
(field_label(id, "Days until deletion"))
|
||||
input type="number" id=(id) name="days_until_deletion"
|
||||
value=(days) min="1" max="365" required class=(FIELD_INPUT_CLASS);
|
||||
p class=(FIELD_HELP_CLASS) { (help) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn statement_field(id_prefix: &str, help: &str) -> Markup {
|
||||
let id = format!("{id_prefix}-statement");
|
||||
html! {
|
||||
div class="space-y-1" {
|
||||
(field_label(&id, "Note to the user"))
|
||||
textarea id=(id) name="statement" rows="2" maxlength=(STATEMENT_MAX_CHARS)
|
||||
class=(FIELD_INPUT_CLASS) {}
|
||||
p class=(FIELD_HELP_CLASS) { (help) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn radio_group(legend: &str, name: &str, id_prefix: &str, options: &[(&str, &str)]) -> Markup {
|
||||
checked_radio_group(legend, name, id_prefix, options, None)
|
||||
}
|
||||
|
||||
fn checked_radio_group(
|
||||
legend: &str,
|
||||
name: &str,
|
||||
id_prefix: &str,
|
||||
options: &[(&str, &str)],
|
||||
checked: Option<&str>,
|
||||
) -> Markup {
|
||||
html! {
|
||||
fieldset class="space-y-1" {
|
||||
legend class=(FIELD_LABEL_CLASS) { (legend) (required_mark()) }
|
||||
div class="flex flex-wrap gap-x-4 gap-y-1" {
|
||||
@for (value, label) in options {
|
||||
@let id = format!("{id_prefix}-{name}-{value}");
|
||||
div class="flex items-center gap-2" {
|
||||
input type="radio" id=(id) name=(name) value=(value) required
|
||||
checked[checked == Some(*value)]
|
||||
class="h-4 w-4 accent-neutral-900";
|
||||
label for=(id) class="text-sm text-neutral-900" { (label) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn codes_attribute(handling: EnforcementHandling) -> String {
|
||||
ENFORCEMENT_REASONS
|
||||
.iter()
|
||||
.filter(|reason| reason.handling == handling)
|
||||
.map(|reason| reason.code.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
pub fn reason_select(
|
||||
id_prefix: &str,
|
||||
kind: generated_types::EnforcementKindType,
|
||||
allow_restricted: bool,
|
||||
) -> Markup {
|
||||
let id = format!("{id_prefix}-reason");
|
||||
html! {
|
||||
div class="space-y-1" {
|
||||
(required_label(&id, "Reason"))
|
||||
select id=(id) name="reason_code" required class=(FIELD_INPUT_CLASS) {
|
||||
option value="" disabled selected { "Choose a reason" }
|
||||
@for (handling, group) in HANDLING_GROUPS
|
||||
.iter()
|
||||
.filter(|(handling, _)| allow_restricted || *handling != EnforcementHandling::Restricted)
|
||||
{
|
||||
@let reasons = ENFORCEMENT_REASONS
|
||||
.iter()
|
||||
.filter(|reason| reason.handling == *handling && reason.allows(kind))
|
||||
.collect::<Vec<_>>();
|
||||
@if !reasons.is_empty() {
|
||||
optgroup label=(group) {
|
||||
@for reason in reasons {
|
||||
option value=(reason.code) {
|
||||
(admin_flags::deletion_reason_label(reason.code))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn automated_limits_hint(flags: u64) -> Markup {
|
||||
let limits = automated_limits(flags);
|
||||
html! {
|
||||
p class=(FIELD_HELP_CLASS) data-automated-limits="" {
|
||||
@if limits.is_empty() {
|
||||
"No automated limits on this account now."
|
||||
} @else {
|
||||
"Automated limits on this account now: " (limits.join(", ")) "."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enforcement_fields(fields: &DecisionFields<'_>) -> Markup {
|
||||
let prefix = fields.id_prefix;
|
||||
let restricted_codes = codes_attribute(EnforcementHandling::Restricted);
|
||||
let reports_id = format!("{prefix}-report-ids");
|
||||
let messages_id = format!("{prefix}-evidence");
|
||||
let rights_holder_id = format!("{prefix}-rights-holder");
|
||||
let from_report = !fields.report_ids.is_empty();
|
||||
html! {
|
||||
div class="space-y-4" data-enforcement-fields="" {
|
||||
(reason_select(prefix, fields.kind, fields.allow_restricted))
|
||||
(checked_radio_group("How did we find this?", "source", prefix, &[
|
||||
("report", "A report"),
|
||||
("own_review", "Our own review"),
|
||||
], from_report.then_some("report")))
|
||||
div class="space-y-1" {
|
||||
(radio_group("Automated tools", "automation", prefix, &[
|
||||
("none", "Not involved"),
|
||||
("flagged", "Flagged this for review"),
|
||||
]))
|
||||
@if let Some(flags) = fields.user_flags {
|
||||
(automated_limits_hint(flags))
|
||||
}
|
||||
}
|
||||
div class="space-y-1" {
|
||||
label for=(reports_id) class=(FIELD_LABEL_CLASS) {
|
||||
"Reports"
|
||||
span data-show-for-source="report" hidden[!from_report] { (required_mark()) }
|
||||
}
|
||||
textarea id=(reports_id) name="report_ids" rows="2"
|
||||
placeholder="One report ID per line" class=(FIELD_INPUT_CLASS) {
|
||||
(fields.report_ids.join("\n"))
|
||||
}
|
||||
}
|
||||
@if let Some(messages) = fields.messages {
|
||||
div class="space-y-1" {
|
||||
(field_label(&messages_id, "Messages"))
|
||||
textarea id=(messages_id) name="evidence" rows="3"
|
||||
placeholder="One message link or channel_id/message_id per line"
|
||||
class=(FIELD_INPUT_CLASS) {
|
||||
(messages.join("\n"))
|
||||
}
|
||||
p class=(FIELD_HELP_CLASS) data-hide-for-codes=(restricted_codes) {
|
||||
"Messages this user sent in a linked report are included automatically. Without any messages, add a note to the user."
|
||||
}
|
||||
p class=(FIELD_HELP_CLASS) data-show-for-codes=(restricted_codes) hidden {
|
||||
"Messages this user sent in a linked report are included automatically. At least one message is required."
|
||||
}
|
||||
}
|
||||
}
|
||||
div data-hide-for-codes=(restricted_codes) {
|
||||
(statement_field(prefix, "Shown to the user. Never name or describe other people. Not sent for child safety or violent extremism decisions."))
|
||||
}
|
||||
div class="space-y-1" data-show-for-codes=(COPYRIGHT_REASON_CODE) hidden {
|
||||
(required_label(&rights_holder_id, "Rights holder or right concerned"))
|
||||
input type="text" id=(rights_holder_id) name="rights_holder" required disabled
|
||||
maxlength=(RIGHTS_HOLDER_MAX_CHARS) class=(FIELD_INPUT_CLASS);
|
||||
p class=(FIELD_HELP_CLASS) {
|
||||
"For example Example Records, or trademark. This is the one place a name is shown to the user."
|
||||
}
|
||||
}
|
||||
div class="space-y-2" data-show-for-codes=(restricted_codes) hidden {
|
||||
(checkbox("confirmations", "evidence_preserved", "Evidence is preserved", false, true))
|
||||
(checkbox("confirmations", "reports_filed_or_not_required", "Any required report is filed, or none is required", false, true))
|
||||
}
|
||||
(internal_note_field(prefix, "Staff only. Saved as the first note on the decision."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const NETWORK_BLOCK_DEFAULT_DAYS: u64 = 90;
|
||||
|
||||
fn network_note(network: &generated_types::NetworkBlockCandidatesResponseNetworksItem) -> String {
|
||||
use generated_types::NetworkBlockCandidatesResponseNetworksItemSkipReason as SkipReason;
|
||||
match network.skip_reason {
|
||||
Some(SkipReason::Exempt) => "Exempt, never blocked".to_owned(),
|
||||
Some(SkipReason::SpecialPurpose) => {
|
||||
"Private or special-purpose range, never blocked".to_owned()
|
||||
}
|
||||
None => {
|
||||
let others = u64::try_from(*network.other_unrestricted_accounts).unwrap_or(0);
|
||||
format!(
|
||||
"{} last active here",
|
||||
count_noun(
|
||||
others,
|
||||
"other unrestricted account",
|
||||
"other unrestricted accounts"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_blockable(network: &generated_types::NetworkBlockCandidatesResponseNetworksItem) -> bool {
|
||||
network.skip_reason.is_none() && *network.other_unrestricted_accounts == 0
|
||||
}
|
||||
|
||||
pub fn network_block_fields(
|
||||
id_prefix: &str,
|
||||
candidates: &ApiResult<generated_types::NetworkBlockCandidatesResponse>,
|
||||
) -> Markup {
|
||||
let days_id = format!("{id_prefix}-network-block-days");
|
||||
html! {
|
||||
fieldset class="space-y-2" data-network-blocks="" {
|
||||
legend class=(FIELD_LABEL_CLASS) { "Block networks this account used" }
|
||||
@match candidates {
|
||||
Err(_) => {
|
||||
p class="text-sm text-red-700" { "Failed to load the networks this account used." }
|
||||
}
|
||||
Ok(response) if response.networks.is_empty() => {
|
||||
p class=(FIELD_HELP_CLASS) { "This account used no networks in the last 30 days." }
|
||||
}
|
||||
Ok(response) => {
|
||||
p class=(FIELD_HELP_CLASS) {
|
||||
"Networks from the last 30 days. A network other unrestricted accounts use can't be blocked."
|
||||
}
|
||||
@for network in &response.networks {
|
||||
div {
|
||||
(checkbox("block_networks", &network.ip, &network.ip, false, is_blockable(network)))
|
||||
p class={(FIELD_HELP_CLASS) " pl-7"} {
|
||||
"Last used " (format_admin_timestamp(&network.last_seen_at)) ". " (network_note(network)) "."
|
||||
}
|
||||
}
|
||||
}
|
||||
div class="space-y-1" {
|
||||
(field_label(&days_id, "Block length in days"))
|
||||
input type="number" id=(days_id) name="network_block_days"
|
||||
value=(NETWORK_BLOCK_DEFAULT_DAYS) min="1" max="365" class=(FIELD_INPUT_CLASS);
|
||||
p class=(FIELD_HELP_CLASS) { "Ending the decision or deleting the account removes the blocks sooner." }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn network_blocks_from_form(
|
||||
form: &MultiValueForm,
|
||||
) -> Result<Option<generated_types::NetworkBlockRequest>, String> {
|
||||
let ips = form
|
||||
.values("block_networks")
|
||||
.iter()
|
||||
.map(|ip| ip.trim().to_owned())
|
||||
.filter(|ip| !ip.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
if ips.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let days = match form.parse_value::<u64>("network_block_days") {
|
||||
Ok(Some(days)) => days,
|
||||
Ok(None) => NETWORK_BLOCK_DEFAULT_DAYS,
|
||||
Err(_) => return Err("Enter the block length as a number of days".to_owned()),
|
||||
};
|
||||
let days =
|
||||
std::num::NonZeroU64::new(days).ok_or("The block length must be at least one day")?;
|
||||
Ok(Some(generated_types::NetworkBlockRequest { days, ips }))
|
||||
}
|
||||
|
||||
pub fn skipped_networks_summary(
|
||||
skipped: &[generated_types::SkippedNetworkBlock],
|
||||
) -> Option<String> {
|
||||
use generated_types::SkippedNetworkBlockReason as Reason;
|
||||
if skipped.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let entries = skipped
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
let reason = match entry.reason {
|
||||
Reason::Exempt => "exempt",
|
||||
Reason::SpecialPurpose => "special-purpose range",
|
||||
Reason::AlreadyBlocked => "already blocked",
|
||||
};
|
||||
format!("{} ({reason})", entry.ip)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Some(format!("Not blocked: {}.", entries.join(", ")))
|
||||
}
|
||||
|
||||
pub fn basis_fields(fields: &BasisFields<'_>) -> Markup {
|
||||
let prefix = fields.decision.id_prefix;
|
||||
let plain = fields
|
||||
.choices
|
||||
.iter()
|
||||
.map(|(value, _)| *value)
|
||||
.filter(|value| *value != "enforcement")
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
html! {
|
||||
(radio_group("Basis", "basis", prefix, fields.choices))
|
||||
fieldset class="space-y-4" data-show-for-basis="enforcement" hidden disabled {
|
||||
(enforcement_fields(&fields.decision))
|
||||
(fields.enforcement_extra)
|
||||
}
|
||||
fieldset data-show-for-basis=(plain) hidden disabled {
|
||||
(internal_note_field(&format!("{prefix}-plain"), fields.plain_note_help))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn basis_from_form(form: &MultiValueForm) -> Result<BasisSubmission, String> {
|
||||
match form.first("basis") {
|
||||
Some("enforcement") => decision_from_form(form).map(BasisSubmission::Enforcement),
|
||||
Some("holder_request") => Ok(BasisSubmission::HolderRequest(form.clean("internal_note"))),
|
||||
Some("housekeeping") => Ok(BasisSubmission::Housekeeping(form.clean("internal_note"))),
|
||||
_ => Err("Choose a basis".to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decision_from_form(form: &MultiValueForm) -> Result<DecisionSubmission, String> {
|
||||
let reason_code = form
|
||||
.first("reason_code")
|
||||
.and_then(|value| value.trim().parse::<i32>().ok())
|
||||
.and_then(|code| generated_types::EnforcementReasonCodeType::try_from(code).ok())
|
||||
.ok_or("Choose a reason")?;
|
||||
let source = form
|
||||
.first("source")
|
||||
.and_then(|value| generated_types::EnforcementDecisionInputSource::try_from(value).ok())
|
||||
.ok_or("Choose how we found this")?;
|
||||
let automation = form
|
||||
.first("automation")
|
||||
.and_then(|value| generated_types::EnforcementDecisionInputAutomation::try_from(value).ok())
|
||||
.ok_or("Choose whether automated tools were involved")?;
|
||||
let report_ids = lines(form.first("report_ids"))
|
||||
.map(|line| {
|
||||
is_snowflake(line)
|
||||
.then(|| snowflake(line))
|
||||
.ok_or_else(|| format!("Not a report ID: {line}"))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let evidence = lines(form.first("evidence"))
|
||||
.map(|line| {
|
||||
parse_message_reference(line)
|
||||
.map(|(channel_id, message_id)| {
|
||||
generated_types::EnforcementDecisionInputEvidenceItem {
|
||||
channel_id: snowflake(&channel_id),
|
||||
message_id: snowflake(&message_id),
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| format!("Not a message link or channel_id/message_id: {line}"))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let confirmations = form
|
||||
.values("confirmations")
|
||||
.iter()
|
||||
.map(|value| {
|
||||
generated_types::EnforcementDecisionInputConfirmationsItem::try_from(value.as_str())
|
||||
.map_err(|_| format!("Unknown confirmation: {value}"))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let internal_note = form.clean("internal_note").ok_or("Add an internal note")?;
|
||||
Ok(DecisionSubmission {
|
||||
decision: generated_types::EnforcementDecisionInput {
|
||||
automation,
|
||||
confirmations,
|
||||
evidence,
|
||||
reason_code,
|
||||
report_ids,
|
||||
rights_holder: form.clean("rights_holder"),
|
||||
source,
|
||||
statement: form.clean("statement"),
|
||||
},
|
||||
internal_note,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn bulk_decision_from_form(
|
||||
form: &MultiValueForm,
|
||||
) -> Result<generated_types::BulkEnforcementDecisionInput, String> {
|
||||
let reason_code = form
|
||||
.first("reason_code")
|
||||
.and_then(|value| value.trim().parse::<i32>().ok())
|
||||
.and_then(|code| generated_types::EnforcementReasonCodeType::try_from(code).ok())
|
||||
.ok_or("Choose a reason")?;
|
||||
let automation = form
|
||||
.first("automation")
|
||||
.and_then(|value| {
|
||||
generated_types::BulkEnforcementDecisionInputAutomation::try_from(value).ok()
|
||||
})
|
||||
.ok_or("Choose whether automated tools were involved")?;
|
||||
let statement = form.clean("statement").ok_or("Add a note to the user")?;
|
||||
Ok(generated_types::BulkEnforcementDecisionInput {
|
||||
automation,
|
||||
reason_code,
|
||||
source: generated_types::BulkEnforcementDecisionInputSource::OwnReview,
|
||||
statement,
|
||||
})
|
||||
}
|
||||
|
||||
fn lines(value: Option<&str>) -> impl Iterator<Item = &str> {
|
||||
value
|
||||
.unwrap_or_default()
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty())
|
||||
}
|
||||
|
||||
pub(crate) fn is_snowflake(value: &str) -> bool {
|
||||
(1..=20).contains(&value.len()) && value.bytes().all(|byte| byte.is_ascii_digit())
|
||||
}
|
||||
|
||||
fn snowflake_pair(channel_id: &str, message_id: &str) -> Option<(String, String)> {
|
||||
(is_snowflake(channel_id) && is_snowflake(message_id))
|
||||
.then(|| (channel_id.to_owned(), message_id.to_owned()))
|
||||
}
|
||||
|
||||
pub fn parse_message_reference(line: &str) -> Option<(String, String)> {
|
||||
let line = line.trim();
|
||||
if let Some((channel_id, message_id)) = line.split_once(['/', ','])
|
||||
&& let Some(pair) = snowflake_pair(channel_id.trim(), message_id.trim())
|
||||
{
|
||||
return Some(pair);
|
||||
}
|
||||
let url = url::Url::parse(line)
|
||||
.or_else(|_| url::Url::parse("https://admin.invalid/")?.join(line))
|
||||
.ok()?;
|
||||
let query_value = |key: &str| {
|
||||
url.query_pairs()
|
||||
.find(|(name, _)| name == key)
|
||||
.map(|(_, value)| value.into_owned())
|
||||
};
|
||||
if let (Some(channel_id), Some(message_id)) =
|
||||
(query_value("channel_id"), query_value("message_id"))
|
||||
{
|
||||
return snowflake_pair(&channel_id, &message_id);
|
||||
}
|
||||
let segments: Vec<&str> = url.path_segments()?.filter(|s| !s.is_empty()).collect();
|
||||
match segments.as_slice() {
|
||||
[.., "channels", _, channel_id, message_id] => snowflake_pair(channel_id, message_id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TakeAction<'a> {
|
||||
pub author_id: Option<&'a str>,
|
||||
pub report_id: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub fn can_take_action(admin_acls: &[String]) -> bool {
|
||||
acl::has_any_permission(admin_acls, &[acl::USER_TEMP_BAN, acl::USER_UPDATE_FLAGS])
|
||||
}
|
||||
|
||||
pub fn take_action_href(
|
||||
base: &str,
|
||||
user_id: &str,
|
||||
report_id: Option<&str>,
|
||||
message: Option<(&str, &str)>,
|
||||
) -> String {
|
||||
let mut href = format!("{base}/users/{user_id}?tab=moderation");
|
||||
if let Some(report_id) = report_id {
|
||||
href.push_str("&report_id=");
|
||||
href.push_str(&urlencoding::encode(report_id));
|
||||
}
|
||||
if let Some((channel_id, message_id)) = message {
|
||||
href.push_str("&message=");
|
||||
href.push_str(&urlencoding::encode(&format!("{channel_id}/{message_id}")));
|
||||
}
|
||||
href
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::api::generated::types::EnforcementKindType;
|
||||
|
||||
fn form(body: &str) -> MultiValueForm {
|
||||
MultiValueForm::parse(body.as_bytes())
|
||||
}
|
||||
|
||||
fn render(kind: EnforcementKindType, flags: u64) -> String {
|
||||
enforcement_fields(&DecisionFields {
|
||||
id_prefix: "suspend",
|
||||
kind,
|
||||
report_ids: &["1600000000000000001".to_owned()],
|
||||
messages: Some(&["1700000000000000001/1700000000000000002".to_owned()]),
|
||||
user_flags: Some(flags),
|
||||
allow_restricted: true,
|
||||
})
|
||||
.into_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deletion_days_default_to_the_closure_floor_of_each_mode() {
|
||||
let hosted = deletion_days_field("close-days", false).into_string();
|
||||
assert!(hosted.contains(r#"name="days_until_deletion" value="74""#));
|
||||
assert!(
|
||||
hosted.contains("At least 74 days when a rule was broken. At least 14 days otherwise.")
|
||||
);
|
||||
let self_hosted = deletion_days_field("close-days", true).into_string();
|
||||
assert!(self_hosted.contains(r#"name="days_until_deletion" value="14""#));
|
||||
assert!(self_hosted.contains(">At least 14 days.</p>"));
|
||||
assert!(!self_hosted.contains("74"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fieldset_renders_every_input_with_prefills() {
|
||||
let html = render(EnforcementKindType::Suspension, 0);
|
||||
for marker in [
|
||||
r#"name="reason_code""#,
|
||||
r#"name="source" value="report""#,
|
||||
r#"name="source" value="own_review""#,
|
||||
r#"name="automation" value="none""#,
|
||||
r#"name="automation" value="flagged""#,
|
||||
r#"name="report_ids""#,
|
||||
r#"name="evidence""#,
|
||||
r#"name="statement""#,
|
||||
r#"name="rights_holder""#,
|
||||
r#"name="confirmations" value="evidence_preserved""#,
|
||||
r#"name="confirmations" value="reports_filed_or_not_required""#,
|
||||
r#"name="internal_note""#,
|
||||
">1600000000000000001</textarea>",
|
||||
">1700000000000000001/1700000000000000002</textarea>",
|
||||
"No automated limits on this account now.",
|
||||
] {
|
||||
assert!(html.contains(marker), "missing {marker}\n{html}");
|
||||
}
|
||||
assert!(!html.contains(r#"name="automation" value="automated""#));
|
||||
assert_eq!(html.matches("checked").count(), 1);
|
||||
assert!(html.contains(r#"name="source" value="report" required checked"#));
|
||||
assert!(html.contains(r#"<span data-show-for-source="report"><span"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fieldset_without_a_report_leaves_the_source_open() {
|
||||
let html = enforcement_fields(&DecisionFields {
|
||||
id_prefix: "limit",
|
||||
kind: EnforcementKindType::FeatureLimit,
|
||||
report_ids: &[],
|
||||
messages: Some(&[]),
|
||||
user_flags: None,
|
||||
allow_restricted: true,
|
||||
})
|
||||
.into_string();
|
||||
assert!(!html.contains("checked"));
|
||||
assert!(html.contains(r#"<span data-show-for-source="report" hidden><span"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_fields_carry_a_mark() {
|
||||
let html = render(EnforcementKindType::FeatureLimit, 0);
|
||||
let mark = r#"<span class="text-red-500 ml-0.5">*</span>"#;
|
||||
for label in [
|
||||
"Reason",
|
||||
"How did we find this?",
|
||||
"Automated tools",
|
||||
"Internal note",
|
||||
"Rights holder or right concerned",
|
||||
] {
|
||||
assert!(html.contains(&format!("{label}{mark}")), "unmarked {label}");
|
||||
}
|
||||
for label in ["Messages<", "Note to the user<"] {
|
||||
assert!(html.contains(label), "marked {label}");
|
||||
}
|
||||
assert_eq!(html.matches(mark).count(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rights_holder_and_confirmations_start_hidden_and_follow_their_codes() {
|
||||
let html = render(EnforcementKindType::Suspension, 0);
|
||||
assert!(html.contains(r#"data-show-for-codes="31" hidden"#));
|
||||
assert!(html.contains(r#"data-show-for-codes="9 13 24" hidden"#));
|
||||
assert!(html.contains(r#"data-hide-for-codes="9 13 24""#));
|
||||
assert!(html.contains(r#"name="rights_holder" required disabled"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasons_are_grouped_by_handling_and_filtered_by_kind() {
|
||||
let suspend = render(EnforcementKindType::Suspension, 0);
|
||||
assert!(suspend.contains(
|
||||
r#"<optgroup label="Restricted: no details in the notice"><option value="9">"#
|
||||
));
|
||||
assert!(suspend.contains(
|
||||
r#"<optgroup label="Protected: fewer details in the notice"><option value="7">"#
|
||||
));
|
||||
for omitted in [1, 2, 19, 30, 31] {
|
||||
assert!(
|
||||
!suspend.contains(&format!(r#"<option value="{omitted}">"#)),
|
||||
"suspension offers {omitted}"
|
||||
);
|
||||
}
|
||||
let warn = render(EnforcementKindType::Warning, 0);
|
||||
assert!(!warn.contains("Restricted: no details"));
|
||||
assert!(warn.contains(r#"<option value="31">Copyright</option>"#));
|
||||
assert!(warn.contains(r#"<option value="29">"#));
|
||||
assert!(!warn.contains(r#"<option value="30">"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automated_limits_list_the_neutral_hook_flags() {
|
||||
let html = render(
|
||||
EnforcementKindType::Warning,
|
||||
user_flag_bits::ACCOUNT_LIMITED
|
||||
| user_flag_bits::PROFILE_HIDDEN
|
||||
| user_flag_bits::SPAMMER,
|
||||
);
|
||||
assert!(
|
||||
html.contains("Automated limits on this account now: Account limited, Profile hidden.")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decision_from_form_builds_the_request() {
|
||||
let submission = decision_from_form(&form(
|
||||
"reason_code=31&source=report&automation=flagged\
|
||||
&report_ids=1600000000000000001%0D%0A%0D%0A1600000000000000002\
|
||||
&evidence=1700000000000000001%2F1700000000000000002%0Ahttps%3A%2F%2Fweb.example%2Fchannels%2F%40me%2F1700000000000000003%2F1700000000000000004\
|
||||
&statement=+Please+stop+&rights_holder=Example+Records\
|
||||
&internal_note=Ticket+42",
|
||||
))
|
||||
.expect("valid form");
|
||||
assert_eq!(submission.internal_note, "Ticket 42");
|
||||
let body = serde_json::to_value(&submission.decision).expect("serializable decision");
|
||||
assert_eq!(
|
||||
body,
|
||||
serde_json::json!({
|
||||
"reason_code": 31,
|
||||
"source": "report",
|
||||
"automation": "flagged",
|
||||
"report_ids": ["1600000000000000001", "1600000000000000002"],
|
||||
"evidence": [
|
||||
{"channel_id": "1700000000000000001", "message_id": "1700000000000000002"},
|
||||
{"channel_id": "1700000000000000003", "message_id": "1700000000000000004"}
|
||||
],
|
||||
"statement": "Please stop",
|
||||
"rights_holder": "Example Records"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decision_from_form_sends_restricted_confirmations_and_omits_empty_fields() {
|
||||
let submission = decision_from_form(&form(
|
||||
"reason_code=13&source=own_review&automation=none&report_ids=&evidence=1%2F2\
|
||||
&statement=&confirmations=evidence_preserved\
|
||||
&confirmations=reports_filed_or_not_required&internal_note=Filed",
|
||||
))
|
||||
.expect("valid form");
|
||||
let body = serde_json::to_value(&submission.decision).expect("serializable decision");
|
||||
assert_eq!(
|
||||
body,
|
||||
serde_json::json!({
|
||||
"reason_code": 13,
|
||||
"source": "own_review",
|
||||
"automation": "none",
|
||||
"evidence": [{"channel_id": "1", "message_id": "2"}],
|
||||
"confirmations": ["evidence_preserved", "reports_filed_or_not_required"]
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decision_from_form_refuses_incomplete_input() {
|
||||
let cases = [
|
||||
(
|
||||
"source=report&automation=none&internal_note=x",
|
||||
"Choose a reason",
|
||||
),
|
||||
(
|
||||
"reason_code=2&source=report&automation=none&internal_note=x",
|
||||
"Choose a reason",
|
||||
),
|
||||
(
|
||||
"reason_code=3&automation=none&internal_note=x",
|
||||
"Choose how we found this",
|
||||
),
|
||||
(
|
||||
"reason_code=3&source=report&automation=automated&internal_note=x",
|
||||
"Choose whether automated tools were involved",
|
||||
),
|
||||
(
|
||||
"reason_code=3&source=report&automation=none&internal_note=+",
|
||||
"Add an internal note",
|
||||
),
|
||||
(
|
||||
"reason_code=3&source=report&automation=none&internal_note=x&report_ids=abc",
|
||||
"Not a report ID: abc",
|
||||
),
|
||||
(
|
||||
"reason_code=3&source=report&automation=none&internal_note=x&evidence=hello",
|
||||
"Not a message link or channel_id/message_id: hello",
|
||||
),
|
||||
];
|
||||
for (body, expected) in cases {
|
||||
let error = decision_from_form(&form(body)).err();
|
||||
assert_eq!(error.as_deref(), Some(expected), "{body}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basis_fields_hide_the_decision_until_a_rule_was_broken() {
|
||||
let html = basis_fields(&BasisFields {
|
||||
decision: DecisionFields {
|
||||
id_prefix: "shred",
|
||||
kind: EnforcementKindType::ContentRemoval,
|
||||
report_ids: &[],
|
||||
messages: None,
|
||||
user_flags: None,
|
||||
allow_restricted: false,
|
||||
},
|
||||
choices: &[
|
||||
("enforcement", "A rule was broken"),
|
||||
("holder_request", "The account holder asked"),
|
||||
("housekeeping", "Housekeeping"),
|
||||
],
|
||||
plain_note_help: "Staff only.",
|
||||
enforcement_extra: html! {},
|
||||
})
|
||||
.into_string();
|
||||
assert!(html.contains(r#"data-show-for-basis="enforcement" hidden disabled"#));
|
||||
assert!(
|
||||
html.contains(r#"data-show-for-basis="holder_request housekeeping" hidden disabled"#)
|
||||
);
|
||||
assert!(!html.contains(r#"name="evidence""#));
|
||||
assert!(!html.contains("Restricted: no details"));
|
||||
assert!(!html.contains("data-automated-limits"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basis_from_form_reads_each_basis() {
|
||||
assert!(matches!(
|
||||
basis_from_form(&form("basis=holder_request&internal_note=Asked+by+email")),
|
||||
Ok(BasisSubmission::HolderRequest(Some(note))) if note == "Asked by email"
|
||||
));
|
||||
assert!(matches!(
|
||||
basis_from_form(&form("basis=housekeeping")),
|
||||
Ok(BasisSubmission::Housekeeping(None))
|
||||
));
|
||||
let enforcement = basis_from_form(&form(
|
||||
"basis=enforcement&reason_code=3&source=own_review&automation=none&internal_note=x",
|
||||
))
|
||||
.expect("valid enforcement basis");
|
||||
assert_eq!(enforcement.internal_note(), Some("x"));
|
||||
assert!(matches!(
|
||||
enforcement.moderation_basis(),
|
||||
generated_types::ModerationBasis::Enforcement(_)
|
||||
));
|
||||
assert_eq!(
|
||||
basis_from_form(&form("internal_note=x")).err().as_deref(),
|
||||
Some("Choose a basis")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_references_accept_ids_admin_links_and_app_links() {
|
||||
let expected = Some(("111".to_owned(), "222".to_owned()));
|
||||
for line in [
|
||||
"111/222",
|
||||
" 111 / 222 ",
|
||||
"111,222",
|
||||
"https://admin.example/messages?channel_id=111&message_id=222&context_limit=50",
|
||||
"/messages?channel_id=111&message_id=222",
|
||||
"https://web.example/channels/@me/111/222",
|
||||
"https://web.example/channels/999/111/222",
|
||||
] {
|
||||
assert_eq!(parse_message_reference(line), expected, "{line}");
|
||||
}
|
||||
for line in [
|
||||
"111",
|
||||
"abc/222",
|
||||
"https://web.example/channels/999/111",
|
||||
"https://web.example/invite/111/222",
|
||||
"/messages?channel_id=111",
|
||||
] {
|
||||
assert_eq!(parse_message_reference(line), None, "{line}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_action_links_open_the_moderation_tab_with_prefills() {
|
||||
assert_eq!(
|
||||
take_action_href("/admin", "1", Some("2"), Some(("3", "4"))),
|
||||
"/admin/users/1?tab=moderation&report_id=2&message=3%2F4"
|
||||
);
|
||||
assert_eq!(
|
||||
take_action_href("", "1", None, None),
|
||||
"/users/1?tab=moderation"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ pub struct Message {
|
||||
pub channel_nsfw: Option<bool>,
|
||||
pub channel_content_warning_level: Option<i32>,
|
||||
pub channel_content_warning_text: Option<String>,
|
||||
pub guild_id: Option<String>,
|
||||
pub guild_nsfw: Option<bool>,
|
||||
pub attachments: Vec<Attachment>,
|
||||
pub missing_attachments: Vec<MissingAttachment>,
|
||||
@@ -85,6 +86,7 @@ fn message_from_value(value: &Value) -> Message {
|
||||
channel_content_warning_text: value["channel_content_warning_text"]
|
||||
.as_str()
|
||||
.map(ToOwned::to_owned),
|
||||
guild_id: value_id(&value["guild_id"]),
|
||||
guild_nsfw: value["guild_nsfw"].as_bool(),
|
||||
attachments,
|
||||
missing_attachments,
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
use maud::{Markup, PreEscaped, html};
|
||||
|
||||
use super::badge::{BadgeVariant, badge};
|
||||
use super::enforcement_fields::{TakeAction, take_action_href};
|
||||
use super::icons::paperclip_icon;
|
||||
use super::media::user_avatar_url;
|
||||
use super::nsfw_indicators::{attachment_nsfw_badge, channel_nsfw_state_badge};
|
||||
use super::user_display::format_user_display;
|
||||
use crate::config::AdminConfig;
|
||||
use crate::routes::auth::json_string;
|
||||
use crate::utils::timestamps::format_admin_timestamp;
|
||||
|
||||
use super::message_data::{Attachment, Message};
|
||||
@@ -110,6 +110,48 @@ fn render_missing_attachments(msg: &Message) -> Markup {
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_button(value: &str, label: &str) -> Markup {
|
||||
html! {
|
||||
button type="button"
|
||||
class="cursor-pointer text-blue-600 hover:underline"
|
||||
data-copy-value=(value)
|
||||
onclick="window.__adminCopyToClipboard && window.__adminCopyToClipboard(this.dataset.copyValue, this, 'Copied')" {
|
||||
(label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn message_meta(msg: &Message, jump_link: Option<&str>, take_action: Option<&str>) -> Markup {
|
||||
html! {
|
||||
div class="mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-neutral-400 text-xs" {
|
||||
span class="select-all" { (msg.id) }
|
||||
@if let Some(href) = take_action {
|
||||
a href=(href) class="text-blue-600 hover:underline" { "Take action" }
|
||||
}
|
||||
@if let Some(link) = jump_link {
|
||||
(copy_button(link, "Copy link"))
|
||||
}
|
||||
(copy_button(&msg.id, "Copy message ID"))
|
||||
@if !msg.channel_id.is_empty() {
|
||||
(copy_button(&msg.channel_id, "Copy channel ID"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn message_jump_link(web_app_endpoint: &str, msg: &Message) -> Option<String> {
|
||||
if msg.channel_id.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(format!(
|
||||
"{}/channels/{}/{}/{}",
|
||||
web_app_endpoint.trim_end_matches('/'),
|
||||
msg.guild_id.as_deref().unwrap_or("@me"),
|
||||
msg.channel_id,
|
||||
msg.id
|
||||
))
|
||||
}
|
||||
|
||||
fn message_row(
|
||||
base_path: &str,
|
||||
avatar_url: &str,
|
||||
@@ -117,6 +159,7 @@ fn message_row(
|
||||
include_delete: bool,
|
||||
is_highlighted: bool,
|
||||
is_grouped: bool,
|
||||
meta: &Markup,
|
||||
) -> Markup {
|
||||
let hover = if is_highlighted {
|
||||
" hover:bg-amber-100"
|
||||
@@ -150,9 +193,7 @@ fn message_row(
|
||||
(render_other_attachments(msg, has_content_or_images))
|
||||
}
|
||||
(render_missing_attachments(msg))
|
||||
div class="mt-1 text-neutral-400 text-xs" {
|
||||
span class="" { (msg.id) }
|
||||
}
|
||||
(meta)
|
||||
@if include_delete && !has_images {
|
||||
div class="absolute top-0 right-2 hidden group-hover:block" {
|
||||
button type="button"
|
||||
@@ -233,9 +274,7 @@ fn message_row(
|
||||
(render_other_attachments(msg, has_content_or_images))
|
||||
}
|
||||
(render_missing_attachments(msg))
|
||||
div class="mt-1 text-neutral-400 text-xs" {
|
||||
span class="" { (msg.id) }
|
||||
}
|
||||
(meta)
|
||||
}
|
||||
@if include_delete && !has_images {
|
||||
div class="absolute top-1 right-2 hidden group-hover:block" {
|
||||
@@ -257,6 +296,7 @@ pub fn message_list(
|
||||
messages: &[Message],
|
||||
include_delete: bool,
|
||||
highlight_message_id: Option<&str>,
|
||||
take_action: Option<&TakeAction<'_>>,
|
||||
) -> Markup {
|
||||
html! {
|
||||
div class="divide-y-0" {
|
||||
@@ -268,68 +308,95 @@ pub fn message_list(
|
||||
@let avatar_url = user_avatar_url(
|
||||
config, &msg.author_id, msg.author_avatar.as_deref(), 160, true,
|
||||
);
|
||||
@let take_action_href = take_action
|
||||
.filter(|action| {
|
||||
msg.webhook_id.is_none()
|
||||
&& action.author_id.is_none_or(|author_id| author_id == msg.author_id)
|
||||
})
|
||||
.map(|action| take_action_href(
|
||||
base_path,
|
||||
&msg.author_id,
|
||||
action.report_id,
|
||||
Some((&msg.channel_id, &msg.id)),
|
||||
));
|
||||
@let jump_link = message_jump_link(&config.web_app_endpoint, msg);
|
||||
@let meta = message_meta(msg, jump_link.as_deref(), take_action_href.as_deref());
|
||||
(message_row(
|
||||
base_path, &avatar_url, msg,
|
||||
include_delete, is_highlighted, is_grouped,
|
||||
&meta,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn message_deletion_script(csrf_token: &str) -> Markup {
|
||||
let csrf = json_string(csrf_token);
|
||||
let script = r#"(function() {
|
||||
var csrf = __CSRF__;
|
||||
function bp() {
|
||||
return document.documentElement.dataset.basePath || '';
|
||||
}
|
||||
function toast(level, message) {
|
||||
document.body.dispatchEvent(new CustomEvent('showFlash', {detail: {level: level, message: message}}));
|
||||
}
|
||||
function post(action, fields) {
|
||||
fields.append('_csrf', csrf);
|
||||
return fetch(bp() + '/messages?action=' + action, {
|
||||
method: 'POST',
|
||||
body: fields,
|
||||
credentials: 'same-origin',
|
||||
headers: {'x-csrf-token': csrf}
|
||||
});
|
||||
}
|
||||
function deleteMessage(b) {
|
||||
var fields = new URLSearchParams();
|
||||
fields.append('channel_id', b.dataset.channelId || '');
|
||||
fields.append('message_id', b.dataset.messageId || '');
|
||||
b.disabled = true;
|
||||
b.textContent = 'Deleting...';
|
||||
toast('info', 'Deleting message...');
|
||||
post('delete', fields).then(function(r) {
|
||||
if (!r.ok) throw new Error('Failed');
|
||||
var row = b.closest('[data-message-id]');
|
||||
if (row) {
|
||||
row.style.opacity = '0.5';
|
||||
row.style.pointerEvents = 'none';
|
||||
}
|
||||
b.textContent = 'Deleted';
|
||||
toast('success', 'Message deleted.');
|
||||
}).catch(function() {
|
||||
b.disabled = false;
|
||||
b.textContent = 'Delete';
|
||||
toast('error', 'Failed to delete message.');
|
||||
});
|
||||
pub fn message_delete_prefill_script() -> Markup {
|
||||
html! {
|
||||
script defer { (PreEscaped(MESSAGE_DELETE_PREFILL_SCRIPT)) }
|
||||
}
|
||||
}
|
||||
|
||||
const MESSAGE_DELETE_PREFILL_SCRIPT: &str = r#"(function() {
|
||||
if (window.__fluxerMessageDeletePrefill) return;
|
||||
window.__fluxerMessageDeletePrefill = true;
|
||||
document.addEventListener('click', function(e) {
|
||||
var t = e.target;
|
||||
if (!(t instanceof HTMLElement)) return;
|
||||
var d = t.closest('.delete-message-btn');
|
||||
if (d instanceof HTMLButtonElement) {
|
||||
e.preventDefault();
|
||||
deleteMessage(d);
|
||||
}
|
||||
var button = t.closest('.delete-message-btn');
|
||||
var form = document.getElementById('delete-message-form');
|
||||
if (!(button instanceof HTMLButtonElement) || !form) return;
|
||||
e.preventDefault();
|
||||
form.querySelector('[name="channel_id"]').value = button.dataset.channelId || '';
|
||||
form.querySelector('[name="message_id"]').value = button.dataset.messageId || '';
|
||||
form.scrollIntoView({behavior: 'smooth', block: 'center'});
|
||||
form.querySelector('[name="basis"]').focus({preventScroll: true});
|
||||
});
|
||||
})();"#
|
||||
.replace("__CSRF__", &csrf);
|
||||
html! {
|
||||
script defer { (PreEscaped(script)) }
|
||||
})();"#;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::templates::components::message_data::ordered_messages;
|
||||
use serde_json::json;
|
||||
|
||||
fn message(guild_id: serde_json::Value, channel_id: serde_json::Value) -> Message {
|
||||
ordered_messages(&[json!({
|
||||
"id": "1560373081531219968",
|
||||
"channel_id": channel_id,
|
||||
"guild_id": guild_id,
|
||||
"author_id": "1",
|
||||
"author_username": "someone",
|
||||
})])
|
||||
.remove(0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_link_uses_guild_id_and_web_app_endpoint() {
|
||||
let msg = message(json!("42"), json!("7"));
|
||||
assert_eq!(
|
||||
message_jump_link("https://web.fluxer.example/", &msg).as_deref(),
|
||||
Some("https://web.fluxer.example/channels/42/7/1560373081531219968")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_link_falls_back_to_me_for_dms() {
|
||||
let msg = message(json!(null), json!("7"));
|
||||
assert_eq!(
|
||||
message_jump_link("https://web.fluxer.example", &msg).as_deref(),
|
||||
Some("https://web.fluxer.example/channels/@me/7/1560373081531219968")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn meta_without_channel_skips_link_and_channel_copy() {
|
||||
let msg = message(json!(null), json!(null));
|
||||
assert_eq!(message_jump_link("https://web.fluxer.example", &msg), None);
|
||||
let html = message_meta(&msg, None, None).into_string();
|
||||
assert!(html.contains(r#"data-copy-value="1560373081531219968""#));
|
||||
assert!(html.contains("Copy message ID"));
|
||||
assert!(!html.contains("Copy link"));
|
||||
assert!(!html.contains("Copy channel ID"));
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ pub mod data_field;
|
||||
pub mod detail_tabs;
|
||||
pub mod drawer;
|
||||
pub mod empty_state;
|
||||
pub mod enforcement_fields;
|
||||
pub mod error_display;
|
||||
pub mod form;
|
||||
pub mod icons;
|
||||
@@ -21,6 +22,7 @@ pub mod report_category;
|
||||
pub mod report_webhook;
|
||||
pub mod resource_link;
|
||||
pub mod section_card;
|
||||
pub mod staff_notes;
|
||||
pub mod table;
|
||||
pub mod tooltip;
|
||||
pub mod user_display;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::{
|
||||
templates::components::{
|
||||
enforcement_fields::{FIELD_HELP_CLASS, FIELD_INPUT_CLASS, field_label},
|
||||
form::{csrf_input, form_actions, submit_button},
|
||||
section_card::section_card_simple,
|
||||
},
|
||||
utils::timestamps::format_admin_timestamp,
|
||||
};
|
||||
use maud::{Markup, html};
|
||||
|
||||
const NOTE_MAX_CHARS: usize = 2000;
|
||||
|
||||
pub struct StaffNote<'a> {
|
||||
pub note_id: &'a str,
|
||||
pub author: Markup,
|
||||
pub body: &'a str,
|
||||
pub created_at: &'a str,
|
||||
}
|
||||
|
||||
pub struct StaffNoteForm<'a> {
|
||||
pub action: String,
|
||||
pub refresh_target: &'a str,
|
||||
pub field_id: String,
|
||||
pub help: &'a str,
|
||||
pub csrf_token: &'a str,
|
||||
}
|
||||
|
||||
pub fn staff_notes_section(notes: &[StaffNote], form: Option<StaffNoteForm>) -> Markup {
|
||||
html! {
|
||||
(section_card_simple("Notes", html! {
|
||||
div class="space-y-4" {
|
||||
@if notes.is_empty() {
|
||||
p class="text-sm text-neutral-500" { "No notes." }
|
||||
} @else {
|
||||
ol class="space-y-3" {
|
||||
@for note in notes {
|
||||
li class="space-y-1" data-note=(note.note_id) {
|
||||
p class="text-xs text-neutral-500" {
|
||||
(note.author)
|
||||
" on " (format_admin_timestamp(note.created_at))
|
||||
}
|
||||
p class="whitespace-pre-wrap break-words text-sm text-neutral-900" { (note.body) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@if let Some(form) = form {
|
||||
form method="post" action=(form.action)
|
||||
data-admin-refresh-on-success=(form.refresh_target) {
|
||||
(csrf_input(form.csrf_token))
|
||||
div class="space-y-3" {
|
||||
div class="space-y-1" {
|
||||
(field_label(&form.field_id, "Add a note"))
|
||||
textarea id=(form.field_id) name="body" rows="2" required maxlength=(NOTE_MAX_CHARS)
|
||||
class=(FIELD_INPUT_CLASS) {}
|
||||
p class=(FIELD_HELP_CLASS) { (form.help) }
|
||||
}
|
||||
(form_actions(submit_button("Add note")))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::utils::user_tag::user_tag;
|
||||
use crate::{api::types::AdminUser, utils::user_tag::user_tag};
|
||||
use maud::{Markup, html};
|
||||
|
||||
pub const SYSTEM_USER_ID: &str = "0";
|
||||
|
||||
pub fn format_user_display(
|
||||
global_name: Option<&str>,
|
||||
@@ -16,3 +19,29 @@ pub fn format_user_display(
|
||||
_ => "Unknown".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user_name(users: &[AdminUser], id: &str) -> String {
|
||||
if id == SYSTEM_USER_ID {
|
||||
return "System".to_owned();
|
||||
}
|
||||
users.iter().find(|user| user.id == id).map_or_else(
|
||||
|| id.to_owned(),
|
||||
|user| {
|
||||
format_user_display(
|
||||
user.global_name.as_deref(),
|
||||
Some(&user.username),
|
||||
Some(&user.discriminator),
|
||||
user.bot,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn user_link(base: &str, users: &[AdminUser], id: &str) -> Markup {
|
||||
if id == SYSTEM_USER_ID {
|
||||
return html! { "System" };
|
||||
}
|
||||
html! {
|
||||
a href={(base) "/users/" (id)} class="text-blue-600 hover:underline" { (user_name(users, id)) }
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@ use maud::{DOCTYPE, Markup, PreEscaped, html};
|
||||
use super::components::drawer::drawer_controller_script;
|
||||
use super::layout_header::render_header;
|
||||
use super::layout_scripts::{
|
||||
ADMIN_ACTION_FORM_SCRIPT, COPY_TO_CLIPBOARD_SCRIPT, HTMX_FLASH_SCRIPT,
|
||||
HTMX_SCROLL_PRESERVER_SCRIPT, SH_LINK_REWRITE_SCRIPT, SIDEBAR_SCRIPT,
|
||||
ADMIN_ACTION_FORM_SCRIPT, COPY_TO_CLIPBOARD_SCRIPT, ENFORCEMENT_FIELDS_SCRIPT,
|
||||
HTMX_FLASH_SCRIPT, HTMX_SCROLL_PRESERVER_SCRIPT, SH_LINK_REWRITE_SCRIPT, SIDEBAR_SCRIPT,
|
||||
};
|
||||
use super::layout_sidebar::render_sidebar;
|
||||
|
||||
@@ -128,6 +128,7 @@ pub fn admin_layout_ext(
|
||||
script defer { (PreEscaped(HTMX_SCROLL_PRESERVER_SCRIPT)) }
|
||||
script defer { (PreEscaped(HTMX_FLASH_SCRIPT)) }
|
||||
script defer { (PreEscaped(COPY_TO_CLIPBOARD_SCRIPT)) }
|
||||
script defer { (PreEscaped(ENFORCEMENT_FIELDS_SCRIPT)) }
|
||||
(drawer_controller_script())
|
||||
@if let Some(extra) = options.extra_scripts {
|
||||
script defer { (PreEscaped(extra)) }
|
||||
|
||||
@@ -571,6 +571,12 @@ pub const HTMX_FLASH_SCRIPT: &str = r#"
|
||||
showToast(failed ? 'error' : 'success', isLoginResponse(xhr) ? 'Session expired. Sign in again.' : failed ? 'Action failed.' : 'Done.');
|
||||
markToastHandled(event.detail);
|
||||
}, true);
|
||||
|
||||
document.addEventListener('htmx:sendError', function (event) {
|
||||
if (requestVerb(event.detail) !== 'get') return;
|
||||
var path = event.detail.pathInfo && event.detail.pathInfo.finalRequestPath;
|
||||
if (path) window.location.assign(path);
|
||||
}, true);
|
||||
})();
|
||||
"#;
|
||||
|
||||
@@ -622,3 +628,44 @@ pub const SH_LINK_REWRITE_SCRIPT: &str = r#"
|
||||
})();
|
||||
|
||||
"#;
|
||||
|
||||
pub const ENFORCEMENT_FIELDS_SCRIPT: &str = r#"
|
||||
(function () {
|
||||
if (window.__fluxerEnforcementFields) return;
|
||||
window.__fluxerEnforcementFields = true;
|
||||
function codes(section, name) {
|
||||
var value = section.getAttribute(name);
|
||||
return value === null ? null : value.split(' ');
|
||||
}
|
||||
function syncReason(form, code) {
|
||||
form.querySelectorAll('[data-show-for-codes], [data-hide-for-codes]').forEach(function (section) {
|
||||
var showFor = codes(section, 'data-show-for-codes');
|
||||
var hideFor = codes(section, 'data-hide-for-codes');
|
||||
var shown = showFor ? showFor.indexOf(code) >= 0 : hideFor.indexOf(code) < 0;
|
||||
section.hidden = !shown;
|
||||
section.querySelectorAll('input, textarea, select').forEach(function (control) {
|
||||
control.disabled = !shown;
|
||||
});
|
||||
});
|
||||
}
|
||||
function syncChoice(form, name, value) {
|
||||
form.querySelectorAll('[data-show-for-' + name + ']').forEach(function (section) {
|
||||
var shown = codes(section, 'data-show-for-' + name).indexOf(value) >= 0;
|
||||
section.hidden = !shown;
|
||||
section.disabled = !shown;
|
||||
});
|
||||
}
|
||||
document.addEventListener('change', function (event) {
|
||||
var control = event.target;
|
||||
var form = control instanceof Element ? control.closest('form') : null;
|
||||
if (!form) return;
|
||||
if (control instanceof HTMLSelectElement && control.name === 'reason_code') {
|
||||
syncReason(form, control.value);
|
||||
} else if (control instanceof HTMLSelectElement && (control.name === 'resolution' || control.name === 'duration_hours')) {
|
||||
syncChoice(form, control.name, control.value);
|
||||
} else if (control instanceof HTMLInputElement && control.type === 'radio' && (control.name === 'basis' || control.name === 'limit' || control.name === 'source')) {
|
||||
syncChoice(form, control.name, control.value);
|
||||
}
|
||||
});
|
||||
})();
|
||||
"#;
|
||||
@@ -40,6 +40,12 @@ pub const NAV_SECTIONS: &[NavSection] = &[
|
||||
title: "Moderation",
|
||||
items: &[
|
||||
item!("Reports", "/reports", "reports", [acl::REPORT_VIEW]),
|
||||
item!(
|
||||
"Decisions",
|
||||
"/decisions",
|
||||
"decisions",
|
||||
[acl::ENFORCEMENT_VIEW]
|
||||
),
|
||||
item!(
|
||||
"Discovery Review",
|
||||
"/discovery",
|
||||
|
||||
@@ -21,11 +21,17 @@ pub fn format_action(action: &str) -> String {
|
||||
pub fn action_badge_variant(action: &str) -> BadgeVariant {
|
||||
match action {
|
||||
"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
|
||||
"unban" | "cancel_deletion" | "unban_ip" | "unban_email" | "end_enforcement_decision" => {
|
||||
BadgeVariant::Success
|
||||
}
|
||||
"delete_message" => BadgeVariant::Warning,
|
||||
"update_flags"
|
||||
| "update_features"
|
||||
| "set_acls"
|
||||
| "update_settings"
|
||||
| "annotate_ban"
|
||||
| "annotate_enforcement_decision"
|
||||
| "annotate_report" => BadgeVariant::Info,
|
||||
"delete_message" | "warn_user" => BadgeVariant::Warning,
|
||||
_ => BadgeVariant::Default,
|
||||
}
|
||||
}
|
||||
@@ -126,6 +132,15 @@ pub fn target_cell(base: &str, entry: &AuditLogEntry) -> Markup {
|
||||
}
|
||||
}))
|
||||
},
|
||||
"enforcement_decision" => html! {
|
||||
a href={(base) "/decisions/" (entry.target_id)}
|
||||
class="text-neutral-900 underline decoration-neutral-300 hover:text-neutral-600 hover:decoration-neutral-500 text-sm" {
|
||||
div class="flex flex-col" {
|
||||
span class="text-sm font-medium" { "Decision" }
|
||||
span class="text-xs text-neutral-500 break-all" { "ID: " (&entry.target_id) }
|
||||
}
|
||||
}
|
||||
},
|
||||
"message" => html! {
|
||||
div class="flex flex-col" {
|
||||
span class="text-sm font-medium" { "Message" }
|
||||
|
||||
@@ -22,6 +22,8 @@ pub struct BanConfig {
|
||||
pub active_page: &'static str,
|
||||
pub show_bulk_tools: bool,
|
||||
pub show_duration: bool,
|
||||
pub add_title: Option<&'static str>,
|
||||
pub add_help: Option<&'static str>,
|
||||
}
|
||||
|
||||
const IP_BAN_DURATIONS: &[(u32, &str)] = &[
|
||||
@@ -53,6 +55,10 @@ pub const BAN_CONFIGS: &[BanConfig] = &[
|
||||
active_page: "ip-bans",
|
||||
show_bulk_tools: false,
|
||||
show_duration: true,
|
||||
add_title: Some("Block a network"),
|
||||
add_help: Some(
|
||||
"For accounts, block networks from the account's suspension or closure instead.",
|
||||
),
|
||||
},
|
||||
BanConfig {
|
||||
title: "Email Bans",
|
||||
@@ -65,6 +71,8 @@ pub const BAN_CONFIGS: &[BanConfig] = &[
|
||||
active_page: "email-bans",
|
||||
show_bulk_tools: false,
|
||||
show_duration: false,
|
||||
add_title: None,
|
||||
add_help: None,
|
||||
},
|
||||
BanConfig {
|
||||
title: "Phrase Bans",
|
||||
@@ -77,6 +85,8 @@ pub const BAN_CONFIGS: &[BanConfig] = &[
|
||||
active_page: "phrase-bans",
|
||||
show_bulk_tools: false,
|
||||
show_duration: false,
|
||||
add_title: None,
|
||||
add_help: None,
|
||||
},
|
||||
BanConfig {
|
||||
title: "URL Blocklist",
|
||||
@@ -89,6 +99,8 @@ pub const BAN_CONFIGS: &[BanConfig] = &[
|
||||
active_page: "url-bans",
|
||||
show_bulk_tools: false,
|
||||
show_duration: false,
|
||||
add_title: None,
|
||||
add_help: None,
|
||||
},
|
||||
BanConfig {
|
||||
title: "File SHA Blocklist",
|
||||
@@ -101,6 +113,8 @@ pub const BAN_CONFIGS: &[BanConfig] = &[
|
||||
active_page: "file-sha-bans",
|
||||
show_bulk_tools: true,
|
||||
show_duration: false,
|
||||
add_title: None,
|
||||
add_help: None,
|
||||
},
|
||||
BanConfig {
|
||||
title: "Avatar Hash Blocklist",
|
||||
@@ -113,6 +127,8 @@ pub const BAN_CONFIGS: &[BanConfig] = &[
|
||||
active_page: "avatar-hash-bans",
|
||||
show_bulk_tools: false,
|
||||
show_duration: false,
|
||||
add_title: None,
|
||||
add_help: None,
|
||||
},
|
||||
BanConfig {
|
||||
title: "URL Domain Blocklist",
|
||||
@@ -125,6 +141,8 @@ pub const BAN_CONFIGS: &[BanConfig] = &[
|
||||
active_page: "url-domain-bans",
|
||||
show_bulk_tools: false,
|
||||
show_duration: false,
|
||||
add_title: None,
|
||||
add_help: None,
|
||||
},
|
||||
BanConfig {
|
||||
title: "Profile Substring Blocklist",
|
||||
@@ -137,6 +155,8 @@ pub const BAN_CONFIGS: &[BanConfig] = &[
|
||||
active_page: "profile-substring-bans",
|
||||
show_bulk_tools: false,
|
||||
show_duration: false,
|
||||
add_title: None,
|
||||
add_help: None,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -187,7 +207,14 @@ fn ban_card(base: &str, cfg: &BanConfig, csrf_token: &str) -> Markup {
|
||||
html! {
|
||||
div class="rounded-lg border border-neutral-200 bg-white p-4 shadow-sm sm:p-6" {
|
||||
h3 class="text-base font-medium text-neutral-900 mb-4" {
|
||||
"Ban " (cfg.input_label)
|
||||
@if let Some(title) = cfg.add_title {
|
||||
(title)
|
||||
} @else {
|
||||
"Ban " (cfg.input_label)
|
||||
}
|
||||
}
|
||||
@if let Some(help) = cfg.add_help {
|
||||
p class="text-sm text-neutral-500 mb-4" { (help) }
|
||||
}
|
||||
form method="post" action={(base) (cfg.route) "?action=ban&_csrf=" (csrf_token)}
|
||||
hx-post={(base) (cfg.route) "?action=ban&_csrf=" (csrf_token)}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::admin_flags::{DELETION_REASONS, DEPRECATED_GUILD_FEATURES, GUILD_FEATURES};
|
||||
use crate::admin_flags::{DEPRECATED_GUILD_FEATURES, GUILD_FEATURES};
|
||||
use crate::{
|
||||
acl,
|
||||
api::generated::types::EnforcementKindType,
|
||||
config::AdminConfig,
|
||||
middleware::auth::AuthContext,
|
||||
templates::{
|
||||
components::{
|
||||
enforcement_fields::{
|
||||
FIELD_HELP_CLASS, FIELD_INPUT_CLASS, deletion_days_field, field_label,
|
||||
internal_note_field, radio_group, reason_select,
|
||||
},
|
||||
form::{
|
||||
FORM_SELECT_CLASS, checkbox, csrf_input, danger_button, form_actions,
|
||||
form_field_group, opt_out_checkbox, select_chevron, submit_button, text_input,
|
||||
textarea_input,
|
||||
checkbox, csrf_input, danger_button, form_actions, form_field_group, submit_button,
|
||||
text_input, textarea_input,
|
||||
},
|
||||
page_container::page_header,
|
||||
section_card::section_card_simple,
|
||||
@@ -28,12 +32,7 @@ fn guild_feature_label(feature: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bulk_actions_page(
|
||||
config: &AdminConfig,
|
||||
auth: &AuthContext,
|
||||
csrf_token: &str,
|
||||
username_sign_in: bool,
|
||||
) -> Markup {
|
||||
pub fn bulk_actions_page(config: &AdminConfig, auth: &AuthContext, csrf_token: &str) -> Markup {
|
||||
let base = &config.base_path;
|
||||
let admin_acls = auth
|
||||
.admin_user
|
||||
@@ -54,7 +53,7 @@ pub fn bulk_actions_page(
|
||||
(bulk_add_guild_members_section(base, csrf_token))
|
||||
}
|
||||
@if acl::has_permission(admin_acls, acl::BULK_DELETE_USERS) {
|
||||
(bulk_schedule_deletion_section(base, csrf_token, username_sign_in))
|
||||
(bulk_close_accounts_section(base, config.self_hosted, csrf_token))
|
||||
}
|
||||
@if acl::has_permission(admin_acls, acl::BULK_DELETE_USER_MESSAGES) {
|
||||
(bulk_delete_user_messages_section(base, csrf_token))
|
||||
@@ -187,53 +186,53 @@ fn bulk_add_guild_members_section(base: &str, csrf_token: &str) -> Markup {
|
||||
)
|
||||
}
|
||||
|
||||
fn bulk_schedule_deletion_section(base: &str, csrf_token: &str, username_sign_in: bool) -> Markup {
|
||||
fn bulk_decision_fields(
|
||||
id_prefix: &str,
|
||||
kind: EnforcementKindType,
|
||||
statement_help: &str,
|
||||
) -> Markup {
|
||||
let statement_id = format!("{id_prefix}-statement");
|
||||
html! {
|
||||
fieldset class="space-y-4" data-show-for-basis="enforcement" hidden disabled {
|
||||
(reason_select(id_prefix, kind, false))
|
||||
(radio_group("Automated tools", "automation", id_prefix, &[
|
||||
("none", "Not involved"),
|
||||
("flagged", "Flagged this for review"),
|
||||
]))
|
||||
div class="space-y-1" {
|
||||
(field_label(&statement_id, "Note to the user"))
|
||||
textarea id=(statement_id) name="statement" rows="2" maxlength="500" required
|
||||
class=(FIELD_INPUT_CLASS) {}
|
||||
p class=(FIELD_HELP_CLASS) { (statement_help) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn bulk_close_accounts_section(base: &str, self_hosted: bool, csrf_token: &str) -> Markup {
|
||||
section_card_simple(
|
||||
"Bulk Schedule User Deletion",
|
||||
"Close accounts",
|
||||
html! {
|
||||
form method="post" action={(base) "/bulk-actions?action=bulk-schedule-user-deletion"} {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-4" {
|
||||
(textarea_input("user_ids", "User IDs (one per line)", "123456789\n987654321", "", 5, true))
|
||||
(form_field_group("Deletion Reason", "reason_code", true, None,
|
||||
Some("User requested skips identifier bans and pending report resolution. Every other reason applies them."),
|
||||
html! {
|
||||
div class="relative" {
|
||||
select id="reason_code" name="reason_code" required
|
||||
class=(FORM_SELECT_CLASS) {
|
||||
option value="" selected { "Select a reason" }
|
||||
@for &(value, label) in DELETION_REASONS {
|
||||
option value=(value) { (label) }
|
||||
}
|
||||
}
|
||||
(select_chevron())
|
||||
}
|
||||
},
|
||||
(radio_group("Basis", "basis", "bulk-close", &[
|
||||
("enforcement", "A rule was broken"),
|
||||
("holder_request", "The account holders asked"),
|
||||
("housekeeping", "Inactivity"),
|
||||
]))
|
||||
(deletion_days_field("bulk-close-days", self_hosted))
|
||||
(bulk_decision_fields(
|
||||
"bulk-close",
|
||||
EnforcementKindType::Closure,
|
||||
"Shown to every account in the job. It is the only fact a bulk closure can give, because messages and reports cannot be attached in bulk. Never name or describe other people.",
|
||||
))
|
||||
p class="text-neutral-500 text-sm" {
|
||||
"Users that already have a pending deletion are skipped and listed as failed with the reason already scheduled. Cancel those from the user page first to schedule them again."
|
||||
(internal_note_field("bulk-close", "Staff only. Saved as the first note on each decision and in the audit log."))
|
||||
p class=(FIELD_HELP_CLASS) {
|
||||
"Accounts that already have a pending deletion are skipped and listed as failed. Each account closed for a broken rule gets its own decision and notice."
|
||||
}
|
||||
(text_input("public_reason", "Public Reason (optional)", "", "Terms of service violation"))
|
||||
(form_field_group("Days Until Deletion", "days_until_deletion", true, None,
|
||||
Some("Moderation reasons are held for at least 60 days. Only User requested allows 14."),
|
||||
html! {
|
||||
input type="number" id="days_until_deletion" name="days_until_deletion"
|
||||
value="60" min="14" max="365" required
|
||||
class="w-full rounded-lg border border-neutral-300 bg-white \
|
||||
text-neutral-900 text-sm h-8 px-3 py-1.5 \
|
||||
focus:border-brand-primary focus:outline-none \
|
||||
focus:ring-2 focus:ring-brand-primary/20";
|
||||
},
|
||||
))
|
||||
(text_input("audit_log_reason", "Audit Log Reason (optional)", "", "Reason for this bulk operation"))
|
||||
@if username_sign_in {
|
||||
input type="hidden" name="notify_user_present" value="1";
|
||||
} @else {
|
||||
(opt_out_checkbox("notify_user", "Email each user about the scheduled deletion"))
|
||||
}
|
||||
(form_actions(html! {
|
||||
(danger_button("Schedule Deletion"))
|
||||
}))
|
||||
(form_actions(danger_button("Close accounts")))
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -242,19 +241,30 @@ fn bulk_schedule_deletion_section(base: &str, csrf_token: &str, username_sign_in
|
||||
|
||||
fn bulk_delete_user_messages_section(base: &str, csrf_token: &str) -> Markup {
|
||||
section_card_simple(
|
||||
"Bulk Delete User Messages",
|
||||
"Delete all messages",
|
||||
html! {
|
||||
form method="post" action={(base) "/bulk-actions?action=bulk-delete-user-messages"} {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-4" {
|
||||
p class="text-neutral-500 text-sm" {
|
||||
"Deletes every message authored by each user across all channels. This cannot be undone."
|
||||
"Deletes every message each account wrote, in every channel. This cannot be undone."
|
||||
}
|
||||
(textarea_input("user_ids", "User IDs (one per line)", "123456789\n987654321", "", 5, true))
|
||||
(text_input("audit_log_reason", "Audit Log Reason (optional)", "", "Reason for this bulk operation"))
|
||||
(form_actions(html! {
|
||||
(danger_button("Delete All Messages"))
|
||||
}))
|
||||
(radio_group("Basis", "basis", "bulk-delete-messages", &[
|
||||
("enforcement", "A rule was broken"),
|
||||
("holder_request", "The account holders asked"),
|
||||
("housekeeping", "Housekeeping"),
|
||||
]))
|
||||
(bulk_decision_fields(
|
||||
"bulk-delete-messages",
|
||||
EnforcementKindType::ContentRemoval,
|
||||
"Shown to every account in the job. It is the only fact a bulk removal can give, because messages and reports cannot be attached in bulk. Never name or describe other people.",
|
||||
))
|
||||
(internal_note_field("bulk-delete-messages", "Staff only. Saved as the first note on each decision and in the audit log."))
|
||||
p class=(FIELD_HELP_CLASS) {
|
||||
"Each account whose messages are removed for a broken rule gets its own decision and notice."
|
||||
}
|
||||
(form_actions(danger_button("Delete all messages")))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,749 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::{
|
||||
acl,
|
||||
admin_flags::{self, EnforcementHandling},
|
||||
api::{
|
||||
client::ApiResult,
|
||||
generated::types::{
|
||||
EnforcementDecisionListResponse, EnforcementDecisionListResponseDecisionsItem,
|
||||
EnforcementDecisionResponse, EnforcementDecisionResponseEvidenceItem,
|
||||
EnforcementDecisionResponseNotice, EnforcementKindType,
|
||||
},
|
||||
types::AdminUser,
|
||||
},
|
||||
config::AdminConfig,
|
||||
middleware::auth::AuthContext,
|
||||
templates::{
|
||||
components::{
|
||||
enforcement_fields::{
|
||||
FIELD_HELP_CLASS, FIELD_INPUT_CLASS, FIELD_LABEL_CLASS, field_label,
|
||||
internal_note_field, statement_field,
|
||||
},
|
||||
form::{csrf_input, form_actions, submit_button},
|
||||
page_container::{page_header, page_header_with_back},
|
||||
section_card::section_card_simple,
|
||||
staff_notes::{StaffNote, StaffNoteForm, staff_notes_section},
|
||||
user_display::{SYSTEM_USER_ID, user_link},
|
||||
},
|
||||
layout::admin_layout,
|
||||
},
|
||||
utils::timestamps::format_admin_timestamp,
|
||||
};
|
||||
use maud::{Markup, html};
|
||||
|
||||
const CORRECTION_LINE: &str = "If this decision was reported to an authority, file a correction.";
|
||||
const DECISIONS_PAGE_KEY: &str = "decisions";
|
||||
const PERMANENT_SUSPENSION_THRESHOLD_YEARS: i32 = 50;
|
||||
|
||||
pub fn kind_label(kind: EnforcementKindType) -> &'static str {
|
||||
match kind {
|
||||
EnforcementKindType::Warning => "Warning",
|
||||
EnforcementKindType::Suspension => "Suspension",
|
||||
EnforcementKindType::Closure => "Closure",
|
||||
EnforcementKindType::FeatureLimit => "Feature limit",
|
||||
EnforcementKindType::ContentRemoval => "Content removal",
|
||||
EnforcementKindType::ProfileReset => "Profile reset",
|
||||
EnforcementKindType::CommunityRemoval => "Community removal",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state_label(
|
||||
status: &str,
|
||||
end_outcome: Option<&str>,
|
||||
replaced: bool,
|
||||
expired: bool,
|
||||
) -> &'static str {
|
||||
match (status, end_outcome) {
|
||||
("ended", Some("lifted")) => "Lifted",
|
||||
("ended", Some("reversed")) => "Reversed",
|
||||
("ended", Some("expired")) => "Expired",
|
||||
("ended", _) => "Ended",
|
||||
_ if replaced => "Replaced",
|
||||
_ if expired => "Expired",
|
||||
_ => "Active",
|
||||
}
|
||||
}
|
||||
|
||||
fn source_label(source: &str) -> &'static str {
|
||||
match source {
|
||||
"report" => "A report",
|
||||
_ => "Our own review",
|
||||
}
|
||||
}
|
||||
|
||||
fn automation_label(automation: &str) -> &'static str {
|
||||
match automation {
|
||||
"flagged" => "Flagged this for review",
|
||||
"automated" => "Decided by automated tools, waiting for a person",
|
||||
_ => "Not involved",
|
||||
}
|
||||
}
|
||||
|
||||
fn notice_status_label(status: &str) -> &'static str {
|
||||
match status {
|
||||
"sent" => "Sent",
|
||||
"undelivered" => "Not delivered. The user sees it when they sign in",
|
||||
_ => "Pending",
|
||||
}
|
||||
}
|
||||
|
||||
fn notice_channel_label(channel: &str) -> &'static str {
|
||||
match channel {
|
||||
"email" => "Email",
|
||||
"system_dm" => "System DM",
|
||||
_ => "Sign-in screen",
|
||||
}
|
||||
}
|
||||
|
||||
fn limit_label(limit: &str) -> &'static str {
|
||||
match limit {
|
||||
"direct_messages" => "Direct messages",
|
||||
"reports" => "Reports",
|
||||
_ => "Purchases",
|
||||
}
|
||||
}
|
||||
|
||||
fn end_acl(kind: EnforcementKindType, removed_all_messages: bool) -> &'static str {
|
||||
match kind {
|
||||
EnforcementKindType::Suspension => acl::USER_TEMP_BAN,
|
||||
EnforcementKindType::Closure => acl::USER_DELETE,
|
||||
EnforcementKindType::Warning | EnforcementKindType::FeatureLimit => acl::USER_UPDATE_FLAGS,
|
||||
EnforcementKindType::ContentRemoval if removed_all_messages => acl::MESSAGE_DELETE_ALL,
|
||||
EnforcementKindType::ContentRemoval => acl::MESSAGE_SHRED,
|
||||
EnforcementKindType::ProfileReset => acl::USER_UPDATE_PROFILE,
|
||||
EnforcementKindType::CommunityRemoval => acl::GUILD_DELETE,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_end(
|
||||
admin_acls: &[String],
|
||||
kind: EnforcementKindType,
|
||||
removed_all_messages: bool,
|
||||
) -> bool {
|
||||
acl::has_permission(admin_acls, acl::ENFORCEMENT_VIEW)
|
||||
&& acl::has_permission(admin_acls, end_acl(kind, removed_all_messages))
|
||||
}
|
||||
|
||||
pub fn can_lift(kind: EnforcementKindType) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
EnforcementKindType::Suspension
|
||||
| EnforcementKindType::Closure
|
||||
| EnforcementKindType::FeatureLimit
|
||||
)
|
||||
}
|
||||
|
||||
fn is_restricted(reason_code: i32) -> bool {
|
||||
admin_flags::enforcement_reason(reason_code)
|
||||
.is_some_and(|reason| reason.handling == EnforcementHandling::Restricted)
|
||||
}
|
||||
|
||||
pub fn ends_label(
|
||||
kind: EnforcementKindType,
|
||||
ends_at: Option<&str>,
|
||||
deletion_at: Option<&str>,
|
||||
) -> String {
|
||||
match (kind, ends_at, deletion_at) {
|
||||
(EnforcementKindType::Closure, _, Some(at)) => {
|
||||
format!("Deleted on {}", format_admin_timestamp(at))
|
||||
}
|
||||
(_, Some(at), _) => format!("Until {}", format_admin_timestamp(at)),
|
||||
(EnforcementKindType::Suspension, None, _) => "Permanent".to_owned(),
|
||||
(EnforcementKindType::FeatureLimit, None, _) => "No end date".to_owned(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn suspension_label(until: &str) -> String {
|
||||
let permanent =
|
||||
time::OffsetDateTime::parse(until, &time::format_description::well_known::Rfc3339)
|
||||
.is_ok_and(|until| {
|
||||
until.year() - time::OffsetDateTime::now_utc().year()
|
||||
>= PERMANENT_SUSPENSION_THRESHOLD_YEARS
|
||||
});
|
||||
if permanent {
|
||||
"Banned permanently".to_owned()
|
||||
} else {
|
||||
format!("Suspended until {}", format_admin_timestamp(until))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EndDecisionForm<'a> {
|
||||
pub base: &'a str,
|
||||
pub decision_id: &'a str,
|
||||
pub can_lift: bool,
|
||||
pub reason_code: i32,
|
||||
pub csrf_token: &'a str,
|
||||
pub refresh_target: &'a str,
|
||||
}
|
||||
|
||||
pub fn end_decision_form(form: &EndDecisionForm<'_>) -> Markup {
|
||||
let prefix = format!("end-{}", form.decision_id);
|
||||
let restricted = is_restricted(form.reason_code);
|
||||
let lifted_id = format!("{prefix}-outcome-lifted");
|
||||
let reversed_id = format!("{prefix}-outcome-reversed");
|
||||
html! {
|
||||
form method="post"
|
||||
action={(form.base) "/decisions/" (form.decision_id) "?action=end"}
|
||||
data-admin-refresh-on-success=(form.refresh_target)
|
||||
data-end-decision-form="" {
|
||||
(csrf_input(form.csrf_token))
|
||||
div class="space-y-4" {
|
||||
fieldset class="space-y-1" {
|
||||
legend class=(FIELD_LABEL_CLASS) { "Outcome" }
|
||||
@if form.can_lift {
|
||||
div class="flex items-center gap-2" {
|
||||
input type="radio" id=(lifted_id) name="outcome" value="lifted" required
|
||||
class="h-4 w-4 accent-neutral-900";
|
||||
label for=(lifted_id) class="text-sm text-neutral-900" { "Lifted" }
|
||||
}
|
||||
}
|
||||
div class="flex flex-wrap items-center gap-2" {
|
||||
input type="radio" id=(reversed_id) name="outcome" value="reversed" required
|
||||
class="peer h-4 w-4 accent-neutral-900";
|
||||
label for=(reversed_id) class="text-sm text-neutral-900" { "Reversed" }
|
||||
@if restricted {
|
||||
p class="hidden w-full text-sm text-amber-800 peer-checked:block" data-correction-line="" {
|
||||
(CORRECTION_LINE)
|
||||
}
|
||||
}
|
||||
}
|
||||
p class=(FIELD_HELP_CLASS) {
|
||||
@if form.can_lift { "Lifted ends a correct decision early. " }
|
||||
"Reversed says the decision was wrong."
|
||||
}
|
||||
}
|
||||
(statement_field(&prefix, "Shown to the user in the email. Never name or describe other people."))
|
||||
(internal_note_field(&prefix, "Staff only. Saved in the audit log."))
|
||||
(form_actions(submit_button("End decision")))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn field(label: &str, value: Markup) -> Markup {
|
||||
html! {
|
||||
div class="flex min-w-0 flex-col gap-1" {
|
||||
dt class="text-xs text-neutral-500" { (label) }
|
||||
dd class="break-words text-sm text-neutral-900" { (value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn subject_link(base: &str, decision: &EnforcementDecisionResponse, users: &[AdminUser]) -> Markup {
|
||||
let id = decision.subject_id.as_str();
|
||||
if decision.subject_type.to_string() == "guild" {
|
||||
return html! {
|
||||
a href={(base) "/guilds/" (id)} class="text-blue-600 hover:underline" {
|
||||
(decision.subject_name.as_deref().unwrap_or(id))
|
||||
}
|
||||
};
|
||||
}
|
||||
user_link(base, users, id)
|
||||
}
|
||||
|
||||
fn summary_section(
|
||||
base: &str,
|
||||
decision: &EnforcementDecisionResponse,
|
||||
users: &[AdminUser],
|
||||
) -> Markup {
|
||||
let ends = ends_label(
|
||||
decision.kind,
|
||||
decision.ends_at.as_deref(),
|
||||
decision.deletion_at.as_deref(),
|
||||
);
|
||||
let end_outcome = decision.end_outcome.map(|outcome| outcome.to_string());
|
||||
html! {
|
||||
(section_card_simple("Decision", html! {
|
||||
dl class="grid grid-cols-1 gap-4 sm:grid-cols-2" {
|
||||
(field("Status", html! {
|
||||
(state_label(&decision.status.to_string(), end_outcome.as_deref(), decision.replaced, decision.expired))
|
||||
}))
|
||||
(field("Kind", html! { (kind_label(decision.kind)) }))
|
||||
(field("Subject", subject_link(base, decision, users)))
|
||||
@if let Some(recipient) = decision.recipient_user_id.as_deref().filter(|id| *id != decision.subject_id.as_str()) {
|
||||
(field("Notice goes to", user_link(base, users, recipient)))
|
||||
}
|
||||
(field("Reason", html! {
|
||||
(admin_flags::deletion_reason_label(*decision.reason_code))
|
||||
@if let Some(rule) = &decision.notice.rule {
|
||||
" ("
|
||||
@if let Some(url) = &rule.url {
|
||||
a href=(url) target="_blank" rel="noopener noreferrer" class="text-blue-600 hover:underline" { (rule.title) }
|
||||
} @else {
|
||||
(rule.title)
|
||||
}
|
||||
")"
|
||||
}
|
||||
}))
|
||||
(field("Decided", html! { (format_admin_timestamp(&decision.created_at)) }))
|
||||
(field("Decided by", user_link(base, users, &decision.decided_by)))
|
||||
@if !ends.is_empty() {
|
||||
(field("Ends", html! { (ends) }))
|
||||
}
|
||||
(field("How we found this", html! { (source_label(&decision.source.to_string())) }))
|
||||
(field("Automated tools", html! { (automation_label(&decision.automation.to_string())) }))
|
||||
@if let Some(limit) = decision.limit {
|
||||
(field("Limit", html! { (limit_label(&limit.to_string())) }))
|
||||
}
|
||||
@if !decision.profile_fields.is_empty() {
|
||||
(field("Profile fields", html! { (decision.profile_fields.join(", ")) }))
|
||||
}
|
||||
@if decision.removed_all_messages {
|
||||
(field("Removed messages", html! { "All messages" }))
|
||||
} @else if let Some(count) = &decision.removed_message_count {
|
||||
(field("Removed messages", html! { (**count) }))
|
||||
}
|
||||
@if let Some(at) = &decision.payment_at {
|
||||
(field("Charge date", html! { (format_admin_timestamp(at)) }))
|
||||
}
|
||||
@if let Some(rights_holder) = &decision.rights_holder {
|
||||
(field("Rights holder or right concerned", html! { (rights_holder) }))
|
||||
}
|
||||
@if let Some(statement) = &decision.statement {
|
||||
(field("Note to the user", html! { span class="whitespace-pre-wrap" { (statement) } }))
|
||||
}
|
||||
@if let Some(at) = &decision.ended_at {
|
||||
(field("Ended", html! {
|
||||
(format_admin_timestamp(at))
|
||||
@if let Some(by) = &decision.ended_by {
|
||||
" by " (user_link(base, users, by))
|
||||
}
|
||||
}))
|
||||
}
|
||||
@if let Some(statement) = &decision.end_statement {
|
||||
(field("Note to the user on ending", html! { span class="whitespace-pre-wrap" { (statement) } }))
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn text_list(items: &[String]) -> Markup {
|
||||
html! {
|
||||
@if !items.is_empty() {
|
||||
ul class="list-disc space-y-1 pl-5" {
|
||||
@for item in items { li { (item) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn current_notice(notice: &EnforcementDecisionResponseNotice) -> Markup {
|
||||
html! {
|
||||
div class="space-y-3 rounded-lg border border-neutral-200 bg-neutral-50 p-4 text-sm text-neutral-800" data-current-notice="" {
|
||||
p class="font-medium" { (notice.summary) }
|
||||
(text_list(¬ice.effects))
|
||||
@if let Some(rule) = ¬ice.rule {
|
||||
p { "Rule: " (rule.title) }
|
||||
}
|
||||
(text_list(¬ice.facts))
|
||||
@if let Some(statement) = ¬ice.statement {
|
||||
p class="whitespace-pre-wrap" { (statement) }
|
||||
}
|
||||
(text_list(¬ice.how_we_decided))
|
||||
@if let Some(appeal) = ¬ice.appeal {
|
||||
p {
|
||||
"Appeals: " (appeal.email)
|
||||
@if let Some(deadline) = &appeal.deadline {
|
||||
" by " (deadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
(text_list(¬ice.redress))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn notice_section(decision: &EnforcementDecisionResponse) -> Markup {
|
||||
let channels = decision
|
||||
.notice_channels
|
||||
.iter()
|
||||
.map(|channel| notice_channel_label(&channel.to_string()))
|
||||
.collect::<Vec<_>>();
|
||||
html! {
|
||||
(section_card_simple("Notice", html! {
|
||||
div class="space-y-4" {
|
||||
dl class="grid grid-cols-1 gap-4 sm:grid-cols-2" {
|
||||
(field("Status", html! { (notice_status_label(&decision.notice_status.to_string())) }))
|
||||
(field("Channels", html! {
|
||||
@if channels.is_empty() { "None" } @else { (channels.join(", ")) }
|
||||
}))
|
||||
@if let Some(locale) = &decision.notice_locale {
|
||||
(field("Locale", html! { (locale) }))
|
||||
}
|
||||
@if let Some(at) = &decision.notice_sent_at {
|
||||
(field("Received", html! { (format_admin_timestamp(at)) }))
|
||||
}
|
||||
}
|
||||
div class="space-y-1" {
|
||||
p class=(FIELD_LABEL_CLASS) { "Text as sent" }
|
||||
@if let Some(text) = &decision.notice_text {
|
||||
pre class="whitespace-pre-wrap break-words rounded-lg bg-neutral-50 p-4 font-sans text-sm text-neutral-800" data-notice-text="" { (text) }
|
||||
} @else {
|
||||
p class="text-sm text-neutral-500" { "Nothing was sent yet." }
|
||||
}
|
||||
}
|
||||
div class="space-y-1" {
|
||||
p class=(FIELD_LABEL_CLASS) { "What the user sees now" }
|
||||
(current_notice(&decision.notice))
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn evidence_row(
|
||||
base: &str,
|
||||
item: &EnforcementDecisionResponseEvidenceItem,
|
||||
restricted: bool,
|
||||
) -> Markup {
|
||||
html! {
|
||||
li class="space-y-1 py-3" data-evidence-message=(item.message_id.as_str()) {
|
||||
div class="flex flex-wrap items-baseline gap-x-3 text-xs text-neutral-500" {
|
||||
span { (format_admin_timestamp(&item.timestamp)) }
|
||||
span { "Message " (item.message_id.as_str()) }
|
||||
@if let Some(channel_id) = &item.channel_id {
|
||||
a href={(base) "/messages?channel_id=" (channel_id.as_str()) "&message_id=" (item.message_id.as_str()) "&context_limit=50"}
|
||||
class="text-blue-600 hover:underline" { "Open in message tools" }
|
||||
}
|
||||
}
|
||||
@if let Some(content) = &item.content {
|
||||
p class="whitespace-pre-wrap break-words text-sm text-neutral-900" { (content) }
|
||||
} @else if restricted {
|
||||
p class="text-sm text-neutral-500" { "Content is not stored for this reason." }
|
||||
} @else {
|
||||
p class="text-sm text-neutral-500" { "Content is hidden. Needs the message:lookup permission." }
|
||||
}
|
||||
@if let Some(attachments) = &item.attachments {
|
||||
@for attachment in attachments {
|
||||
p class="text-xs text-neutral-600" { "Attachment: " (attachment.filename) }
|
||||
}
|
||||
} @else if *item.attachment_count > 0 {
|
||||
p class="text-xs text-neutral-600" { "Attachments: " (*item.attachment_count) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn evidence_section(base: &str, decision: &EnforcementDecisionResponse) -> Markup {
|
||||
let restricted = is_restricted(*decision.reason_code);
|
||||
html! {
|
||||
(section_card_simple("Evidence", html! {
|
||||
@if decision.evidence.is_empty() {
|
||||
p class="text-sm text-neutral-500" { "No messages were recorded." }
|
||||
} @else {
|
||||
ul class="divide-y divide-neutral-100" {
|
||||
@for item in &decision.evidence {
|
||||
(evidence_row(base, item, restricted))
|
||||
}
|
||||
}
|
||||
}
|
||||
@if let Some(values) = &decision.profile_values {
|
||||
@if !values.is_empty() {
|
||||
dl class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2" {
|
||||
@for (name, value) in values {
|
||||
(field(&format!("Previous {name}"), html! { (value) }))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn reports_section(base: &str, decision: &EnforcementDecisionResponse) -> Markup {
|
||||
html! {
|
||||
@if !decision.report_ids.is_empty() {
|
||||
(section_card_simple("Reports", html! {
|
||||
ul class="space-y-1 text-sm" {
|
||||
@for report_id in &decision.report_ids {
|
||||
li {
|
||||
a href={(base) "/reports/" (report_id.as_str())} class="text-blue-600 hover:underline" {
|
||||
(report_id.as_str())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn network_blocks_section(decision: &EnforcementDecisionResponse) -> Markup {
|
||||
html! {
|
||||
@if !decision.network_blocks.is_empty() {
|
||||
(section_card_simple("Network blocks", html! {
|
||||
ul class="space-y-1 text-sm text-neutral-900" {
|
||||
@for block in &decision.network_blocks {
|
||||
li {
|
||||
span class="font-mono" { (block.ip) }
|
||||
@if let Some(expires_at) = &block.expires_at {
|
||||
span class="text-neutral-500" { ", until " (format_admin_timestamp(expires_at)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
p class="mt-2 text-xs text-neutral-500" {
|
||||
"Ending the decision removes these blocks. Deleting the account removes them too."
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn notes_section(
|
||||
base: &str,
|
||||
decision: &EnforcementDecisionResponse,
|
||||
users: &[AdminUser],
|
||||
can_annotate: bool,
|
||||
csrf_token: &str,
|
||||
) -> Markup {
|
||||
let notes: Vec<StaffNote> = decision
|
||||
.notes
|
||||
.iter()
|
||||
.map(|note| StaffNote {
|
||||
note_id: note.note_id.as_str(),
|
||||
author: user_link(base, users, ¬e.author_id),
|
||||
body: ¬e.body,
|
||||
created_at: ¬e.created_at,
|
||||
})
|
||||
.collect();
|
||||
staff_notes_section(
|
||||
¬es,
|
||||
can_annotate.then(|| StaffNoteForm {
|
||||
action: format!(
|
||||
"{base}/decisions/{}?action=add_note",
|
||||
decision.decision_id.as_str()
|
||||
),
|
||||
refresh_target: "#decision-detail",
|
||||
field_id: format!("note-{}", decision.decision_id.as_str()),
|
||||
help: "Staff only. Notes cannot be changed and expire after a year.",
|
||||
csrf_token,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn decision_user_ids(decision: &EnforcementDecisionResponse) -> Vec<String> {
|
||||
let mut ids: Vec<String> = [
|
||||
Some(&decision.decided_by),
|
||||
decision.recipient_user_id.as_ref(),
|
||||
decision.ended_by.as_ref(),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.chain(decision.notes.iter().map(|note| ¬e.author_id))
|
||||
.map(|id| id.as_str().to_owned())
|
||||
.collect();
|
||||
if decision.subject_type.to_string() == "user" {
|
||||
ids.push(decision.subject_id.as_str().to_owned());
|
||||
}
|
||||
ids.retain(|id| id != SYSTEM_USER_ID);
|
||||
ids.sort_unstable();
|
||||
ids.dedup();
|
||||
ids
|
||||
}
|
||||
|
||||
pub fn decision_detail(
|
||||
config: &AdminConfig,
|
||||
admin_acls: &[String],
|
||||
decision: &EnforcementDecisionResponse,
|
||||
users: &[AdminUser],
|
||||
csrf_token: &str,
|
||||
) -> Markup {
|
||||
let base = &config.base_path;
|
||||
let decision_id = decision.decision_id.as_str();
|
||||
let expired_end = decision
|
||||
.end_outcome
|
||||
.map(|outcome| outcome.to_string())
|
||||
.as_deref()
|
||||
== Some("expired");
|
||||
let open = (decision.status.to_string() == "active" && !decision.replaced) || expired_end;
|
||||
html! {
|
||||
div id="decision-detail" class="space-y-6" {
|
||||
(summary_section(base, decision, users))
|
||||
@if open
|
||||
&& can_end(admin_acls, decision.kind, decision.removed_all_messages)
|
||||
&& (decision.network_blocks.is_empty() || acl::has_permission(admin_acls, acl::BAN_IP_REMOVE))
|
||||
{
|
||||
(section_card_simple("End decision", end_decision_form(&EndDecisionForm {
|
||||
base,
|
||||
decision_id,
|
||||
can_lift: can_lift(decision.kind) && !expired_end,
|
||||
reason_code: *decision.reason_code,
|
||||
csrf_token,
|
||||
refresh_target: "#decision-detail",
|
||||
})))
|
||||
}
|
||||
(notice_section(decision))
|
||||
(evidence_section(base, decision))
|
||||
(reports_section(base, decision))
|
||||
(network_blocks_section(decision))
|
||||
(notes_section(
|
||||
base,
|
||||
decision,
|
||||
users,
|
||||
acl::has_permission(admin_acls, acl::ENFORCEMENT_ANNOTATE),
|
||||
csrf_token,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enforcement_decision_page(
|
||||
config: &AdminConfig,
|
||||
auth: &AuthContext,
|
||||
decision_id: &str,
|
||||
decision: Option<&EnforcementDecisionResponse>,
|
||||
users: &[AdminUser],
|
||||
csrf_token: &str,
|
||||
is_htmx: bool,
|
||||
) -> Markup {
|
||||
let base = &config.base_path;
|
||||
let title = format!("Decision {decision_id}");
|
||||
let admin_acls = auth
|
||||
.admin_user
|
||||
.as_ref()
|
||||
.map(|user| user.acls.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
let content = html! {
|
||||
(page_header_with_back(&title, None, &format!("{base}/decisions"), Some("Back to decisions")))
|
||||
@match decision {
|
||||
Some(decision) => { (decision_detail(config, admin_acls, decision, users, csrf_token)) }
|
||||
None => {
|
||||
(section_card_simple("Decision", html! {
|
||||
p class="text-sm text-neutral-500" { "This decision was not found or could not be loaded." }
|
||||
}))
|
||||
}
|
||||
}
|
||||
};
|
||||
if is_htmx {
|
||||
content
|
||||
} else {
|
||||
admin_layout(config, auth, &title, DECISIONS_PAGE_KEY, None, content)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decisions_page(
|
||||
config: &AdminConfig,
|
||||
auth: &AuthContext,
|
||||
lookup_error: Option<&str>,
|
||||
review_queue: Option<&ApiResult<EnforcementDecisionListResponse>>,
|
||||
) -> Markup {
|
||||
let base = &config.base_path;
|
||||
let content = html! {
|
||||
(page_header("Decisions", Some("Look up a decision by its ID, or work through the review queue.")))
|
||||
(section_card_simple("Find a decision", html! {
|
||||
form method="get" action={(base) "/decisions"} class="space-y-3" {
|
||||
div class="space-y-1" {
|
||||
(field_label("decision-lookup", "Decision ID"))
|
||||
input type="text" id="decision-lookup" name="id" required inputmode="numeric"
|
||||
class=(FIELD_INPUT_CLASS);
|
||||
p class=(FIELD_HELP_CLASS) { "The ID from the notice or the appeal email." }
|
||||
@if let Some(error) = lookup_error {
|
||||
p class="text-sm text-red-700" { (error) }
|
||||
}
|
||||
}
|
||||
(form_actions(submit_button("Open decision")))
|
||||
}
|
||||
}))
|
||||
@if let Some(queue) = review_queue {
|
||||
(section_card_simple("Review queue", html! {
|
||||
p class=(FIELD_HELP_CLASS) {
|
||||
"Automated limits waiting for a person, such as purchase limits after a chargeback. End the limit, or record a warning, suspension or closure on the account, to take it out of the queue."
|
||||
}
|
||||
@match queue {
|
||||
Ok(response) => { (review_queue_table(base, &response.decisions)) }
|
||||
Err(_) => { p class="text-sm text-red-700" { "Failed to load the review queue." } }
|
||||
}
|
||||
}))
|
||||
}
|
||||
};
|
||||
admin_layout(config, auth, "Decisions", DECISIONS_PAGE_KEY, None, content)
|
||||
}
|
||||
|
||||
fn review_queue_table(
|
||||
base: &str,
|
||||
decisions: &[EnforcementDecisionListResponseDecisionsItem],
|
||||
) -> Markup {
|
||||
html! {
|
||||
@if decisions.is_empty() {
|
||||
p class="text-sm text-neutral-500" { "Nothing is waiting for review." }
|
||||
} @else {
|
||||
div class="overflow-x-auto" {
|
||||
table class="min-w-full divide-y divide-neutral-200 text-sm" data-review-queue="" {
|
||||
thead {
|
||||
tr class="text-left text-xs text-neutral-500" {
|
||||
@for header in ["ID", "Account", "Reason", "Payment", "Date"] {
|
||||
th class="px-3 py-2 font-medium" { (header) }
|
||||
}
|
||||
}
|
||||
}
|
||||
tbody class="divide-y divide-neutral-100" {
|
||||
@for decision in decisions {
|
||||
tr {
|
||||
td class="px-3 py-2" {
|
||||
a href={(base) "/decisions/" (decision.decision_id.as_str())} class="text-blue-600 hover:underline" {
|
||||
(decision.decision_id.as_str())
|
||||
}
|
||||
}
|
||||
td class="px-3 py-2" {
|
||||
a href={(base) "/users/" (decision.subject_id.as_str()) "?tab=moderation"} class="text-blue-600 hover:underline" {
|
||||
(decision.subject_id.as_str())
|
||||
}
|
||||
}
|
||||
td class="px-3 py-2" { (admin_flags::deletion_reason_label(*decision.reason_code)) }
|
||||
td class="whitespace-nowrap px-3 py-2" {
|
||||
(decision.payment_at.as_deref().map(format_admin_timestamp).unwrap_or_default())
|
||||
}
|
||||
td class="whitespace-nowrap px-3 py-2" { (format_admin_timestamp(&decision.created_at)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decisions_table(
|
||||
base: &str,
|
||||
decisions: &[EnforcementDecisionListResponseDecisionsItem],
|
||||
) -> Markup {
|
||||
html! {
|
||||
@if decisions.is_empty() {
|
||||
p class="text-sm text-neutral-500" { "No decisions." }
|
||||
} @else {
|
||||
div class="overflow-x-auto" {
|
||||
table class="min-w-full divide-y divide-neutral-200 text-sm" data-decisions-table="" {
|
||||
thead {
|
||||
tr class="text-left text-xs text-neutral-500" {
|
||||
@for header in ["ID", "Kind", "Reason", "Status", "Date"] {
|
||||
th class="px-3 py-2 font-medium" { (header) }
|
||||
}
|
||||
}
|
||||
}
|
||||
tbody class="divide-y divide-neutral-100" {
|
||||
@for decision in decisions {
|
||||
@let end_outcome = decision.end_outcome.map(|outcome| outcome.to_string());
|
||||
tr {
|
||||
td class="px-3 py-2" {
|
||||
a href={(base) "/decisions/" (decision.decision_id.as_str())} class="text-blue-600 hover:underline" {
|
||||
(decision.decision_id.as_str())
|
||||
}
|
||||
}
|
||||
td class="px-3 py-2" { (kind_label(decision.kind)) }
|
||||
td class="px-3 py-2" { (admin_flags::deletion_reason_label(*decision.reason_code)) }
|
||||
td class="px-3 py-2" {
|
||||
(state_label(&decision.status.to_string(), end_outcome.as_deref(), decision.replaced, decision.expired))
|
||||
}
|
||||
td class="whitespace-nowrap px-3 py-2" { (format_admin_timestamp(&decision.created_at)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
use crate::{
|
||||
acl,
|
||||
api::types::GuildInfo,
|
||||
api::{generated::types::EnforcementKindType, types::GuildInfo},
|
||||
config::AdminConfig,
|
||||
templates::components::{
|
||||
enforcement_fields::{BasisFields, DecisionFields, basis_fields},
|
||||
form::{csrf_input, danger_button, form_actions, submit_button},
|
||||
page_container::card_with_header,
|
||||
},
|
||||
@@ -144,17 +145,32 @@ pub fn moderation_tab(
|
||||
}
|
||||
|
||||
@if can_delete {
|
||||
(card_with_header("Delete Guild", html! {
|
||||
p class="mb-3 text-sm text-neutral-500" {
|
||||
"Deleting a guild permanently removes it and all associated data. \
|
||||
This action cannot be undone."
|
||||
}
|
||||
(card_with_header("Remove community", html! {
|
||||
form method="post"
|
||||
action={(base) "/guilds/" (guild.id) "?action=delete_guild&tab=moderation"} {
|
||||
(csrf_input(csrf_token))
|
||||
(form_actions(html! {
|
||||
(danger_button("Delete Guild"))
|
||||
}))
|
||||
div class="space-y-4" {
|
||||
p class="text-sm text-neutral-500" {
|
||||
"Removes the community with its channels, messages and settings. It can't be undone. Child safety and violent extremism reasons can't be used, because the removal deletes the content."
|
||||
}
|
||||
(basis_fields(&BasisFields {
|
||||
decision: DecisionFields {
|
||||
id_prefix: "remove-community",
|
||||
kind: EnforcementKindType::CommunityRemoval,
|
||||
report_ids: &[],
|
||||
messages: Some(&[]),
|
||||
user_flags: None,
|
||||
allow_restricted: false,
|
||||
},
|
||||
choices: &[
|
||||
("enforcement", "A rule was broken"),
|
||||
("holder_request", "The owner asked"),
|
||||
],
|
||||
plain_note_help: "Staff only. Saved in the audit log.",
|
||||
enforcement_extra: html! {},
|
||||
}))
|
||||
(form_actions(danger_button("Remove community")))
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -5,13 +5,14 @@ use crate::{
|
||||
middleware::auth::AuthContext,
|
||||
templates::{
|
||||
components::{
|
||||
enforcement_fields::{INTERNAL_NOTE_MAX_CHARS, TakeAction},
|
||||
error_display::error_alert,
|
||||
form::{
|
||||
FORM_INPUT_CLASS, csrf_input, danger_button, form_actions, form_field_group,
|
||||
submit_button,
|
||||
},
|
||||
message_data::{Message, ordered_messages, value_id},
|
||||
message_list::{message_deletion_script, message_list},
|
||||
message_list::{message_delete_prefill_script, message_list},
|
||||
page_container::{card, page_header},
|
||||
},
|
||||
layout::LayoutOptions,
|
||||
@@ -193,6 +194,7 @@ pub struct MessagesPageParams<'a> {
|
||||
pub prefill_channel_id: Option<&'a str>,
|
||||
pub can_lookup: bool,
|
||||
pub can_delete: bool,
|
||||
pub can_take_action: bool,
|
||||
pub lookup_result: Option<&'a Value>,
|
||||
pub browse_result: Option<&'a Value>,
|
||||
pub search_result: Option<&'a Value>,
|
||||
@@ -202,11 +204,32 @@ pub struct MessagesPageParams<'a> {
|
||||
pub error: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct MessageActions {
|
||||
pub delete: bool,
|
||||
pub take_action: bool,
|
||||
}
|
||||
|
||||
impl MessageActions {
|
||||
fn take_action(self) -> Option<&'static TakeAction<'static>> {
|
||||
self.take_action.then_some(&ANY_AUTHOR)
|
||||
}
|
||||
}
|
||||
|
||||
const ANY_AUTHOR: TakeAction<'static> = TakeAction {
|
||||
author_id: None,
|
||||
report_id: None,
|
||||
};
|
||||
|
||||
pub fn messages_page(
|
||||
config: &AdminConfig,
|
||||
auth: &AuthContext,
|
||||
params: &MessagesPageParams<'_>,
|
||||
) -> Markup {
|
||||
let actions = MessageActions {
|
||||
delete: params.can_delete,
|
||||
take_action: params.can_take_action,
|
||||
};
|
||||
let content = html! {
|
||||
(page_header("Message Tools", None))
|
||||
|
||||
@@ -217,19 +240,19 @@ pub fn messages_page(
|
||||
@if let (Some(result), Some(channel_id), Some(query)) = (
|
||||
params.search_result, params.browse_channel_id, params.search_query_text,
|
||||
) {
|
||||
(search_result_card(config, result, channel_id, query, params.can_delete))
|
||||
(search_result_card(config, result, channel_id, query, actions))
|
||||
}
|
||||
@if params.search_result.is_none()
|
||||
&& let (Some(result), Some(channel_id)) = (
|
||||
params.browse_result, params.browse_channel_id,
|
||||
) {
|
||||
(browse_result_card(
|
||||
config, result, channel_id, params.can_delete,
|
||||
config, result, channel_id, actions,
|
||||
params.csrf_token, params.context_limit,
|
||||
))
|
||||
}
|
||||
@if let Some(result) = params.lookup_result {
|
||||
(lookup_result_card(config, result, params.can_delete, params.context_limit))
|
||||
(lookup_result_card(config, result, actions, params.context_limit))
|
||||
}
|
||||
@if params.can_lookup {
|
||||
(browse_channel_form(config, params.csrf_token, params.prefill_channel_id))
|
||||
@@ -238,8 +261,8 @@ pub fn messages_page(
|
||||
}
|
||||
@if params.can_delete {
|
||||
(delete_message_form(config, params.csrf_token))
|
||||
(message_delete_prefill_script())
|
||||
}
|
||||
(message_deletion_script(params.csrf_token))
|
||||
}
|
||||
};
|
||||
admin_layout_ext(
|
||||
@@ -260,7 +283,7 @@ pub fn messages_page(
|
||||
pub fn browse_messages_fragment(
|
||||
config: &AdminConfig,
|
||||
result: &Value,
|
||||
show_delete: bool,
|
||||
actions: MessageActions,
|
||||
highlight_message_id: Option<&str>,
|
||||
) -> Markup {
|
||||
let messages = response_messages(result);
|
||||
@@ -280,8 +303,9 @@ pub fn browse_messages_fragment(
|
||||
config,
|
||||
&config.base_path,
|
||||
&messages,
|
||||
show_delete,
|
||||
actions.delete,
|
||||
highlight_message_id,
|
||||
actions.take_action(),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -291,7 +315,7 @@ fn browse_result_card(
|
||||
config: &AdminConfig,
|
||||
result: &Value,
|
||||
channel_id: &str,
|
||||
show_delete: bool,
|
||||
actions: MessageActions,
|
||||
csrf_token: &str,
|
||||
context_limit: u32,
|
||||
) -> Markup {
|
||||
@@ -328,7 +352,7 @@ fn browse_result_card(
|
||||
config,
|
||||
channel_id,
|
||||
messages: &messages,
|
||||
show_delete,
|
||||
actions,
|
||||
has_more,
|
||||
has_newer: false,
|
||||
focus_message_id: None,
|
||||
@@ -343,7 +367,7 @@ fn search_result_card(
|
||||
result: &Value,
|
||||
channel_id: &str,
|
||||
query_text: &str,
|
||||
show_delete: bool,
|
||||
actions: MessageActions,
|
||||
) -> Markup {
|
||||
let messages = response_messages(result);
|
||||
let total = result
|
||||
@@ -369,8 +393,9 @@ fn search_result_card(
|
||||
config,
|
||||
&config.base_path,
|
||||
&messages,
|
||||
show_delete,
|
||||
actions.delete,
|
||||
None,
|
||||
actions.take_action(),
|
||||
))
|
||||
}
|
||||
})
|
||||
@@ -379,7 +404,7 @@ fn search_result_card(
|
||||
fn lookup_result_card(
|
||||
config: &AdminConfig,
|
||||
result: &Value,
|
||||
show_delete: bool,
|
||||
actions: MessageActions,
|
||||
context_limit: u32,
|
||||
) -> Markup {
|
||||
let messages = response_messages(result);
|
||||
@@ -401,7 +426,7 @@ fn lookup_result_card(
|
||||
config,
|
||||
channel_id,
|
||||
messages: &messages,
|
||||
show_delete,
|
||||
actions,
|
||||
has_more: true,
|
||||
has_newer: true,
|
||||
focus_message_id: message_id,
|
||||
@@ -415,7 +440,7 @@ struct MessageScrollPane<'a> {
|
||||
config: &'a AdminConfig,
|
||||
channel_id: &'a str,
|
||||
messages: &'a [Message],
|
||||
show_delete: bool,
|
||||
actions: MessageActions,
|
||||
has_more: bool,
|
||||
has_newer: bool,
|
||||
focus_message_id: Option<&'a str>,
|
||||
@@ -454,8 +479,9 @@ fn message_scroll_pane(pane: MessageScrollPane<'_>) -> Markup {
|
||||
pane.config,
|
||||
&pane.config.base_path,
|
||||
pane.messages,
|
||||
pane.show_delete,
|
||||
pane.actions.delete,
|
||||
pane.focus_message_id,
|
||||
pane.actions.take_action(),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -639,12 +665,13 @@ fn lookup_by_attachment_form(config: &AdminConfig, csrf_token: &str) -> Markup {
|
||||
fn delete_message_form(config: &AdminConfig, csrf_token: &str) -> Markup {
|
||||
let base = &config.base_path;
|
||||
card(html! {
|
||||
h3 class="mb-4 text-base font-semibold text-neutral-900" {
|
||||
"Delete Message"
|
||||
}
|
||||
form method="post" action={(base) "/messages?action=delete"} {
|
||||
h3 class="mb-4 text-base font-semibold text-neutral-900" { "Delete message" }
|
||||
form id="delete-message-form" method="post" action={(base) "/messages?action=delete"} {
|
||||
(csrf_input(csrf_token))
|
||||
div class="flex flex-col gap-4" {
|
||||
p class="text-neutral-600 text-sm" {
|
||||
"For a message that broke a rule, use Take action instead, so a decision records the removal and the user is told."
|
||||
}
|
||||
(form_field_group(
|
||||
"Channel ID", "delete-message-channel-id", true, None, None,
|
||||
html! {
|
||||
@@ -662,16 +689,26 @@ fn delete_message_form(config: &AdminConfig, csrf_token: &str) -> Markup {
|
||||
},
|
||||
))
|
||||
(form_field_group(
|
||||
"Audit Log Reason (optional)",
|
||||
"delete-message-audit-log-reason", false, None, None,
|
||||
"Basis", "delete-message-basis", true, None, None,
|
||||
html! {
|
||||
input type="text" id="delete-message-audit-log-reason"
|
||||
name="audit_log_reason" placeholder="Reason for deletion"
|
||||
select id="delete-message-basis" name="basis" required class=(FORM_INPUT_CLASS) {
|
||||
option value="" disabled selected { "Choose a basis" }
|
||||
option value="holder_request" { "The account holder asked" }
|
||||
option value="housekeeping" { "Housekeeping" }
|
||||
}
|
||||
},
|
||||
))
|
||||
(form_field_group(
|
||||
"Internal note (optional)",
|
||||
"delete-message-internal-note", false, None, None,
|
||||
html! {
|
||||
input type="text" id="delete-message-internal-note"
|
||||
name="internal_note" maxlength=(INTERNAL_NOTE_MAX_CHARS)
|
||||
class=(FORM_INPUT_CLASS);
|
||||
},
|
||||
))
|
||||
(form_actions(html! {
|
||||
(danger_button("Delete Message"))
|
||||
(danger_button("Delete message"))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ pub mod bans;
|
||||
pub(crate) mod blocklist_helpers;
|
||||
pub mod bulk_actions;
|
||||
pub mod discovery;
|
||||
pub mod enforcement_decision;
|
||||
pub mod gateway;
|
||||
pub mod gateway_leaderboard;
|
||||
pub mod gateway_nodes;
|
||||
|
||||
@@ -22,13 +22,14 @@ use crate::{
|
||||
data_field, data_field_link_mono, data_field_mono, data_field_muted,
|
||||
data_field_text,
|
||||
},
|
||||
enforcement_fields::{TakeAction, can_take_action, take_action_href},
|
||||
form::{
|
||||
FORM_CONTROL_CLASS, FORM_LABEL_CLASS, checkbox, csrf_input, danger_button,
|
||||
opt_out_checkbox, select_chevron,
|
||||
},
|
||||
media::user_avatar_url,
|
||||
message_data::ordered_messages,
|
||||
message_list::{message_deletion_script, message_list},
|
||||
message_list::message_list,
|
||||
nsfw_indicators::{
|
||||
adult_content_badge, channel_nsfw_state_badge, content_warning_badge,
|
||||
},
|
||||
@@ -40,6 +41,8 @@ use crate::{
|
||||
},
|
||||
resource_link::{ResourceType, resource_link},
|
||||
section_card::section_card,
|
||||
staff_notes::{StaffNote, StaffNoteForm, staff_notes_section},
|
||||
user_display::user_link,
|
||||
},
|
||||
layout::admin_layout,
|
||||
},
|
||||
@@ -93,10 +96,15 @@ fn reported_user_label(report: &ReportEntry) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
fn has_acl(auth: &AuthContext, permission: &str) -> bool {
|
||||
fn admin_acls(auth: &AuthContext) -> &[String] {
|
||||
auth.admin_user
|
||||
.as_ref()
|
||||
.is_some_and(|admin| acl::has_permission(&admin.acls, permission))
|
||||
.map(|admin| admin.acls.as_slice())
|
||||
.unwrap_or(&[])
|
||||
}
|
||||
|
||||
fn has_acl(auth: &AuthContext, permission: &str) -> bool {
|
||||
acl::has_permission(admin_acls(auth), permission)
|
||||
}
|
||||
|
||||
fn basic_info_section(config: &AdminConfig, auth: &AuthContext, report: &ReportEntry) -> Markup {
|
||||
@@ -506,18 +514,38 @@ fn status_card(config: &AdminConfig, report: &ReportEntry) -> Markup {
|
||||
}
|
||||
}
|
||||
|
||||
fn actions_card(config: &AdminConfig, report: &ReportEntry, csrf_token: &str) -> Markup {
|
||||
fn take_action_subject(report: &ReportEntry) -> Option<&str> {
|
||||
match report.report_type {
|
||||
0 | 1 => report.reported_user_id.as_deref(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn actions_card(
|
||||
config: &AdminConfig,
|
||||
auth: &AuthContext,
|
||||
report: &ReportEntry,
|
||||
csrf_token: &str,
|
||||
) -> Markup {
|
||||
let base = &config.base_path;
|
||||
let reported_message = report
|
||||
.reported_channel_id
|
||||
.as_deref()
|
||||
.zip(report.reported_message_id.as_deref());
|
||||
html! {
|
||||
(section_card(Some("Actions"), None, None, html! {
|
||||
div class="flex flex-col gap-3" {
|
||||
@if report.status == 0 {
|
||||
(resolve_report_form(base, &report.report_id, csrf_token))
|
||||
}
|
||||
@if report.report_type == 0 || report.report_type == 1 {
|
||||
@if let Some(ref reported_id) = report.reported_user_id {
|
||||
(nav_link(&format!("{base}/users/{reported_id}"), "View Reported User"))
|
||||
@if let Some(reported_id) = take_action_subject(report) {
|
||||
@if can_take_action(admin_acls(auth)) {
|
||||
(nav_link(
|
||||
&take_action_href(base, reported_id, Some(&report.report_id), reported_message),
|
||||
"Take action",
|
||||
))
|
||||
}
|
||||
(nav_link(&format!("{base}/users/{reported_id}"), "View Reported User"))
|
||||
}
|
||||
@if report.report_type == 2 {
|
||||
@if let Some(ref guild_id) = report.reported_guild_id {
|
||||
@@ -568,6 +596,31 @@ fn resolve_report_form(base: &str, report_id: &str, csrf_token: &str) -> Markup
|
||||
}
|
||||
(select_chevron())
|
||||
}
|
||||
fieldset class="flex flex-col gap-3" data-show-for-resolution="actioned" hidden disabled {
|
||||
label for="outcome" class=(FORM_LABEL_CLASS) { "Outcome" }
|
||||
div class="relative" {
|
||||
select id="outcome" name="outcome" required
|
||||
class={(FORM_CONTROL_CLASS) " h-8 appearance-none px-3 py-1.5 pr-10"} {
|
||||
option value="" selected disabled { "Choose one" }
|
||||
option value="removed" { "Content removed" }
|
||||
option value="warned" { "Warning given" }
|
||||
option value="restricted" { "Account restricted" }
|
||||
option value="action_taken" { "Other action taken" }
|
||||
}
|
||||
(select_chevron())
|
||||
}
|
||||
p class="text-neutral-500 text-xs" {
|
||||
"What happened to the reported content or account. The reporter is told it. Warning given and other action taken tell them we acted but not how."
|
||||
}
|
||||
}
|
||||
fieldset class="flex flex-col gap-3" data-show-for-resolution="duplicate" hidden disabled {
|
||||
label for="duplicate_of" class=(FORM_LABEL_CLASS) { "Duplicate of" }
|
||||
input type="text" id="duplicate_of" name="duplicate_of" required inputmode="numeric"
|
||||
placeholder="Report ID" class={(FORM_CONTROL_CLASS) " h-8 px-3 py-1.5"};
|
||||
p class="text-neutral-500 text-xs" {
|
||||
"The report this one repeats. The reporter is told its outcome, or nothing while it is still open."
|
||||
}
|
||||
}
|
||||
label for="public_comment" class=(FORM_LABEL_CLASS) {
|
||||
"Public comment to the reporter (optional)"
|
||||
}
|
||||
@@ -575,14 +628,14 @@ fn resolve_report_form(base: &str, report_id: &str, csrf_token: &str) -> Markup
|
||||
class={(FORM_CONTROL_CLASS) " px-3 py-2"} {}
|
||||
(opt_out_checkbox("notify_reporter", "Include the public comment in the reporter notice"))
|
||||
p class="text-neutral-500 text-xs" data-resolve-notice-help="" {
|
||||
"The reporter is told the report was resolved either way, unless their account is gone or barred from reporting. \
|
||||
"The reporter is told the outcome, unless their account is gone or was barred from reporting when they filed it. \
|
||||
When unticked, the comment is not sent or shown on the report. It is only kept in the audit log."
|
||||
}
|
||||
button type="submit"
|
||||
class="inline-flex w-full items-center justify-center gap-2 \
|
||||
font-medium rounded-lg bg-neutral-900 text-white \
|
||||
px-4 py-2 text-sm" {
|
||||
"Resolve Report"
|
||||
"Resolve report"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -893,6 +946,36 @@ fn snapshot_guild_group(
|
||||
}
|
||||
}
|
||||
|
||||
fn notes_card(
|
||||
config: &AdminConfig,
|
||||
auth: &AuthContext,
|
||||
report: &ReportEntry,
|
||||
note_authors: &[AdminUser],
|
||||
csrf_token: &str,
|
||||
) -> Markup {
|
||||
let base = &config.base_path;
|
||||
let notes: Vec<StaffNote> = report
|
||||
.notes
|
||||
.iter()
|
||||
.map(|note| StaffNote {
|
||||
note_id: ¬e.note_id,
|
||||
author: user_link(base, note_authors, ¬e.author_id),
|
||||
body: ¬e.body,
|
||||
created_at: ¬e.created_at,
|
||||
})
|
||||
.collect();
|
||||
staff_notes_section(
|
||||
¬es,
|
||||
has_acl(auth, acl::REPORT_RESOLVE).then(|| StaffNoteForm {
|
||||
action: format!("{base}/reports/{}/notes", report.report_id),
|
||||
refresh_target: "#report-detail",
|
||||
field_id: format!("note-{}", report.report_id),
|
||||
help: "Staff only. Never shown to the reporter or the reported account. Notes cannot be changed and are deleted with the report.",
|
||||
csrf_token,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn legal_hold_card(
|
||||
config: &AdminConfig,
|
||||
auth: &AuthContext,
|
||||
@@ -990,7 +1073,7 @@ fn delete_report_card(
|
||||
(section_card(Some("Delete Report"), None, None, html! {
|
||||
div class="flex flex-col gap-3" data-report-delete=(if held { "held" } else { "available" }) {
|
||||
p class="text-neutral-500 text-sm" {
|
||||
"Deletes the report now, with its message context, profile snapshot, stored evidence and search entry. \
|
||||
"Deletes the report now, with its message context, profile snapshot, notes, stored evidence and search entry. \
|
||||
Evidence that another report uses is kept. This cannot be undone."
|
||||
}
|
||||
@if held {
|
||||
@@ -1035,8 +1118,7 @@ fn nav_link(href: &str, label: &str) -> Markup {
|
||||
fn message_context_section(
|
||||
config: &AdminConfig,
|
||||
report: &ReportEntry,
|
||||
include_delete: bool,
|
||||
csrf_token: Option<&str>,
|
||||
take_action: bool,
|
||||
) -> Markup {
|
||||
let Some(values) = &report.message_context else {
|
||||
return html! {};
|
||||
@@ -1058,14 +1140,19 @@ fn message_context_section(
|
||||
config,
|
||||
&config.base_path,
|
||||
&messages,
|
||||
include_delete,
|
||||
false,
|
||||
report.reported_message_id.as_deref(),
|
||||
take_action
|
||||
.then_some(report.reported_user_id.as_deref())
|
||||
.flatten()
|
||||
.map(|user_id| TakeAction {
|
||||
author_id: Some(user_id),
|
||||
report_id: Some(&report.report_id),
|
||||
})
|
||||
.as_ref(),
|
||||
))
|
||||
}
|
||||
}
|
||||
@if let Some(csrf_token) = csrf_token {
|
||||
(message_deletion_script(csrf_token))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1074,6 +1161,7 @@ pub fn report_detail_page(
|
||||
auth: &AuthContext,
|
||||
report: &ReportEntry,
|
||||
live: &LiveProfile,
|
||||
note_authors: &[AdminUser],
|
||||
csrf_token: &str,
|
||||
is_htmx: bool,
|
||||
) -> Markup {
|
||||
@@ -1092,12 +1180,13 @@ pub fn report_detail_page(
|
||||
(report_answers_section(report))
|
||||
(reported_entity_section(config, report))
|
||||
(profile_snapshot_section(config, report, live))
|
||||
(message_context_section(config, report, has_acl(auth, acl::MESSAGE_DELETE), Some(csrf_token)))
|
||||
(message_context_section(config, report, can_take_action(admin_acls(auth))))
|
||||
(additional_info_section(report))
|
||||
(notes_card(config, auth, report, note_authors, csrf_token))
|
||||
}
|
||||
div class="space-y-6" {
|
||||
(status_card(config, report))
|
||||
(actions_card(config, report, csrf_token))
|
||||
(actions_card(config, auth, report, csrf_token))
|
||||
(legal_hold_card(config, auth, report, csrf_token))
|
||||
(delete_report_card(config, auth, report, csrf_token))
|
||||
}
|
||||
@@ -1118,7 +1207,7 @@ pub fn report_detail_fragment(config: &AdminConfig, report: &ReportEntry) -> Mar
|
||||
(basic_info_section_fragment(config, report))
|
||||
(report_answers_section(report))
|
||||
(reported_entity_section(config, report))
|
||||
(message_context_section(config, report, false, None))
|
||||
(message_context_section(config, report, false))
|
||||
(additional_info_section(report))
|
||||
a href={(base) "/reports/" (&report.report_id)}
|
||||
class="inline-flex min-h-[44px] w-full items-center justify-center \
|
||||
|
||||
@@ -2,12 +2,17 @@
|
||||
|
||||
use crate::{
|
||||
acl,
|
||||
api::types::{
|
||||
AccountIdentityMode, AdminUser, PasswordResetLinkResponse, UserSession, WebAuthnCredential,
|
||||
api::{
|
||||
generated::types::EnforcementKindType,
|
||||
types::{
|
||||
AccountIdentityMode, AdminUser, PasswordResetLinkResponse, UserSession,
|
||||
WebAuthnCredential,
|
||||
},
|
||||
},
|
||||
config::AdminConfig,
|
||||
templates::components::{
|
||||
alert::{AlertVariant, alert},
|
||||
enforcement_fields::{BasisFields, DecisionFields, FIELD_HELP_CLASS, basis_fields},
|
||||
form::{checkbox, csrf_input, form_actions, submit_button},
|
||||
page_container::card_with_header,
|
||||
},
|
||||
@@ -286,7 +291,7 @@ fn clear_fields_card(base: &str, user: &AdminUser, csrf_token: &str) -> Markup {
|
||||
}
|
||||
let action_url = format!("{base}/users/{}?action=clear_fields&tab=account", user.id);
|
||||
html! {
|
||||
(card_with_header("Clear Profile Fields", html! {
|
||||
(card_with_header("Clear profile fields", html! {
|
||||
form method="post"
|
||||
action=(&action_url)
|
||||
hx-post=(&action_url)
|
||||
@@ -300,10 +305,31 @@ fn clear_fields_card(base: &str, user: &AdminUser, csrf_token: &str) -> Markup {
|
||||
@if user.banner.is_some() { (checkbox("fields[]", "banner", "Banner", false, true)) }
|
||||
@if user.bio.is_some() { (checkbox("fields[]", "bio", "Bio", false, true)) }
|
||||
@if user.pronouns.is_some() { (checkbox("fields[]", "pronouns", "Pronouns", false, true)) }
|
||||
@if user.global_name.is_some() { (checkbox("fields[]", "global_name", "Display Name", false, true)) }
|
||||
@if user.global_name.is_some() { (checkbox("fields[]", "global_name", "Display name", false, true)) }
|
||||
}
|
||||
(basis_fields(&BasisFields {
|
||||
decision: DecisionFields {
|
||||
id_prefix: "clear-fields",
|
||||
kind: EnforcementKindType::ProfileReset,
|
||||
report_ids: &[],
|
||||
messages: Some(&[]),
|
||||
user_flags: Some(user.flags),
|
||||
allow_restricted: true,
|
||||
},
|
||||
choices: &[
|
||||
("enforcement", "A rule was broken"),
|
||||
("holder_request", "The account holder asked"),
|
||||
("housekeeping", "Housekeeping"),
|
||||
],
|
||||
plain_note_help: "Staff only. Saved in the audit log.",
|
||||
enforcement_extra: html! {
|
||||
p class=(FIELD_HELP_CLASS) {
|
||||
"The current values are saved as evidence before they are cleared. The cleared fields are the facts, so evidence and a note to the user are optional."
|
||||
}
|
||||
},
|
||||
}))
|
||||
(form_actions(html! {
|
||||
(submit_button("Clear Selected Fields"))
|
||||
(submit_button("Clear selected fields"))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,47 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::admin_flags::DELETION_REASONS;
|
||||
use crate::admin_flags::{ENFORCEMENT_LIMITS, deletion_reason_label};
|
||||
use crate::utils::plural::{count_noun, noun_for};
|
||||
use crate::{
|
||||
acl,
|
||||
api::{
|
||||
client::{ApiError, ApiResult},
|
||||
generated::types::{
|
||||
EnforcementDecisionListResponse, EnforcementDecisionListResponseDecisionsItem,
|
||||
EnforcementKindType, NetworkBlockCandidatesResponse,
|
||||
},
|
||||
types::{AdminUser, AuditLogEntry, MessageShredStatusResponse},
|
||||
},
|
||||
config::AdminConfig,
|
||||
templates::components::{
|
||||
form::{
|
||||
checkbox, csrf_input, danger_button, form_actions, opt_out_checkbox, submit_button,
|
||||
templates::{
|
||||
components::{
|
||||
enforcement_fields::{
|
||||
BasisFields, DecisionFields, FIELD_HELP_CLASS, FIELD_INPUT_CLASS,
|
||||
FIELD_LABEL_CLASS, basis_fields, deletion_days_field, enforcement_fields,
|
||||
field_label, internal_note_field, network_block_fields, required_mark,
|
||||
},
|
||||
form::{checkbox, csrf_input, danger_button, form_actions, submit_button},
|
||||
page_container::card_with_header,
|
||||
},
|
||||
pages::enforcement_decision::{
|
||||
EndDecisionForm, can_end, can_lift, decisions_table, end_decision_form, ends_label,
|
||||
kind_label, suspension_label,
|
||||
},
|
||||
page_container::card_with_header,
|
||||
},
|
||||
utils::timestamps::format_admin_timestamp,
|
||||
};
|
||||
use maud::{Markup, html};
|
||||
|
||||
const TEMP_BAN_DURATIONS: &[(&str, &str)] = &[
|
||||
const REFRESH_TARGET: &str = "#user-tab-content";
|
||||
const REPORTS_LIMIT_DEFAULT_DAYS: i64 = 90;
|
||||
|
||||
const CONTENT_BASIS_CHOICES: &[(&str, &str)] = &[
|
||||
("enforcement", "A rule was broken"),
|
||||
("holder_request", "The account holder asked"),
|
||||
("housekeeping", "Housekeeping"),
|
||||
];
|
||||
|
||||
const SUSPENSION_DURATIONS: &[(&str, &str)] = &[
|
||||
("1", "1 hour"),
|
||||
("12", "12 hours"),
|
||||
("24", "1 day"),
|
||||
@@ -31,45 +53,36 @@ const TEMP_BAN_DURATIONS: &[(&str, &str)] = &[
|
||||
("0", "Permanent"),
|
||||
];
|
||||
|
||||
pub struct CurrentBan<'a> {
|
||||
pub struct LegacySuspension<'a> {
|
||||
pub entry: &'a AuditLogEntry,
|
||||
pub notes: Vec<&'a AuditLogEntry>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DecisionPrefill {
|
||||
pub report_ids: Vec<String>,
|
||||
pub messages: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ModerationContext<'a> {
|
||||
pub deletion_scheduler: Option<&'a AdminUser>,
|
||||
pub current_ban: Option<CurrentBan<'a>>,
|
||||
pub username_sign_in: bool,
|
||||
pub legacy_suspension: Option<LegacySuspension<'a>>,
|
||||
pub decisions: Option<&'a ApiResult<EnforcementDecisionListResponse>>,
|
||||
pub network_candidates: Option<&'a ApiResult<NetworkBlockCandidatesResponse>>,
|
||||
pub prefill: DecisionPrefill,
|
||||
}
|
||||
|
||||
pub fn find_current_ban<'a>(user: &AdminUser, logs: &'a [AuditLogEntry]) -> Option<CurrentBan<'a>> {
|
||||
pub fn find_legacy_suspension<'a>(
|
||||
user: &AdminUser,
|
||||
logs: &'a [AuditLogEntry],
|
||||
) -> Option<LegacySuspension<'a>> {
|
||||
let banned_until = user.temp_banned_until.as_deref()?;
|
||||
let entry = logs.iter().find(|log| {
|
||||
log.action == "temp_ban"
|
||||
&& log.target_id == user.id
|
||||
&& log.metadata.get("banned_until").map(String::as_str) == Some(banned_until)
|
||||
})?;
|
||||
let mut notes: Vec<&AuditLogEntry> = logs
|
||||
.iter()
|
||||
.filter(|log| {
|
||||
log.action == "annotate_ban"
|
||||
&& log.metadata.get("ban_audit_log_id") == Some(&entry.log_id)
|
||||
})
|
||||
.collect();
|
||||
notes.sort_by(|a, b| a.created_at.cmp(&b.created_at));
|
||||
Some(CurrentBan { entry, notes })
|
||||
}
|
||||
|
||||
fn deletion_reason_label(code: i32) -> String {
|
||||
let value = code.to_string();
|
||||
DELETION_REASONS
|
||||
.iter()
|
||||
.find(|(candidate, _)| *candidate == value)
|
||||
.map_or_else(
|
||||
|| format!("Reason {code}"),
|
||||
|(_, label)| (*label).to_owned(),
|
||||
)
|
||||
Some(LegacySuspension { entry })
|
||||
}
|
||||
|
||||
fn admin_name(entry: &AuditLogEntry) -> String {
|
||||
@@ -91,169 +104,256 @@ pub fn moderation_tab(
|
||||
delete_all_messages_dry_run: Option<(u64, u64)>,
|
||||
) -> Markup {
|
||||
let base = &config.base_path;
|
||||
let can_limit = !user.bot && acl::has_permission(admin_acls, acl::USER_UPDATE_FLAGS);
|
||||
let can_delete_all_messages = acl::has_permission(admin_acls, acl::MESSAGE_DELETE_ALL);
|
||||
let can_shred_messages = acl::has_permission(admin_acls, acl::MESSAGE_SHRED);
|
||||
let decisions = context
|
||||
.decisions
|
||||
.and_then(|result| result.as_ref().ok())
|
||||
.map(|response| response.decisions.as_slice())
|
||||
.unwrap_or_default();
|
||||
html! {
|
||||
div class="space-y-6" {
|
||||
(current_restriction_card(base, user, csrf_token, admin_acls, context, decisions))
|
||||
div class="grid grid-cols-1 gap-6 md:grid-cols-2" {
|
||||
(ban_actions_card(base, user, csrf_token, context.current_ban.as_ref(), context.username_sign_in))
|
||||
(deletion_card(base, user, csrf_token, context.deletion_scheduler, context.username_sign_in))
|
||||
@if acl::has_permission(admin_acls, acl::USER_TEMP_BAN) {
|
||||
(suspend_card(base, user, csrf_token, context))
|
||||
}
|
||||
@if acl::has_permission(admin_acls, acl::USER_DELETE) {
|
||||
(close_card(base, config.self_hosted, user, csrf_token, context))
|
||||
}
|
||||
@if can_limit {
|
||||
(limit_card(base, user, csrf_token, &context.prefill))
|
||||
}
|
||||
@if can_limit {
|
||||
(warn_card(base, user, csrf_token, &context.prefill))
|
||||
}
|
||||
}
|
||||
@if let Some(result) = context.decisions {
|
||||
(card_with_header("Decisions", html! {
|
||||
@match result {
|
||||
Ok(response) => { (decisions_table(base, &response.decisions)) }
|
||||
Err(_) => { p class="text-sm text-red-700" { "Failed to load decisions." } }
|
||||
}
|
||||
}))
|
||||
}
|
||||
@if can_delete_all_messages {
|
||||
(delete_all_messages_card(base, user, csrf_token, delete_all_messages_dry_run))
|
||||
(delete_all_messages_card(base, user, csrf_token, &context.prefill, delete_all_messages_dry_run))
|
||||
}
|
||||
@if can_shred_messages {
|
||||
(message_shred_card(base, user, csrf_token, message_shred_job_id, message_shred_status))
|
||||
(message_shred_card(base, user, csrf_token, &context.prefill, message_shred_job_id, message_shred_status))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_user_checkbox(username_sign_in: bool, label: &str) -> Markup {
|
||||
if username_sign_in {
|
||||
html! { input type="hidden" name="notify_user_present" value="1"; }
|
||||
} else {
|
||||
opt_out_checkbox("notify_user", label)
|
||||
}
|
||||
}
|
||||
|
||||
fn ban_actions_card(
|
||||
fn current_restriction_card(
|
||||
base: &str,
|
||||
user: &AdminUser,
|
||||
csrf_token: &str,
|
||||
current_ban: Option<&CurrentBan<'_>>,
|
||||
username_sign_in: bool,
|
||||
admin_acls: &[String],
|
||||
context: &ModerationContext<'_>,
|
||||
decisions: &[EnforcementDecisionListResponseDecisionsItem],
|
||||
) -> Markup {
|
||||
html! {
|
||||
(card_with_header("Ban Actions", html! {
|
||||
@if user.temp_banned_until.is_some() {
|
||||
(current_ban_details(base, user, csrf_token, current_ban))
|
||||
form method="post"
|
||||
action={(base) "/users/" (user.id) "?action=unban&tab=moderation"} {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-3" {
|
||||
(form_label("Public Reason (optional, shown to the user)"))
|
||||
input type="text" name="public_reason"
|
||||
placeholder="Enter public unban reason..." maxlength="512"
|
||||
class="block w-full rounded-md border border-neutral-300 \
|
||||
px-3 py-2 text-sm shadow-sm \
|
||||
focus:border-brand-primary focus:outline-none \
|
||||
focus:ring-1 focus:ring-brand-primary";
|
||||
(form_label("Private Reason (optional, audit log)"))
|
||||
input type="text" name="private_reason"
|
||||
placeholder="Enter private unban reason (audit log)..."
|
||||
class="block w-full rounded-md border border-neutral-300 \
|
||||
px-3 py-2 text-sm shadow-sm \
|
||||
focus:border-brand-primary focus:outline-none \
|
||||
focus:ring-1 focus:ring-brand-primary";
|
||||
(notify_user_checkbox(username_sign_in, "Email the user that the suspension was lifted"))
|
||||
(form_actions(html! {
|
||||
(submit_button("Unban User"))
|
||||
}))
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
form method="post"
|
||||
action={(base) "/users/" (user.id) "?action=temp_ban&tab=moderation"} {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-3" {
|
||||
(form_label("Duration"))
|
||||
select name="duration"
|
||||
class="block w-full rounded-md border border-neutral-300 \
|
||||
px-3 py-2 text-sm shadow-sm \
|
||||
focus:border-brand-primary focus:outline-none \
|
||||
focus:ring-1 focus:ring-brand-primary" {
|
||||
@for &(value, label) in TEMP_BAN_DURATIONS {
|
||||
option value=(value) { (label) }
|
||||
}
|
||||
}
|
||||
(form_label("Public Reason (optional)"))
|
||||
input type="text" name="reason"
|
||||
placeholder="Enter public ban reason..." maxlength="512"
|
||||
class="block w-full rounded-md border border-neutral-300 \
|
||||
px-3 py-2 text-sm shadow-sm \
|
||||
focus:border-brand-primary focus:outline-none \
|
||||
focus:ring-1 focus:ring-brand-primary";
|
||||
(form_label("Private Reason (optional)"))
|
||||
input type="text" name="private_reason"
|
||||
placeholder="Enter private ban reason (audit log)..."
|
||||
class="block w-full rounded-md border border-neutral-300 \
|
||||
px-3 py-2 text-sm shadow-sm \
|
||||
focus:border-brand-primary focus:outline-none \
|
||||
focus:ring-1 focus:ring-brand-primary";
|
||||
(notify_user_checkbox(username_sign_in, "Email the user about this suspension (temporary bans only)"))
|
||||
(form_actions(html! {
|
||||
(submit_button("Ban/Suspend User"))
|
||||
}))
|
||||
}
|
||||
(card_with_header("Current restriction", html! {
|
||||
div data-current-restriction="" {
|
||||
@if let Some(decision_id) = &user.enforcement_decision_id {
|
||||
(decision_restriction(base, user, csrf_token, admin_acls, decision_id, decisions))
|
||||
} @else if let Some(until) = &user.temp_banned_until {
|
||||
(legacy_suspension_restriction(base, user, csrf_token, admin_acls, until, context.legacy_suspension.as_ref()))
|
||||
} @else if let Some(pending) = &user.pending_deletion_at {
|
||||
(legacy_deletion_restriction(base, user, csrf_token, admin_acls, pending, context.deletion_scheduler))
|
||||
} @else {
|
||||
p class="text-sm text-neutral-500" { "No current restriction." }
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn current_ban_details(
|
||||
fn decision_restriction(
|
||||
base: &str,
|
||||
user: &AdminUser,
|
||||
csrf_token: &str,
|
||||
current_ban: Option<&CurrentBan<'_>>,
|
||||
admin_acls: &[String],
|
||||
decision_id: &str,
|
||||
decisions: &[EnforcementDecisionListResponseDecisionsItem],
|
||||
) -> Markup {
|
||||
let Some(ban) = current_ban else {
|
||||
let decision = decisions
|
||||
.iter()
|
||||
.find(|decision| decision.decision_id.as_str() == decision_id);
|
||||
let state = match (decision, user.temp_banned_until.as_deref()) {
|
||||
(Some(decision), _) => {
|
||||
let ends = ends_label(
|
||||
decision.kind,
|
||||
decision.ends_at.as_deref(),
|
||||
decision.deletion_at.as_deref(),
|
||||
);
|
||||
format!(
|
||||
"{} for {}. {ends}",
|
||||
kind_label(decision.kind),
|
||||
deletion_reason_label(*decision.reason_code)
|
||||
)
|
||||
}
|
||||
(None, Some(until)) => suspension_label(until),
|
||||
(None, None) => "Restricted by a decision.".to_owned(),
|
||||
};
|
||||
html! {
|
||||
div class="space-y-4" {
|
||||
p class="text-sm text-neutral-900" {
|
||||
(state.trim_end())
|
||||
" "
|
||||
a href={(base) "/decisions/" (decision_id)} class="text-blue-600 hover:underline" {
|
||||
"Decision " (decision_id)
|
||||
}
|
||||
}
|
||||
@match decision {
|
||||
Some(decision) if can_end(admin_acls, decision.kind, decision.removed_all_messages) => {
|
||||
(end_decision_form(&EndDecisionForm {
|
||||
base,
|
||||
decision_id,
|
||||
can_lift: can_lift(decision.kind),
|
||||
reason_code: *decision.reason_code,
|
||||
csrf_token,
|
||||
refresh_target: REFRESH_TARGET,
|
||||
}))
|
||||
}
|
||||
Some(_) => {}
|
||||
None => {
|
||||
p class="text-sm text-neutral-500" { "Open the decision to see or end it." }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn legacy_suspension_restriction(
|
||||
base: &str,
|
||||
user: &AdminUser,
|
||||
csrf_token: &str,
|
||||
admin_acls: &[String],
|
||||
until: &str,
|
||||
legacy_suspension: Option<&LegacySuspension<'_>>,
|
||||
) -> Markup {
|
||||
html! {
|
||||
div class="space-y-4" {
|
||||
p class="text-sm text-neutral-900" {
|
||||
(suspension_label(until)) ". Recorded before decisions, so it has no decision."
|
||||
}
|
||||
(legacy_suspension_details(base, legacy_suspension))
|
||||
@if acl::has_permission(admin_acls, acl::USER_TEMP_BAN) {
|
||||
form method="post"
|
||||
action={(base) "/users/" (user.id) "?action=unban&tab=moderation"}
|
||||
data-admin-refresh-on-success=(REFRESH_TARGET) {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-3" {
|
||||
(internal_note_field("lift", "Staff only. Saved in the audit log."))
|
||||
p class=(FIELD_HELP_CLASS) { "The user is told by email that the suspension was lifted." }
|
||||
(form_actions(submit_button("Lift suspension")))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn legacy_suspension_details(
|
||||
base: &str,
|
||||
legacy_suspension: Option<&LegacySuspension<'_>>,
|
||||
) -> Markup {
|
||||
let Some(suspension) = legacy_suspension else {
|
||||
return html! {
|
||||
p class="mb-4 text-sm text-neutral-500" {
|
||||
"The audit log entry of this ban could not be loaded, so notes cannot be added here."
|
||||
p class="text-sm text-neutral-500" {
|
||||
"The audit log entry of this suspension could not be loaded."
|
||||
}
|
||||
};
|
||||
};
|
||||
html! {
|
||||
div class="mb-4 space-y-3" {
|
||||
dl class="space-y-1 text-sm text-neutral-700" {
|
||||
div {
|
||||
dt class="inline font-medium" { "Banned by: " }
|
||||
dd class="inline" {
|
||||
a href={(base) "/users/" (ban.entry.admin_user_id)} class="underline" {
|
||||
(admin_name(ban.entry))
|
||||
}
|
||||
" on " (format_admin_timestamp(&ban.entry.created_at))
|
||||
dl class="space-y-1 text-sm text-neutral-700" {
|
||||
div {
|
||||
dt class="inline font-medium" { "Suspended by: " }
|
||||
dd class="inline" {
|
||||
a href={(base) "/users/" (suspension.entry.admin_user_id)} class="underline" {
|
||||
(admin_name(suspension.entry))
|
||||
}
|
||||
}
|
||||
div {
|
||||
dt class="inline font-medium" { "Reason: " }
|
||||
dd class="inline" { (ban.entry.audit_log_reason.as_deref().unwrap_or("None recorded")) }
|
||||
" on " (format_admin_timestamp(&suspension.entry.created_at))
|
||||
}
|
||||
}
|
||||
@if !ban.notes.is_empty() {
|
||||
ul class="space-y-1 text-sm text-neutral-700" {
|
||||
@for note in &ban.notes {
|
||||
li {
|
||||
span class="font-medium" { (admin_name(note)) }
|
||||
" (" (format_admin_timestamp(¬e.created_at)) "): "
|
||||
(note.audit_log_reason.as_deref().unwrap_or(""))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
form method="post"
|
||||
action={(base) "/users/" (user.id) "?action=annotate_ban&tab=moderation"} {
|
||||
(csrf_input(csrf_token))
|
||||
input type="hidden" name="ban_audit_log_id" value=(ban.entry.log_id);
|
||||
div class="space-y-3" {
|
||||
(form_label("Add a note to this ban"))
|
||||
textarea name="note" rows="2" required maxlength="512"
|
||||
placeholder="Appended to the ban. The original reason is kept."
|
||||
class="block w-full rounded-md border border-neutral-300 \
|
||||
px-3 py-2 text-sm shadow-sm \
|
||||
focus:border-brand-primary focus:outline-none \
|
||||
focus:ring-1 focus:ring-brand-primary" {}
|
||||
(form_actions(html! {
|
||||
(submit_button("Add Note"))
|
||||
}))
|
||||
}
|
||||
div {
|
||||
dt class="inline font-medium" { "Internal note: " }
|
||||
dd class="inline" { (suspension.entry.audit_log_reason.as_deref().unwrap_or("None recorded")) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn suspend_card(
|
||||
base: &str,
|
||||
user: &AdminUser,
|
||||
csrf_token: &str,
|
||||
context: &ModerationContext<'_>,
|
||||
) -> Markup {
|
||||
let prefill = &context.prefill;
|
||||
html! {
|
||||
(card_with_header("Suspend account", html! {
|
||||
form method="post"
|
||||
action={(base) "/users/" (user.id) "?action=suspend&tab=moderation"}
|
||||
data-admin-refresh-on-success=(REFRESH_TARGET) {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-4" {
|
||||
div class="space-y-1" {
|
||||
(field_label("suspend-duration", "Duration"))
|
||||
select id="suspend-duration" name="duration_hours" required class=(FIELD_INPUT_CLASS) {
|
||||
@for &(value, label) in SUSPENSION_DURATIONS {
|
||||
option value=(value) { (label) }
|
||||
}
|
||||
}
|
||||
}
|
||||
(enforcement_fields(&DecisionFields {
|
||||
id_prefix: "suspend",
|
||||
kind: EnforcementKindType::Suspension,
|
||||
report_ids: &prefill.report_ids,
|
||||
messages: Some(&prefill.messages),
|
||||
user_flags: Some(user.flags),
|
||||
allow_restricted: true,
|
||||
}))
|
||||
(remove_evidence_field())
|
||||
@if let Some(candidates) = context.network_candidates {
|
||||
fieldset data-show-for-duration_hours="0" hidden disabled {
|
||||
(network_block_fields("suspend", candidates))
|
||||
}
|
||||
}
|
||||
(form_actions(submit_button("Suspend account")))
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn warn_card(base: &str, user: &AdminUser, csrf_token: &str, prefill: &DecisionPrefill) -> Markup {
|
||||
html! {
|
||||
(card_with_header("Warn", html! {
|
||||
form method="post"
|
||||
action={(base) "/users/" (user.id) "?action=warn&tab=moderation"}
|
||||
data-admin-refresh-on-success=(REFRESH_TARGET) {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-4" {
|
||||
(enforcement_fields(&DecisionFields {
|
||||
id_prefix: "warn",
|
||||
kind: EnforcementKindType::Warning,
|
||||
report_ids: &prefill.report_ids,
|
||||
messages: Some(&prefill.messages),
|
||||
user_flags: Some(user.flags),
|
||||
allow_restricted: true,
|
||||
}))
|
||||
(form_actions(submit_button("Send warning")))
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_deletion_summary(
|
||||
base: &str,
|
||||
user: &AdminUser,
|
||||
@@ -274,7 +374,7 @@ fn pending_deletion_summary(
|
||||
.map(format_admin_timestamp)
|
||||
.unwrap_or_default();
|
||||
let markup = html! {
|
||||
dl class="mb-4 space-y-1 text-sm text-neutral-700" {
|
||||
dl class="space-y-1 text-sm text-neutral-700" {
|
||||
div {
|
||||
dt class="inline font-medium" { "Scheduled by: " }
|
||||
dd class="inline" {
|
||||
@@ -317,81 +417,159 @@ fn pending_deletion_summary(
|
||||
)
|
||||
}
|
||||
|
||||
fn deletion_card(
|
||||
fn legacy_deletion_restriction(
|
||||
base: &str,
|
||||
user: &AdminUser,
|
||||
csrf_token: &str,
|
||||
admin_acls: &[String],
|
||||
pending: &str,
|
||||
scheduler: Option<&AdminUser>,
|
||||
username_sign_in: bool,
|
||||
) -> Markup {
|
||||
let (confirmation, summary) = pending_deletion_summary(base, user, scheduler);
|
||||
html! {
|
||||
(card_with_header("Account Deletion", html! {
|
||||
@if let Some(pending) = &user.pending_deletion_at {
|
||||
@let (confirmation, summary) = pending_deletion_summary(base, user, scheduler);
|
||||
(summary)
|
||||
div class="space-y-4" {
|
||||
p class="text-sm text-neutral-900" {
|
||||
"Scheduled for deletion. No decision owns it, so it is cancelled here."
|
||||
}
|
||||
(summary)
|
||||
@if acl::has_permission(admin_acls, acl::USER_DELETE) {
|
||||
form method="post"
|
||||
action={(base) "/users/" (user.id) "?action=cancel_deletion&tab=moderation"} {
|
||||
action={(base) "/users/" (user.id) "?action=cancel_deletion&tab=moderation"}
|
||||
data-admin-refresh-on-success=(REFRESH_TARGET) {
|
||||
(csrf_input(csrf_token))
|
||||
input type="hidden" name="expected_pending_deletion_at" value=(pending);
|
||||
div class="space-y-3" {
|
||||
(form_label("Private Reason"))
|
||||
input type="text" name="private_reason" required
|
||||
placeholder="Why this deletion is being canceled (audit log)..."
|
||||
class="block w-full rounded-md border border-neutral-300 \
|
||||
px-3 py-2 text-sm shadow-sm \
|
||||
focus:border-brand-primary focus:outline-none \
|
||||
focus:ring-1 focus:ring-brand-primary";
|
||||
@if !username_sign_in {
|
||||
(checkbox("notify_user", "true", "Email the user that the deletion was canceled", false, true))
|
||||
}
|
||||
(internal_note_field("cancel-deletion", "Staff only. Saved in the audit log."))
|
||||
p class=(FIELD_HELP_CLASS) { "The user is told by email that the deletion was cancelled." }
|
||||
(checkbox("confirm", "true", &confirmation, false, true))
|
||||
(form_actions(html! {
|
||||
(danger_button("Cancel Deletion"))
|
||||
}))
|
||||
(form_actions(danger_button("Cancel deletion")))
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
form method="post"
|
||||
action={(base) "/users/" (user.id) "?action=schedule_deletion&tab=moderation"} {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-3" {
|
||||
(form_label("Days until deletion"))
|
||||
input type="number" name="days_until_deletion" value="60"
|
||||
min="60" max="365"
|
||||
class="block w-full rounded-md border border-neutral-300 \
|
||||
px-3 py-2 text-sm shadow-sm \
|
||||
focus:border-brand-primary focus:outline-none \
|
||||
focus:ring-1 focus:ring-brand-primary";
|
||||
(form_label("Reason"))
|
||||
select name="reason_code" required
|
||||
class="block w-full rounded-md border border-neutral-300 \
|
||||
px-3 py-2 text-sm shadow-sm \
|
||||
focus:border-brand-primary focus:outline-none \
|
||||
focus:ring-1 focus:ring-brand-primary" {
|
||||
option value="" disabled selected { "Choose a reason" }
|
||||
@for &(value, label) in DELETION_REASONS {
|
||||
option value=(value) { (label) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn close_card(
|
||||
base: &str,
|
||||
self_hosted: bool,
|
||||
user: &AdminUser,
|
||||
csrf_token: &str,
|
||||
context: &ModerationContext<'_>,
|
||||
) -> Markup {
|
||||
let prefill = &context.prefill;
|
||||
html! {
|
||||
(card_with_header("Close account", html! {
|
||||
form method="post"
|
||||
action={(base) "/users/" (user.id) "?action=close&tab=moderation"}
|
||||
data-admin-refresh-on-success=(REFRESH_TARGET) {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-4" {
|
||||
(basis_fields(&BasisFields {
|
||||
decision: DecisionFields {
|
||||
id_prefix: "close",
|
||||
kind: EnforcementKindType::Closure,
|
||||
report_ids: &prefill.report_ids,
|
||||
messages: Some(&prefill.messages),
|
||||
user_flags: Some(user.flags),
|
||||
allow_restricted: true,
|
||||
},
|
||||
choices: &[
|
||||
("enforcement", "A rule was broken"),
|
||||
("holder_request", "The account holder asked"),
|
||||
("housekeeping", "Inactivity"),
|
||||
],
|
||||
plain_note_help: "Staff only. Saved in the audit log. The holder can cancel the deletion by signing in.",
|
||||
enforcement_extra: html! {
|
||||
(remove_evidence_field())
|
||||
@if let Some(candidates) = context.network_candidates {
|
||||
(network_block_fields("close", candidates))
|
||||
}
|
||||
},
|
||||
}))
|
||||
(deletion_days_field("close-days", self_hosted))
|
||||
@if let Some(pending) = &user.pending_deletion_at {
|
||||
input type="hidden" name="replace_pending_deletion_at" value=(pending);
|
||||
p class=(FIELD_HELP_CLASS) { "This replaces the deletion already scheduled." }
|
||||
}
|
||||
(form_actions(danger_button("Close account")))
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_evidence_field() -> Markup {
|
||||
html! {
|
||||
div class="space-y-1" {
|
||||
(checkbox("remove_evidence", "true", "Remove these messages", false, true))
|
||||
p class=(FIELD_HELP_CLASS) { "Removes the messages listed above after they are saved as evidence." }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn content_basis_fields<'a>(
|
||||
id_prefix: &'a str,
|
||||
user: &'a AdminUser,
|
||||
prefill: &'a DecisionPrefill,
|
||||
snapshot_entries: bool,
|
||||
) -> BasisFields<'a> {
|
||||
BasisFields {
|
||||
decision: DecisionFields {
|
||||
id_prefix,
|
||||
kind: EnforcementKindType::ContentRemoval,
|
||||
report_ids: &prefill.report_ids,
|
||||
messages: (!snapshot_entries).then_some(prefill.messages.as_slice()),
|
||||
user_flags: Some(user.flags),
|
||||
allow_restricted: snapshot_entries,
|
||||
},
|
||||
choices: CONTENT_BASIS_CHOICES,
|
||||
plain_note_help: "Staff only. Saved in the audit log.",
|
||||
enforcement_extra: html! {},
|
||||
}
|
||||
}
|
||||
|
||||
fn limit_card(base: &str, user: &AdminUser, csrf_token: &str, prefill: &DecisionPrefill) -> Markup {
|
||||
let reports_end = (time::OffsetDateTime::now_utc()
|
||||
+ time::Duration::days(REPORTS_LIMIT_DEFAULT_DAYS))
|
||||
.date()
|
||||
.to_string();
|
||||
html! {
|
||||
(card_with_header("Limit features", html! {
|
||||
form method="post"
|
||||
action={(base) "/users/" (user.id) "?action=limit_features&tab=moderation"}
|
||||
data-admin-refresh-on-success=(REFRESH_TARGET) {
|
||||
(csrf_input(csrf_token))
|
||||
div class="space-y-4" {
|
||||
(enforcement_fields(&DecisionFields {
|
||||
id_prefix: "limit",
|
||||
kind: EnforcementKindType::FeatureLimit,
|
||||
report_ids: &prefill.report_ids,
|
||||
messages: Some(&prefill.messages),
|
||||
user_flags: Some(user.flags),
|
||||
allow_restricted: true,
|
||||
}))
|
||||
fieldset class="space-y-1" {
|
||||
legend class=(FIELD_LABEL_CLASS) { "Limit" (required_mark()) }
|
||||
div class="flex flex-wrap gap-x-4 gap-y-1" {
|
||||
@for limit in ENFORCEMENT_LIMITS {
|
||||
@let id = format!("limit-{}", limit.value);
|
||||
@let codes = limit.codes.iter().map(ToString::to_string).collect::<Vec<_>>().join(" ");
|
||||
div class="flex items-center gap-2" data-show-for-codes=(codes) hidden {
|
||||
input type="radio" id=(id) name="limit" value=(limit.value) required disabled
|
||||
class="h-4 w-4 accent-neutral-900";
|
||||
label for=(id) class="text-sm text-neutral-900" { (limit.label) }
|
||||
}
|
||||
}
|
||||
}
|
||||
(form_label("Public Reason (optional)"))
|
||||
input type="text" name="public_reason"
|
||||
placeholder="Enter public reason..." maxlength="512"
|
||||
class="block w-full rounded-md border border-neutral-300 \
|
||||
px-3 py-2 text-sm shadow-sm \
|
||||
focus:border-brand-primary focus:outline-none \
|
||||
focus:ring-1 focus:ring-brand-primary";
|
||||
(form_label("Private Reason (optional)"))
|
||||
input type="text" name="private_reason"
|
||||
placeholder="Enter private reason (audit log)..."
|
||||
class="block w-full rounded-md border border-neutral-300 \
|
||||
px-3 py-2 text-sm shadow-sm \
|
||||
focus:border-brand-primary focus:outline-none \
|
||||
focus:ring-1 focus:ring-brand-primary";
|
||||
(notify_user_checkbox(username_sign_in, "Email the user about the scheduled deletion"))
|
||||
(form_actions(html! {
|
||||
(submit_button("Schedule Deletion"))
|
||||
}))
|
||||
p class=(FIELD_HELP_CLASS) { "Each reason offers only the limits it allows." }
|
||||
}
|
||||
fieldset class="space-y-1" data-show-for-limit="reports" hidden disabled {
|
||||
(field_label("limit-ends-on", "Ends on"))
|
||||
input type="date" id="limit-ends-on" name="ends_on" value=(reports_end) required class=(FIELD_INPUT_CLASS);
|
||||
p class=(FIELD_HELP_CLASS) { "At most 365 days from now." }
|
||||
}
|
||||
(form_actions(submit_button("Limit features")))
|
||||
}
|
||||
}
|
||||
}))
|
||||
@@ -402,36 +580,34 @@ fn delete_all_messages_card(
|
||||
base: &str,
|
||||
user: &AdminUser,
|
||||
csrf_token: &str,
|
||||
prefill: &DecisionPrefill,
|
||||
dry_run: Option<(u64, u64)>,
|
||||
) -> Markup {
|
||||
let action = format!(
|
||||
"{base}/users/{}?action=delete_all_messages&tab=moderation",
|
||||
user.id
|
||||
);
|
||||
html! {
|
||||
(card_with_header("Delete All Messages", html! {
|
||||
(card_with_header("Delete all messages", html! {
|
||||
div class="space-y-4" {
|
||||
p class="text-sm text-neutral-600" {
|
||||
"Locate every message this user has ever sent and permanently remove them. First run a dry run to see how many channels and messages will be affected."
|
||||
"Removes every message this user has sent. Preview the count first. Child safety and violent extremism reasons can't be used here, because the messages can't all be saved as evidence first. Use the message shredder with the messages instead."
|
||||
}
|
||||
form method="post"
|
||||
action={(base) "/users/" (user.id) "?action=delete_all_messages&tab=moderation"}
|
||||
data-admin-result-form="true" {
|
||||
form method="post" action=(action) data-admin-result-form="true" {
|
||||
(csrf_input(csrf_token))
|
||||
input type="hidden" name="dry_run" value="true";
|
||||
(form_actions(html! {
|
||||
(submit_button("Preview Deletion"))
|
||||
}))
|
||||
(form_actions(submit_button("Preview deletion")))
|
||||
}
|
||||
@if let Some((channel_count, message_count)) = dry_run {
|
||||
div class="space-y-3 rounded-lg border border-neutral-200 bg-neutral-50 p-4" {
|
||||
form method="post" action=(action)
|
||||
class="space-y-4 rounded-lg border border-neutral-200 bg-neutral-50 p-4" {
|
||||
(csrf_input(csrf_token))
|
||||
input type="hidden" name="dry_run" value="false";
|
||||
p class="text-sm text-neutral-700" {
|
||||
"Channels: " (channel_count) " · Messages: " (message_count)
|
||||
}
|
||||
form method="post"
|
||||
action={(base) "/users/" (user.id) "?action=delete_all_messages&tab=moderation"} {
|
||||
(csrf_input(csrf_token))
|
||||
input type="hidden" name="dry_run" value="false";
|
||||
(form_actions(html! {
|
||||
(danger_button("Delete All Messages"))
|
||||
}))
|
||||
(count_noun(message_count, "message", "messages")) " in " (count_noun(channel_count, "channel", "channels")) "."
|
||||
}
|
||||
(basis_fields(&content_basis_fields("delete-all", user, prefill, false)))
|
||||
(form_actions(danger_button("Delete all messages")))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -443,23 +619,24 @@ fn message_shred_card(
|
||||
base: &str,
|
||||
user: &AdminUser,
|
||||
csrf_token: &str,
|
||||
prefill: &DecisionPrefill,
|
||||
job_id: Option<&str>,
|
||||
status: Option<&ApiResult<MessageShredStatusResponse>>,
|
||||
) -> Markup {
|
||||
html! {
|
||||
(card_with_header("Message Shredder", html! {
|
||||
(card_with_header("Message shredder", html! {
|
||||
div class="space-y-4" {
|
||||
p class="text-sm text-neutral-600" {
|
||||
"Upload a CSV file where each row includes the channel_id and message_id separated by a comma. Large files are chunked server-side automatically."
|
||||
"Upload a CSV file with one channel_id,message_id pair per row. When a rule was broken, the first 50 messages are saved as evidence before they are removed, and every message for child safety and violent extremism reasons."
|
||||
}
|
||||
form method="post"
|
||||
action={(base) "/users/" (user.id) "?action=message_shred&tab=moderation"}
|
||||
id="message-shred-form" {
|
||||
(csrf_input(csrf_token))
|
||||
input type="hidden" name="csv_data" id="message-shred-csv-data";
|
||||
div class="space-y-3" {
|
||||
div class="space-y-4" {
|
||||
div class="space-y-2" {
|
||||
(form_label("CSV File"))
|
||||
(field_label("message-shred-file", "CSV file"))
|
||||
input id="message-shred-file" type="file" accept=".csv" class="hidden";
|
||||
div class="flex items-center gap-2" {
|
||||
button type="button"
|
||||
@@ -472,10 +649,11 @@ fn message_shred_card(
|
||||
}
|
||||
}
|
||||
}
|
||||
(basis_fields(&content_basis_fields("shred", user, prefill, true)))
|
||||
button type="submit"
|
||||
id="message-shred-submit"
|
||||
class="inline-flex w-full items-center justify-center gap-2 rounded-lg bg-neutral-900 px-4 py-2 text-base font-medium text-white transition-all duration-150 hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-2 focus:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50" {
|
||||
span { "Shred Messages" }
|
||||
span { "Shred messages" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -498,7 +676,7 @@ fn message_shred_status_section(
|
||||
html! {
|
||||
div class="space-y-3 rounded-lg border border-neutral-200 bg-white p-4" {
|
||||
div class="flex items-center justify-between gap-3" {
|
||||
h3 class="text-sm font-medium text-neutral-900" { "Message Shred Status" }
|
||||
h3 class="text-sm font-medium text-neutral-900" { "Message shred status" }
|
||||
a href={(base) "/users/" (user.id) "?tab=moderation"}
|
||||
class="text-sm text-neutral-500 hover:text-neutral-900" {
|
||||
"Clear"
|
||||
@@ -623,12 +801,6 @@ fn message_shred_form_script() -> Markup {
|
||||
}
|
||||
}
|
||||
|
||||
fn form_label(text: &str) -> Markup {
|
||||
maud::html! {
|
||||
label class="block text-sm font-medium text-neutral-700" { (text) }
|
||||
}
|
||||
}
|
||||
|
||||
const MESSAGE_SHRED_FORM_SCRIPT: &str = r#"
|
||||
(function () {
|
||||
var form = document.getElementById('message-shred-form');
|
||||
@@ -662,7 +834,7 @@ const MESSAGE_SHRED_FORM_SCRIPT: &str = r#"
|
||||
function resetSubmit(label) {
|
||||
processing = false;
|
||||
submitButton.disabled = false;
|
||||
submitButton.querySelector('span').textContent = label || 'Shred Messages';
|
||||
submitButton.querySelector('span').textContent = label || 'Shred messages';
|
||||
}
|
||||
file.addEventListener('change', function () {
|
||||
if (fileName) {
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::{
|
||||
form::{checkbox, csrf_input, form_actions, submit_button},
|
||||
page_container::{card_with_header, detail_row},
|
||||
},
|
||||
templates::pages::enforcement_decision::suspension_label,
|
||||
utils::{
|
||||
bigint::format_discriminator,
|
||||
timestamps::{format_admin_timestamp, snowflake_creation_date},
|
||||
@@ -64,12 +65,8 @@ fn render_overview_tab(
|
||||
) -> Markup {
|
||||
html! {
|
||||
div class="space-y-6" {
|
||||
@if let Some(until) = &user.temp_banned_until {
|
||||
div class="rounded-lg border border-red-200 bg-red-50 p-4 text-sm font-medium text-red-900" {
|
||||
"Temporarily banned until " (format_admin_timestamp(until))
|
||||
}
|
||||
}
|
||||
@if user.temp_banned_until.is_none() {
|
||||
(restriction_banner(config, user))
|
||||
@if user.temp_banned_until.is_none() && user.enforcement_decision_id.is_none() {
|
||||
@if let Some(pending) = &user.pending_deletion_at {
|
||||
div class="rounded-lg border border-orange-200 bg-orange-50 p-4 text-sm text-orange-900" {
|
||||
div class="font-medium" { "Scheduled for deletion: " (format_admin_timestamp(pending)) }
|
||||
@@ -262,6 +259,30 @@ fn render_overview_tab(
|
||||
}
|
||||
}
|
||||
|
||||
fn restriction_banner(config: &AdminConfig, user: &AdminUser) -> Markup {
|
||||
let base = &config.base_path;
|
||||
html! {
|
||||
@match (&user.enforcement_decision_id, &user.temp_banned_until) {
|
||||
(Some(decision_id), until) => {
|
||||
div class="rounded-lg border border-red-200 bg-red-50 p-4 text-sm font-medium text-red-900" data-restriction-banner="" {
|
||||
@match (until, &user.pending_deletion_at) {
|
||||
(Some(until), _) => { (suspension_label(until)) ". " }
|
||||
(None, Some(pending)) => { "Scheduled for deletion on " (format_admin_timestamp(pending)) ". " }
|
||||
(None, None) => { "Restricted by a decision. " }
|
||||
}
|
||||
a href={(base) "/decisions/" (decision_id)} class="underline" { "Decision " (decision_id) }
|
||||
}
|
||||
}
|
||||
(None, Some(until)) => {
|
||||
div class="rounded-lg border border-red-200 bg-red-50 p-4 text-sm font-medium text-red-900" data-restriction-banner="" {
|
||||
(suspension_label(until)) ". Recorded before decisions, so it has no decision."
|
||||
}
|
||||
}
|
||||
(None, None) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flags_card(
|
||||
config: &AdminConfig,
|
||||
user: &AdminUser,
|
||||
@@ -269,118 +290,58 @@ fn flags_card(
|
||||
csrf_token: &str,
|
||||
) -> Markup {
|
||||
let can_update_flags = acl::has_permission(admin_acls, acl::USER_UPDATE_FLAGS);
|
||||
let suspended = user.temp_banned_until.is_some();
|
||||
html! {
|
||||
div class="space-y-6" {
|
||||
(u64_flag_form(
|
||||
(flag_form_shell(
|
||||
config,
|
||||
&user.id,
|
||||
"User Flags",
|
||||
"User flags",
|
||||
"update_flags",
|
||||
"flags[]",
|
||||
user.flags,
|
||||
admin_flags::USER_FLAGS,
|
||||
csrf_token,
|
||||
can_update_flags,
|
||||
Some(acl::USER_UPDATE_FLAGS),
|
||||
html! {
|
||||
@for flag in admin_flags::USER_FLAGS {
|
||||
(checkbox(
|
||||
"flags[]",
|
||||
&flag.value.to_string(),
|
||||
flag.name,
|
||||
user.flags & flag.value != 0,
|
||||
can_update_flags && admin_flags::user_flag_is_editable(flag.value, user.flags, suspended),
|
||||
))
|
||||
}
|
||||
},
|
||||
))
|
||||
(i32_flag_form(
|
||||
(flag_form_shell(
|
||||
config,
|
||||
&user.id,
|
||||
"Premium Flags",
|
||||
"Premium flags",
|
||||
"update_premium_flags",
|
||||
"flags[]",
|
||||
user.premium_flags,
|
||||
admin_flags::PREMIUM_FLAGS,
|
||||
csrf_token,
|
||||
can_update_flags,
|
||||
Some(acl::USER_UPDATE_FLAGS),
|
||||
html! {
|
||||
@for flag in admin_flags::PREMIUM_FLAGS {
|
||||
(checkbox(
|
||||
"flags[]",
|
||||
&flag.value.to_string(),
|
||||
flag.name,
|
||||
user.premium_flags & flag.value != 0,
|
||||
can_update_flags && admin_flags::premium_flag_is_editable(flag.value),
|
||||
))
|
||||
}
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn u64_flag_form(
|
||||
config: &AdminConfig,
|
||||
user_id: &str,
|
||||
title: &str,
|
||||
action: &str,
|
||||
input_name: &str,
|
||||
value: u64,
|
||||
flags: &[admin_flags::U64Flag],
|
||||
csrf_token: &str,
|
||||
can_edit: bool,
|
||||
required_acl: Option<&str>,
|
||||
) -> Markup {
|
||||
flag_form_shell(
|
||||
config,
|
||||
user_id,
|
||||
title,
|
||||
action,
|
||||
value.to_string(),
|
||||
csrf_token,
|
||||
can_edit,
|
||||
required_acl,
|
||||
html! {
|
||||
@for flag in flags {
|
||||
(flag_checkbox(
|
||||
input_name,
|
||||
flag.value.to_string(),
|
||||
flag.name,
|
||||
value & flag.value != 0,
|
||||
can_edit,
|
||||
))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn i32_flag_form(
|
||||
config: &AdminConfig,
|
||||
user_id: &str,
|
||||
title: &str,
|
||||
action: &str,
|
||||
input_name: &str,
|
||||
value: i32,
|
||||
flags: &[admin_flags::I32Flag],
|
||||
csrf_token: &str,
|
||||
can_edit: bool,
|
||||
required_acl: Option<&str>,
|
||||
) -> Markup {
|
||||
flag_form_shell(
|
||||
config,
|
||||
user_id,
|
||||
title,
|
||||
action,
|
||||
value.to_string(),
|
||||
csrf_token,
|
||||
can_edit,
|
||||
required_acl,
|
||||
html! {
|
||||
@for flag in flags {
|
||||
(flag_checkbox(
|
||||
input_name,
|
||||
flag.value.to_string(),
|
||||
flag.name,
|
||||
value & flag.value != 0,
|
||||
can_edit,
|
||||
))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn flag_form_shell(
|
||||
config: &AdminConfig,
|
||||
user_id: &str,
|
||||
title: &str,
|
||||
action: &str,
|
||||
_raw_value: String,
|
||||
csrf_token: &str,
|
||||
can_edit: bool,
|
||||
required_acl: Option<&str>,
|
||||
checkboxes: Markup,
|
||||
) -> Markup {
|
||||
card_with_header(
|
||||
@@ -392,14 +353,15 @@ fn flag_form_shell(
|
||||
(checkboxes)
|
||||
}
|
||||
@if can_edit {
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Restriction flags are read-only. Use the moderation tab."
|
||||
}
|
||||
(form_actions(html! {
|
||||
(submit_button("Save"))
|
||||
}))
|
||||
} @else {
|
||||
@if let Some(required) = required_acl {
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Read-only \u{2014} " (required) " permission required."
|
||||
}
|
||||
p class="text-xs text-neutral-500" {
|
||||
"Read-only. Needs the " (acl::USER_UPDATE_FLAGS) " permission."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,10 +105,6 @@ impl MultiValueForm {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn opt_out_value(&self, key: &str) -> bool {
|
||||
!self.contains_key(&format!("{key}_present")) || self.bool_value(key)
|
||||
}
|
||||
|
||||
pub fn list_values(&self, key: &str) -> Vec<String> {
|
||||
self.fields
|
||||
.get(key)
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -140,6 +140,128 @@ async fn report_legal_hold_form_places_and_clears_the_hold() {
|
||||
assert_eq!(received.lock().unwrap().len(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn report_notes_show_the_author_and_time_and_the_form_adds_one() {
|
||||
let received = Arc::new(Mutex::new(Vec::<Value>::new()));
|
||||
let sink = received.clone();
|
||||
let api = Router::new()
|
||||
.route(
|
||||
"/admin/reports/1800000000000000001",
|
||||
routing::get(|| async { json_response(report_with_note()) }),
|
||||
)
|
||||
.route(
|
||||
"/admin/reports/1800000000000000001/notes",
|
||||
routing::post(move |Json(body): Json<Value>| {
|
||||
let sink = sink.clone();
|
||||
async move {
|
||||
sink.lock().unwrap().push(body.clone());
|
||||
json_response(json!({
|
||||
"note_id": "1950000000000000002",
|
||||
"author_id": "1500000000000000000",
|
||||
"body": body["body"],
|
||||
"created_at": "2026-05-27T08:00:00.000Z"
|
||||
}))
|
||||
}
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/admin/users",
|
||||
routing::get(|| async {
|
||||
json_response(json!({ "users": [user("1500000000000000000", "AdminUser")] }))
|
||||
}),
|
||||
)
|
||||
.fallback(mock_api);
|
||||
let app = setup_with_api(api).await;
|
||||
let (headers, page) = get_with_headers(&app, "/reports/1800000000000000001", &[]).await;
|
||||
let note_start = page
|
||||
.find(r#"data-note="1950000000000000001""#)
|
||||
.unwrap_or_else(|| panic!("missing note\n{page}"));
|
||||
let note = &page[note_start..page[note_start..].find("</li>").unwrap() + note_start];
|
||||
assert!(
|
||||
note.contains(r#"href="/users/1500000000000000000""#),
|
||||
"{note}"
|
||||
);
|
||||
assert!(note.contains("AdminUser"), "{note}");
|
||||
assert!(note.contains("May 26, 2026, 1:30 PM UTC"), "{note}");
|
||||
assert!(
|
||||
note.contains("Reporter sent more context by email"),
|
||||
"{note}"
|
||||
);
|
||||
let csrf_token = csrf_cookie(&headers)
|
||||
.unwrap_or_else(|| panic!("report page did not set csrf_token cookie\n{page}"));
|
||||
assert_form_has_csrf(&page, "/reports/1800000000000000001/notes", &csrf_token);
|
||||
let cookie = format!("{}; csrf_token={}", app.session_cookie, csrf_token);
|
||||
let post = |body: String| {
|
||||
let app = &app;
|
||||
let cookie = cookie.clone();
|
||||
async move {
|
||||
let (status, headers, text) = post_form_with_headers(
|
||||
app,
|
||||
"/reports/1800000000000000001/notes",
|
||||
&[
|
||||
("HX-Request", "true"),
|
||||
("HX-Target", "flash-container"),
|
||||
("Cookie", &cookie),
|
||||
],
|
||||
&body,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::NO_CONTENT, "{text}");
|
||||
headers
|
||||
.get("X-Fluxer-Admin-Toast")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or_else(|| panic!("missing toast header\n{text}"))
|
||||
.to_owned()
|
||||
}
|
||||
};
|
||||
|
||||
let toast = post(format!(
|
||||
"_csrf={csrf_token}&body=++Waiting+on+the+guild+owner++"
|
||||
))
|
||||
.await;
|
||||
assert!(toast.contains("success"), "{toast}");
|
||||
assert!(toast.contains("Note added"), "{toast}");
|
||||
let toast = post(format!("_csrf={csrf_token}&body=+")).await;
|
||||
assert!(toast.contains("error"), "{toast}");
|
||||
assert!(toast.contains("Write a note first"), "{toast}");
|
||||
assert_eq!(
|
||||
*received.lock().unwrap(),
|
||||
vec![json!({"body": "Waiting on the guild owner"})]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn report_notes_hide_the_form_without_report_resolve() {
|
||||
let api = Router::new()
|
||||
.route(
|
||||
"/admin/users/@me",
|
||||
routing::get(|| async {
|
||||
let mut admin = user("1500000000000000000", "AdminUser");
|
||||
admin["acls"] = json!(["report:view"]);
|
||||
json_response(json!({ "user": admin }))
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/admin/reports/1800000000000000001",
|
||||
routing::get(|| async { json_response(report_with_note()) }),
|
||||
)
|
||||
.fallback(mock_api);
|
||||
let app = setup_with_api(api).await;
|
||||
let (_, page) = get_with_headers(&app, "/reports/1800000000000000001", &[]).await;
|
||||
assert!(
|
||||
page.contains("Reporter sent more context by email"),
|
||||
"{page}"
|
||||
);
|
||||
assert!(
|
||||
page.contains(r#"href="/users/1500000000000000000""#),
|
||||
"{page}"
|
||||
);
|
||||
assert!(
|
||||
!page.contains(r#"action="/reports/1800000000000000001/notes""#),
|
||||
"{page}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn report_delete_form_confirms_sends_the_reason_and_toasts_a_held_report() {
|
||||
let received = Arc::new(Mutex::new(Vec::<Option<String>>::new()));
|
||||
@@ -746,6 +868,17 @@ fn searched_report() -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
fn report_with_note() -> Value {
|
||||
let mut report = searched_report();
|
||||
report["notes"] = json!([{
|
||||
"note_id": "1950000000000000001",
|
||||
"author_id": "1500000000000000000",
|
||||
"body": "Reporter sent more context by email",
|
||||
"created_at": "2026-05-26T13:30:00.000Z"
|
||||
}]);
|
||||
report
|
||||
}
|
||||
|
||||
fn report_reasons() -> Value {
|
||||
json!({
|
||||
"reasons": [
|
||||
|
||||
@@ -43,87 +43,119 @@ async fn submit(uri: &str, fields: &str) -> CapturedRequest {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn temp_ban_sends_notify_user_from_the_checkbox() {
|
||||
let uri = format!("/users/{USER_ID}?action=temp_ban&tab=moderation");
|
||||
let checked = submit(
|
||||
&uri,
|
||||
"duration=24&reason=Spam¬ify_user_present=1¬ify_user=true",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(checked.route, format!("PUT /admin/users/{USER_ID}/ban"));
|
||||
assert_eq!(checked.body["notify_user"], json!(true));
|
||||
let unchecked = submit(&uri, "duration=24&reason=Spam¬ify_user_present=1").await;
|
||||
assert_eq!(unchecked.body["notify_user"], json!(false));
|
||||
let stale_form = submit(&uri, "duration=24&reason=Spam").await;
|
||||
assert_eq!(stale_form.body["notify_user"], json!(true));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unban_sends_the_public_reason_in_the_body_and_the_private_reason_as_a_header() {
|
||||
async fn unban_sends_the_internal_note_as_a_header_and_no_body() {
|
||||
let uri = format!("/users/{USER_ID}?action=unban&tab=moderation");
|
||||
let checked = submit(
|
||||
&uri,
|
||||
"public_reason=Appeal%20accepted&private_reason=Private%20staff%20note¬ify_user_present=1¬ify_user=true",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(checked.route, format!("DELETE /admin/users/{USER_ID}/ban"));
|
||||
assert_eq!(checked.body["notify_user"], json!(true));
|
||||
assert_eq!(checked.body["public_reason"], json!("Appeal accepted"));
|
||||
let captured = submit(&uri, "internal_note=Private%20staff%20note").await;
|
||||
assert_eq!(captured.route, format!("DELETE /admin/users/{USER_ID}/ban"));
|
||||
assert_eq!(
|
||||
checked.audit_log_reason.as_deref(),
|
||||
captured.audit_log_reason.as_deref(),
|
||||
Some("Private staff note")
|
||||
);
|
||||
assert!(!checked.body.to_string().contains("Private staff note"));
|
||||
let unchecked = submit(
|
||||
assert!(!captured.body.to_string().contains("Private staff note"));
|
||||
}
|
||||
|
||||
const DECISION_FIELDS: &str = "reason_code=3&source=own_review&automation=none&statement=You%20sent%20the%20same%20link%20to%20many%20people.";
|
||||
|
||||
#[tokio::test]
|
||||
async fn close_sends_the_basis_and_never_a_notify_flag() {
|
||||
let uri = format!("/users/{USER_ID}?action=close&tab=moderation");
|
||||
let holder = submit(
|
||||
&uri,
|
||||
"private_reason=Private%20staff%20note¬ify_user_present=1",
|
||||
"basis=holder_request&days_until_deletion=30&internal_note=Asked%20by%20email",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(unchecked.body["notify_user"], json!(false));
|
||||
assert!(!unchecked.body.to_string().contains("Private staff note"));
|
||||
let stale_form = submit(&uri, "").await;
|
||||
assert_eq!(stale_form.body["notify_user"], json!(true));
|
||||
assert_eq!(holder.route, format!("PUT /admin/users/{USER_ID}/deletion"));
|
||||
assert_eq!(
|
||||
holder.body,
|
||||
json!({"basis": {"type": "holder_request"}, "days_until_deletion": 30})
|
||||
);
|
||||
assert_eq!(holder.audit_log_reason.as_deref(), Some("Asked by email"));
|
||||
let enforcement = submit(
|
||||
&uri,
|
||||
&format!(
|
||||
"basis=enforcement&days_until_deletion=74&{DECISION_FIELDS}&internal_note=Spam%20review&remove_evidence=true"
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(enforcement.body["basis"]["type"], json!("enforcement"));
|
||||
assert_eq!(
|
||||
enforcement.body["basis"]["decision"]["reason_code"],
|
||||
json!(3)
|
||||
);
|
||||
assert_eq!(enforcement.body["remove_evidence"], json!(true));
|
||||
assert_eq!(enforcement.audit_log_reason.as_deref(), Some("Spam review"));
|
||||
assert!(enforcement.body.get("notify_user").is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn schedule_deletion_sends_notify_user_from_the_checkbox() {
|
||||
let uri = format!("/users/{USER_ID}?action=schedule_deletion&tab=moderation");
|
||||
let checked = submit(
|
||||
async fn cancel_deletion_sends_only_the_pending_deletion() {
|
||||
let uri = format!("/users/{USER_ID}?action=cancel_deletion&tab=moderation");
|
||||
let captured = submit(
|
||||
&uri,
|
||||
"reason_code=3&days_until_deletion=60¬ify_user_present=1¬ify_user=true",
|
||||
"expected_pending_deletion_at=2026-12-01T00%3A00%3A00.000Z&confirm=true&internal_note=Asked%20to%20keep%20it",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
checked.route,
|
||||
format!("PUT /admin/users/{USER_ID}/deletion")
|
||||
captured.route,
|
||||
format!("DELETE /admin/users/{USER_ID}/deletion")
|
||||
);
|
||||
assert_eq!(
|
||||
captured.body,
|
||||
json!({"expected_pending_deletion_at": "2026-12-01T00:00:00.000Z"})
|
||||
);
|
||||
assert_eq!(
|
||||
captured.audit_log_reason.as_deref(),
|
||||
Some("Asked to keep it")
|
||||
);
|
||||
assert_eq!(checked.body["notify_user"], json!(true));
|
||||
let unchecked = submit(
|
||||
&uri,
|
||||
"reason_code=3&days_until_deletion=60¬ify_user_present=1",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(unchecked.body["notify_user"], json!(false));
|
||||
let stale_form = submit(&uri, "reason_code=3&days_until_deletion=60").await;
|
||||
assert_eq!(stale_form.body["notify_user"], json!(true));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bulk_schedule_deletion_sends_notify_user_from_the_checkbox() {
|
||||
let uri = "/bulk-actions?action=bulk-schedule-user-deletion";
|
||||
let fields = format!("user_ids={USER_ID}&reason_code=3&days_until_deletion=60");
|
||||
let checked = submit(
|
||||
uri,
|
||||
&format!("{fields}¬ify_user_present=1¬ify_user=true"),
|
||||
async fn limit_features_sends_one_limit_with_its_end() {
|
||||
let uri = format!("/users/{USER_ID}?action=limit_features&tab=moderation");
|
||||
let captured = submit(
|
||||
&uri,
|
||||
"limit=reports&ends_on=2026-12-01&reason_code=29&source=own_review&automation=none&statement=False%20reports&internal_note=Report%20review",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(checked.route, "POST /admin/bulk-jobs");
|
||||
assert_eq!(checked.body["task"], json!("schedule_user_deletion"));
|
||||
assert_eq!(checked.body["notify_user"], json!(true));
|
||||
let unchecked = submit(uri, &format!("{fields}¬ify_user_present=1")).await;
|
||||
assert_eq!(unchecked.body["notify_user"], json!(false));
|
||||
let stale_form = submit(uri, &fields).await;
|
||||
assert_eq!(stale_form.body["notify_user"], json!(true));
|
||||
assert_eq!(
|
||||
captured.route,
|
||||
format!("PUT /admin/users/{USER_ID}/feature-limits")
|
||||
);
|
||||
assert_eq!(captured.body["limit"], json!("reports"));
|
||||
assert_eq!(captured.body["ends_at"], json!("2026-12-01T00:00:00Z"));
|
||||
assert_eq!(captured.body["decision"]["reason_code"], json!(29));
|
||||
let purchases = submit(
|
||||
&uri,
|
||||
"limit=purchases&ends_on=2026-12-01&reason_code=14&source=own_review&automation=none&statement=Disputes&internal_note=Billing",
|
||||
)
|
||||
.await;
|
||||
assert!(purchases.body.get("ends_at").is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bulk_close_sends_one_bulk_decision() {
|
||||
let captured = submit(
|
||||
"/bulk-actions?action=bulk-schedule-user-deletion",
|
||||
&format!(
|
||||
"user_ids={USER_ID}&basis=enforcement&days_until_deletion=74&reason_code=3&automation=none&statement=Same%20invite%20link&internal_note=Spam%20sweep"
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(captured.route, "POST /admin/bulk-jobs");
|
||||
assert_eq!(captured.body["task"], json!("schedule_user_deletion"));
|
||||
assert_eq!(
|
||||
captured.body["basis"],
|
||||
json!({
|
||||
"type": "enforcement",
|
||||
"decision": {
|
||||
"reason_code": 3,
|
||||
"source": "own_review",
|
||||
"automation": "none",
|
||||
"statement": "Same invite link"
|
||||
}
|
||||
})
|
||||
);
|
||||
assert_eq!(captured.audit_log_reason.as_deref(), Some("Spam sweep"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -131,12 +163,13 @@ async fn report_resolve_sends_notify_reporter_from_the_checkbox() {
|
||||
let uri = format!("/reports/{REPORT_ID}/resolve");
|
||||
let checked = submit(
|
||||
&uri,
|
||||
"resolution=actioned&public_comment=Handled¬ify_reporter_present=1¬ify_reporter=true",
|
||||
"resolution=actioned&outcome=removed&public_comment=Handled¬ify_reporter_present=1¬ify_reporter=true",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(checked.route, format!("PATCH /admin/reports/{REPORT_ID}"));
|
||||
assert_eq!(checked.body["public_comment"], json!("Handled"));
|
||||
assert_eq!(checked.body["resolution"], json!("actioned"));
|
||||
assert_eq!(checked.body["outcome"], json!("removed"));
|
||||
assert_eq!(checked.body["notify_reporter"], json!(true));
|
||||
let unchecked = submit(
|
||||
&uri,
|
||||
@@ -144,8 +177,19 @@ async fn report_resolve_sends_notify_reporter_from_the_checkbox() {
|
||||
)
|
||||
.await;
|
||||
assert_eq!(unchecked.body["notify_reporter"], json!(false));
|
||||
let stale_form = submit(&uri, "resolution=duplicate&public_comment=Handled").await;
|
||||
assert_eq!(stale_form.body["notify_reporter"], json!(true));
|
||||
assert_eq!(unchecked.body["outcome"], json!("no_violation"));
|
||||
let duplicate = submit(
|
||||
&uri,
|
||||
"resolution=duplicate&outcome=restricted&duplicate_of=1400000000000000001&public_comment=Handled",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(duplicate.body["notify_reporter"], json!(true));
|
||||
assert!(duplicate.body.get("outcome").is_none());
|
||||
assert_eq!(duplicate.body["duplicate_of"], json!("1400000000000000001"));
|
||||
let warned = submit(&uri, "resolution=actioned&outcome=warned").await;
|
||||
assert_eq!(warned.body["outcome"], json!("warned"));
|
||||
let other_action = submit(&uri, "resolution=actioned&outcome=action_taken").await;
|
||||
assert_eq!(other_action.body["outcome"], json!("action_taken"));
|
||||
}
|
||||
|
||||
struct TestApp {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {DomainRoutedEmailProvider} from '@pkgs/email/src/DomainRoutedEmailProvider';
|
||||
import type {EmailMessage} from '@pkgs/email/src/EmailProviderTypes';
|
||||
import {EmailService} from '@pkgs/email/src/EmailService';
|
||||
import {describe, expect, it, vi} from 'vitest';
|
||||
|
||||
function message(to: string): EmailMessage {
|
||||
return {to, from: {email: '[email protected]', name: 'Example'}, subject: 'Hello', text: 'Body'};
|
||||
}
|
||||
|
||||
describe('DomainRoutedEmailProvider', () => {
|
||||
it.each([
|
||||
{to: '[email protected]', route: 'fallback'},
|
||||
{to: '[email protected]', route: 'fallback'},
|
||||
{to: '[email protected]', route: 'fallback'},
|
||||
{to: '[email protected]', route: 'primary'},
|
||||
{to: '[email protected]', route: 'primary'},
|
||||
{to: '[email protected]', route: 'primary'},
|
||||
{to: '"[email protected]"@example.com', route: 'primary'},
|
||||
])('sends $to through the $route provider', async ({to, route}) => {
|
||||
const primary = {sendEmail: vi.fn().mockResolvedValue(true)};
|
||||
const fallback = {sendEmail: vi.fn().mockResolvedValue(false)};
|
||||
const provider = new DomainRoutedEmailProvider(primary, fallback, ['gmail.com', 'googlemail.com']);
|
||||
const sent = message(to);
|
||||
const result = await provider.sendEmail(sent);
|
||||
const [chosen, other] = route === 'fallback' ? [fallback, primary] : [primary, fallback];
|
||||
expect(chosen.sendEmail).toHaveBeenCalledWith(sent);
|
||||
expect(other.sendEmail).not.toHaveBeenCalled();
|
||||
expect(result).toBe(route === 'primary');
|
||||
});
|
||||
|
||||
it('sends every address through the fallback provider when the domains are *', async () => {
|
||||
const primary = {sendEmail: vi.fn().mockResolvedValue(true)};
|
||||
const fallback = {sendEmail: vi.fn().mockResolvedValue(true)};
|
||||
const provider = new DomainRoutedEmailProvider(primary, fallback, ['*']);
|
||||
await provider.sendEmail(message('[email protected]'));
|
||||
await provider.sendEmail(message('[email protected]'));
|
||||
expect(fallback.sendEmail).toHaveBeenCalledTimes(2);
|
||||
expect(primary.sendEmail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('never reaches either provider for a bounced address', async () => {
|
||||
const primary = {sendEmail: vi.fn().mockResolvedValue(true)};
|
||||
const fallback = {sendEmail: vi.fn().mockResolvedValue(true)};
|
||||
const service = new EmailService(
|
||||
{
|
||||
enabled: true,
|
||||
fromEmail: '[email protected]',
|
||||
fromName: 'Example',
|
||||
appBaseUrl: 'https://example.com',
|
||||
appealsEmail: null,
|
||||
safetyEmail: null,
|
||||
supportEmail: null,
|
||||
productName: 'Example',
|
||||
},
|
||||
new DomainRoutedEmailProvider(primary, fallback, ['gmail.com']),
|
||||
{isEmailBounced: async () => true},
|
||||
);
|
||||
expect(await service.sendPasswordResetEmail('[email protected]', 'user', 'token')).toBe(false);
|
||||
expect(primary.sendEmail).not.toHaveBeenCalled();
|
||||
expect(fallback.sendEmail).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {createLogger} from '@fluxer/logger/src/Logger';
|
||||
import type {EmailMessage, IEmailProvider} from '@pkgs/email/src/EmailProviderTypes';
|
||||
|
||||
const logger = createLogger('@pkgs/email/src/DomainRoutedEmailProvider');
|
||||
|
||||
export class DomainRoutedEmailProvider implements IEmailProvider {
|
||||
private readonly primary: IEmailProvider;
|
||||
private readonly fallback: IEmailProvider;
|
||||
private readonly fallbackDomains: ReadonlySet<string>;
|
||||
|
||||
constructor(primary: IEmailProvider, fallback: IEmailProvider, fallbackDomains: ReadonlyArray<string>) {
|
||||
this.primary = primary;
|
||||
this.fallback = fallback;
|
||||
this.fallbackDomains = new Set(fallbackDomains);
|
||||
}
|
||||
|
||||
async sendEmail(message: EmailMessage): Promise<boolean> {
|
||||
const domain = message.to
|
||||
.slice(message.to.lastIndexOf('@') + 1)
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const useFallback = this.fallbackDomains.has('*') || this.fallbackDomains.has(domain);
|
||||
logger.debug({domain, provider: useFallback ? 'fallback' : 'primary'}, 'Routing email');
|
||||
return (useFallback ? this.fallback : this.primary).sendEmail(message);
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,6 @@ export interface EmailConfig {
|
||||
fromName: string;
|
||||
replyTo?: string | null;
|
||||
appBaseUrl: string;
|
||||
termsUrl: string | null;
|
||||
guidelinesUrl: string | null;
|
||||
appealsEmail: string | null;
|
||||
safetyEmail: string | null;
|
||||
supportEmail: string | null;
|
||||
|
||||
@@ -4,8 +4,8 @@ import {createLogger} from '@fluxer/logger/src/Logger';
|
||||
import type {EmailConfig, IEmailProvider, UserBouncedEmailChecker} from '@pkgs/email/src/EmailProviderTypes';
|
||||
import {getEmailTemplate} from '@pkgs/email/src/email_i18n/EmailI18n';
|
||||
import type {
|
||||
EmailLegalLinks,
|
||||
EmailTemplateVariables,
|
||||
ReporterOutcome,
|
||||
ReportReceivedTargetKind,
|
||||
} from '@pkgs/email/src/email_i18n/EmailI18nTypes';
|
||||
import type {EmailTemplateKey} from '@pkgs/email/src/email_i18n/EmailI18nTypes.generated';
|
||||
@@ -21,20 +21,6 @@ function formatMinorUnitAmount(amountMinor: number, currency: string, locale: st
|
||||
return formatter.format(amountMinor / 10 ** fractionDigits);
|
||||
}
|
||||
|
||||
function optionalReason(reason: string | null): string | null {
|
||||
return reason?.trim() || null;
|
||||
}
|
||||
|
||||
function legalLinkVariables(config: EmailConfig): {
|
||||
termsUrl: string | null;
|
||||
guidelinesUrl: string | null;
|
||||
legalLinks: EmailLegalLinks;
|
||||
} {
|
||||
const {termsUrl, guidelinesUrl} = config;
|
||||
const legalLinks = termsUrl ? (guidelinesUrl ? 'both' : 'terms') : guidelinesUrl ? 'guidelines' : 'none';
|
||||
return {termsUrl, guidelinesUrl, legalLinks};
|
||||
}
|
||||
|
||||
export class EmailService implements IEmailService {
|
||||
private readonly config: EmailConfig;
|
||||
private readonly provider: IEmailProvider | null;
|
||||
@@ -90,40 +76,6 @@ export class EmailService implements IEmailService {
|
||||
});
|
||||
}
|
||||
|
||||
async sendAccountTempBannedEmail(
|
||||
email: string,
|
||||
username: string,
|
||||
reason: string | null,
|
||||
durationHours: number,
|
||||
bannedUntil: Date,
|
||||
locale: string | null = null,
|
||||
): Promise<boolean> {
|
||||
return this.sendTemplatedEmail(email, 'account_temp_banned', locale, {
|
||||
username,
|
||||
reason: optionalReason(reason),
|
||||
durationHours,
|
||||
bannedUntil,
|
||||
...legalLinkVariables(this.config),
|
||||
appeals_email: this.config.appealsEmail,
|
||||
});
|
||||
}
|
||||
|
||||
async sendAccountScheduledForDeletionEmail(
|
||||
email: string,
|
||||
username: string,
|
||||
reason: string | null,
|
||||
deletionDate: Date,
|
||||
locale: string | null = null,
|
||||
): Promise<boolean> {
|
||||
return this.sendTemplatedEmail(email, 'account_scheduled_deletion', locale, {
|
||||
username,
|
||||
reason: optionalReason(reason),
|
||||
deletionDate,
|
||||
...legalLinkVariables(this.config),
|
||||
appeals_email: this.config.appealsEmail,
|
||||
});
|
||||
}
|
||||
|
||||
async sendSelfDeletionScheduledEmail(
|
||||
email: string,
|
||||
username: string,
|
||||
@@ -136,14 +88,12 @@ export class EmailService implements IEmailService {
|
||||
async sendAccountDeletionRequestedEmail(
|
||||
email: string,
|
||||
username: string,
|
||||
reason: string | null,
|
||||
deletionDate: Date,
|
||||
deletionDate: string,
|
||||
locale: string | null = null,
|
||||
): Promise<boolean> {
|
||||
return this.sendTemplatedEmail(email, 'account_deletion_scheduled_requested', locale, {
|
||||
username,
|
||||
reason: optionalReason(reason),
|
||||
deletionDate,
|
||||
deletion_date: deletionDate,
|
||||
safety_email: this.config.safetyEmail,
|
||||
});
|
||||
}
|
||||
@@ -151,15 +101,13 @@ export class EmailService implements IEmailService {
|
||||
async sendAccountDeletionInactivityEmail(
|
||||
email: string,
|
||||
username: string,
|
||||
reason: string | null,
|
||||
deletionDate: Date,
|
||||
deletionDate: string,
|
||||
locale: string | null = null,
|
||||
): Promise<boolean> {
|
||||
return this.sendTemplatedEmail(email, 'account_deletion_scheduled_inactivity', locale, {
|
||||
username,
|
||||
reason: optionalReason(reason),
|
||||
deletionDate,
|
||||
safety_email: this.config.safetyEmail,
|
||||
deletion_date: deletionDate,
|
||||
support_email: this.config.supportEmail,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -174,28 +122,20 @@ export class EmailService implements IEmailService {
|
||||
});
|
||||
}
|
||||
|
||||
async sendUnbanNotification(
|
||||
async sendEnforcementNoticeEmail(
|
||||
email: string,
|
||||
username: string,
|
||||
reason: string | null,
|
||||
variables: EmailTemplateVariables['enforcement_notice'],
|
||||
locale: string | null = null,
|
||||
): Promise<boolean> {
|
||||
return this.sendTemplatedEmail(email, 'unban_notification', locale, {username, reason: optionalReason(reason)});
|
||||
return this.sendTemplatedEmail(email, 'enforcement_notice', locale, variables);
|
||||
}
|
||||
|
||||
async sendScheduledDeletionNotification(
|
||||
async sendEnforcementEndedEmail(
|
||||
email: string,
|
||||
username: string,
|
||||
deletionDate: Date,
|
||||
reason: string | null,
|
||||
variables: EmailTemplateVariables['enforcement_ended'],
|
||||
locale: string | null = null,
|
||||
): Promise<boolean> {
|
||||
return this.sendTemplatedEmail(email, 'scheduled_deletion_notification', locale, {
|
||||
username,
|
||||
deletionDate,
|
||||
reason: optionalReason(reason),
|
||||
appeals_email: this.config.appealsEmail,
|
||||
});
|
||||
return this.sendTemplatedEmail(email, 'enforcement_ended', locale, variables);
|
||||
}
|
||||
|
||||
async sendInactivityWarningEmail(
|
||||
@@ -250,6 +190,7 @@ export class EmailService implements IEmailService {
|
||||
username: string,
|
||||
reportId: string,
|
||||
publicComment: string,
|
||||
outcome: ReporterOutcome,
|
||||
locale: string | null = null,
|
||||
): Promise<boolean> {
|
||||
return this.sendTemplatedEmail(email, 'report_resolved', locale, {
|
||||
@@ -257,7 +198,8 @@ export class EmailService implements IEmailService {
|
||||
reportId,
|
||||
publicComment,
|
||||
hasComment: publicComment ? 'yes' : 'no',
|
||||
safety_email: this.config.safetyEmail,
|
||||
outcome,
|
||||
appeals_email: this.config.appealsEmail,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -265,12 +207,14 @@ export class EmailService implements IEmailService {
|
||||
email: string,
|
||||
reportId: string,
|
||||
publicComment: string,
|
||||
outcome: ReporterOutcome,
|
||||
locale: string | null = null,
|
||||
): Promise<boolean> {
|
||||
return this.sendTemplatedEmail(email, 'dsa_report_resolved', locale, {
|
||||
reportId,
|
||||
publicComment,
|
||||
hasComment: publicComment ? 'yes' : 'no',
|
||||
outcome,
|
||||
appeals_email: this.config.appealsEmail,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {ReportReceivedTargetKind} from '@pkgs/email/src/email_i18n/EmailI18nTypes';
|
||||
import type {
|
||||
EmailTemplateVariables,
|
||||
ReporterOutcome,
|
||||
ReportReceivedTargetKind,
|
||||
} from '@pkgs/email/src/email_i18n/EmailI18nTypes';
|
||||
|
||||
export interface IEmailService {
|
||||
sendPasswordResetEmail(email: string, username: string, resetToken: string, locale?: string | null): Promise<boolean>;
|
||||
@@ -18,21 +22,6 @@ export interface IEmailService {
|
||||
location: string,
|
||||
locale?: string | null,
|
||||
): Promise<boolean>;
|
||||
sendAccountTempBannedEmail(
|
||||
email: string,
|
||||
username: string,
|
||||
reason: string | null,
|
||||
durationHours: number,
|
||||
bannedUntil: Date,
|
||||
locale?: string | null,
|
||||
): Promise<boolean>;
|
||||
sendAccountScheduledForDeletionEmail(
|
||||
email: string,
|
||||
username: string,
|
||||
reason: string | null,
|
||||
deletionDate: Date,
|
||||
locale?: string | null,
|
||||
): Promise<boolean>;
|
||||
sendSelfDeletionScheduledEmail(
|
||||
email: string,
|
||||
username: string,
|
||||
@@ -42,29 +31,24 @@ export interface IEmailService {
|
||||
sendAccountDeletionRequestedEmail(
|
||||
email: string,
|
||||
username: string,
|
||||
reason: string | null,
|
||||
deletionDate: Date,
|
||||
deletionDate: string,
|
||||
locale?: string | null,
|
||||
): Promise<boolean>;
|
||||
sendAccountDeletionInactivityEmail(
|
||||
email: string,
|
||||
username: string,
|
||||
reason: string | null,
|
||||
deletionDate: Date,
|
||||
deletionDate: string,
|
||||
locale?: string | null,
|
||||
): Promise<boolean>;
|
||||
sendAccountDeletionCancelledEmail(email: string, username: string, locale?: string | null): Promise<boolean>;
|
||||
sendUnbanNotification(
|
||||
sendEnforcementNoticeEmail(
|
||||
email: string,
|
||||
username: string,
|
||||
reason: string | null,
|
||||
variables: EmailTemplateVariables['enforcement_notice'],
|
||||
locale?: string | null,
|
||||
): Promise<boolean>;
|
||||
sendScheduledDeletionNotification(
|
||||
sendEnforcementEndedEmail(
|
||||
email: string,
|
||||
username: string,
|
||||
deletionDate: Date,
|
||||
reason: string | null,
|
||||
variables: EmailTemplateVariables['enforcement_ended'],
|
||||
locale?: string | null,
|
||||
): Promise<boolean>;
|
||||
sendInactivityWarningEmail(
|
||||
@@ -89,12 +73,14 @@ export interface IEmailService {
|
||||
username: string,
|
||||
reportId: string,
|
||||
publicComment: string,
|
||||
outcome: ReporterOutcome,
|
||||
locale?: string | null,
|
||||
): Promise<boolean>;
|
||||
sendDsaReportResolvedEmail(
|
||||
email: string,
|
||||
reportId: string,
|
||||
publicComment: string,
|
||||
outcome: ReporterOutcome,
|
||||
locale?: string | null,
|
||||
): Promise<boolean>;
|
||||
sendReportReceivedEmail(
|
||||
|
||||
@@ -25,4 +25,33 @@ describe('SmtpEmailProvider', () => {
|
||||
new SmtpEmailProvider({host: 'smtp.example.com', port, username: 'user', password: 'pass', tlsMode});
|
||||
expect(createTransport).toHaveBeenCalledWith(expect.objectContaining(expected));
|
||||
});
|
||||
|
||||
const message = {
|
||||
to: '[email protected]',
|
||||
from: {email: '[email protected]', name: 'Example'},
|
||||
subject: 'Hi',
|
||||
text: 'Body',
|
||||
};
|
||||
|
||||
it('sends the configured headers verbatim', async () => {
|
||||
const provider = new SmtpEmailProvider({
|
||||
host: 'smtp.example.com',
|
||||
port: 587,
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
headers: {'X-Tracking': '{"enable":0}'},
|
||||
});
|
||||
await provider.sendEmail(message);
|
||||
const {sendMail} = createTransport.mock.results[0].value;
|
||||
expect(sendMail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({headers: {'X-Tracking': {prepared: true, value: '{"enable":0}'}}}),
|
||||
);
|
||||
});
|
||||
|
||||
it('sends no extra headers when none are configured', async () => {
|
||||
const provider = new SmtpEmailProvider({host: 'smtp.example.com', port: 587, username: 'user', password: 'pass'});
|
||||
await provider.sendEmail(message);
|
||||
const {sendMail} = createTransport.mock.results[0].value;
|
||||
expect(sendMail.mock.calls[0][0]).not.toHaveProperty('headers');
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@ interface SmtpEmailConfig {
|
||||
username: string;
|
||||
password: string;
|
||||
tlsMode?: SmtpTlsMode | null;
|
||||
headers?: Record<string, string>;
|
||||
connectionTimeoutMs?: number;
|
||||
greetingTimeoutMs?: number;
|
||||
socketTimeoutMs?: number;
|
||||
@@ -23,11 +24,17 @@ export class SmtpEmailProvider implements IEmailProvider {
|
||||
private readonly host: string;
|
||||
private readonly port: number;
|
||||
private readonly tlsMode: SmtpTlsMode;
|
||||
private readonly headers: Record<string, {prepared: true; value: string}> | undefined;
|
||||
|
||||
constructor(config: SmtpEmailConfig) {
|
||||
this.host = config.host;
|
||||
this.port = config.port;
|
||||
this.tlsMode = resolveSmtpTlsMode(config.tlsMode, config.port);
|
||||
this.headers = config.headers
|
||||
? Object.fromEntries(
|
||||
Object.entries(config.headers).map(([name, value]) => [name, {prepared: true as const, value}]),
|
||||
)
|
||||
: undefined;
|
||||
this.transporter = nodemailer.createTransport({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
@@ -55,6 +62,7 @@ export class SmtpEmailProvider implements IEmailProvider {
|
||||
to: message.to,
|
||||
from: `${message.from.name} <${message.from.email}>`,
|
||||
...(message.replyTo ? {replyTo: message.replyTo} : {}),
|
||||
...(this.headers ? {headers: this.headers} : {}),
|
||||
subject: message.subject,
|
||||
text: message.text,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {ReportReceivedTargetKind} from '@pkgs/email/src/email_i18n/EmailI18nTypes';
|
||||
import type {
|
||||
EmailTemplateVariables,
|
||||
ReporterOutcome,
|
||||
ReportReceivedTargetKind,
|
||||
} from '@pkgs/email/src/email_i18n/EmailI18nTypes';
|
||||
import type {IEmailService} from '@pkgs/email/src/IEmailService';
|
||||
|
||||
export interface SentEmailRecord {
|
||||
@@ -10,6 +14,10 @@ export interface SentEmailRecord {
|
||||
metadata: Record<string, string>;
|
||||
}
|
||||
|
||||
function stringMetadata(variables: Record<string, string | null>): Record<string, string> {
|
||||
return Object.fromEntries(Object.entries(variables).map(([key, value]) => [key, value ?? '']));
|
||||
}
|
||||
|
||||
export class TestEmailService implements IEmailService {
|
||||
private readonly sentEmails: Array<SentEmailRecord> = [];
|
||||
|
||||
@@ -50,34 +58,6 @@ export class TestEmailService implements IEmailService {
|
||||
return this.record(email, 'ip_authorization', {token: authorizationToken, ip: ipAddress, location});
|
||||
}
|
||||
|
||||
async sendAccountTempBannedEmail(
|
||||
email: string,
|
||||
_username: string,
|
||||
reason: string | null,
|
||||
durationHours: number,
|
||||
bannedUntil: Date,
|
||||
_locale?: string | null,
|
||||
): Promise<boolean> {
|
||||
return this.record(email, 'account_temp_banned', {
|
||||
reason: reason ?? '',
|
||||
duration_hours: durationHours.toString(10),
|
||||
banned_until: bannedUntil.toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
async sendAccountScheduledForDeletionEmail(
|
||||
email: string,
|
||||
_username: string,
|
||||
reason: string | null,
|
||||
deletionDate: Date,
|
||||
_locale?: string | null,
|
||||
): Promise<boolean> {
|
||||
return this.record(email, 'account_scheduled_deletion', {
|
||||
reason: reason ?? '',
|
||||
deletion_date: deletionDate.toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
async sendSelfDeletionScheduledEmail(
|
||||
email: string,
|
||||
_username: string,
|
||||
@@ -90,53 +70,39 @@ export class TestEmailService implements IEmailService {
|
||||
async sendAccountDeletionRequestedEmail(
|
||||
email: string,
|
||||
_username: string,
|
||||
reason: string | null,
|
||||
deletionDate: Date,
|
||||
deletionDate: string,
|
||||
_locale?: string | null,
|
||||
): Promise<boolean> {
|
||||
return this.record(email, 'account_deletion_scheduled_requested', {
|
||||
reason: reason ?? '',
|
||||
deletion_date: deletionDate.toISOString(),
|
||||
});
|
||||
return this.record(email, 'account_deletion_scheduled_requested', {deletion_date: deletionDate});
|
||||
}
|
||||
|
||||
async sendAccountDeletionInactivityEmail(
|
||||
email: string,
|
||||
_username: string,
|
||||
reason: string | null,
|
||||
deletionDate: Date,
|
||||
deletionDate: string,
|
||||
_locale?: string | null,
|
||||
): Promise<boolean> {
|
||||
return this.record(email, 'account_deletion_scheduled_inactivity', {
|
||||
reason: reason ?? '',
|
||||
deletion_date: deletionDate.toISOString(),
|
||||
});
|
||||
return this.record(email, 'account_deletion_scheduled_inactivity', {deletion_date: deletionDate});
|
||||
}
|
||||
|
||||
async sendAccountDeletionCancelledEmail(email: string, _username: string, _locale?: string | null): Promise<boolean> {
|
||||
return this.record(email, 'account_deletion_cancelled');
|
||||
}
|
||||
|
||||
async sendUnbanNotification(
|
||||
async sendEnforcementNoticeEmail(
|
||||
email: string,
|
||||
_username: string,
|
||||
reason: string | null,
|
||||
variables: EmailTemplateVariables['enforcement_notice'],
|
||||
_locale?: string | null,
|
||||
): Promise<boolean> {
|
||||
return this.record(email, 'unban_notification', {reason: reason ?? ''});
|
||||
return this.record(email, 'enforcement_notice', stringMetadata(variables));
|
||||
}
|
||||
|
||||
async sendScheduledDeletionNotification(
|
||||
async sendEnforcementEndedEmail(
|
||||
email: string,
|
||||
_username: string,
|
||||
deletionDate: Date,
|
||||
reason: string | null,
|
||||
variables: EmailTemplateVariables['enforcement_ended'],
|
||||
_locale?: string | null,
|
||||
): Promise<boolean> {
|
||||
return this.record(email, 'scheduled_deletion_notification', {
|
||||
deletion_date: deletionDate.toISOString(),
|
||||
reason: reason ?? '',
|
||||
});
|
||||
return this.record(email, 'enforcement_ended', stringMetadata(variables));
|
||||
}
|
||||
|
||||
async sendInactivityWarningEmail(
|
||||
@@ -178,18 +144,20 @@ export class TestEmailService implements IEmailService {
|
||||
_username: string,
|
||||
reportId: string,
|
||||
publicComment: string,
|
||||
outcome: ReporterOutcome,
|
||||
_locale?: string | null,
|
||||
): Promise<boolean> {
|
||||
return this.record(email, 'report_resolved', {report_id: reportId, public_comment: publicComment});
|
||||
return this.record(email, 'report_resolved', {report_id: reportId, public_comment: publicComment, outcome});
|
||||
}
|
||||
|
||||
async sendDsaReportResolvedEmail(
|
||||
email: string,
|
||||
reportId: string,
|
||||
publicComment: string,
|
||||
outcome: ReporterOutcome,
|
||||
_locale?: string | null,
|
||||
): Promise<boolean> {
|
||||
return this.record(email, 'dsa_report_resolved', {report_id: reportId, public_comment: publicComment});
|
||||
return this.record(email, 'dsa_report_resolved', {report_id: reportId, public_comment: publicComment, outcome});
|
||||
}
|
||||
|
||||
async sendReportReceivedEmail(
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {EmailTemplateVariables} from '@pkgs/email/src/email_i18n/EmailI18nT
|
||||
import type {EmailTemplate, EmailTemplateKey} from '@pkgs/email/src/email_i18n/EmailI18nTypes.generated';
|
||||
|
||||
const DEFAULT_LOCALE = 'en-US';
|
||||
const DOUBLED_PERIOD = /(?<!\.)\.\.(?!\.)/g;
|
||||
|
||||
function formatEmailDate(value: unknown, locale: string, style: string | null): string {
|
||||
const options: Intl.DateTimeFormatOptions = {timeZone: 'UTC', day: 'numeric', month: 'short', year: 'numeric'};
|
||||
@@ -57,8 +58,8 @@ const emailI18n = createStaticI18n<EmailTemplateKey, EmailTemplate, Record<strin
|
||||
messageFormatOptions: {customFormatters: {date: formatEmailDate, time: formatEmailTime}},
|
||||
},
|
||||
(template, variables, mf) => {
|
||||
const compiledSubject = String(compileMessage(mf, template.subject)(variables));
|
||||
const compiledBody = String(compileMessage(mf, template.body)(variables));
|
||||
const compiledSubject = String(compileMessage(mf, template.subject)(variables)).replace(DOUBLED_PERIOD, '.');
|
||||
const compiledBody = String(compileMessage(mf, template.body)(variables)).replace(DOUBLED_PERIOD, '.');
|
||||
return {subject: compiledSubject, body: compiledBody};
|
||||
},
|
||||
);
|
||||
|
||||
@@ -14,44 +14,53 @@ const FIXTURE: {[K in EmailTemplateKey]: EmailTemplateVariables[K]} = {
|
||||
account_deletion_cancelled: {username: 'testuser', safety_email: '[email protected]'},
|
||||
account_deletion_scheduled_inactivity: {
|
||||
username: 'testuser',
|
||||
reason: 'Inactive',
|
||||
deletionDate: DATE,
|
||||
safety_email: '[email protected]',
|
||||
deletion_date: 'Dec 1, 2026',
|
||||
support_email: '[email protected]',
|
||||
},
|
||||
account_deletion_scheduled_requested: {
|
||||
username: 'testuser',
|
||||
reason: 'Requested',
|
||||
deletionDate: DATE,
|
||||
deletion_date: 'Dec 14, 2026',
|
||||
safety_email: '[email protected]',
|
||||
},
|
||||
account_scheduled_deletion: {
|
||||
username: 'testuser',
|
||||
reason: 'Spam',
|
||||
deletionDate: DATE,
|
||||
termsUrl: 'https://example.com/terms',
|
||||
guidelinesUrl: 'https://example.com/guidelines',
|
||||
legalLinks: 'both',
|
||||
appeals_email: '[email protected]',
|
||||
},
|
||||
account_temp_banned: {
|
||||
username: 'testuser',
|
||||
reason: 'Spam',
|
||||
durationHours: 24,
|
||||
bannedUntil: DATE,
|
||||
termsUrl: 'https://example.com/terms',
|
||||
guidelinesUrl: 'https://example.com/guidelines',
|
||||
legalLinks: 'both',
|
||||
appeals_email: '[email protected]',
|
||||
},
|
||||
donation_confirmation: {amount: '$5.00', currency: 'USD', interval: 'month', manageUrl: 'https://example.com/m'},
|
||||
donation_magic_link: {manageUrl: 'https://example.com/m', expiresAt: DATE},
|
||||
dsa_report_resolved: {reportId: '1', publicComment: 'Thanks', hasComment: 'yes', appeals_email: '[email protected]'},
|
||||
dsa_report_resolved: {
|
||||
reportId: '1',
|
||||
publicComment: 'Thanks',
|
||||
hasComment: 'yes',
|
||||
outcome: 'removed',
|
||||
appeals_email: '[email protected]',
|
||||
},
|
||||
dsa_report_verification: {code: '123456', expiresAt: DATE},
|
||||
email_change_new: {username: 'testuser', code: '123456', expiresAt: DATE},
|
||||
email_change_original: {username: 'testuser', code: '123456', expiresAt: DATE},
|
||||
email_change_revert: {username: 'testuser', newEmail: '[email protected]', revertUrl: 'https://example.com/r'},
|
||||
email_verification: {username: 'testuser', verifyUrl: 'https://example.com/verify'},
|
||||
gift_chargeback_notification: {username: 'testuser', support_email: '[email protected]'},
|
||||
enforcement_ended: {
|
||||
username: 'testuser',
|
||||
outcome: 'reversed',
|
||||
decision_id: '123',
|
||||
restores_access: 'yes',
|
||||
statement: 'We checked again',
|
||||
content_removed: 'no',
|
||||
},
|
||||
enforcement_notice: {
|
||||
username: 'testuser',
|
||||
kind_label: 'suspension',
|
||||
neutral: 'no',
|
||||
summary: 'We suspended your account.',
|
||||
effects: 'You cannot sign in.',
|
||||
rule_title: 'Spam',
|
||||
rule_url: 'https://example.com/guidelines',
|
||||
facts: 'We found spam.',
|
||||
statement: null,
|
||||
how_we_decided: 'A person decided.',
|
||||
appeals_email: '[email protected]',
|
||||
appeal_deadline: 'Dec 1, 2026',
|
||||
redress: null,
|
||||
decision_id: '123',
|
||||
},
|
||||
harvest_completed: {
|
||||
username: 'testuser',
|
||||
downloadUrl: 'https://example.com/d',
|
||||
@@ -82,16 +91,10 @@ const FIXTURE: {[K in EmailTemplateKey]: EmailTemplateVariables[K]} = {
|
||||
reportId: '1',
|
||||
publicComment: 'Thanks',
|
||||
hasComment: 'yes',
|
||||
safety_email: '[email protected]',
|
||||
},
|
||||
scheduled_deletion_notification: {
|
||||
username: 'testuser',
|
||||
deletionDate: DATE,
|
||||
reason: 'Payment fraud',
|
||||
outcome: 'restricted',
|
||||
appeals_email: '[email protected]',
|
||||
},
|
||||
self_deletion_scheduled: {username: 'testuser', deletionDate: DATE},
|
||||
unban_notification: {username: 'testuser', reason: 'Appeal accepted'},
|
||||
};
|
||||
|
||||
describe('EmailI18n locale files', () => {
|
||||
@@ -111,6 +114,16 @@ describe('EmailI18n locale files', () => {
|
||||
);
|
||||
}
|
||||
});
|
||||
it.each(['en-US', ...LOCALES])('%s gives every reporter outcome its own wording', (locale) => {
|
||||
for (const key of ['report_resolved', 'dsa_report_resolved'] as const) {
|
||||
const bodies = (['removed', 'restricted', 'action_taken', 'no_violation'] as const).map((outcome) => {
|
||||
const result = getEmailTemplate(key, locale, {...FIXTURE[key], outcome}, 'Fluxer');
|
||||
return result.ok ? result.value.body : null;
|
||||
});
|
||||
expect(bodies, key).not.toContain(null);
|
||||
expect(new Set(bodies).size, key).toBe(bodies.length);
|
||||
}
|
||||
});
|
||||
it.each(['en-US', ...LOCALES])('%s renders every template with UTC times', (locale) => {
|
||||
for (const key of TEMPLATE_KEYS) {
|
||||
const result = getEmailTemplate(key, locale, FIXTURE[key], 'Fluxer');
|
||||
|
||||
@@ -7,19 +7,11 @@ export const EMAIL_I18N_MESSAGES = {
|
||||
},
|
||||
account_deletion_scheduled_inactivity: {
|
||||
subject: 'Your {product_name} account will be deleted due to inactivity',
|
||||
body: 'Hello {username},\n\nYour {product_name} account has been inactive for a long time, so it is scheduled for permanent deletion on:\n\n{deletionDate, date, full} at {deletionDate, time, short}{reason, select, null {} other {\n\nReason: {reason}}}\n\n{safety_email, select, null {If you want to keep your account, contact the administrators of this instance before that date.} other {If you want to keep your account, contact {safety_email} from this email address before that date.}}\n\n– {product_name} Team',
|
||||
body: 'Hello {username},\n\nYour {product_name} account has been inactive for a long time, so we scheduled it for deletion on {deletion_date}.\n\nTo keep your account, sign in before that date. {support_email, select, null {If you have questions, contact the administrators of this instance.} other {If you have questions, contact {support_email}.}}\n\nThe {product_name} team',
|
||||
},
|
||||
account_deletion_scheduled_requested: {
|
||||
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. {safety_email, select, null {If you didn't request this, or you want to keep your account, contact the administrators of this instance before that date.} other {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_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}\n}This is a serious enforcement action. Your account data will be permanently deleted on the scheduled date.\n\n{legalLinks, select,\n both {Please review:\n- Terms of Service: {termsUrl}\n- Community Guidelines: {guidelinesUrl}\n\n}\n terms {Please review:\n- Terms of Service: {termsUrl}\n\n}\n guidelines {Please review:\n- Community Guidelines: {guidelinesUrl}\n\n}\n other {}\n}Appeals process:\nIf you believe this enforcement decision was incorrect or unjustified, you have 60 days to submit an appeal. {appeals_email, select, null {Contact the administrators of this instance.} other {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',
|
||||
},
|
||||
account_temp_banned: {
|
||||
subject: 'Your {product_name} account has been temporarily suspended',
|
||||
body: "Hello {username},\n\nYour {product_name} account has been temporarily suspended for violating our Terms of Service or Community Guidelines.\n\nDuration: {durationHours, plural,\n =1 {1 hour}\n other {# hours}\n}\nSuspended until: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Reason: {reason}\n\n}\n}During this time, you won't be able to access your account.\n\n{legalLinks, select,\n both {Please review:\n- Terms of Service: {termsUrl}\n- Community Guidelines: {guidelinesUrl}\n\n}\n terms {Please review:\n- Terms of Service: {termsUrl}\n\n}\n guidelines {Please review:\n- Community Guidelines: {guidelinesUrl}\n\n}\n other {}\n}If you believe this enforcement decision was incorrect or unjustified, you can submit an appeal. {appeals_email, select, null {Contact the administrators of this instance and clearly explain why you believe the decision was incorrect.} other {Email {appeals_email} from this email address and clearly explain why you believe the decision was incorrect.}} We'll review your appeal and respond with our decision.\n\n– {product_name} Safety Team",
|
||||
body: "Hello {username},\n\nAs you asked, we scheduled your {product_name} account for deletion on {deletion_date}.\n\nTo keep your account, sign in before {deletion_date}. {safety_email, select, null {If you didn't ask for this, contact the administrators of this instance.} other {If you didn't ask for this, contact {safety_email}.}}\n\nThe {product_name} team",
|
||||
},
|
||||
donation_confirmation: {
|
||||
subject: 'Thank you for your {product_name} donation',
|
||||
@@ -31,7 +23,7 @@ export const EMAIL_I18N_MESSAGES = {
|
||||
},
|
||||
dsa_report_resolved: {
|
||||
subject: 'We made a decision on your {product_name} report',
|
||||
body: "Hello,\n\nOur Safety Team has reviewed your Digital Services Act report (ID: {reportId}) and made a decision.{hasComment, select, yes {\n\nResponse from the Safety Team:\n{publicComment}} other {}}\n\nWe don't share details of any action taken against another person's account, because that is their personal data.\n\nIf you disagree with this decision, you can appeal free of charge within 60 days. {appeals_email, select, null {Contact the administrators of this instance, include your report ID, and explain why you think the decision is wrong.} other {Email {appeals_email} from this email address, include your report ID, and explain why you think the decision is wrong.}} If you are in the EU, you can also refer the dispute to a certified out-of-court dispute settlement body. None of this affects your right to go to court.\n\n– {product_name} Safety Team",
|
||||
body: "Hello,\n\nWe reviewed your report (ID: {reportId}).\n\n{outcome, select, removed {We removed the content you reported because it broke our rules.} restricted {We found that the account you reported broke our rules, and we restricted it. We don't share what action we took against another account, because that is their personal data.} action_taken {We found that what you reported broke our rules, and we took action. We don't share what action we took against another account, because that is their personal data.} no_violation {We found that the content or account you reported doesn't break our rules, so we left it as it is.} other {We closed your report.}}{hasComment, select, yes {\n\nNote from our team: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nIf you disagree, email {appeals_email} within 60 days and include the report ID. If you're in the EU, you can also refer this to a certified out-of-court dispute settlement body or complain to the Digital Services Coordinator in your country.}}\n\nThe {product_name} team",
|
||||
},
|
||||
dsa_report_verification: {
|
||||
subject: 'Verify your email for a DSA report',
|
||||
@@ -57,6 +49,16 @@ export const EMAIL_I18N_MESSAGES = {
|
||||
subject: 'Perks from your redeemed gift have been removed',
|
||||
body: "Hello {username},\n\nA gift code you redeemed was originally paid for by someone else. That payment has since been reversed (a chargeback).\n\nBecause of this, we've removed the perks that were added to your account when you redeemed the gift.\n\n{support_email, select, null {If you think this is a mistake, please contact the administrators of this instance and include any details you have about the gift code and when you redeemed it.} other {If you think this is a mistake, please contact our support team and include any details you have about the gift code and when you redeemed it.}}\n\n– {product_name} Team",
|
||||
},
|
||||
enforcement_ended: {
|
||||
subject:
|
||||
'{outcome, select, reversed {We reversed a decision about your {product_name} account} other {A restriction on your {product_name} account has ended}}',
|
||||
body: "Hello {username},\n\n{outcome, select, reversed {{decision_id, select, null {We looked again at a decision about your account and reversed it.} other {We looked again at decision {decision_id} and reversed it.}} We're sorry for the trouble.} other {{decision_id, select, null {We lifted the restriction on your account.} other {We lifted the restriction from decision {decision_id}.}}}}{restores_access, select, yes { You can sign in and use {product_name} again.} other {}}{statement, select, null {} other {\n\nNote from our team: {statement}}}{content_removed, select, yes {\n\nContent we removed can't be restored.} other {}}\n\nThe {product_name} team",
|
||||
},
|
||||
enforcement_notice: {
|
||||
subject:
|
||||
'{neutral, select, yes {An update about your {product_name} account} other {{kind_label, select, warning {A warning about your {product_name} account} suspension {Your {product_name} account is suspended} ban {Your {product_name} account is banned} closure {Your {product_name} account is closed} feature_limit {Some features are limited on your {product_name} account} content_removal {We removed messages you sent on {product_name}} profile_reset {We reset parts of your {product_name} profile} community_removal {We removed your community on {product_name}} bot {An update about your bot on {product_name}} other {An update about your {product_name} account}}}}',
|
||||
body: 'Hello {username},\n\n{summary}\n\n{effects}\n\nWhy\nRule: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nNote from our team: {statement}}}\n\nHow we decided\n{how_we_decided}{appeals_email, select, null {} other {\n\nHow to appeal\nIf you think we got this wrong, email {appeals_email} by {appeal_deadline}. Write from the email address on your account if you can, and include the decision ID. Appeals are free, and a person reviews each one.{redress, select, null {} other {\n\n{redress}}}}}\n\nDecision ID: {decision_id}\n\nThe {product_name} team',
|
||||
},
|
||||
harvest_completed: {
|
||||
subject: 'Your {product_name} data export is ready to download',
|
||||
body: "Hello {username},\n\nYour data export is ready.\n\nDownload link:\n{downloadUrl}\n\nMessages included: {totalMessages, number}\nFile size: {fileSizeMB, number} MB\n\nThis link expires on {expiresAt, date, full} at {expiresAt, time, short}.\n\n{support_email, select, null {If you didn't request this export, please change your password immediately and contact the administrators of this instance.} other {If you didn't request this export, please change your password immediately and contact our support team.}}\n\n– {product_name} Team",
|
||||
@@ -87,18 +89,10 @@ export const EMAIL_I18N_MESSAGES = {
|
||||
},
|
||||
report_resolved: {
|
||||
subject: 'Your {product_name} report has been reviewed',
|
||||
body: 'Hello {username},\n\nYour report (ID: {reportId}) has been reviewed by our Safety Team.{hasComment, select, yes {\n\nResponse from the Safety Team:\n{publicComment}} other {}}\n\nThanks for helping keep {product_name} safe for everyone. We take all reports seriously and appreciate your contribution to the community.\n\n{safety_email, select, null {If you have any questions or concerns about this outcome, please contact the administrators of this instance.} other {If you have any questions or concerns about this outcome, please contact {safety_email}.}}\n\n– {product_name} Safety Team',
|
||||
},
|
||||
scheduled_deletion_notification: {
|
||||
subject: 'Your {product_name} account will be permanently deleted',
|
||||
body: 'Hello {username},\n\nYour {product_name} account has been scheduled for permanent deletion.\n\nScheduled deletion: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Reason: {reason}\n\n}\n}This is a serious enforcement action. Your account data will be permanently deleted on the scheduled date.\n\nIf you believe this enforcement decision was incorrect, you can submit an appeal. {appeals_email, select, null {Contact the administrators of this instance.} other {Email {appeals_email} from this email address.}}\n\n– {product_name} Safety Team',
|
||||
body: "Hello {username},\n\nWe reviewed your report (ID: {reportId}).\n\n{outcome, select, removed {We removed the content you reported because it broke our rules.} restricted {We found that the account you reported broke our rules, and we restricted it. We don't share what action we took against another account, because that is their personal data.} action_taken {We found that what you reported broke our rules, and we took action. We don't share what action we took against another account, because that is their personal data.} no_violation {We found that the content or account you reported doesn't break our rules, so we left it as it is.} other {We closed your report.}}{hasComment, select, yes {\n\nNote from our team: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nIf you disagree, email {appeals_email} within 60 days and include the report ID. If you're in the EU, you can also refer this to a certified out-of-court dispute settlement body or complain to the Digital Services Coordinator in your country.}}\n\nThe {product_name} team",
|
||||
},
|
||||
self_deletion_scheduled: {
|
||||
subject: 'Your {product_name} account deletion is scheduled',
|
||||
body: "Hello {username},\n\nYou requested to delete your {product_name} account. Your account is scheduled for permanent deletion on:\n\n{deletionDate, date, full} at {deletionDate, time, short}\n\nIf you didn't request this, sign in to your account to cancel the deletion. We also recommend changing your password to secure your account.\n\n– {product_name} Team",
|
||||
},
|
||||
unban_notification: {
|
||||
subject: 'Your {product_name} account suspension has been lifted',
|
||||
body: 'Hello {username},\n\nGood news: your {product_name} account suspension has been lifted.\n\n{reason, select,\n null {}\n other {Reason: {reason}\n\n}\n}You can now log back in and continue using {product_name} as normal.\n\n– {product_name} Safety Team',
|
||||
},
|
||||
} as const;
|
||||
@@ -4,8 +4,6 @@ export type EmailTemplateKey =
|
||||
| 'account_deletion_cancelled'
|
||||
| 'account_deletion_scheduled_inactivity'
|
||||
| 'account_deletion_scheduled_requested'
|
||||
| 'account_scheduled_deletion'
|
||||
| 'account_temp_banned'
|
||||
| 'donation_confirmation'
|
||||
| 'donation_magic_link'
|
||||
| 'dsa_report_resolved'
|
||||
@@ -14,6 +12,8 @@ export type EmailTemplateKey =
|
||||
| 'email_change_original'
|
||||
| 'email_change_revert'
|
||||
| 'email_verification'
|
||||
| 'enforcement_ended'
|
||||
| 'enforcement_notice'
|
||||
| 'gift_chargeback_notification'
|
||||
| 'harvest_completed'
|
||||
| 'inactivity_warning'
|
||||
@@ -23,9 +23,7 @@ export type EmailTemplateKey =
|
||||
| 'password_reset'
|
||||
| 'report_received'
|
||||
| 'report_resolved'
|
||||
| 'scheduled_deletion_notification'
|
||||
| 'self_deletion_scheduled'
|
||||
| 'unban_notification';
|
||||
| 'self_deletion_scheduled';
|
||||
|
||||
export interface EmailTemplate {
|
||||
subject: string;
|
||||
|
||||
@@ -2,7 +2,18 @@
|
||||
|
||||
export type ReportReceivedTargetKind = 'message' | 'user' | 'guild';
|
||||
|
||||
export type EmailLegalLinks = 'both' | 'terms' | 'guidelines' | 'none';
|
||||
export type ReporterOutcome = 'removed' | 'restricted' | 'action_taken' | 'no_violation';
|
||||
|
||||
export type EnforcementNoticeKindLabel =
|
||||
| 'warning'
|
||||
| 'suspension'
|
||||
| 'ban'
|
||||
| 'closure'
|
||||
| 'feature_limit'
|
||||
| 'content_removal'
|
||||
| 'profile_reset'
|
||||
| 'community_removal'
|
||||
| 'bot';
|
||||
|
||||
export interface EmailTemplateVariables {
|
||||
account_deletion_cancelled: {
|
||||
@@ -11,35 +22,14 @@ export interface EmailTemplateVariables {
|
||||
};
|
||||
account_deletion_scheduled_inactivity: {
|
||||
username: string;
|
||||
reason: string | null;
|
||||
deletionDate: Date;
|
||||
safety_email: string | null;
|
||||
deletion_date: string;
|
||||
support_email: string | null;
|
||||
};
|
||||
account_deletion_scheduled_requested: {
|
||||
username: string;
|
||||
reason: string | null;
|
||||
deletionDate: Date;
|
||||
deletion_date: string;
|
||||
safety_email: string | null;
|
||||
};
|
||||
account_scheduled_deletion: {
|
||||
username: string;
|
||||
reason: string | null;
|
||||
deletionDate: Date;
|
||||
termsUrl: string | null;
|
||||
guidelinesUrl: string | null;
|
||||
legalLinks: EmailLegalLinks;
|
||||
appeals_email: string | null;
|
||||
};
|
||||
account_temp_banned: {
|
||||
username: string;
|
||||
reason: string | null;
|
||||
durationHours: number;
|
||||
bannedUntil: Date;
|
||||
termsUrl: string | null;
|
||||
guidelinesUrl: string | null;
|
||||
legalLinks: EmailLegalLinks;
|
||||
appeals_email: string | null;
|
||||
};
|
||||
donation_confirmation: {
|
||||
amount: string;
|
||||
currency: string;
|
||||
@@ -54,6 +44,7 @@ export interface EmailTemplateVariables {
|
||||
reportId: string;
|
||||
publicComment: string;
|
||||
hasComment: 'yes' | 'no';
|
||||
outcome: ReporterOutcome;
|
||||
appeals_email: string | null;
|
||||
};
|
||||
dsa_report_verification: {
|
||||
@@ -83,6 +74,30 @@ export interface EmailTemplateVariables {
|
||||
username: string;
|
||||
support_email: string | null;
|
||||
};
|
||||
enforcement_ended: {
|
||||
username: string;
|
||||
outcome: 'lifted' | 'reversed';
|
||||
decision_id: string | null;
|
||||
restores_access: 'yes' | 'no';
|
||||
statement: string | null;
|
||||
content_removed: 'yes' | 'no';
|
||||
};
|
||||
enforcement_notice: {
|
||||
username: string;
|
||||
kind_label: EnforcementNoticeKindLabel;
|
||||
neutral: 'yes' | 'no';
|
||||
summary: string;
|
||||
effects: string;
|
||||
rule_title: string;
|
||||
rule_url: string | null;
|
||||
facts: string;
|
||||
statement: string | null;
|
||||
how_we_decided: string;
|
||||
appeals_email: string | null;
|
||||
appeal_deadline: string;
|
||||
redress: string | null;
|
||||
decision_id: string;
|
||||
};
|
||||
harvest_completed: {
|
||||
username: string;
|
||||
downloadUrl: string;
|
||||
@@ -127,20 +142,11 @@ export interface EmailTemplateVariables {
|
||||
reportId: string;
|
||||
publicComment: string;
|
||||
hasComment: 'yes' | 'no';
|
||||
safety_email: string | null;
|
||||
};
|
||||
scheduled_deletion_notification: {
|
||||
username: string;
|
||||
deletionDate: Date;
|
||||
reason: string | null;
|
||||
outcome: ReporterOutcome;
|
||||
appeals_email: string | null;
|
||||
};
|
||||
self_deletion_scheduled: {
|
||||
username: string;
|
||||
deletionDate: Date;
|
||||
};
|
||||
unban_notification: {
|
||||
username: string;
|
||||
reason: string | null;
|
||||
};
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"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, select, null {إذا كنت ترغب في الاحتفاظ بحسابك، فتواصل مع مسؤولي هذا المثيل قبل ذلك التاريخ.} other {إذا كنت ترغب في الاحتفاظ بحسابك، فاتصل بـ{safety_email} من عنوان البريد الإلكتروني هذا قبل ذلك التاريخ.}}\n\n– فريق {product_name}"
|
||||
"body": "مرحبًا {username}،\n\nلم يكن حسابك في {product_name} نشطًا لفترة طويلة، لذا جدولنا حذفه في {deletion_date}.\n\nللاحتفاظ بحسابك، سجّل الدخول قبل ذلك التاريخ. {support_email, select, null {إذا كانت لديك أسئلة، فتواصل مع مسؤولي هذا المثيل.} other {إذا كانت لديك أسئلة، فتواصل مع {support_email}.}}\n\nفريق {product_name}"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"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, select, null {إذا لم تطلب هذا، أو كنت ترغب في الاحتفاظ بحسابك، فتواصل مع مسؤولي هذا المثيل قبل ذلك التاريخ.} other {إذا لم تطلب هذا، أو كنت ترغب في الاحتفاظ بحسابك، فاتصل بـ{safety_email} من عنوان البريد الإلكتروني هذا قبل ذلك التاريخ.}}\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{legalLinks, select,\n both {يرجى مراجعة:\n- شروط الخدمة: {termsUrl}\n- إرشادات المجتمع: {guidelinesUrl}\n\n}\n terms {يرجى مراجعة:\n- شروط الخدمة: {termsUrl}\n\n}\n guidelines {يرجى مراجعة:\n- إرشادات المجتمع: {guidelinesUrl}\n\n}\n other {}\n}عملية الاستئناف:\nإذا كنت تعتقد أن قرار الإنفاذ هذا كان غير صحيح أو غير مبرر، لديك 60 يومًا لتقديم استئناف. {appeals_email, select, null {تواصل مع مسؤولي هذا المثيل.} other {أرسل بريدًا إلكترونيًا إلى {appeals_email} من عنوان البريد الإلكتروني هذا.}}\n\nفي استئنافك:\n- اشرح بوضوح سبب اعتقادك أن قرار الإنفاذ كان غير صحيح أو غير مبرر\n- قدم أي دليل أو سياق ذي صلة\n\nسيقوم عضو من فريق أمان {product_name} بمراجعة استئنافك وقد يوقف الحذف المعلق مؤقتًا إلى حين التوصل إلى قرار نهائي.\n\n– فريق أمان {product_name}"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "تم تعليق حسابك في {product_name} مؤقتًا",
|
||||
"body": "مرحبًا {username}،\n\nتم تعليق حسابك في {product_name} مؤقتًا لانتهاك شروط الخدمة أو إرشادات المجتمع الخاصة بنا.\n\nالمدة: {durationHours, plural,\n =1 {ساعة واحدة}\n zero {# ساعة}\n two {ساعتان}\n few {# ساعات}\n many {# ساعة}\n other {# ساعة}\n}\nمعلق حتى: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {السبب: {reason}\n\n}\n}خلال هذه الفترة، لن تتمكن من الوصول إلى حسابك.\n\n{legalLinks, select,\n both {يرجى مراجعة:\n- شروط الخدمة: {termsUrl}\n- إرشادات المجتمع: {guidelinesUrl}\n\n}\n terms {يرجى مراجعة:\n- شروط الخدمة: {termsUrl}\n\n}\n guidelines {يرجى مراجعة:\n- إرشادات المجتمع: {guidelinesUrl}\n\n}\n other {}\n}إذا كنت تعتقد أن قرار الإنفاذ هذا كان غير صحيح أو غير مبرر، يمكنك تقديم استئناف. {appeals_email, select, null {تواصل مع مسؤولي هذا المثيل واشرح بوضوح سبب اعتقادك أن القرار كان غير صحيح.} other {أرسل بريدًا إلكترونيًا إلى {appeals_email} من عنوان البريد الإلكتروني هذا واشرح بوضوح سبب اعتقادك أن القرار كان غير صحيح.}} سنراجع استئنافك ونرد بقرارنا.\n\n– فريق أمان {product_name}"
|
||||
"body": "مرحبًا {username}،\n\nبناءً على طلبك، جدولنا حذف حسابك في {product_name} في {deletion_date}.\n\nللاحتفاظ بحسابك، سجّل الدخول قبل {deletion_date}. {safety_email, select, null {إذا لم تطلب ذلك، فتواصل مع مسؤولي هذا المثيل.} other {إذا لم تطلب ذلك، فتواصل مع {safety_email}.}}\n\nفريق {product_name}"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "شكرًا لك على تبرعك لـ{product_name}",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "اتخذنا قرارًا بشأن بلاغك في {product_name}",
|
||||
"body": "مرحبًا،\n\nراجع فريق الأمان لدينا بلاغك بموجب قانون الخدمات الرقمية (المعرف: {reportId}) واتخذ قرارًا.{hasComment, select, yes {\n\nرد فريق الأمان:\n{publicComment}} other {}}\n\nلا نشارك تفاصيل أي إجراء يُتخذ بحق حساب شخص آخر، لأنها بياناته الشخصية.\n\nإذا كنت لا توافق على هذا القرار، يمكنك تقديم استئناف مجانًا خلال 60 يومًا. {appeals_email, select, null {تواصل مع مسؤولي هذا المثيل، واذكر معرف بلاغك، واشرح سبب اعتقادك أن القرار غير صحيح.} other {أرسل بريدًا إلكترونيًا إلى {appeals_email} من عنوان البريد الإلكتروني هذا، واذكر معرف بلاغك، واشرح سبب اعتقادك أن القرار غير صحيح.}} إذا كنت في الاتحاد الأوروبي، يمكنك أيضًا إحالة النزاع إلى هيئة معتمدة لتسوية المنازعات خارج المحكمة. لا يؤثر أي من ذلك على حقك في اللجوء إلى القضاء.\n\n– فريق أمان {product_name}"
|
||||
"body": "مرحبًا،\n\nراجعنا بلاغك (المعرّف: {reportId}).\n\n{outcome, select, removed {أزلنا المحتوى الذي أبلغت عنه لأنه خالف قواعدنا.} restricted {وجدنا أن الحساب الذي أبلغت عنه خالف قواعدنا، وقيّدناه. لا نشارك الإجراء الذي اتخذناه ضد حساب آخر، لأن ذلك من بيانات صاحبه الشخصية.} action_taken {وجدنا أن ما أبلغت عنه خالف قواعدنا، واتخذنا إجراءً. لا نشارك الإجراء الذي اتخذناه ضد حساب آخر، لأن ذلك من بيانات صاحبه الشخصية.} no_violation {وجدنا أن المحتوى أو الحساب الذي أبلغت عنه لا يخالف قواعدنا، لذا تركناه كما هو.} other {أغلقنا بلاغك.}}{hasComment, select, yes {\n\nملاحظة من فريقنا: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nإذا كنت لا توافق، فراسل {appeals_email} خلال 60 يومًا واذكر معرّف البلاغ. إذا كنت في الاتحاد الأوروبي، يمكنك أيضًا إحالة الأمر إلى هيئة معتمدة لتسوية النزاعات خارج المحاكم أو تقديم شكوى إلى منسق الخدمات الرقمية (Digital Services Coordinator) في بلدك.}}\n\nفريق {product_name}"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "تحقق من بريدك الإلكتروني لتقديم بلاغ DSA",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "تحقق من بريدك الإلكتروني في {product_name}",
|
||||
"body": "مرحبًا {username}،\n\nيرجى التحقق من عنوان البريد الإلكتروني لحسابك في {product_name} بالنقر على الرابط أدناه:\n\n{verifyUrl}\n\nإذا لم تقم بإنشاء حساب {product_name}، يمكنك تجاهل هذا البريد الإلكتروني بأمان.\n\nهذا الرابط صالح لمدة 24 ساعة.\n\n– فريق {product_name}"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {ألغينا قرارًا بشأن حسابك في {product_name}} other {انتهى تقييد على حسابك في {product_name}}}",
|
||||
"body": "مرحبًا {username}،\n\n{outcome, select, reversed {{decision_id, select, null {راجعنا مجددًا قرارًا بشأن حسابك وألغيناه.} other {راجعنا مجددًا القرار {decision_id} وألغيناه.}} نعتذر عن الإزعاج.} other {{decision_id, select, null {رفعنا التقييد عن حسابك.} other {رفعنا التقييد الناتج عن القرار {decision_id}.}}}}{restores_access, select, yes { يمكنك تسجيل الدخول واستخدام {product_name} مجددًا.} other {}}{statement, select, null {} other {\n\nملاحظة من فريقنا: {statement}}}{content_removed, select, yes {\n\nلا يمكن استعادة المحتوى الذي أزلناه.} other {}}\n\nفريق {product_name}"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {تحديث بشأن حسابك في {product_name}} other {{kind_label, select, warning {تحذير بشأن حسابك في {product_name}} suspension {تم تعليق حسابك في {product_name}} ban {تم حظر حسابك في {product_name}} closure {تم إغلاق حسابك في {product_name}} feature_limit {تم تقييد بعض الميزات في حسابك في {product_name}} content_removal {أزلنا رسائل أرسلتها في {product_name}} profile_reset {أعدنا تعيين أجزاء من ملفك الشخصي في {product_name}} community_removal {أزلنا مجتمعك في {product_name}} bot {تحديث بشأن البوت الخاص بك في {product_name}} other {تحديث بشأن حسابك في {product_name}}}}}",
|
||||
"body": "مرحبًا {username}،\n\n{summary}\n\n{effects}\n\nالسبب\nالقاعدة: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nملاحظة من فريقنا: {statement}}}\n\nكيف اتخذنا القرار\n{how_we_decided}{appeals_email, select, null {} other {\n\nكيفية الطعن\nإذا كنت تعتقد أننا أخطأنا، فراسل {appeals_email} في موعد أقصاه {appeal_deadline}. راسلنا من عنوان البريد الإلكتروني المرتبط بحسابك إن أمكن، واذكر معرّف القرار. الطعن مجاني، ويراجع كل طعن شخص من فريقنا.{redress, select, null {} other {\n\n{redress}}}}}\n\nمعرّف القرار: {decision_id}\n\nفريق {product_name}"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "تمت إزالة مزايا الهدية التي استرددتها",
|
||||
"body": "مرحبًا {username}،\n\nدفع شخص آخر في الأصل ثمن رمز الهدية الذي استرددته، وقد تم التراجع عن تلك الدفعة لاحقًا (رد مبلغ مدفوع).\n\nولهذا السبب أزلنا المزايا التي أُضيفت إلى حسابك عند استرداد الهدية.\n\n{support_email, select, null {إذا كنت تعتقد أن هذا خطأ، يرجى التواصل مع مسؤولي هذا المثيل مع ذكر أي تفاصيل لديك عن رمز الهدية ووقت استردادك له.} other {إذا كنت تعتقد أن هذا خطأ، يرجى الاتصال بفريق الدعم لدينا مع ذكر أي تفاصيل لديك عن رمز الهدية ووقت استردادك له.}}\n\n– فريق {product_name}"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "تمت مراجعة بلاغك في {product_name}",
|
||||
"body": "مرحبًا {username}،\n\nتمت مراجعة بلاغك (المعرف: {reportId}) من قبل فريق الأمان لدينا.{hasComment, select, yes {\n\nرد فريق الأمان:\n{publicComment}} other {}}\n\nشكرًا لمساعدتك في الحفاظ على {product_name} آمنًا للجميع. نحن نأخذ جميع البلاغات على محمل الجد ونقدر مساهمتك في المجتمع.\n\n{safety_email, select, null {إذا كان لديك أي أسئلة أو مخاوف بشأن هذه النتيجة، يرجى التواصل مع مسؤولي هذا المثيل.} other {إذا كان لديك أي أسئلة أو مخاوف بشأن هذه النتيجة، يرجى الاتصال بـ{safety_email}.}}\n\n– فريق أمان {product_name}"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"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إذا كنت تعتقد أن قرار الإنفاذ هذا كان غير صحيح، يمكنك تقديم استئناف. {appeals_email, select, null {تواصل مع مسؤولي هذا المثيل.} other {أرسل بريدًا إلكترونيًا إلى {appeals_email} من عنوان البريد الإلكتروني هذا.}}\n\n– فريق أمان {product_name}"
|
||||
"body": "مرحبًا {username}،\n\nراجعنا بلاغك (المعرّف: {reportId}).\n\n{outcome, select, removed {أزلنا المحتوى الذي أبلغت عنه لأنه خالف قواعدنا.} restricted {وجدنا أن الحساب الذي أبلغت عنه خالف قواعدنا، وقيّدناه. لا نشارك الإجراء الذي اتخذناه ضد حساب آخر، لأن ذلك من بيانات صاحبه الشخصية.} action_taken {وجدنا أن ما أبلغت عنه خالف قواعدنا، واتخذنا إجراءً. لا نشارك الإجراء الذي اتخذناه ضد حساب آخر، لأن ذلك من بيانات صاحبه الشخصية.} no_violation {وجدنا أن المحتوى أو الحساب الذي أبلغت عنه لا يخالف قواعدنا، لذا تركناه كما هو.} other {أغلقنا بلاغك.}}{hasComment, select, yes {\n\nملاحظة من فريقنا: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nإذا كنت لا توافق، فراسل {appeals_email} خلال 60 يومًا واذكر معرّف البلاغ. إذا كنت في الاتحاد الأوروبي، يمكنك أيضًا إحالة الأمر إلى هيئة معتمدة لتسوية النزاعات خارج المحاكم أو تقديم شكوى إلى منسق الخدمات الرقمية (Digital Services Coordinator) في بلدك.}}\n\nفريق {product_name}"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "تمت جدولة حذف حسابك في {product_name}",
|
||||
"body": "مرحبًا {username}،\n\nلقد طلبت حذف حسابك في {product_name}. تمت جدولة حسابك للحذف الدائم في:\n\n{deletionDate, date, full} الساعة {deletionDate, time, short}\n\nإذا لم تطلب هذا، سجل الدخول إلى حسابك لإلغاء الحذف. نوصي أيضًا بتغيير كلمة المرور الخاصة بك لتأمين حسابك.\n\n– فريق {product_name}"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "تم رفع تعليق حسابك في {product_name}",
|
||||
"body": "مرحبًا {username}،\n\nأخبار سارة: تم رفع تعليق حسابك في {product_name}.\n\n{reason, select,\n null {}\n other {السبب: {reason}\n\n}\n}يمكنك الآن تسجيل الدخول مرة أخرى ومواصلة استخدام {product_name} كالمعتاد.\n\n– فريق أمان {product_name}"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"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, select, null {Ако искаш да запазиш акаунта си, свържи се с администраторите на тази инстанция преди тази дата.} other {Ако искаш да запазиш акаунта си, свържи се с {safety_email} от този имейл адрес преди тази дата.}}\n\n– Екип на {product_name}"
|
||||
"body": "Здравей, {username},\n\nАкаунтът ти във {product_name} е неактивен от дълго време, затова насрочихме изтриването му за {deletion_date}.\n\nЗа да запазиш акаунта си, влез в него преди тази дата. {support_email, select, null {Ако имаш въпроси, свържи се с администраторите на тази инстанция.} other {Ако имаш въпроси, пиши на {support_email}.}}\n\nЕкипът на {product_name}"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"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, select, null {Ако не си поискал това или искаш да запазиш акаунта си, свържи се с администраторите на тази инстанция преди тази дата.} other {Ако не си поискал това или искаш да запазиш акаунта си, свържи се с {safety_email} от този имейл адрес преди тази дата.}}\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{legalLinks, select,\n both {Прегледай:\n- Общи условия: {termsUrl}\n- Насоки на общността: {guidelinesUrl}\n\n}\n terms {Прегледай:\n- Общи условия: {termsUrl}\n\n}\n guidelines {Прегледай:\n- Насоки на общността: {guidelinesUrl}\n\n}\n other {}\n}Процес на обжалване:\nАко смяташ, че това решение е неправилно или необосновано, имаш 60 дни да подадеш обжалване. {appeals_email, select, null {Свържи се с администраторите на тази инстанция.} other {Изпрати имейл на {appeals_email} от този имейл адрес.}}\n\nВ обжалването си:\n- Обясни ясно защо смяташ, че решението е неправилно или необосновано\n- Приложи всички релевантни доказателства или контекст\n\nЧлен на екипа за безопасност на {product_name} ще прегледа обжалването ти и може да спре предстоящото изтриване, докато не бъде взето окончателно решение.\n\n– Екип за безопасност на {product_name}"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "Акаунтът ти във {product_name} е временно спрян",
|
||||
"body": "Здравей, {username},\n\nАкаунтът ти във {product_name} е временно спрян поради нарушаване на нашите Общи условия или Насоки на общността.\n\nПродължителност: {durationHours, plural,\n =1 {1 час}\n other {# часа}\n}\nСпрян до: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Причина: {reason}\n\n}\n}През това време няма да имаш достъп до акаунта си.\n\n{legalLinks, select,\n both {Прегледай:\n- Общи условия: {termsUrl}\n- Насоки на общността: {guidelinesUrl}\n\n}\n terms {Прегледай:\n- Общи условия: {termsUrl}\n\n}\n guidelines {Прегледай:\n- Насоки на общността: {guidelinesUrl}\n\n}\n other {}\n}Ако смяташ, че това решение е неправилно или необосновано, можеш да подадеш обжалване. {appeals_email, select, null {Свържи се с администраторите на тази инстанция и ясно обясни защо смяташ, че решението е неправилно.} other {Изпрати имейл на {appeals_email} от този имейл адрес и ясно обясни защо смяташ, че решението е неправилно.}} Ще прегледаме обжалването ти и ще ти отговорим с решението си.\n\n– Екип за безопасност на {product_name}"
|
||||
"body": "Здравей, {username},\n\nПо твое желание насрочихме изтриването на акаунта ти във {product_name} за {deletion_date}.\n\nЗа да запазиш акаунта си, влез в него преди {deletion_date}. {safety_email, select, null {Ако заявката не е от теб, свържи се с администраторите на тази инстанция.} other {Ако заявката не е от теб, пиши на {safety_email}.}}\n\nЕкипът на {product_name}"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Благодарим ти за дарението към {product_name}",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "Взехме решение по сигнала ти във {product_name}",
|
||||
"body": "Здравей,\n\nНашият екип за безопасност прегледа сигнала ти по Закона за цифровите услуги (ID: {reportId}) и взе решение.{hasComment, select, yes {\n\nОтговор от екипа за безопасност:\n{publicComment}} other {}}\n\nНе споделяме подробности за действия, предприети срещу акаунта на друг човек, защото това са негови лични данни.\n\nАко не си съгласен с това решение, можеш да го обжалваш безплатно в срок от 60 дни. {appeals_email, select, null {Свържи се с администраторите на тази инстанция, посочи ID на сигнала си и обясни защо смяташ, че решението е неправилно.} other {Изпрати имейл на {appeals_email} от този имейл адрес, посочи ID на сигнала си и обясни защо смяташ, че решението е неправилно.}} Ако се намираш в ЕС, можеш също да отнесеш спора към сертифициран орган за извънсъдебно решаване на спорове. Нищо от това не засяга правото ти да се обърнеш към съда.\n\n– Екип за безопасност на {product_name}"
|
||||
"body": "Здравей,\n\nПрегледахме сигнала ти (ID: {reportId}).\n\n{outcome, select, removed {Премахнахме съдържанието от сигнала ти, защото нарушаваше правилата ни.} restricted {Установихме, че акаунтът от сигнала ти е нарушил правилата ни, и го ограничихме. Не споделяме какви мерки сме предприели срещу друг акаунт, защото това са лични данни на собственика му.} action_taken {Установихме, че посоченото в сигнала ти е нарушило правилата ни, и предприехме мерки. Не споделяме какви мерки сме предприели срещу друг акаунт, защото това са лични данни на собственика му.} no_violation {Установихме, че съдържанието или акаунтът от сигнала ти не нарушава правилата ни, затова не променихме нищо.} other {Закрихме сигнала ти.}}{hasComment, select, yes {\n\nБележка от нашия екип: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nАко не приемаш решението, пиши на {appeals_email} в рамките на 60 дни и посочи ID на сигнала. Ако си в ЕС, можеш също да отнесеш случая до сертифициран орган за извънсъдебно решаване на спорове или да подадеш жалба до координатора за цифровите услуги в твоята държава.}}\n\nЕкипът на {product_name}"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Потвърди имейла си за сигнал по DSA",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Потвърди имейл адреса си във {product_name}",
|
||||
"body": "Здравей, {username},\n\nПотвърди имейл адреса за акаунта си във {product_name}, като кликнеш върху линка по-долу:\n\n{verifyUrl}\n\nАко не си създал акаунт във {product_name}, можеш спокойно да игнорираш този имейл.\n\nТози линк е валиден 24 часа.\n\n– Екип на {product_name}"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {Отменихме решение за акаунта ти във {product_name}} other {Ограничението на акаунта ти във {product_name} приключи}}",
|
||||
"body": "Здравей, {username},\n\n{outcome, select, reversed {{decision_id, select, null {Прегледахме отново решение за акаунта ти и го отменихме.} other {Прегледахме отново решение {decision_id} и го отменихме.}} Съжаляваме за неудобството.} other {{decision_id, select, null {Премахнахме ограничението на акаунта ти.} other {Премахнахме ограничението по решение {decision_id}.}}}}{restores_access, select, yes { Отново можеш да влизаш и да използваш {product_name}.} other {}}{statement, select, null {} other {\n\nБележка от нашия екип: {statement}}}{content_removed, select, yes {\n\nПремахнатото съдържание не може да бъде възстановено.} other {}}\n\nЕкипът на {product_name}"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {Информация за акаунта ти във {product_name}} other {{kind_label, select, warning {Предупреждение за акаунта ти във {product_name}} suspension {Акаунтът ти във {product_name} е спрян} ban {Акаунтът ти във {product_name} е блокиран} closure {Акаунтът ти във {product_name} е закрит} feature_limit {Някои функции на акаунта ти във {product_name} са ограничени} content_removal {Премахнахме твои съобщения във {product_name}} profile_reset {Нулирахме части от профила ти във {product_name}} community_removal {Премахнахме твоята общност във {product_name}} bot {Информация за твоя бот във {product_name}} other {Информация за акаунта ти във {product_name}}}}}",
|
||||
"body": "Здравей, {username},\n\n{summary}\n\n{effects}\n\nЗащо\nПравило: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nБележка от нашия екип: {statement}}}\n\nКак решихме\n{how_we_decided}{appeals_email, select, null {} other {\n\nКак да обжалваш\nАко смяташ, че сме сгрешили, пиши на {appeals_email} до {appeal_deadline}. Ако можеш, пиши от имейл адреса на акаунта си и посочи ID на решението. Обжалването е безплатно и всяка жалба се преглежда от човек.{redress, select, null {} other {\n\n{redress}}}}}\n\nID на решението: {decision_id}\n\nЕкипът на {product_name}"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Предимствата от осребрения ти подарък са премахнати",
|
||||
"body": "Здравей, {username},\n\nКод за подарък, който си осребрил, първоначално е бил платен от някой друг. Оттогава плащането е върнато чрез оспорване на трансакция.\n\nПоради това премахнахме предимствата, които бяха добавени към акаунта ти, когато осребри подаръка.\n\n{support_email, select, null {Ако смяташ, че това е грешка, свържи се с администраторите на тази инстанция и посочи всички подробности, които имаш за кода за подарък и кога си го осребрил.} other {Ако смяташ, че това е грешка, свържи се с нашия екип за поддръжка и посочи всички подробности, които имаш за кода за подарък и кога си го осребрил.}}\n\n– Екип на {product_name}"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "Сигналът ти във {product_name} е прегледан",
|
||||
"body": "Здравей, {username},\n\nСигналът ти (ID: {reportId}) е прегледан от нашия екип за безопасност.{hasComment, select, yes {\n\nОтговор от екипа за безопасност:\n{publicComment}} other {}}\n\nБлагодарим ти, че помагаш на {product_name} да остане безопасно място за всички. Приемаме всички сигнали сериозно и ценим приноса ти към общността.\n\n{safety_email, select, null {Ако имаш въпроси или притеснения относно този резултат, свържи се с администраторите на тази инстанция.} other {Ако имаш въпроси или притеснения относно този резултат, свържи се с {safety_email}.}}\n\n– Екип за безопасност на {product_name}"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"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Ако смяташ, че това решение е неправилно, можеш да подадеш обжалване. {appeals_email, select, null {Свържи се с администраторите на тази инстанция.} other {Изпрати имейл на {appeals_email} от този имейл адрес.}}\n\n– Екип за безопасност на {product_name}"
|
||||
"body": "Здравей, {username},\n\nПрегледахме сигнала ти (ID: {reportId}).\n\n{outcome, select, removed {Премахнахме съдържанието от сигнала ти, защото нарушаваше правилата ни.} restricted {Установихме, че акаунтът от сигнала ти е нарушил правилата ни, и го ограничихме. Не споделяме какви мерки сме предприели срещу друг акаунт, защото това са лични данни на собственика му.} action_taken {Установихме, че посоченото в сигнала ти е нарушило правилата ни, и предприехме мерки. Не споделяме какви мерки сме предприели срещу друг акаунт, защото това са лични данни на собственика му.} no_violation {Установихме, че съдържанието или акаунтът от сигнала ти не нарушава правилата ни, затова не променихме нищо.} other {Закрихме сигнала ти.}}{hasComment, select, yes {\n\nБележка от нашия екип: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nАко не приемаш решението, пиши на {appeals_email} в рамките на 60 дни и посочи ID на сигнала. Ако си в ЕС, можеш също да отнесеш случая до сертифициран орган за извънсъдебно решаване на спорове или да подадеш жалба до координатора за цифровите услуги в твоята държава.}}\n\nЕкипът на {product_name}"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "Изтриването на акаунта ти във {product_name} е насрочено",
|
||||
"body": "Здравей, {username},\n\nПоискал си изтриване на акаунта си във {product_name}. Акаунтът ти е насрочен за постоянно изтриване на:\n\n{deletionDate, date, full} в {deletionDate, time, short}\n\nАко не си поискал това, влез в акаунта си, за да отмениш изтриването. Препоръчваме също да смениш паролата си, за да защитиш акаунта си.\n\n– Екип на {product_name}"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "Спирането на акаунта ти във {product_name} е отменено",
|
||||
"body": "Здравей, {username},\n\nДобра новина: спирането на акаунта ти във {product_name} е отменено.\n\n{reason, select,\n null {}\n other {Причина: {reason}\n\n}\n}Вече можеш да влезеш отново и да продължиш да използваш {product_name} както обикновено.\n\n– Екип за безопасност на {product_name}"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"subject": "Váš účet {product_name} bude smazán kvůli neaktivitě",
|
||||
"body": "Dobrý den, {username},\n\nVáš účet {product_name} byl dlouho neaktivní, a proto je jeho trvalé smazání naplánováno na:\n\n{deletionDate, date, full} v {deletionDate, time, short}{reason, select, null {} other {\n\nDůvod: {reason}}}\n\n{safety_email, select, null {Pokud si chcete účet ponechat, kontaktujte před tímto datem správce této instance.} other {Pokud si chcete účet ponechat, kontaktujte před tímto datem {safety_email} z této e-mailové adresy.}}\n\n– Tým {product_name}"
|
||||
"body": "Dobrý den, {username},\n\nVáš účet {product_name} byl dlouho neaktivní, a proto jsme jeho smazání naplánovali na {deletion_date}.\n\nPokud si chcete účet ponechat, přihlaste se před tímto datem. {support_email, select, null {Pokud máte dotazy, kontaktujte správce této instance.} other {Pokud máte dotazy, kontaktujte {support_email}.}}\n\nTým {product_name}"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"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. {safety_email, select, null {Pokud jste o smazání nežádali nebo si chcete účet ponechat, kontaktujte před tímto datem správce této instance.} other {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_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}\n}Jedná se o závažné opatření. Data vašeho účtu budou v naplánovaný den trvale smazána.\n\n{legalLinks, select,\n both {Přečtěte si:\n- Podmínky služby: {termsUrl}\n- Komunitní pravidla: {guidelinesUrl}\n\n}\n terms {Přečtěte si:\n- Podmínky služby: {termsUrl}\n\n}\n guidelines {Přečtěte si:\n- Komunitní pravidla: {guidelinesUrl}\n\n}\n other {}\n}Postup 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í. {appeals_email, select, null {Kontaktujte správce této instance.} other {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}"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "Váš účet {product_name} byl dočasně pozastaven",
|
||||
"body": "Dobrý den, {username},\n\nVáš účet {product_name} byl dočasně pozastaven kvůli porušení našich podmínek služby nebo komunitních pravidel.\n\nDoba trvání: {durationHours, plural,\n one {# hodina}\n few {# hodiny}\n many {# hodiny}\n other {# hodin}\n}\nPozastaven do: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Důvod: {reason}\n\n}\n}Během této doby nebudete mít přístup ke svému účtu.\n\n{legalLinks, select,\n both {Přečtěte si:\n- Podmínky služby: {termsUrl}\n- Komunitní pravidla: {guidelinesUrl}\n\n}\n terms {Přečtěte si:\n- Podmínky služby: {termsUrl}\n\n}\n guidelines {Přečtěte si:\n- Komunitní pravidla: {guidelinesUrl}\n\n}\n other {}\n}Pokud se domníváte, že toto rozhodnutí bylo nesprávné nebo neopodstatněné, můžete podat odvolání. {appeals_email, select, null {Kontaktujte správce této instance a jasně vysvětlete, proč považujete rozhodnutí za nesprávné.} other {Pošlete e-mail na {appeals_email} z této e-mailové adresy a jasně vysvětlete, proč považujete rozhodnutí za nesprávné.}} Vaše odvolání přezkoumáme a sdělíme vám své rozhodnutí.\n\n– Bezpečnostní tým {product_name}"
|
||||
"body": "Dobrý den, {username},\n\nNa vaši žádost jsme naplánovali smazání vašeho účtu {product_name} na {deletion_date}.\n\nPokud si chcete účet ponechat, přihlaste se před {deletion_date}. {safety_email, select, null {Pokud tato žádost nepochází od vás, kontaktujte správce této instance.} other {Pokud tato žádost nepochází od vás, kontaktujte {safety_email}.}}\n\nTým {product_name}"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Děkujeme za váš dar pro {product_name}",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "Rozhodli jsme o vašem nahlášení na {product_name}",
|
||||
"body": "Dobrý den,\n\nNáš bezpečnostní tým přezkoumal vaše nahlášení podle nařízení o digitálních službách (ID: {reportId}) a rozhodl o něm.{hasComment, select, yes {\n\nOdpověď bezpečnostního týmu:\n{publicComment}} other {}}\n\nPodrobnosti o opatřeních přijatých vůči účtu jiné osoby nesdělujeme, protože jde o její osobní údaje.\n\nPokud s tímto rozhodnutím nesouhlasíte, můžete se do 60 dnů bezplatně odvolat. {appeals_email, select, null {Kontaktujte správce této instance, uveďte ID svého nahlášení a vysvětlete, proč považujete rozhodnutí za nesprávné.} other {Pošlete e-mail na {appeals_email} z této e-mailové adresy, uveďte ID svého nahlášení a vysvětlete, proč považujete rozhodnutí za nesprávné.}} Pokud jste v EU, můžete spor také předložit certifikovanému subjektu pro mimosoudní řešení sporů. Nic z toho nemá vliv na vaše právo obrátit se na soud.\n\n– Bezpečnostní tým {product_name}"
|
||||
"body": "Dobrý den,\n\nPosoudili jsme vaše nahlášení (ID: {reportId}).\n\n{outcome, select, removed {Nahlášený obsah jsme odstranili, protože porušoval naše pravidla.} restricted {Zjistili jsme, že nahlášený účet porušil naše pravidla, a omezili jsme ho. Nesdělujeme, jaké opatření jsme vůči jinému účtu přijali, protože jde o osobní údaje jeho majitele.} action_taken {Zjistili jsme, že nahlášený obsah nebo účet porušil naše pravidla, a přijali jsme opatření. Nesdělujeme, jaké opatření jsme vůči jinému účtu přijali, protože jde o osobní údaje jeho majitele.} no_violation {Zjistili jsme, že nahlášený obsah nebo účet naše pravidla neporušuje, a proto jsme vše ponechali beze změny.} other {Vaše nahlášení jsme uzavřeli.}}{hasComment, select, yes {\n\nPoznámka od našeho týmu: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nPokud nesouhlasíte, napište do 60 dnů na {appeals_email} a uveďte ID nahlášení. Pokud se nacházíte v EU, můžete věc také předložit certifikovanému subjektu pro mimosoudní řešení sporů nebo podat stížnost koordinátorovi digitálních služeb ve své zemi.}}\n\nTým {product_name}"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Ověřte svůj e-mail pro nahlášení podle DSA",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Ověřte svou e-mailovou adresu pro {product_name}",
|
||||
"body": "Dobrý den, {username},\n\nKliknutím na odkaz níže ověřte e-mailovou adresu svého účtu {product_name}:\n\n{verifyUrl}\n\nPokud jste si účet {product_name} nevytvořili, můžete tento e-mail bez obav ignorovat.\n\nTento odkaz je platný 24 hodin.\n\n– Tým {product_name}"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {Zrušili jsme rozhodnutí týkající se vašeho účtu {product_name}} other {Omezení vašeho účtu {product_name} skončilo}}",
|
||||
"body": "Dobrý den, {username},\n\n{outcome, select, reversed {{decision_id, select, null {Znovu jsme posoudili rozhodnutí týkající se vašeho účtu a zrušili ho.} other {Znovu jsme posoudili rozhodnutí {decision_id} a zrušili ho.}} Omlouváme se za potíže.} other {{decision_id, select, null {Ukončili jsme omezení vašeho účtu.} other {Ukončili jsme omezení uložené rozhodnutím {decision_id}.}}}}{restores_access, select, yes { Můžete se znovu přihlásit a používat {product_name}.} other {}}{statement, select, null {} other {\n\nPoznámka od našeho týmu: {statement}}}{content_removed, select, yes {\n\nOdstraněný obsah nelze obnovit.} other {}}\n\nTým {product_name}"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {Informace o vašem účtu {product_name}} other {{kind_label, select, warning {Varování týkající se vašeho účtu {product_name}} suspension {Váš účet {product_name} je pozastaven} ban {Váš účet {product_name} je zablokován} closure {Váš účet {product_name} je zrušen} feature_limit {Některé funkce vašeho účtu {product_name} jsou omezeny} content_removal {Odstranili jsme vaše zprávy na {product_name}} profile_reset {Resetovali jsme část vašeho profilu {product_name}} community_removal {Odstranili jsme vaši komunitu na {product_name}} bot {Informace o vašem botovi na {product_name}} other {Informace o vašem účtu {product_name}}}}}",
|
||||
"body": "Dobrý den, {username},\n\n{summary}\n\n{effects}\n\nProč\nPravidlo: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nPoznámka od našeho týmu: {statement}}}\n\nJak jsme rozhodli\n{how_we_decided}{appeals_email, select, null {} other {\n\nJak se odvolat\nPokud si myslíte, že jsme udělali chybu, napište na {appeals_email} do {appeal_deadline}. Pokud můžete, pište z e-mailové adresy svého účtu a uveďte ID rozhodnutí. Odvolání je zdarma a každé posoudí člověk.{redress, select, null {} other {\n\n{redress}}}}}\n\nID rozhodnutí: {decision_id}\n\nTým {product_name}"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Výhody z uplatněného dárku byly odstraněny",
|
||||
"body": "Dobrý den, {username},\n\nDárkový kód, který jste uplatnili, původně zaplatil někdo jiný. Tato platba byla následně vrácena na základě reklamace (chargeback).\n\nProto jsme odebrali výhody, které váš účet získal při uplatnění dárku.\n\n{support_email, select, null {Pokud se domníváte, že jde o chybu, kontaktujte správce této instance a uveďte všechny dostupné podrobnosti o dárkovém kódu a o tom, kdy jste ho uplatnili.} other {Pokud se domníváte, že jde o chybu, kontaktujte náš tým podpory a uveďte všechny dostupné podrobnosti o dárkovém kódu a o tom, kdy jste ho uplatnili.}}\n\n– Tým {product_name}"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "Vaše nahlášení na {product_name} bylo přezkoumáno",
|
||||
"body": "Dobrý den, {username},\n\nNáš bezpečnostní tým přezkoumal vaše nahlášení (ID: {reportId}).{hasComment, select, yes {\n\nOdpověď bezpečnostního týmu:\n{publicComment}} other {}}\n\nDěkujeme, že pomáháte udržovat {product_name} bezpečný pro všechny. Všechna nahlášení bereme vážně a vážíme si vašeho přínosu komunitě.\n\n{safety_email, select, null {Pokud máte k tomuto výsledku jakékoli dotazy nebo výhrady, kontaktujte správce této instance.} other {Pokud máte k tomuto výsledku jakékoli dotazy nebo výhrady, kontaktujte {safety_email}.}}\n\n– Bezpečnostní tým {product_name}"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"subject": "Váš účet {product_name} bude trvale smazán",
|
||||
"body": "Dobrý den, {username},\n\nBylo 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}\n}Jedná se o závažné opatření. Data vašeho účtu budou v naplánovaný den trvale smazána.\n\nPokud se domníváte, že toto rozhodnutí bylo nesprávné, můžete podat odvolání. {appeals_email, select, null {Kontaktujte správce této instance.} other {Pošlete e-mail na {appeals_email} z této e-mailové adresy.}}\n\n– Bezpečnostní tým {product_name}"
|
||||
"body": "Dobrý den, {username},\n\nPosoudili jsme vaše nahlášení (ID: {reportId}).\n\n{outcome, select, removed {Nahlášený obsah jsme odstranili, protože porušoval naše pravidla.} restricted {Zjistili jsme, že nahlášený účet porušil naše pravidla, a omezili jsme ho. Nesdělujeme, jaké opatření jsme vůči jinému účtu přijali, protože jde o osobní údaje jeho majitele.} action_taken {Zjistili jsme, že nahlášený obsah nebo účet porušil naše pravidla, a přijali jsme opatření. Nesdělujeme, jaké opatření jsme vůči jinému účtu přijali, protože jde o osobní údaje jeho majitele.} no_violation {Zjistili jsme, že nahlášený obsah nebo účet naše pravidla neporušuje, a proto jsme vše ponechali beze změny.} other {Vaše nahlášení jsme uzavřeli.}}{hasComment, select, yes {\n\nPoznámka od našeho týmu: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nPokud nesouhlasíte, napište do 60 dnů na {appeals_email} a uveďte ID nahlášení. Pokud se nacházíte v EU, můžete věc také předložit certifikovanému subjektu pro mimosoudní řešení sporů nebo podat stížnost koordinátorovi digitálních služeb ve své zemi.}}\n\nTým {product_name}"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "Smazání vašeho účtu {product_name} je naplánováno",
|
||||
"body": "Dobrý den, {username},\n\nPožádali jste o smazání svého účtu {product_name}. Trvalé smazání je naplánováno na:\n\n{deletionDate, date, full} v {deletionDate, time, short}\n\nPokud jste o smazání nežádali, přihlaste se ke svému účtu a smazání zrušte. Doporučujeme také změnit heslo, abyste svůj účet zabezpečili.\n\n– Tým {product_name}"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "Pozastavení vašeho účtu {product_name} bylo zrušeno",
|
||||
"body": "Dobrý den, {username},\n\nDobrá zpráva: pozastavení vašeho účtu {product_name} bylo zrušeno.\n\n{reason, select,\n null {}\n other {Důvod: {reason}\n\n}\n}Nyní se můžete znovu přihlásit a používat {product_name} jako obvykle.\n\n– Bezpečnostní tým {product_name}"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"subject": "Din {product_name}-konto slettes på grund af inaktivitet",
|
||||
"body": "Hej {username},\n\nDin {product_name}-konto har været inaktiv i lang tid, så den slettes permanent den:\n\n{deletionDate, date, full} kl. {deletionDate, time, short}{reason, select, null {} other {\n\nÅrsag: {reason}}}\n\n{safety_email, select, null {Hvis du vil beholde din konto, skal du kontakte administratorerne af denne instans inden denne dato.} other {Hvis du vil beholde din konto, skal du kontakte {safety_email} fra denne e-mailadresse inden denne dato.}}\n\n– Teamet bag {product_name}"
|
||||
"body": "Hej {username},\n\nDin {product_name}-konto har været inaktiv i lang tid, så vi har planlagt sletning af kontoen den {deletion_date}.\n\nHvis du vil beholde din konto, skal du logge ind før den dato. {support_email, select, null {Hvis du har spørgsmål, så kontakt administratorerne af denne instans.} other {Hvis du har spørgsmål, så kontakt {support_email}.}}\n\nTeamet bag {product_name}"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"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. {safety_email, select, null {Hvis du ikke har anmodet om dette, eller hvis du vil beholde din konto, skal du kontakte administratorerne af denne instans inden denne dato.} other {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_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}\n}Dette er en alvorlig sanktion. Dine kontodata bliver permanent slettet på den planlagte dato.\n\n{legalLinks, select,\n both {Gennemgå:\n- Servicevilkår: {termsUrl}\n- Retningslinjer for fællesskabet: {guidelinesUrl}\n\n}\n terms {Gennemgå:\n- Servicevilkår: {termsUrl}\n\n}\n guidelines {Gennemgå:\n- Retningslinjer for fællesskabet: {guidelinesUrl}\n\n}\n other {}\n}Klageproces:\nHvis du mener, at denne beslutning om sanktioner var forkert eller uberettiget, har du 60 dage til at indsende en klage. {appeals_email, select, null {Kontakt administratorerne af denne instans.} other {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}"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "Din {product_name}-konto er midlertidigt suspenderet",
|
||||
"body": "Hej {username},\n\nDin {product_name}-konto er midlertidigt suspenderet på grund af overtrædelser af vores servicevilkår eller retningslinjer for fællesskabet.\n\nVarighed: {durationHours, plural,\n =1 {1 time}\n other {# timer}\n}\nSuspenderet indtil: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Årsag: {reason}\n\n}\n}I denne periode vil du ikke kunne få adgang til din konto.\n\n{legalLinks, select,\n both {Gennemgå:\n- Servicevilkår: {termsUrl}\n- Retningslinjer for fællesskabet: {guidelinesUrl}\n\n}\n terms {Gennemgå:\n- Servicevilkår: {termsUrl}\n\n}\n guidelines {Gennemgå:\n- Retningslinjer for fællesskabet: {guidelinesUrl}\n\n}\n other {}\n}Hvis du mener, at denne beslutning om sanktioner var forkert eller uberettiget, kan du indsende en klage. {appeals_email, select, null {Kontakt administratorerne af denne instans og forklar tydeligt, hvorfor du mener, at beslutningen var forkert.} other {Send en e-mail til {appeals_email} fra denne e-mailadresse og forklar tydeligt, hvorfor du mener, at beslutningen var forkert.}} Vi gennemgår din klage og giver dig besked om vores afgørelse.\n\n– Sikkerhedsteamet hos {product_name}"
|
||||
"body": "Hej {username},\n\nSom du har bedt om, har vi planlagt sletning af din {product_name}-konto den {deletion_date}.\n\nHvis du vil beholde din konto, skal du logge ind før {deletion_date}. {safety_email, select, null {Hvis du ikke har bedt om dette, så kontakt administratorerne af denne instans.} other {Hvis du ikke har bedt om dette, så kontakt {safety_email}.}}\n\nTeamet bag {product_name}"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Tak for din {product_name}-donation",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "Vi har truffet en afgørelse om din anmeldelse på {product_name}",
|
||||
"body": "Hej,\n\nVores sikkerhedsteam har behandlet din anmeldelse i henhold til Digital Services Act (ID: {reportId}) og truffet en afgørelse.{hasComment, select, yes {\n\nSvar fra sikkerhedsteamet:\n{publicComment}} other {}}\n\nVi deler ikke oplysninger om tiltag mod en anden persons konto, fordi det er den persons personoplysninger.\n\nHvis du er uenig i afgørelsen, kan du klage gratis inden for 60 dage. {appeals_email, select, null {Kontakt administratorerne af denne instans, oplys dit anmeldelses-ID, og forklar, hvorfor du mener, at afgørelsen er forkert.} other {Send en e-mail til {appeals_email} fra denne e-mailadresse, oplys dit anmeldelses-ID, og forklar, hvorfor du mener, at afgørelsen er forkert.}} Hvis du befinder dig i EU, kan du også indbringe tvisten for et certificeret organ for udenretslig tvistbilæggelse. Intet af dette påvirker din ret til at gå til domstolene.\n\n– Sikkerhedsteamet hos {product_name}"
|
||||
"body": "Hej,\n\nVi har gennemgået din anmeldelse (ID: {reportId}).\n\n{outcome, select, removed {Vi har fjernet det indhold, du anmeldte, fordi det brød vores regler.} restricted {Vi har konstateret, at den konto, du anmeldte, brød vores regler, og vi har begrænset den. Vi oplyser ikke, hvilke tiltag vi har truffet over for en anden konto, fordi det er den persons personoplysninger.} action_taken {Vi har konstateret, at det, du anmeldte, brød vores regler, og vi har grebet ind. Vi oplyser ikke, hvilke tiltag vi har truffet over for en anden konto, fordi det er den persons personoplysninger.} no_violation {Vi har konstateret, at det indhold eller den konto, du anmeldte, ikke bryder vores regler, så vi har ikke foretaget os noget.} other {Vi har lukket din anmeldelse.}}{hasComment, select, yes {\n\nBesked fra vores team: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nHvis du er uenig, så send en e-mail til {appeals_email} inden for 60 dage, og oplys anmeldelses-ID'et. Hvis du befinder dig i EU, kan du også indbringe sagen for et certificeret organ for udenretslig tvistbilæggelse eller klage til koordinatoren for digitale tjenester i dit land.}}\n\nTeamet bag {product_name}"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Bekræft din e-mailadresse til en DSA-anmeldelse",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Bekræft din {product_name}-e-mailadresse",
|
||||
"body": "Hej {username},\n\nBekræft e-mailadressen for din {product_name}-konto ved at klikke på linket nedenfor:\n\n{verifyUrl}\n\nHvis du ikke har oprettet en {product_name}-konto, kan du trygt ignorere denne e-mail.\n\nDette link er gyldigt i 24 timer.\n\n– Teamet bag {product_name}"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {Vi har omgjort en afgørelse om din {product_name}-konto} other {En begrænsning på din {product_name}-konto er ophørt}}",
|
||||
"body": "Hej {username},\n\n{outcome, select, reversed {{decision_id, select, null {Vi har set på en afgørelse om din konto igen og omgjort den.} other {Vi har set på afgørelse {decision_id} igen og omgjort den.}} Vi beklager besværet.} other {{decision_id, select, null {Vi har ophævet begrænsningen på din konto.} other {Vi har ophævet begrænsningen fra afgørelse {decision_id}.}}}}{restores_access, select, yes { Du kan logge ind og bruge {product_name} igen.} other {}}{statement, select, null {} other {\n\nBesked fra vores team: {statement}}}{content_removed, select, yes {\n\nIndhold, vi har fjernet, kan ikke gendannes.} other {}}\n\nTeamet bag {product_name}"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {Nyt om din {product_name}-konto} other {{kind_label, select, warning {En advarsel om din {product_name}-konto} suspension {Din {product_name}-konto er suspenderet} ban {Din {product_name}-konto er udelukket} closure {Din {product_name}-konto er lukket} feature_limit {Nogle funktioner er begrænset på din {product_name}-konto} content_removal {Vi har fjernet beskeder, du har sendt på {product_name}} profile_reset {Vi har nulstillet dele af din {product_name}-profil} community_removal {Vi har fjernet dit fællesskab på {product_name}} bot {Nyt om din bot på {product_name}} other {Nyt om din {product_name}-konto}}}}",
|
||||
"body": "Hej {username},\n\n{summary}\n\n{effects}\n\nHvorfor\nRegel: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nBesked fra vores team: {statement}}}\n\nSådan traf vi afgørelsen\n{how_we_decided}{appeals_email, select, null {} other {\n\nSådan klager du\nHvis du mener, at vi har taget fejl, så send en e-mail til {appeals_email} senest {appeal_deadline}. Skriv fra e-mailadressen på din konto, hvis du kan, og oplys afgørelses-ID'et. Det er gratis at klage, og en person gennemgår hver klage.{redress, select, null {} other {\n\n{redress}}}}}\n\nAfgørelses-ID: {decision_id}\n\nTeamet bag {product_name}"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Fordele fra din indløste gave er fjernet",
|
||||
"body": "Hej {username},\n\nEn gavekode, du indløste, blev oprindeligt betalt af en anden. Den betaling er siden annulleret (en tilbageførsel).\n\nDerfor har vi fjernet de fordele, der blev tilføjet din konto, da du indløste gaven.\n\n{support_email, select, null {Hvis du mener, at dette er en fejl, kan du kontakte administratorerne af denne instans og oplyse alle detaljer, du har om gavekoden, og hvornår du indløste den.} other {Hvis du mener, at dette er en fejl, kan du kontakte vores supportteam og oplyse alle detaljer, du har om gavekoden, og hvornår du indløste den.}}\n\n– Teamet bag {product_name}"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "Din anmeldelse på {product_name} er blevet behandlet",
|
||||
"body": "Hej {username},\n\nDin anmeldelse (ID: {reportId}) er blevet behandlet af vores sikkerhedsteam.{hasComment, select, yes {\n\nSvar fra sikkerhedsteamet:\n{publicComment}} other {}}\n\nTak for din hjælp til at holde {product_name} sikkert for alle. Vi tager alle anmeldelser alvorligt og værdsætter dit bidrag til fællesskabet.\n\n{safety_email, select, null {Hvis du har spørgsmål eller bekymringer vedrørende dette resultat, kan du kontakte administratorerne af denne instans.} other {Hvis du har spørgsmål eller bekymringer vedrørende dette resultat, kan du kontakte {safety_email}.}}\n\n– Sikkerhedsteamet hos {product_name}"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"subject": "Din {product_name}-konto slettes permanent",
|
||||
"body": "Hej {username},\n\nVi har planlagt at slette din {product_name}-konto permanent.\n\nPlanlagt sletning: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Årsag: {reason}\n\n}\n}Dette er en alvorlig sanktion. Dine kontodata bliver permanent slettet på den planlagte dato.\n\nHvis du mener, at denne beslutning om sanktioner var forkert, kan du indsende en klage. {appeals_email, select, null {Kontakt administratorerne af denne instans.} other {Send en e-mail til {appeals_email} fra denne e-mailadresse.}}\n\n– Sikkerhedsteamet hos {product_name}"
|
||||
"body": "Hej {username},\n\nVi har gennemgået din anmeldelse (ID: {reportId}).\n\n{outcome, select, removed {Vi har fjernet det indhold, du anmeldte, fordi det brød vores regler.} restricted {Vi har konstateret, at den konto, du anmeldte, brød vores regler, og vi har begrænset den. Vi oplyser ikke, hvilke tiltag vi har truffet over for en anden konto, fordi det er den persons personoplysninger.} action_taken {Vi har konstateret, at det, du anmeldte, brød vores regler, og vi har grebet ind. Vi oplyser ikke, hvilke tiltag vi har truffet over for en anden konto, fordi det er den persons personoplysninger.} no_violation {Vi har konstateret, at det indhold eller den konto, du anmeldte, ikke bryder vores regler, så vi har ikke foretaget os noget.} other {Vi har lukket din anmeldelse.}}{hasComment, select, yes {\n\nBesked fra vores team: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nHvis du er uenig, så send en e-mail til {appeals_email} inden for 60 dage, og oplys anmeldelses-ID'et. Hvis du befinder dig i EU, kan du også indbringe sagen for et certificeret organ for udenretslig tvistbilæggelse eller klage til koordinatoren for digitale tjenester i dit land.}}\n\nTeamet bag {product_name}"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "Sletningen af din {product_name}-konto er planlagt",
|
||||
"body": "Hej {username},\n\nDu har anmodet om at slette din {product_name}-konto. Din konto slettes permanent den:\n\n{deletionDate, date, full} kl. {deletionDate, time, short}\n\nHvis du ikke har anmodet om dette, skal du logge ind på din konto for at annullere sletningen. Vi anbefaler også, at du ændrer din adgangskode for at sikre din konto.\n\n– Teamet bag {product_name}"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "Suspensionen af din {product_name}-konto er ophævet",
|
||||
"body": "Hej {username},\n\nGode nyheder: Suspensionen af din {product_name}-konto er ophævet.\n\n{reason, select,\n null {}\n other {Årsag: {reason}\n\n}\n}Du kan nu logge ind igen og fortsætte med at bruge {product_name} som normalt.\n\n– Sikkerhedsteamet hos {product_name}"
|
||||
}
|
||||
}
|
||||
+14
-22
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"subject": "Dein {product_name}-Account wird wegen Inaktivität gelöscht",
|
||||
"body": "Hallo {username},\n\ndein {product_name}-Account war lange inaktiv und wird deshalb an folgendem Termin dauerhaft gelöscht:\n\n{deletionDate, date, full} um {deletionDate, time, short}{reason, select, null {} other {\n\nGrund: {reason}}}\n\n{safety_email, select, null {Wenn du deinen Account behalten möchtest, kontaktiere vor diesem Termin die Administratoren dieser Instanz.} other {Wenn du deinen Account behalten möchtest, kontaktiere vor diesem Termin {safety_email} von dieser E-Mail-Adresse aus.}}\n\n– {product_name}-Team"
|
||||
"body": "Hallo {username},\n\ndein {product_name}-Account war lange inaktiv. Deshalb haben wir seine Löschung für den {deletion_date} geplant.\n\nWenn du deinen Account behalten möchtest, melde dich vor diesem Datum an. {support_email, select, null {Wenn du Fragen hast, kontaktiere die Administratoren dieser Instanz.} other {Wenn du Fragen hast, kontaktiere {support_email}.}}\n\nDein {product_name}-Team"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"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. {safety_email, select, null {Wenn du das nicht beantragt hast oder deinen Account behalten möchtest, kontaktiere vor diesem Termin die Administratoren dieser Instanz.} other {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_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}\n}Dies ist eine ernsthafte Maßnahme. Deine Accountdaten werden am geplanten Datum dauerhaft gelöscht.\n\n{legalLinks, select,\n both {Bitte lies dir Folgendes durch:\n- Nutzungsbedingungen: {termsUrl}\n- Community-Richtlinien: {guidelinesUrl}\n\n}\n terms {Bitte lies dir Folgendes durch:\n- Nutzungsbedingungen: {termsUrl}\n\n}\n guidelines {Bitte lies dir Folgendes durch:\n- Community-Richtlinien: {guidelinesUrl}\n\n}\n other {}\n}Einspruchsverfahren:\nWenn du glaubst, dass diese Entscheidung falsch oder ungerechtfertigt war, hast du 60 Tage Zeit, Einspruch einzulegen. {appeals_email, select, null {Wende dich an die Administratoren dieser Instanz.} other {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"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "Dein {product_name}-Account wurde vorübergehend gesperrt",
|
||||
"body": "Hallo {username},\n\ndein {product_name}-Account wurde vorübergehend gesperrt, weil du gegen unsere Nutzungsbedingungen oder Community-Richtlinien verstoßen hast.\n\nDauer: {durationHours, plural,\n one {1 Stunde}\n other {# Stunden}\n}\nGesperrt bis: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Grund: {reason}\n\n}\n}Während dieser Zeit kannst du nicht auf deinen Account zugreifen.\n\n{legalLinks, select,\n both {Bitte lies dir Folgendes durch:\n- Nutzungsbedingungen: {termsUrl}\n- Community-Richtlinien: {guidelinesUrl}\n\n}\n terms {Bitte lies dir Folgendes durch:\n- Nutzungsbedingungen: {termsUrl}\n\n}\n guidelines {Bitte lies dir Folgendes durch:\n- Community-Richtlinien: {guidelinesUrl}\n\n}\n other {}\n}Wenn du glaubst, dass diese Entscheidung falsch oder ungerechtfertigt war, kannst du Einspruch einlegen. {appeals_email, select, null {Wende dich an die Administratoren dieser Instanz und erkläre klar, warum du die Entscheidung für falsch hältst.} other {Sende eine E-Mail von dieser E-Mail-Adresse an {appeals_email} und erkläre klar, warum du die Entscheidung für falsch hältst.}} Wir werden deinen Einspruch prüfen und dir unsere Entscheidung mitteilen.\n\n– {product_name}-Sicherheitsteam"
|
||||
"body": "Hallo {username},\n\nwie gewünscht haben wir die Löschung deines {product_name}-Accounts für den {deletion_date} geplant.\n\nWenn du deinen Account behalten möchtest, melde dich vor dem {deletion_date} an. {safety_email, select, null {Wenn du das nicht beantragt hast, kontaktiere die Administratoren dieser Instanz.} other {Wenn du das nicht beantragt hast, kontaktiere {safety_email}.}}\n\nDein {product_name}-Team"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Vielen Dank für deine {product_name}-Spende",
|
||||
@@ -28,8 +20,8 @@
|
||||
"body": "Hallo,\n\nklicke auf den Link unten, um auf dein Spendenportal zuzugreifen:\n\n{manageUrl}\n\nIm Portal kannst du Abonnements verwalten, Rechnungen herunterladen und deine Spendenübersicht einsehen.\n\nDieser Link läuft am {expiresAt, date, full} um {expiresAt, time, short} ab.\n\nWenn du diesen Link nicht angefordert hast, kannst du diese E-Mail ignorieren.\n\n– {product_name}-Team"
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "Wir haben über deine {product_name}-Meldung entschieden",
|
||||
"body": "Hallo,\n\nunser Sicherheitsteam hat deine Meldung nach dem Digital Services Act (ID: {reportId}) geprüft und eine Entscheidung getroffen.{hasComment, select, yes {\n\nAntwort vom Sicherheitsteam:\n{publicComment}} other {}}\n\nWir geben keine Einzelheiten zu Maßnahmen gegen den Account einer anderen Person weiter, da es sich um deren personenbezogene Daten handelt.\n\nWenn du mit dieser Entscheidung nicht einverstanden bist, kannst du innerhalb von 60 Tagen kostenlos Einspruch einlegen. {appeals_email, select, null {Wende dich an die Administratoren dieser Instanz, gib deine Meldungs-ID an und erkläre, warum du die Entscheidung für falsch hältst.} other {Sende eine E-Mail von dieser E-Mail-Adresse an {appeals_email}, gib deine Meldungs-ID an und erkläre, warum du die Entscheidung für falsch hältst.}} Wenn du dich in der EU befindest, kannst du die Streitigkeit außerdem einer zertifizierten außergerichtlichen Streitbeilegungsstelle vorlegen. Dein Recht, vor Gericht zu gehen, bleibt davon unberührt.\n\n– {product_name}-Sicherheitsteam"
|
||||
"subject": "Wir haben über deine Meldung auf {product_name} entschieden",
|
||||
"body": "Hallo,\n\nwir haben deine Meldung (ID: {reportId}) geprüft.\n\n{outcome, select, removed {Wir haben den gemeldeten Inhalt entfernt, weil er gegen unsere Regeln verstoßen hat.} restricted {Wir haben festgestellt, dass der gemeldete Account gegen unsere Regeln verstoßen hat, und ihn eingeschränkt. Welche Maßnahme wir gegen einen anderen Account ergriffen haben, teilen wir nicht mit, weil das personenbezogene Daten dieser Person sind.} action_taken {Wir haben festgestellt, dass das, was du gemeldet hast, gegen unsere Regeln verstoßen hat, und haben Maßnahmen ergriffen. Welche Maßnahme wir gegen einen anderen Account ergriffen haben, teilen wir nicht mit, weil das personenbezogene Daten dieser Person sind.} no_violation {Wir haben festgestellt, dass der gemeldete Inhalt oder Account nicht gegen unsere Regeln verstößt. Deshalb haben wir nichts geändert.} other {Wir haben deine Meldung abgeschlossen.}}{hasComment, select, yes {\n\nHinweis von unserem Team: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nWenn du nicht einverstanden bist, sende innerhalb von 60 Tagen eine E-Mail an {appeals_email} und gib die Meldungs-ID an. Wenn du dich in der EU befindest, kannst du die Sache auch einer zertifizierten außergerichtlichen Streitbeilegungsstelle vorlegen oder dich beim Koordinator für digitale Dienste in deinem Land beschweren.}}\n\nDein {product_name}-Team"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Bestätige deine E-Mail-Adresse für eine DSA-Meldung",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Bestätige deine {product_name}-E-Mail-Adresse",
|
||||
"body": "Hallo {username},\n\nbitte bestätige die E-Mail-Adresse für deinen {product_name}-Account, indem du auf den untenstehenden Link klickst:\n\n{verifyUrl}\n\nWenn du keinen {product_name}-Account erstellt hast, kannst du diese E-Mail ignorieren.\n\nDieser Link ist 24 Stunden gültig.\n\n– {product_name}-Team"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {Wir haben eine Entscheidung zu deinem {product_name}-Account zurückgenommen} other {Eine Einschränkung deines {product_name}-Accounts ist beendet}}",
|
||||
"body": "Hallo {username},\n\n{outcome, select, reversed {{decision_id, select, null {wir haben eine Entscheidung zu deinem Account noch einmal geprüft und zurückgenommen.} other {wir haben die Entscheidung {decision_id} noch einmal geprüft und zurückgenommen.}} Entschuldige bitte die Umstände.} other {{decision_id, select, null {wir haben die Einschränkung deines Accounts aufgehoben.} other {wir haben die Einschränkung aus der Entscheidung {decision_id} aufgehoben.}}}}{restores_access, select, yes { Du kannst dich wieder anmelden und {product_name} nutzen.} other {}}{statement, select, null {} other {\n\nHinweis von unserem Team: {statement}}}{content_removed, select, yes {\n\nInhalte, die wir entfernt haben, können nicht wiederhergestellt werden.} other {}}\n\nDein {product_name}-Team"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {Eine Mitteilung zu deinem {product_name}-Account} other {{kind_label, select, warning {Eine Verwarnung zu deinem {product_name}-Account} suspension {Dein {product_name}-Account ist gesperrt} ban {Dein {product_name}-Account ist dauerhaft gesperrt} closure {Dein {product_name}-Account ist geschlossen} feature_limit {Einige Funktionen deines {product_name}-Accounts sind eingeschränkt} content_removal {Wir haben Nachrichten entfernt, die du auf {product_name} gesendet hast} profile_reset {Wir haben Teile deines {product_name}-Profils zurückgesetzt} community_removal {Wir haben deine Community auf {product_name} entfernt} bot {Eine Mitteilung zu deinem Bot auf {product_name}} other {Eine Mitteilung zu deinem {product_name}-Account}}}}",
|
||||
"body": "Hallo {username},\n\n{summary}\n\n{effects}\n\nWarum\nRegel: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nHinweis von unserem Team: {statement}}}\n\nSo haben wir entschieden\n{how_we_decided}{appeals_email, select, null {} other {\n\nSo legst du Einspruch ein\nWenn du glaubst, dass wir falsch entschieden haben, sende bis zum {appeal_deadline} eine E-Mail an {appeals_email}. Schreib nach Möglichkeit von der E-Mail-Adresse deines Accounts aus und gib die Entscheidungs-ID an. Ein Einspruch ist kostenlos und wird immer von einer Person geprüft.{redress, select, null {} other {\n\n{redress}}}}}\n\nEntscheidungs-ID: {decision_id}\n\nDein {product_name}-Team"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Vorteile deines eingelösten Geschenks wurden entfernt",
|
||||
"body": "Hallo {username},\n\nein von dir eingelöster Geschenkcode wurde ursprünglich von jemand anderem bezahlt. Diese Zahlung wurde inzwischen rückgängig gemacht (eine Rückbuchung).\n\nAus diesem Grund haben wir die Vorteile entfernt, die deinem Account hinzugefügt wurden, als du das Geschenk eingelöst hast.\n\n{support_email, select, null {Wenn du glaubst, dass dies ein Fehler ist, kontaktiere bitte die Administratoren dieser Instanz und gib alle Details an, die du über den Geschenkcode und den Zeitpunkt der Einlösung hast.} other {Wenn du glaubst, dass dies ein Fehler ist, kontaktiere bitte unser Support-Team und gib alle Details an, die du über den Geschenkcode und den Zeitpunkt der Einlösung hast.}}\n\n– {product_name}-Team"
|
||||
@@ -84,19 +84,11 @@
|
||||
"body": "Hallo,\n\nwir haben deine Meldung nach dem Digital Services Act (DSA) über {targetKind, select, message {eine Nachricht} user {einen Account} guild {eine Community} other {Inhalte}} auf {product_name} erhalten.\n\nMeldungs-ID: {reportId}\n\nUnser Sicherheitsteam prüft deine Meldung. Sobald wir eine Entscheidung getroffen haben, schreiben wir dir an diese E-Mail-Adresse. Bitte bewahre diese E-Mail für deine Unterlagen auf.\n\n– {product_name}-Sicherheitsteam"
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "Deine {product_name}-Meldung wurde geprüft",
|
||||
"body": "Hallo {username},\n\ndeine Meldung (ID: {reportId}) wurde von unserem Sicherheitsteam geprüft.{hasComment, select, yes {\n\nAntwort vom Sicherheitsteam:\n{publicComment}} other {}}\n\nDanke, dass du hilfst, {product_name} für alle sicher zu halten. Wir nehmen alle Meldungen ernst und schätzen deinen Beitrag zur Community.\n\n{safety_email, select, null {Wenn du Fragen oder Bedenken zu diesem Ergebnis hast, kontaktiere bitte die Administratoren dieser Instanz.} other {Wenn du Fragen oder Bedenken zu diesem Ergebnis hast, kontaktiere bitte {safety_email}.}}\n\n– {product_name}-Sicherheitsteam"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"subject": "Dein {product_name}-Account wird dauerhaft gelöscht",
|
||||
"body": "Hallo {username},\n\ndein {product_name}-Account wurde 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}\n}Dies ist eine ernsthafte Maßnahme. Deine Accountdaten werden am geplanten Datum dauerhaft gelöscht.\n\nWenn du glaubst, dass diese Entscheidung falsch war, kannst du Einspruch einlegen. {appeals_email, select, null {Wende dich an die Administratoren dieser Instanz.} other {Sende eine E-Mail von dieser E-Mail-Adresse an {appeals_email}.}}\n\n– {product_name}-Sicherheitsteam"
|
||||
"subject": "Deine Meldung auf {product_name} wurde geprüft",
|
||||
"body": "Hallo {username},\n\nwir haben deine Meldung (ID: {reportId}) geprüft.\n\n{outcome, select, removed {Wir haben den gemeldeten Inhalt entfernt, weil er gegen unsere Regeln verstoßen hat.} restricted {Wir haben festgestellt, dass der gemeldete Account gegen unsere Regeln verstoßen hat, und ihn eingeschränkt. Welche Maßnahme wir gegen einen anderen Account ergriffen haben, teilen wir nicht mit, weil das personenbezogene Daten dieser Person sind.} action_taken {Wir haben festgestellt, dass das, was du gemeldet hast, gegen unsere Regeln verstoßen hat, und haben Maßnahmen ergriffen. Welche Maßnahme wir gegen einen anderen Account ergriffen haben, teilen wir nicht mit, weil das personenbezogene Daten dieser Person sind.} no_violation {Wir haben festgestellt, dass der gemeldete Inhalt oder Account nicht gegen unsere Regeln verstößt. Deshalb haben wir nichts geändert.} other {Wir haben deine Meldung abgeschlossen.}}{hasComment, select, yes {\n\nHinweis von unserem Team: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nWenn du nicht einverstanden bist, sende innerhalb von 60 Tagen eine E-Mail an {appeals_email} und gib die Meldungs-ID an. Wenn du dich in der EU befindest, kannst du die Sache auch einer zertifizierten außergerichtlichen Streitbeilegungsstelle vorlegen oder dich beim Koordinator für digitale Dienste in deinem Land beschweren.}}\n\nDein {product_name}-Team"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "Die Löschung deines {product_name}-Accounts ist geplant",
|
||||
"body": "Hallo {username},\n\ndu hast die Löschung deines {product_name}-Accounts beantragt. Dein Account wird an folgendem Termin dauerhaft gelöscht:\n\n{deletionDate, date, full} um {deletionDate, time, short}\n\nWenn du diese Löschung nicht beantragt hast, melde dich in deinem Account an, um die Löschung abzubrechen. Wir empfehlen außerdem, dein Passwort zu ändern, um deinen Account abzusichern.\n\n– {product_name}-Team"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "Die Sperrung deines {product_name}-Accounts wurde aufgehoben",
|
||||
"body": "Hallo {username},\n\ngute Nachrichten: Die Sperrung deines {product_name}-Accounts wurde aufgehoben.\n\n{reason, select,\n null {}\n other {Grund: {reason}\n\n}\n}Du kannst dich jetzt wieder anmelden und {product_name} wie gewohnt nutzen.\n\n– {product_name}-Sicherheitsteam"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"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, select, null {Εάν θέλεις να διατηρήσεις τον λογαριασμό σου, επικοινώνησε με τους διαχειριστές αυτής της παρουσίας πριν από αυτή την ημερομηνία.} other {Εάν θέλεις να διατηρήσεις τον λογαριασμό σου, στείλε email στο {safety_email} από αυτή τη διεύθυνση email πριν από αυτή την ημερομηνία.}}\n\n– Η ομάδα του {product_name}"
|
||||
"body": "Γεια σου {username},\n\nΟ λογαριασμός σου στο {product_name} είναι ανενεργός εδώ και πολύ καιρό, γι' αυτό προγραμματίσαμε τη διαγραφή του για τις {deletion_date}.\n\nΓια να κρατήσεις τον λογαριασμό σου, συνδέσου πριν από αυτή την ημερομηνία. {support_email, select, null {Αν έχεις ερωτήσεις, επικοινώνησε με τους διαχειριστές αυτής της παρουσίας.} other {Αν έχεις ερωτήσεις, επικοινώνησε με το {support_email}.}}\n\nΗ ομάδα του {product_name}"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"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, select, null {Εάν δεν το ζήτησες ή θέλεις να διατηρήσεις τον λογαριασμό σου, επικοινώνησε με τους διαχειριστές αυτής της παρουσίας πριν από αυτή την ημερομηνία.} other {Εάν δεν το ζήτησες ή θέλεις να διατηρήσεις τον λογαριασμό σου, στείλε email στο {safety_email} από αυτή τη διεύθυνση email πριν από αυτή την ημερομηνία.}}\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{legalLinks, select,\n both {Έλεγξε:\n- Όροι παροχής υπηρεσιών: {termsUrl}\n- Οδηγίες κοινότητας: {guidelinesUrl}\n\n}\n terms {Έλεγξε:\n- Όροι παροχής υπηρεσιών: {termsUrl}\n\n}\n guidelines {Έλεγξε:\n- Οδηγίες κοινότητας: {guidelinesUrl}\n\n}\n other {}\n}Διαδικασία προσφυγής:\nΕάν πιστεύεις ότι αυτή η απόφαση ήταν λανθασμένη ή αδικαιολόγητη, έχεις 60 ημέρες για να υποβάλεις προσφυγή. {appeals_email, select, null {Επικοινώνησε με τους διαχειριστές αυτής της παρουσίας.} other {Στείλε email στο {appeals_email} από αυτήν τη διεύθυνση email.}}\n\nΣτην προσφυγή σου:\n- Εξήγησε σαφώς γιατί πιστεύεις ότι η απόφαση ήταν λανθασμένη ή αδικαιολόγητη\n- Παράθεσε τυχόν σχετικά αποδεικτικά στοιχεία ή πλαίσιο\n\nΈνα μέλος της ομάδας ασφαλείας του {product_name} θα εξετάσει την προσφυγή σου και ενδέχεται να αναστείλει την εκκρεμή διαγραφή μέχρι να ληφθεί τελική απόφαση.\n\n– Η ομάδα ασφαλείας του {product_name}"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "Ο λογαριασμός σου στο {product_name} έχει ανασταλεί προσωρινά",
|
||||
"body": "Γεια σου {username},\n\nΟ λογαριασμός σου στο {product_name} έχει ανασταλεί προσωρινά για παραβίαση των Όρων παροχής υπηρεσιών ή των Οδηγιών κοινότητας.\n\nΔιάρκεια: {durationHours, plural,\n =1 {μία ώρα}\n other {# ώρες}\n}\nΑναστολή μέχρι: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Λόγος: {reason}\n\n}\n}Κατά τη διάρκεια αυτής της περιόδου, δεν θα έχεις πρόσβαση στον λογαριασμό σου.\n\n{legalLinks, select,\n both {Έλεγξε:\n- Όροι παροχής υπηρεσιών: {termsUrl}\n- Οδηγίες κοινότητας: {guidelinesUrl}\n\n}\n terms {Έλεγξε:\n- Όροι παροχής υπηρεσιών: {termsUrl}\n\n}\n guidelines {Έλεγξε:\n- Οδηγίες κοινότητας: {guidelinesUrl}\n\n}\n other {}\n}Εάν πιστεύεις ότι αυτή η απόφαση ήταν λανθασμένη ή αδικαιολόγητη, μπορείς να υποβάλεις προσφυγή. {appeals_email, select, null {Επικοινώνησε με τους διαχειριστές αυτής της παρουσίας και εξήγησε σαφώς γιατί πιστεύεις ότι η απόφαση ήταν λανθασμένη.} other {Στείλε email στο {appeals_email} από αυτήν τη διεύθυνση email και εξήγησε σαφώς γιατί πιστεύεις ότι η απόφαση ήταν λανθασμένη.}} Θα εξετάσουμε την προσφυγή σου και θα σου απαντήσουμε με την απόφασή μας.\n\n– Η ομάδα ασφαλείας του {product_name}"
|
||||
"body": "Γεια σου {username},\n\nΌπως ζήτησες, προγραμματίσαμε τη διαγραφή του λογαριασμού σου στο {product_name} για τις {deletion_date}.\n\nΓια να κρατήσεις τον λογαριασμό σου, συνδέσου πριν από τις {deletion_date}. {safety_email, select, null {Αν δεν το ζήτησες εσύ, επικοινώνησε με τους διαχειριστές αυτής της παρουσίας.} other {Αν δεν το ζήτησες εσύ, επικοινώνησε με το {safety_email}.}}\n\nΗ ομάδα του {product_name}"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Ευχαριστούμε για τη δωρεά σου στο {product_name}",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "Λάβαμε απόφαση για την αναφορά σου στο {product_name}",
|
||||
"body": "Γεια σου,\n\nΗ ομάδα ασφαλείας μας εξέτασε την αναφορά σου βάσει της Πράξης για τις ψηφιακές υπηρεσίες (ID: {reportId}) και έλαβε απόφαση.{hasComment, select, yes {\n\nΑπάντηση από την ομάδα ασφαλείας:\n{publicComment}} other {}}\n\nΔεν κοινοποιούμε λεπτομέρειες για ενέργειες που έγιναν σε βάρος του λογαριασμού άλλου ατόμου, επειδή πρόκειται για δικά του προσωπικά δεδομένα.\n\nΕάν διαφωνείς με αυτήν την απόφαση, μπορείς να υποβάλεις προσφυγή δωρεάν εντός 60 ημερών. {appeals_email, select, null {Επικοινώνησε με τους διαχειριστές αυτής της παρουσίας, ανάφερε το ID της αναφοράς σου και εξήγησε γιατί πιστεύεις ότι η απόφαση είναι λανθασμένη.} other {Στείλε email στο {appeals_email} από αυτήν τη διεύθυνση email, ανάφερε το ID της αναφοράς σου και εξήγησε γιατί πιστεύεις ότι η απόφαση είναι λανθασμένη.}} Εάν βρίσκεσαι στην ΕΕ, μπορείς επίσης να παραπέμψεις τη διαφορά σε πιστοποιημένο όργανο εξωδικαστικής επίλυσης διαφορών. Τίποτα από αυτά δεν επηρεάζει το δικαίωμά σου να προσφύγεις στα δικαστήρια.\n\n– Η ομάδα ασφαλείας του {product_name}"
|
||||
"body": "Γεια σου,\n\nΕξετάσαμε την αναφορά σου (ID: {reportId}).\n\n{outcome, select, removed {Αφαιρέσαμε το περιεχόμενο που ανέφερες, επειδή παραβίαζε τους κανόνες μας.} restricted {Διαπιστώσαμε ότι ο λογαριασμός που ανέφερες παραβίασε τους κανόνες μας και τον περιορίσαμε. Δεν κοινοποιούμε ποια μέτρα λάβαμε κατά άλλου λογαριασμού, επειδή αυτό αποτελεί προσωπικά δεδομένα του κατόχου του.} action_taken {Διαπιστώσαμε ότι αυτό που ανέφερες παραβίασε τους κανόνες μας και λάβαμε μέτρα. Δεν κοινοποιούμε ποια μέτρα λάβαμε κατά άλλου λογαριασμού, επειδή αυτό αποτελεί προσωπικά δεδομένα του κατόχου του.} no_violation {Διαπιστώσαμε ότι το περιεχόμενο ή ο λογαριασμός που ανέφερες δεν παραβιάζει τους κανόνες μας, οπότε δεν κάναμε καμία αλλαγή.} other {Κλείσαμε την αναφορά σου.}}{hasComment, select, yes {\n\nΣημείωση από την ομάδα μας: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nΑν διαφωνείς, στείλε email στο {appeals_email} εντός 60 ημερών και ανάφερε το ID της αναφοράς. Αν βρίσκεσαι στην ΕΕ, μπορείς επίσης να παραπέμψεις το θέμα σε πιστοποιημένο φορέα εξωδικαστικής επίλυσης διαφορών ή να υποβάλεις καταγγελία στον συντονιστή ψηφιακών υπηρεσιών της χώρας σου.}}\n\nΗ ομάδα του {product_name}"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Επαλήθευσε το email σου για αναφορά DSA",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Επαλήθευσε τη διεύθυνση email σου στο {product_name}",
|
||||
"body": "Γεια σου {username},\n\nΕπαλήθευσε τη διεύθυνση email για τον λογαριασμό σου στο {product_name} κάνοντας κλικ στον παρακάτω σύνδεσμο:\n\n{verifyUrl}\n\nΕάν δεν δημιούργησες λογαριασμό στο {product_name}, μπορείς να αγνοήσεις με ασφάλεια αυτό το email.\n\nΑυτός ο σύνδεσμος ισχύει για 24 ώρες.\n\n– Η ομάδα του {product_name}"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {Ανακαλέσαμε μια απόφαση για τον λογαριασμό σου στο {product_name}} other {Ένας περιορισμός στον λογαριασμό σου στο {product_name} έληξε}}",
|
||||
"body": "Γεια σου {username},\n\n{outcome, select, reversed {{decision_id, select, null {Εξετάσαμε ξανά μια απόφαση για τον λογαριασμό σου και την ανακαλέσαμε.} other {Εξετάσαμε ξανά την απόφαση {decision_id} και την ανακαλέσαμε.}} Ζητούμε συγγνώμη για την ταλαιπωρία.} other {{decision_id, select, null {Ήραμε τον περιορισμό στον λογαριασμό σου.} other {Ήραμε τον περιορισμό της απόφασης {decision_id}.}}}}{restores_access, select, yes { Μπορείς να συνδεθείς και να χρησιμοποιείς ξανά το {product_name}.} other {}}{statement, select, null {} other {\n\nΣημείωση από την ομάδα μας: {statement}}}{content_removed, select, yes {\n\nΤο περιεχόμενο που αφαιρέσαμε δεν μπορεί να επανέλθει.} other {}}\n\nΗ ομάδα του {product_name}"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {Ενημέρωση για τον λογαριασμό σου στο {product_name}} other {{kind_label, select, warning {Προειδοποίηση για τον λογαριασμό σου στο {product_name}} suspension {Ο λογαριασμός σου στο {product_name} ανεστάλη} ban {Ο λογαριασμός σου στο {product_name} ανεστάλη οριστικά} closure {Ο λογαριασμός σου στο {product_name} έκλεισε} feature_limit {Ορισμένες λειτουργίες του λογαριασμού σου στο {product_name} περιορίστηκαν} content_removal {Αφαιρέσαμε μηνύματα που έστειλες στο {product_name}} profile_reset {Επαναφέραμε ορισμένα μέρη του προφίλ σου στο {product_name}} community_removal {Αφαιρέσαμε την κοινότητά σου από το {product_name}} bot {Ενημέρωση για το bot σου στο {product_name}} other {Ενημέρωση για τον λογαριασμό σου στο {product_name}}}}}",
|
||||
"body": "Γεια σου {username},\n\n{summary}\n\n{effects}\n\nΓιατί\nΚανόνας: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nΣημείωση από την ομάδα μας: {statement}}}\n\nΠώς αποφασίσαμε\n{how_we_decided}{appeals_email, select, null {} other {\n\nΠώς να κάνεις ένσταση\nΑν πιστεύεις ότι κάναμε λάθος, στείλε email στο {appeals_email} έως τις {appeal_deadline}. Αν μπορείς, γράψε από τη διεύθυνση email του λογαριασμού σου και ανάφερε το αναγνωριστικό της απόφασης. Οι ενστάσεις είναι δωρεάν και κάθε μία την εξετάζει ένας άνθρωπος.{redress, select, null {} other {\n\n{redress}}}}}\n\nΑναγνωριστικό απόφασης: {decision_id}\n\nΗ ομάδα του {product_name}"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Τα προνόμια από το δώρο που εξαργύρωσες έχουν αφαιρεθεί",
|
||||
"body": "Γεια σου {username},\n\nΈνας κωδικός δώρου που εξαργύρωσες πληρώθηκε αρχικά από κάποιον άλλο. Αυτή η πληρωμή έχει έκτοτε αντιστραφεί (αμφισβήτηση χρέωσης).\n\nΓι' αυτόν τον λόγο, αφαιρέσαμε τα προνόμια που προστέθηκαν στον λογαριασμό σου όταν εξαργύρωσες το δώρο.\n\n{support_email, select, null {Εάν πιστεύεις ότι πρόκειται για λάθος, επικοινώνησε με τους διαχειριστές αυτής της παρουσίας και ανάφερε όσες λεπτομέρειες έχεις σχετικά με τον κωδικό δώρου και το πότε τον εξαργύρωσες.} other {Εάν πιστεύεις ότι πρόκειται για λάθος, επικοινώνησε με την ομάδα υποστήριξής μας και ανάφερε όσες λεπτομέρειες έχεις σχετικά με τον κωδικό δώρου και το πότε τον εξαργύρωσες.}}\n\n– Η ομάδα του {product_name}"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "Η αναφορά σου στο {product_name} έχει εξεταστεί",
|
||||
"body": "Γεια σου {username},\n\nΗ αναφορά σου (ID: {reportId}) εξετάστηκε από την ομάδα ασφαλείας μας.{hasComment, select, yes {\n\nΑπάντηση από την ομάδα ασφαλείας:\n{publicComment}} other {}}\n\nΕυχαριστούμε που βοηθάς να παραμείνει το {product_name} ασφαλές για όλους. Λαμβάνουμε σοβαρά υπόψη κάθε αναφορά και εκτιμούμε τη συμβολή σου στην κοινότητα.\n\n{safety_email, select, null {Εάν έχεις ερωτήσεις ή απορίες σχετικά με αυτό το αποτέλεσμα, επικοινώνησε με τους διαχειριστές αυτής της παρουσίας.} other {Εάν έχεις ερωτήσεις ή απορίες σχετικά με αυτό το αποτέλεσμα, στείλε email στο {safety_email}.}}\n\n– Η ομάδα ασφαλείας του {product_name}"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"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Εάν πιστεύεις ότι αυτή η απόφαση ήταν λανθασμένη, μπορείς να υποβάλεις προσφυγή. {appeals_email, select, null {Επικοινώνησε με τους διαχειριστές αυτής της παρουσίας.} other {Στείλε email στο {appeals_email} από αυτήν τη διεύθυνση email.}}\n\n– Η ομάδα ασφαλείας του {product_name}"
|
||||
"body": "Γεια σου {username},\n\nΕξετάσαμε την αναφορά σου (ID: {reportId}).\n\n{outcome, select, removed {Αφαιρέσαμε το περιεχόμενο που ανέφερες, επειδή παραβίαζε τους κανόνες μας.} restricted {Διαπιστώσαμε ότι ο λογαριασμός που ανέφερες παραβίασε τους κανόνες μας και τον περιορίσαμε. Δεν κοινοποιούμε ποια μέτρα λάβαμε κατά άλλου λογαριασμού, επειδή αυτό αποτελεί προσωπικά δεδομένα του κατόχου του.} action_taken {Διαπιστώσαμε ότι αυτό που ανέφερες παραβίασε τους κανόνες μας και λάβαμε μέτρα. Δεν κοινοποιούμε ποια μέτρα λάβαμε κατά άλλου λογαριασμού, επειδή αυτό αποτελεί προσωπικά δεδομένα του κατόχου του.} no_violation {Διαπιστώσαμε ότι το περιεχόμενο ή ο λογαριασμός που ανέφερες δεν παραβιάζει τους κανόνες μας, οπότε δεν κάναμε καμία αλλαγή.} other {Κλείσαμε την αναφορά σου.}}{hasComment, select, yes {\n\nΣημείωση από την ομάδα μας: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nΑν διαφωνείς, στείλε email στο {appeals_email} εντός 60 ημερών και ανάφερε το ID της αναφοράς. Αν βρίσκεσαι στην ΕΕ, μπορείς επίσης να παραπέμψεις το θέμα σε πιστοποιημένο φορέα εξωδικαστικής επίλυσης διαφορών ή να υποβάλεις καταγγελία στον συντονιστή ψηφιακών υπηρεσιών της χώρας σου.}}\n\nΗ ομάδα του {product_name}"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "Η διαγραφή του λογαριασμού σου στο {product_name} είναι προγραμματισμένη",
|
||||
"body": "Γεια σου {username},\n\nΖήτησες τη διαγραφή του λογαριασμού σου στο {product_name}. Ο λογαριασμός σου έχει προγραμματιστεί για οριστική διαγραφή:\n\n{deletionDate, date, full} και ώρα {deletionDate, time, short}\n\nΕάν δεν το ζήτησες, συνδέσου στον λογαριασμό σου για να ακυρώσεις τη διαγραφή. Σου προτείνουμε επίσης να αλλάξεις τον κωδικό πρόσβασής σου για να ασφαλίσεις τον λογαριασμό σου.\n\n– Η ομάδα του {product_name}"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "Η αναστολή του λογαριασμού σου στο {product_name} έχει αρθεί",
|
||||
"body": "Γεια σου {username},\n\nΚαλά νέα: η αναστολή του λογαριασμού σου στο {product_name} έχει αρθεί.\n\n{reason, select,\n null {}\n other {Λόγος: {reason}\n\n}\n}Μπορείς τώρα να συνδεθείς ξανά και να συνεχίσεις να χρησιμοποιείς το {product_name} κανονικά.\n\n– Η ομάδα ασφαλείας του {product_name}"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"subject": "Your {product_name} account will be deleted due to inactivity",
|
||||
"body": "Hello {username},\n\nYour {product_name} account has been inactive for a long time, so it is scheduled for permanent deletion on:\n\n{deletionDate, date, full} at {deletionDate, time, short}{reason, select, null {} other {\n\nReason: {reason}}}\n\n{safety_email, select, null {If you want to keep your account, contact the administrators of this instance before that date.} other {If you want to keep your account, contact {safety_email} from this email address before that date.}}\n\n– {product_name} Team"
|
||||
"body": "Hello {username},\n\nYour {product_name} account has been inactive for a long time, so we scheduled it for deletion on {deletion_date}.\n\nTo keep your account, sign in before that date. {support_email, select, null {If you have questions, contact the administrators of this instance.} other {If you have questions, contact {support_email}.}}\n\nThe {product_name} team"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"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. {safety_email, select, null {If you didn't request this, or you want to keep your account, contact the administrators of this instance before that date.} other {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_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}\n}This is a serious enforcement action. Your account data will be permanently deleted on the scheduled date.\n\n{legalLinks, select,\n both {Please review:\n- Terms of Service: {termsUrl}\n- Community Guidelines: {guidelinesUrl}\n\n}\n terms {Please review:\n- Terms of Service: {termsUrl}\n\n}\n guidelines {Please review:\n- Community Guidelines: {guidelinesUrl}\n\n}\n other {}\n}Appeals process:\nIf you believe this enforcement decision was incorrect or unjustified, you have 60 days to submit an appeal. {appeals_email, select, null {Contact the administrators of this instance.} other {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"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "Your {product_name} account has been temporarily suspended",
|
||||
"body": "Hello {username},\n\nYour {product_name} account has been temporarily suspended for violating our Terms of Service or Community Guidelines.\n\nDuration: {durationHours, plural,\n =1 {1 hour}\n other {# hours}\n}\nSuspended until: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Reason: {reason}\n\n}\n}During this time, you won't be able to access your account.\n\n{legalLinks, select,\n both {Please review:\n- Terms of Service: {termsUrl}\n- Community Guidelines: {guidelinesUrl}\n\n}\n terms {Please review:\n- Terms of Service: {termsUrl}\n\n}\n guidelines {Please review:\n- Community Guidelines: {guidelinesUrl}\n\n}\n other {}\n}If you believe this enforcement decision was incorrect or unjustified, you can submit an appeal. {appeals_email, select, null {Contact the administrators of this instance and clearly explain why you believe the decision was incorrect.} other {Email {appeals_email} from this email address and clearly explain why you believe the decision was incorrect.}} We'll review your appeal and respond with our decision.\n\n– {product_name} Safety Team"
|
||||
"body": "Hello {username},\n\nAs you asked, we scheduled your {product_name} account for deletion on {deletion_date}.\n\nTo keep your account, sign in before {deletion_date}. {safety_email, select, null {If you didn't ask for this, contact the administrators of this instance.} other {If you didn't ask for this, contact {safety_email}.}}\n\nThe {product_name} team"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Thank you for your {product_name} donation",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "We made a decision on your {product_name} report",
|
||||
"body": "Hello,\n\nOur Safety Team has reviewed your Digital Services Act report (ID: {reportId}) and made a decision.{hasComment, select, yes {\n\nResponse from the Safety Team:\n{publicComment}} other {}}\n\nWe don't share details of any action taken against another person's account, because that is their personal data.\n\nIf you disagree with this decision, you can appeal free of charge within 60 days. {appeals_email, select, null {Contact the administrators of this instance, include your report ID, and explain why you think the decision is wrong.} other {Email {appeals_email} from this email address, include your report ID, and explain why you think the decision is wrong.}} If you are in the EU, you can also refer the dispute to a certified out-of-court dispute settlement body. None of this affects your right to go to court.\n\n– {product_name} Safety Team"
|
||||
"body": "Hello,\n\nWe reviewed your report (ID: {reportId}).\n\n{outcome, select, removed {We removed the content you reported because it broke our rules.} restricted {We found that the account you reported broke our rules, and we restricted it. We don't share what action we took against another account, because that is their personal data.} action_taken {We found that what you reported broke our rules, and we took action. We don't share what action we took against another account, because that is their personal data.} no_violation {We found that the content or account you reported doesn't break our rules, so we left it as it is.} other {We closed your report.}}{hasComment, select, yes {\n\nNote from our team: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nIf you disagree, email {appeals_email} within 60 days and include the report ID. If you're in the EU, you can also refer this to a certified out-of-court dispute settlement body or complain to the Digital Services Coordinator in your country.}}\n\nThe {product_name} team"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Verify your email for a DSA report",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Verify your {product_name} email address",
|
||||
"body": "Hello {username},\n\nPlease verify the email address for your {product_name} account by clicking the link below:\n\n{verifyUrl}\n\nIf you didn't create a {product_name} account, you can safely ignore this email.\n\nThis link is valid for 24 hours.\n\n– {product_name} Team"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {We reversed a decision about your {product_name} account} other {A restriction on your {product_name} account has ended}}",
|
||||
"body": "Hello {username},\n\n{outcome, select, reversed {{decision_id, select, null {We looked again at a decision about your account and reversed it.} other {We looked again at decision {decision_id} and reversed it.}} We're sorry for the trouble.} other {{decision_id, select, null {We lifted the restriction on your account.} other {We lifted the restriction from decision {decision_id}.}}}}{restores_access, select, yes { You can sign in and use {product_name} again.} other {}}{statement, select, null {} other {\n\nNote from our team: {statement}}}{content_removed, select, yes {\n\nContent we removed can't be restored.} other {}}\n\nThe {product_name} team"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {An update about your {product_name} account} other {{kind_label, select, warning {A warning about your {product_name} account} suspension {Your {product_name} account is suspended} ban {Your {product_name} account is banned} closure {Your {product_name} account is closed} feature_limit {Some features are limited on your {product_name} account} content_removal {We removed messages you sent on {product_name}} profile_reset {We reset parts of your {product_name} profile} community_removal {We removed your community on {product_name}} bot {An update about your bot on {product_name}} other {An update about your {product_name} account}}}}",
|
||||
"body": "Hello {username},\n\n{summary}\n\n{effects}\n\nWhy\nRule: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nNote from our team: {statement}}}\n\nHow we decided\n{how_we_decided}{appeals_email, select, null {} other {\n\nHow to appeal\nIf you think we got this wrong, email {appeals_email} by {appeal_deadline}. Write from the email address on your account if you can, and include the decision ID. Appeals are free, and a person reviews each one.{redress, select, null {} other {\n\n{redress}}}}}\n\nDecision ID: {decision_id}\n\nThe {product_name} team"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Perks from your redeemed gift have been removed",
|
||||
"body": "Hello {username},\n\nA gift code you redeemed was originally paid for by someone else. That payment has since been reversed (a chargeback).\n\nBecause of this, we've removed the perks that were added to your account when you redeemed the gift.\n\n{support_email, select, null {If you think this is a mistake, please contact the administrators of this instance and include any details you have about the gift code and when you redeemed it.} other {If you think this is a mistake, please contact our support team and include any details you have about the gift code and when you redeemed it.}}\n\n– {product_name} Team"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "Your {product_name} report has been reviewed",
|
||||
"body": "Hello {username},\n\nYour report (ID: {reportId}) has been reviewed by our Safety Team.{hasComment, select, yes {\n\nResponse from the Safety Team:\n{publicComment}} other {}}\n\nThanks for helping keep {product_name} safe for everyone. We take all reports seriously and appreciate your contribution to the community.\n\n{safety_email, select, null {If you have any questions or concerns about this outcome, please contact the administrators of this instance.} other {If you have any questions or concerns about this outcome, please contact {safety_email}.}}\n\n– {product_name} Safety Team"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"subject": "Your {product_name} account will be permanently deleted",
|
||||
"body": "Hello {username},\n\nYour {product_name} account has been scheduled for permanent deletion.\n\nScheduled deletion: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Reason: {reason}\n\n}\n}This is a serious enforcement action. Your account data will be permanently deleted on the scheduled date.\n\nIf you believe this enforcement decision was incorrect, you can submit an appeal. {appeals_email, select, null {Contact the administrators of this instance.} other {Email {appeals_email} from this email address.}}\n\n– {product_name} Safety Team"
|
||||
"body": "Hello {username},\n\nWe reviewed your report (ID: {reportId}).\n\n{outcome, select, removed {We removed the content you reported because it broke our rules.} restricted {We found that the account you reported broke our rules, and we restricted it. We don't share what action we took against another account, because that is their personal data.} action_taken {We found that what you reported broke our rules, and we took action. We don't share what action we took against another account, because that is their personal data.} no_violation {We found that the content or account you reported doesn't break our rules, so we left it as it is.} other {We closed your report.}}{hasComment, select, yes {\n\nNote from our team: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nIf you disagree, email {appeals_email} within 60 days and include the report ID. If you're in the EU, you can also refer this to a certified out-of-court dispute settlement body or complain to the Digital Services Coordinator in your country.}}\n\nThe {product_name} team"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "Your {product_name} account deletion is scheduled",
|
||||
"body": "Hello {username},\n\nYou requested to delete your {product_name} account. Your account is scheduled for permanent deletion on:\n\n{deletionDate, date, full} at {deletionDate, time, short}\n\nIf you didn't request this, sign in to your account to cancel the deletion. We also recommend changing your password to secure your account.\n\n– {product_name} Team"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "Your {product_name} account suspension has been lifted",
|
||||
"body": "Hello {username},\n\nGood news: your {product_name} account suspension has been lifted.\n\n{reason, select,\n null {}\n other {Reason: {reason}\n\n}\n}You can now log back in and continue using {product_name} as normal.\n\n– {product_name} Safety Team"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"subject": "Tu cuenta de {product_name} será eliminada debido a inactividad",
|
||||
"body": "Hola {username}:\n\nTu cuenta de {product_name} lleva mucho tiempo inactiva, por lo que su eliminación permanente está programada para el:\n\n{deletionDate, date, full} a las {deletionDate, time, short}{reason, select, null {} other {\n\nMotivo: {reason}}}\n\n{safety_email, select, null {Si quieres conservar tu cuenta, comunícate con los administradores de esta instancia antes de esa fecha.} other {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}"
|
||||
"body": "Hola {username}:\n\nTu cuenta de {product_name} lleva mucho tiempo inactiva, por lo que programamos su eliminación para el {deletion_date}.\n\nPara conservar tu cuenta, inicia sesión antes de esa fecha. {support_email, select, null {Si tienes preguntas, comunícate con los administradores de esta instancia.} other {Si tienes preguntas, comunícate con {support_email}.}}\n\nEl equipo de {product_name}"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"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. {safety_email, select, null {Si no solicitaste esto, o si quieres conservar tu cuenta, comunícate con los administradores de esta instancia antes de esa fecha.} other {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_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}\n}Esta es una medida grave. Los datos de tu cuenta se eliminarán permanentemente en la fecha programada.\n\n{legalLinks, select,\n both {Por favor, revisa:\n- Términos del servicio: {termsUrl}\n- Normas de la comunidad: {guidelinesUrl}\n\n}\n terms {Por favor, revisa:\n- Términos del servicio: {termsUrl}\n\n}\n guidelines {Por favor, revisa:\n- Normas de la comunidad: {guidelinesUrl}\n\n}\n other {}\n}Proceso de apelación:\nSi crees que esta decisión fue incorrecta o injustificada, tienes 60 días para presentar una apelación. {appeals_email, select, null {Comunícate con los administradores de esta instancia.} other {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}"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "Tu cuenta de {product_name} ha sido suspendida temporalmente",
|
||||
"body": "Hola {username}:\n\nTu cuenta de {product_name} ha sido suspendida temporalmente por infringir nuestros Términos del servicio o Normas de la comunidad.\n\nDuración: {durationHours, plural,\n =1 {1 hora}\n other {# horas}\n}\nSuspendida hasta: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Motivo: {reason}\n\n}\n}Durante este tiempo, no podrás acceder a tu cuenta.\n\n{legalLinks, select,\n both {Por favor, revisa:\n- Términos del servicio: {termsUrl}\n- Normas de la comunidad: {guidelinesUrl}\n\n}\n terms {Por favor, revisa:\n- Términos del servicio: {termsUrl}\n\n}\n guidelines {Por favor, revisa:\n- Normas de la comunidad: {guidelinesUrl}\n\n}\n other {}\n}Si crees que esta decisión fue incorrecta o injustificada, puedes presentar una apelación. {appeals_email, select, null {Comunícate con los administradores de esta instancia y explica claramente por qué crees que la decisión fue incorrecta.} other {Envía un correo electrónico a {appeals_email} desde esta dirección de correo electrónico y explica claramente por qué crees que la decisión fue incorrecta.}} Revisaremos tu apelación y te responderemos con nuestra decisión.\n\n– Equipo de seguridad de {product_name}"
|
||||
"body": "Hola {username}:\n\nTal como lo pediste, programamos la eliminación de tu cuenta de {product_name} para el {deletion_date}.\n\nPara conservar tu cuenta, inicia sesión antes del {deletion_date}. {safety_email, select, null {Si no lo pediste tú, comunícate con los administradores de esta instancia.} other {Si no lo pediste tú, comunícate con {safety_email}.}}\n\nEl equipo de {product_name}"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Gracias por tu donación a {product_name}",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "Tomamos una decisión sobre tu reporte en {product_name}",
|
||||
"body": "Hola:\n\nNuestro equipo de seguridad revisó tu reporte conforme a la Ley de Servicios Digitales (ID: {reportId}) y tomó una decisión.{hasComment, select, yes {\n\nRespuesta del equipo de seguridad:\n{publicComment}} other {}}\n\nNo compartimos detalles sobre las medidas tomadas contra la cuenta de otra persona, porque son sus datos personales.\n\nSi no estás de acuerdo con esta decisión, puedes apelar sin costo dentro de un plazo de 60 días. {appeals_email, select, null {Comunícate con los administradores de esta instancia, incluye el ID de tu reporte y explica por qué crees que la decisión es incorrecta.} other {Envía un correo electrónico a {appeals_email} desde esta dirección de correo electrónico, incluye el ID de tu reporte y explica por qué crees que la decisión es incorrecta.}} Si te encuentras en la UE, también puedes remitir la disputa a un órgano certificado de resolución extrajudicial de litigios. Nada de esto afecta tu derecho a acudir a los tribunales.\n\n– Equipo de seguridad de {product_name}"
|
||||
"body": "Hola:\n\nRevisamos tu reporte (ID: {reportId}).\n\n{outcome, select, removed {Eliminamos el contenido que reportaste porque infringía nuestras reglas.} restricted {Comprobamos que la cuenta que reportaste infringía nuestras reglas y la restringimos. No compartimos qué medidas tomamos contra otra cuenta, porque son datos personales de su titular.} action_taken {Comprobamos que lo que reportaste infringía nuestras reglas y tomamos medidas. No compartimos qué medidas tomamos contra otra cuenta, porque son datos personales de su titular.} no_violation {Comprobamos que el contenido o la cuenta que reportaste no infringe nuestras reglas, así que no cambiamos nada.} other {Cerramos tu reporte.}}{hasComment, select, yes {\n\nNota de nuestro equipo: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nSi no estás de acuerdo, escribe a {appeals_email} en un plazo de 60 días e indica el ID del reporte. Si estás en la UE, también puedes someter el asunto a un órgano de resolución extrajudicial de litigios certificado o presentar una reclamación ante el coordinador de servicios digitales de tu país.}}\n\nEl equipo de {product_name}"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Verifica tu correo electrónico para enviar un reporte conforme a la Ley de Servicios Digitales",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Verifica tu dirección de correo electrónico en {product_name}",
|
||||
"body": "Hola {username}:\n\nPor favor, verifica la dirección de correo electrónico de tu cuenta de {product_name} haciendo clic en el siguiente enlace:\n\n{verifyUrl}\n\nSi no creaste una cuenta de {product_name}, puedes ignorar este correo electrónico de forma segura.\n\nEste enlace es válido por 24 horas.\n\n– Equipo de {product_name}"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {Revocamos una decisión sobre tu cuenta de {product_name}} other {Terminó una restricción de tu cuenta de {product_name}}}",
|
||||
"body": "Hola {username}:\n\n{outcome, select, reversed {{decision_id, select, null {Volvimos a revisar una decisión sobre tu cuenta y la revocamos.} other {Volvimos a revisar la decisión {decision_id} y la revocamos.}} Lamentamos las molestias.} other {{decision_id, select, null {Levantamos la restricción de tu cuenta.} other {Levantamos la restricción de la decisión {decision_id}.}}}}{restores_access, select, yes { Ya puedes iniciar sesión y volver a usar {product_name}.} other {}}{statement, select, null {} other {\n\nNota de nuestro equipo: {statement}}}{content_removed, select, yes {\n\nEl contenido que eliminamos no se puede restaurar.} other {}}\n\nEl equipo de {product_name}"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {Información sobre tu cuenta de {product_name}} other {{kind_label, select, warning {Una advertencia sobre tu cuenta de {product_name}} suspension {Tu cuenta de {product_name} está suspendida} ban {Tu cuenta de {product_name} está suspendida de forma permanente} closure {Tu cuenta de {product_name} está cerrada} feature_limit {Algunas funciones de tu cuenta de {product_name} están limitadas} content_removal {Eliminamos mensajes que enviaste en {product_name}} profile_reset {Restablecimos partes de tu perfil de {product_name}} community_removal {Eliminamos tu comunidad en {product_name}} bot {Información sobre tu bot en {product_name}} other {Información sobre tu cuenta de {product_name}}}}}",
|
||||
"body": "Hola {username}:\n\n{summary}\n\n{effects}\n\nPor qué\nRegla: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nNota de nuestro equipo: {statement}}}\n\nCómo lo decidimos\n{how_we_decided}{appeals_email, select, null {} other {\n\nCómo apelar\nSi crees que nos equivocamos, escribe a {appeals_email} a más tardar el {appeal_deadline}. Si puedes, escribe desde la dirección de correo electrónico de tu cuenta e indica el ID de la decisión. Apelar es gratis, y una persona revisa cada apelación.{redress, select, null {} other {\n\n{redress}}}}}\n\nID de la decisión: {decision_id}\n\nEl equipo de {product_name}"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Los beneficios de tu regalo canjeado han sido eliminados",
|
||||
"body": "Hola {username}:\n\nUn código de regalo que canjeaste fue pagado originalmente por otra persona. Ese pago ha sido revertido (un contracargo).\n\nDebido a esto, hemos eliminado los beneficios que se agregaron a tu cuenta cuando canjeaste el regalo.\n\n{support_email, select, null {Si crees que esto es un error, comunícate con los administradores de esta instancia e incluye cualquier detalle que tengas sobre el código de regalo y cuándo lo canjeaste.} other {Si crees que esto es un error, comunícate con nuestro equipo de soporte e incluye cualquier detalle que tengas sobre el código de regalo y cuándo lo canjeaste.}}\n\n– Equipo de {product_name}"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "Se revisó tu reporte en {product_name}",
|
||||
"body": "Hola {username}:\n\nNuestro equipo de seguridad revisó tu reporte (ID: {reportId}).{hasComment, select, yes {\n\nRespuesta del equipo de seguridad:\n{publicComment}} other {}}\n\nGracias por ayudar a mantener {product_name} seguro para todos. Nos tomamos todos los reportes en serio y agradecemos tu contribución a la comunidad.\n\n{safety_email, select, null {Si tienes alguna pregunta o inquietud sobre este resultado, comunícate con los administradores de esta instancia.} other {Si tienes alguna pregunta o inquietud sobre este resultado, comunícate con {safety_email}.}}\n\n– Equipo de seguridad de {product_name}"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"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}.\n\nEliminación programada: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Motivo: {reason}\n\n}\n}Esta es una medida grave. Los datos de tu cuenta se eliminarán permanentemente en la fecha programada.\n\nSi crees que esta decisión fue incorrecta, puedes presentar una apelación. {appeals_email, select, null {Comunícate con los administradores de esta instancia.} other {Envía un correo electrónico a {appeals_email} desde esta dirección de correo electrónico.}}\n\n– Equipo de seguridad de {product_name}"
|
||||
"body": "Hola {username}:\n\nRevisamos tu reporte (ID: {reportId}).\n\n{outcome, select, removed {Eliminamos el contenido que reportaste porque infringía nuestras reglas.} restricted {Comprobamos que la cuenta que reportaste infringía nuestras reglas y la restringimos. No compartimos qué medidas tomamos contra otra cuenta, porque son datos personales de su titular.} action_taken {Comprobamos que lo que reportaste infringía nuestras reglas y tomamos medidas. No compartimos qué medidas tomamos contra otra cuenta, porque son datos personales de su titular.} no_violation {Comprobamos que el contenido o la cuenta que reportaste no infringe nuestras reglas, así que no cambiamos nada.} other {Cerramos tu reporte.}}{hasComment, select, yes {\n\nNota de nuestro equipo: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nSi no estás de acuerdo, escribe a {appeals_email} en un plazo de 60 días e indica el ID del reporte. Si estás en la UE, también puedes someter el asunto a un órgano de resolución extrajudicial de litigios certificado o presentar una reclamación ante el coordinador de servicios digitales de tu país.}}\n\nEl equipo de {product_name}"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "La eliminación de tu cuenta de {product_name} está programada",
|
||||
"body": "Hola {username}:\n\nSolicitaste eliminar tu cuenta de {product_name}. La eliminación permanente de tu cuenta está programada para el:\n\n{deletionDate, date, full} a las {deletionDate, time, short}\n\nSi no solicitaste esto, inicia sesión en tu cuenta para cancelar la eliminación. También te recomendamos cambiar tu contraseña para proteger tu cuenta.\n\n– Equipo de {product_name}"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "Se levantó la suspensión de tu cuenta de {product_name}",
|
||||
"body": "Hola {username}:\n\nBuenas noticias: se levantó la suspensión de tu cuenta de {product_name}.\n\n{reason, select,\n null {}\n other {Motivo: {reason}\n\n}\n}Ahora puedes iniciar sesión de nuevo y seguir usando {product_name} con normalidad.\n\n– Equipo de seguridad de {product_name}"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"subject": "Tu cuenta de {product_name} será eliminada debido a la inactividad",
|
||||
"body": "Hola {username}:\n\nTu cuenta de {product_name} lleva mucho tiempo inactiva, por lo que su eliminación permanente está programada para el:\n\n{deletionDate, date, full} a las {deletionDate, time, short}{reason, select, null {} other {\n\nMotivo: {reason}}}\n\n{safety_email, select, null {Si quieres conservar tu cuenta, ponte en contacto con los administradores de esta instancia antes de esa fecha.} other {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}"
|
||||
"body": "Hola {username}:\n\nTu cuenta de {product_name} lleva mucho tiempo inactiva, por lo que hemos programado su eliminación para el {deletion_date}.\n\nPara conservar tu cuenta, inicia sesión antes de esa fecha. {support_email, select, null {Si tienes preguntas, ponte en contacto con los administradores de esta instancia.} other {Si tienes preguntas, ponte en contacto con {support_email}.}}\n\nEl equipo de {product_name}"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"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. {safety_email, select, null {Si no solicitaste esto, o si quieres conservar tu cuenta, ponte en contacto con los administradores de esta instancia antes de esa fecha.} other {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_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}\n}Esta es una medida grave. Los datos de tu cuenta se eliminarán permanentemente en la fecha programada.\n\n{legalLinks, select,\n both {Por favor, revisa:\n- Condiciones del servicio: {termsUrl}\n- Normas de la comunidad: {guidelinesUrl}\n\n}\n terms {Por favor, revisa:\n- Condiciones del servicio: {termsUrl}\n\n}\n guidelines {Por favor, revisa:\n- Normas de la comunidad: {guidelinesUrl}\n\n}\n other {}\n}Proceso de apelación:\nSi crees que esta decisión fue incorrecta o injustificada, tienes 60 días para presentar una apelación. {appeals_email, select, null {Ponte en contacto con los administradores de esta instancia.} other {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}"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "Tu cuenta de {product_name} ha sido suspendida temporalmente",
|
||||
"body": "Hola {username}:\n\nTu cuenta de {product_name} ha sido suspendida temporalmente por infringir nuestras Condiciones del servicio o Normas de la comunidad.\n\nDuración: {durationHours, plural,\n =1 {1 hora}\n other {# horas}\n}\nSuspendida hasta: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Motivo: {reason}\n\n}\n}Durante este tiempo, no podrás acceder a tu cuenta.\n\n{legalLinks, select,\n both {Por favor, revisa:\n- Condiciones del servicio: {termsUrl}\n- Normas de la comunidad: {guidelinesUrl}\n\n}\n terms {Por favor, revisa:\n- Condiciones del servicio: {termsUrl}\n\n}\n guidelines {Por favor, revisa:\n- Normas de la comunidad: {guidelinesUrl}\n\n}\n other {}\n}Si crees que esta decisión fue incorrecta o injustificada, puedes presentar una apelación. {appeals_email, select, null {Ponte en contacto con los administradores de esta instancia y explica claramente por qué crees que la decisión fue incorrecta.} other {Envía un correo electrónico a {appeals_email} desde esta dirección de correo electrónico y explica claramente por qué crees que la decisión fue incorrecta.}} Revisaremos tu apelación y te responderemos con nuestra decisión.\n\n– Equipo de seguridad de {product_name}"
|
||||
"body": "Hola {username}:\n\nTal como pediste, hemos programado la eliminación de tu cuenta de {product_name} para el {deletion_date}.\n\nPara conservar tu cuenta, inicia sesión antes del {deletion_date}. {safety_email, select, null {Si no lo has pedido tú, ponte en contacto con los administradores de esta instancia.} other {Si no lo has pedido tú, ponte en contacto con {safety_email}.}}\n\nEl equipo de {product_name}"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Gracias por tu donación a {product_name}",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "Hemos tomado una decisión sobre tu denuncia en {product_name}",
|
||||
"body": "Hola:\n\nNuestro equipo de seguridad ha revisado tu denuncia conforme a la Ley de Servicios Digitales (ID: {reportId}) y ha tomado una decisión.{hasComment, select, yes {\n\nRespuesta del equipo de seguridad:\n{publicComment}} other {}}\n\nNo compartimos detalles sobre las medidas adoptadas contra la cuenta de otra persona, porque son sus datos personales.\n\nSi no estás de acuerdo con esta decisión, puedes presentar una apelación de forma gratuita en un plazo de 60 días. {appeals_email, select, null {Ponte en contacto con los administradores de esta instancia, incluye el ID de tu denuncia y explica por qué crees que la decisión es incorrecta.} other {Envía un correo electrónico a {appeals_email} desde esta dirección de correo electrónico, incluye el ID de tu denuncia y explica por qué crees que la decisión es incorrecta.}} Si te encuentras en la UE, también puedes someter el litigio a un órgano certificado de resolución extrajudicial de litigios. Nada de esto afecta a tu derecho a acudir a los tribunales.\n\n– Equipo de seguridad de {product_name}"
|
||||
"body": "Hola:\n\nHemos revisado tu denuncia (ID: {reportId}).\n\n{outcome, select, removed {Hemos eliminado el contenido que denunciaste porque infringía nuestras normas.} restricted {Hemos comprobado que la cuenta que denunciaste infringía nuestras normas y la hemos restringido. No compartimos qué medidas hemos tomado contra otra cuenta, porque son datos personales de su titular.} action_taken {Hemos comprobado que lo que denunciaste infringía nuestras normas y hemos tomado medidas. No compartimos qué medidas hemos tomado contra otra cuenta, porque son datos personales de su titular.} no_violation {Hemos comprobado que el contenido o la cuenta que denunciaste no infringe nuestras normas, así que no hemos cambiado nada.} other {Hemos cerrado tu denuncia.}}{hasComment, select, yes {\n\nNota de nuestro equipo: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nSi no estás de acuerdo, escribe a {appeals_email} en un plazo de 60 días e indica el ID de la denuncia. Si estás en la UE, también puedes someter el asunto a un órgano de resolución extrajudicial de litigios certificado o presentar una reclamación ante el coordinador de servicios digitales de tu país.}}\n\nEl equipo de {product_name}"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Verifica tu correo electrónico para enviar una denuncia conforme a la Ley de Servicios Digitales",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Verifica tu dirección de correo electrónico en {product_name}",
|
||||
"body": "Hola {username}:\n\nVerifica la dirección de correo electrónico de tu cuenta de {product_name} haciendo clic en el siguiente enlace:\n\n{verifyUrl}\n\nSi no creaste una cuenta de {product_name}, puedes ignorar este correo electrónico.\n\nEste enlace es válido durante 24 horas.\n\n– Equipo de {product_name}"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {Hemos revocado una decisión sobre tu cuenta de {product_name}} other {Ha terminado una restricción de tu cuenta de {product_name}}}",
|
||||
"body": "Hola {username}:\n\n{outcome, select, reversed {{decision_id, select, null {Hemos vuelto a revisar una decisión sobre tu cuenta y la hemos revocado.} other {Hemos vuelto a revisar la decisión {decision_id} y la hemos revocado.}} Sentimos las molestias.} other {{decision_id, select, null {Hemos levantado la restricción de tu cuenta.} other {Hemos levantado la restricción de la decisión {decision_id}.}}}}{restores_access, select, yes { Ya puedes iniciar sesión y volver a usar {product_name}.} other {}}{statement, select, null {} other {\n\nNota de nuestro equipo: {statement}}}{content_removed, select, yes {\n\nEl contenido que hemos eliminado no se puede restaurar.} other {}}\n\nEl equipo de {product_name}"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {Información sobre tu cuenta de {product_name}} other {{kind_label, select, warning {Una advertencia sobre tu cuenta de {product_name}} suspension {Tu cuenta de {product_name} está suspendida} ban {Tu cuenta de {product_name} está suspendida de forma permanente} closure {Tu cuenta de {product_name} está cerrada} feature_limit {Algunas funciones de tu cuenta de {product_name} están limitadas} content_removal {Hemos eliminado mensajes que enviaste en {product_name}} profile_reset {Hemos restablecido partes de tu perfil de {product_name}} community_removal {Hemos eliminado tu comunidad en {product_name}} bot {Información sobre tu bot en {product_name}} other {Información sobre tu cuenta de {product_name}}}}}",
|
||||
"body": "Hola {username}:\n\n{summary}\n\n{effects}\n\nPor qué\nNorma: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nNota de nuestro equipo: {statement}}}\n\nCómo lo hemos decidido\n{how_we_decided}{appeals_email, select, null {} other {\n\nCómo apelar\nSi crees que nos hemos equivocado, escribe a {appeals_email} a más tardar el {appeal_deadline}. Si puedes, escribe desde la dirección de correo electrónico de tu cuenta e indica el ID de la decisión. Apelar es gratis, y una persona revisa cada apelación.{redress, select, null {} other {\n\n{redress}}}}}\n\nID de la decisión: {decision_id}\n\nEl equipo de {product_name}"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Se han eliminado las ventajas obtenidas con tu regalo canjeado",
|
||||
"body": "Hola {username}:\n\nUn código de regalo que canjeaste fue pagado originalmente por otra persona. Ese pago ha sido revertido (una devolución de cargo).\n\nDebido a esto, hemos eliminado las ventajas que se añadieron a tu cuenta cuando canjeaste el regalo.\n\n{support_email, select, null {Si crees que esto es un error, ponte en contacto con los administradores de esta instancia e incluye cualquier detalle que tengas sobre el código de regalo y cuándo lo canjeaste.} other {Si crees que esto es un error, ponte en contacto con nuestro equipo de soporte e incluye cualquier detalle que tengas sobre el código de regalo y cuándo lo canjeaste.}}\n\n– Equipo de {product_name}"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "Se ha revisado tu denuncia en {product_name}",
|
||||
"body": "Hola {username}:\n\nNuestro equipo de seguridad ha revisado tu denuncia (ID: {reportId}).{hasComment, select, yes {\n\nRespuesta del equipo de seguridad:\n{publicComment}} other {}}\n\nGracias por ayudarnos a mantener {product_name} seguro para todos. Nos tomamos todas las denuncias en serio y agradecemos tu contribución a la comunidad.\n\n{safety_email, select, null {Si tienes alguna pregunta o inquietud sobre este resultado, ponte en contacto con los administradores de esta instancia.} other {Si tienes alguna pregunta o inquietud sobre este resultado, ponte en contacto con {safety_email}.}}\n\n– Equipo de seguridad de {product_name}"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"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}.\n\nEliminación programada: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Motivo: {reason}\n\n}\n}Esta es una medida grave. Los datos de tu cuenta se eliminarán permanentemente en la fecha programada.\n\nSi crees que esta decisión fue incorrecta, puedes presentar una apelación. {appeals_email, select, null {Ponte en contacto con los administradores de esta instancia.} other {Envía un correo electrónico a {appeals_email} desde esta dirección de correo electrónico.}}\n\n– Equipo de seguridad de {product_name}"
|
||||
"body": "Hola {username}:\n\nHemos revisado tu denuncia (ID: {reportId}).\n\n{outcome, select, removed {Hemos eliminado el contenido que denunciaste porque infringía nuestras normas.} restricted {Hemos comprobado que la cuenta que denunciaste infringía nuestras normas y la hemos restringido. No compartimos qué medidas hemos tomado contra otra cuenta, porque son datos personales de su titular.} action_taken {Hemos comprobado que lo que denunciaste infringía nuestras normas y hemos tomado medidas. No compartimos qué medidas hemos tomado contra otra cuenta, porque son datos personales de su titular.} no_violation {Hemos comprobado que el contenido o la cuenta que denunciaste no infringe nuestras normas, así que no hemos cambiado nada.} other {Hemos cerrado tu denuncia.}}{hasComment, select, yes {\n\nNota de nuestro equipo: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nSi no estás de acuerdo, escribe a {appeals_email} en un plazo de 60 días e indica el ID de la denuncia. Si estás en la UE, también puedes someter el asunto a un órgano de resolución extrajudicial de litigios certificado o presentar una reclamación ante el coordinador de servicios digitales de tu país.}}\n\nEl equipo de {product_name}"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "La eliminación de tu cuenta de {product_name} está programada",
|
||||
"body": "Hola {username}:\n\nSolicitaste eliminar tu cuenta de {product_name}. La eliminación permanente de tu cuenta está programada para el:\n\n{deletionDate, date, full} a las {deletionDate, time, short}\n\nSi no solicitaste esto, inicia sesión en tu cuenta para cancelar la eliminación. También te recomendamos cambiar tu contraseña para proteger tu cuenta.\n\n– Equipo de {product_name}"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "Se ha levantado la suspensión de tu cuenta de {product_name}",
|
||||
"body": "Hola {username}:\n\nBuenas noticias: se ha levantado la suspensión de tu cuenta de {product_name}.\n\n{reason, select,\n null {}\n other {Motivo: {reason}\n\n}\n}Ahora puedes volver a iniciar sesión y seguir usando {product_name} con normalidad.\n\n– Equipo de seguridad de {product_name}"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"subject": "{product_name}-tilisi poistetaan käyttämättömyyden vuoksi",
|
||||
"body": "Hei {username},\n\n{product_name}-tilisi on ollut pitkään käyttämättä, joten se on ajoitettu poistettavaksi pysyvästi:\n\n{deletionDate, date, full} klo {deletionDate, time, short}{reason, select, null {} other {\n\nSyy: {reason}}}\n\n{safety_email, select, null {Jos haluat säilyttää tilisi, ota yhteyttä tämän instanssin ylläpitäjiin ennen tätä päivämäärää.} other {Jos 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"
|
||||
"body": "Hei {username},\n\n{product_name}-tilisi on ollut pitkään käyttämättä, joten ajoitimme sen poistettavaksi {deletion_date}.\n\nJos haluat säilyttää tilisi, kirjaudu sisään ennen tätä päivämäärää. {support_email, select, null {Jos sinulla on kysyttävää, ota yhteyttä tämän instanssin ylläpitäjiin.} other {Jos sinulla on kysyttävää, ota yhteyttä osoitteeseen {support_email}.}}\n\n{product_name}-tiimi"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"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. {safety_email, select, null {Jos et pyytänyt tätä tai haluat säilyttää tilisi, ota yhteyttä tämän instanssin ylläpitäjiin ennen tätä päivämäärää.} other {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_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}\n}Tämä on vakava toimenpide. Tilisi tiedot poistetaan pysyvästi ajoitettuna päivänä.\n\n{legalLinks, select,\n both {Tutustu:\n- Käyttöehdot: {termsUrl}\n- Yhteisön säännöt: {guidelinesUrl}\n\n}\n terms {Tutustu:\n- Käyttöehdot: {termsUrl}\n\n}\n guidelines {Tutustu:\n- Yhteisön säännöt: {guidelinesUrl}\n\n}\n other {}\n}Valitusprosessi:\nJos uskot, että tämä päätös oli virheellinen tai perusteeton, sinulla on 60 päivää aikaa tehdä valitus. {appeals_email, select, null {Ota yhteyttä tämän instanssin ylläpitäjiin.} other {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"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "Käyttäjätilisi {product_name}-palvelussa on tilapäisesti jäädytetty",
|
||||
"body": "Hei {username},\n\n{product_name}-käyttäjätilisi on jäädytetty tilapäisesti käyttöehtojemme tai yhteisösääntöjemme rikkomisen vuoksi.\n\nKesto: {durationHours, plural,\n =1 {1 tunti}\n other {# tuntia}\n}\nJäädytettynä {bannedUntil, date, full} klo {bannedUntil, time, short} asti\n\n{reason, select,\n null {}\n other {Syy: {reason}\n\n}\n}Tänä aikana et voi käyttää tiliäsi.\n\n{legalLinks, select,\n both {Tutustu:\n- Käyttöehdot: {termsUrl}\n- Yhteisön säännöt: {guidelinesUrl}\n\n}\n terms {Tutustu:\n- Käyttöehdot: {termsUrl}\n\n}\n guidelines {Tutustu:\n- Yhteisön säännöt: {guidelinesUrl}\n\n}\n other {}\n}Jos uskot, että tämä päätös oli virheellinen tai perusteeton, voit tehdä valituksen. {appeals_email, select, null {Ota yhteyttä tämän instanssin ylläpitäjiin ja selitä selkeästi, miksi uskot päätöksen olleen virheellinen.} other {Lähetä sähköpostia osoitteeseen {appeals_email} tästä sähköpostiosoitteesta ja selitä selkeästi, miksi uskot päätöksen olleen virheellinen.}} Käsittelemme valituksesi ja ilmoitamme sinulle päätöksemme.\n\n– {product_name}-turvallisuustiimi"
|
||||
"body": "Hei {username},\n\nPyyntösi mukaisesti ajoitimme {product_name}-tilisi poistettavaksi {deletion_date}.\n\nJos haluat säilyttää tilisi, kirjaudu sisään ennen {deletion_date}. {safety_email, select, null {Jos et pyytänyt tätä, ota yhteyttä tämän instanssin ylläpitäjiin.} other {Jos et pyytänyt tätä, ota yhteyttä osoitteeseen {safety_email}.}}\n\n{product_name}-tiimi"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Kiitos {product_name}-lahjoituksestasi",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "Teimme päätöksen {product_name}-ilmoituksestasi",
|
||||
"body": "Hei,\n\nTurvallisuustiimimme on käsitellyt digipalvelusäädöksen mukaisen ilmoituksesi (ID: {reportId}) ja tehnyt päätöksen.{hasComment, select, yes {\n\nTurvallisuustiimin vastaus:\n{publicComment}} other {}}\n\nEmme kerro toisen henkilön tiliin kohdistuneista toimista, koska ne ovat hänen henkilötietojaan.\n\nJos olet eri mieltä päätöksestä, voit valittaa siitä maksutta 60 päivän kuluessa. {appeals_email, select, null {Ota yhteyttä tämän instanssin ylläpitäjiin, kerro ilmoituksesi tunnus ja selitä, miksi pidät päätöstä virheellisenä.} other {Lähetä sähköpostia osoitteeseen {appeals_email} tästä sähköpostiosoitteesta, kerro ilmoituksesi tunnus ja selitä, miksi pidät päätöstä virheellisenä.}} Jos olet EU:ssa, voit myös viedä riidan sertifioidun tuomioistuimen ulkopuolisen riidanratkaisuelimen käsiteltäväksi. Mikään tästä ei vaikuta oikeuteesi viedä asia tuomioistuimeen.\n\n– {product_name}-turvallisuustiimi"
|
||||
"body": "Hei,\n\nKävimme läpi ilmoituksesi (tunnus: {reportId}).\n\n{outcome, select, removed {Poistimme ilmoittamasi sisällön, koska se rikkoi sääntöjämme.} restricted {Totesimme, että ilmoittamasi tili rikkoi sääntöjämme, ja rajoitimme sitä. Emme kerro, mihin toimiin ryhdyimme toista tiliä kohtaan, koska se on kyseisen henkilön henkilötietoa.} action_taken {Totesimme, että ilmoittamasi asia rikkoi sääntöjämme, ja ryhdyimme toimiin. Emme kerro, mihin toimiin ryhdyimme toista tiliä kohtaan, koska se on kyseisen henkilön henkilötietoa.} no_violation {Totesimme, että ilmoittamasi sisältö tai tili ei riko sääntöjämme, joten jätimme sen ennalleen.} other {Suljimme ilmoituksesi.}}{hasComment, select, yes {\n\nTiimimme huomautus: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nJos olet eri mieltä, lähetä sähköpostia osoitteeseen {appeals_email} 60 päivän kuluessa ja mainitse ilmoituksen tunnus. Jos olet EU:ssa, voit myös saattaa asian sertifioidun tuomioistuinten ulkopuolisen riitojenratkaisuelimen käsiteltäväksi tai tehdä valituksen maasi digitaalisten palvelujen koordinaattorille.}}\n\n{product_name}-tiimi"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Vahvista sähköpostiosoitteesi DSA-ilmoitusta varten",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Vahvista sähköpostiosoitteesi {product_name}-tilillä",
|
||||
"body": "Hei {username},\n\nVahvista {product_name}-tilisi sähköpostiosoite napsauttamalla alla olevaa linkkiä:\n\n{verifyUrl}\n\nJos et luonut {product_name}-tiliä, voit jättää tämän sähköpostin turvallisesti huomiotta.\n\nTämä linkki on voimassa 24 tuntia.\n\n– {product_name}-tiimi"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {Kumosimme {product_name}-tiliäsi koskevan päätöksen} other {{product_name}-tilisi rajoitus on päättynyt}}",
|
||||
"body": "Hei {username},\n\n{outcome, select, reversed {{decision_id, select, null {Tarkastelimme uudelleen tiliäsi koskevaa päätöstä ja kumosimme sen.} other {Tarkastelimme uudelleen päätöstä {decision_id} ja kumosimme sen.}} Pahoittelemme aiheutunutta harmia.} other {{decision_id, select, null {Poistimme tiliisi kohdistuneen rajoituksen.} other {Poistimme päätökseen {decision_id} perustuneen rajoituksen.}}}}{restores_access, select, yes { Voit taas kirjautua sisään ja käyttää palvelua {product_name}.} other {}}{statement, select, null {} other {\n\nTiimimme huomautus: {statement}}}{content_removed, select, yes {\n\nPoistamaamme sisältöä ei voi palauttaa.} other {}}\n\n{product_name}-tiimi"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {Tietoa {product_name}-tilistäsi} other {{kind_label, select, warning {Varoitus {product_name}-tilisi toiminnasta} suspension {{product_name}-tilisi on jäädytetty} ban {{product_name}-tilisi on jäädytetty pysyvästi} closure {{product_name}-tilisi on suljettu} feature_limit {Joitakin {product_name}-tilisi ominaisuuksia on rajoitettu} content_removal {Poistimme viestejä, jotka lähetit palvelussa {product_name}} profile_reset {Nollasimme osia {product_name}-profiilistasi} community_removal {Poistimme yhteisösi palvelusta {product_name}} bot {Tietoa botistasi palvelussa {product_name}} other {Tietoa {product_name}-tilistäsi}}}}",
|
||||
"body": "Hei {username},\n\n{summary}\n\n{effects}\n\nMiksi\nSääntö: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nTiimimme huomautus: {statement}}}\n\nMiten päätimme\n{how_we_decided}{appeals_email, select, null {} other {\n\nNäin valitat päätöksestä\nJos uskot, että teimme virheen, lähetä sähköpostia osoitteeseen {appeals_email} viimeistään {appeal_deadline}. Kirjoita tilisi sähköpostiosoitteesta, jos voit, ja mainitse päätöksen tunnus. Valittaminen on maksutonta, ja jokaisen valituksen käsittelee ihminen.{redress, select, null {} other {\n\n{redress}}}}}\n\nPäätöksen tunnus: {decision_id}\n\n{product_name}-tiimi"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Lunastamasi lahjan edut on poistettu",
|
||||
"body": "Hei {username},\n\nLunastamasi lahjakoodin oli alun perin maksanut joku muu. Kyseinen maksu on sittemmin peruutettu (takaisinperintä).\n\nTämän vuoksi olemme poistaneet edut, jotka lisättiin tilillesi, kun lunastit lahjan.\n\n{support_email, select, null {Jos uskot tämän olevan virhe, ota yhteyttä tämän instanssin ylläpitäjiin ja liitä mukaan kaikki tiedot lahjakoodista ja sen lunastusajankohdasta.} other {Jos uskot tämän olevan virhe, ota yhteyttä tukitiimiimme ja liitä mukaan kaikki tiedot lahjakoodista ja sen lunastusajankohdasta.}}\n\n– {product_name}-tiimi"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "{product_name}-ilmoituksesi on käsitelty",
|
||||
"body": "Hei {username},\n\nTurvallisuustiimimme on käsitellyt ilmoituksesi (ID: {reportId}).{hasComment, select, yes {\n\nTurvallisuustiimin vastaus:\n{publicComment}} other {}}\n\nKiitos, että autat pitämään {product_name}-palvelun turvallisena kaikille. Otamme kaikki ilmoitukset vakavasti ja arvostamme panostasi yhteisöön.\n\n{safety_email, select, null {Jos sinulla on kysyttävää tai huolenaiheita tästä päätöksestä, ota yhteyttä tämän instanssin ylläpitäjiin.} other {Jos sinulla on kysyttävää tai huolenaiheita tästä päätöksestä, ota yhteyttä osoitteeseen {safety_email}.}}\n\n– {product_name}-turvallisuustiimi"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"subject": "Käyttäjätilisi {product_name}-palvelussa poistetaan pysyvästi",
|
||||
"body": "Hei {username},\n\n{product_name}-tilisi on ajoitettu poistettavaksi pysyvästi.\n\nAjoitettu poisto: {deletionDate, date, full} klo {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Syy: {reason}\n\n}\n}Tämä on vakava toimenpide. Tilisi tiedot poistetaan pysyvästi ajoitettuna päivänä.\n\nJos uskot, että tämä päätös oli virheellinen, voit tehdä valituksen. {appeals_email, select, null {Ota yhteyttä tämän instanssin ylläpitäjiin.} other {Lähetä sähköpostia osoitteeseen {appeals_email} tästä sähköpostiosoitteesta.}}\n\n– {product_name}-turvallisuustiimi"
|
||||
"body": "Hei {username},\n\nKävimme läpi ilmoituksesi (tunnus: {reportId}).\n\n{outcome, select, removed {Poistimme ilmoittamasi sisällön, koska se rikkoi sääntöjämme.} restricted {Totesimme, että ilmoittamasi tili rikkoi sääntöjämme, ja rajoitimme sitä. Emme kerro, mihin toimiin ryhdyimme toista tiliä kohtaan, koska se on kyseisen henkilön henkilötietoa.} action_taken {Totesimme, että ilmoittamasi asia rikkoi sääntöjämme, ja ryhdyimme toimiin. Emme kerro, mihin toimiin ryhdyimme toista tiliä kohtaan, koska se on kyseisen henkilön henkilötietoa.} no_violation {Totesimme, että ilmoittamasi sisältö tai tili ei riko sääntöjämme, joten jätimme sen ennalleen.} other {Suljimme ilmoituksesi.}}{hasComment, select, yes {\n\nTiimimme huomautus: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nJos olet eri mieltä, lähetä sähköpostia osoitteeseen {appeals_email} 60 päivän kuluessa ja mainitse ilmoituksen tunnus. Jos olet EU:ssa, voit myös saattaa asian sertifioidun tuomioistuinten ulkopuolisen riitojenratkaisuelimen käsiteltäväksi tai tehdä valituksen maasi digitaalisten palvelujen koordinaattorille.}}\n\n{product_name}-tiimi"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "{product_name}-tilisi poistaminen on ajoitettu",
|
||||
"body": "Hei {username},\n\nPyysit {product_name}-tilisi poistamista. Tilisi on ajoitettu poistettavaksi pysyvästi:\n\n{deletionDate, date, full} klo {deletionDate, time, short}\n\nJos et pyytänyt tätä, kirjaudu tilillesi peruuttaaksesi poiston. Suosittelemme myös vaihtamaan salasanasi tilisi turvaamiseksi.\n\n– {product_name}-tiimi"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "{product_name}-tilisi jäädytys on poistettu",
|
||||
"body": "Hei {username},\n\nHyviä uutisia: {product_name}-tilisi jäädytys on poistettu.\n\n{reason, select,\n null {}\n other {Syy: {reason}\n\n}\n}Voit nyt kirjautua takaisin sisään ja jatkaa {product_name}-palvelun käyttöä normaalisti.\n\n– {product_name}-turvallisuustiimi"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"subject": "Votre compte {product_name} sera supprimé pour inactivité",
|
||||
"body": "Bonjour {username},\n\nVotre compte {product_name} est inactif depuis longtemps. Sa suppression définitive est donc prévue le :\n\n{deletionDate, date, full} à {deletionDate, time, short}{reason, select, null {} other {\n\nMotif : {reason}}}\n\n{safety_email, select, null {Si vous souhaitez conserver votre compte, contactez les administrateurs de cette instance avant cette date.} other {Si vous souhaitez conserver votre compte, contactez {safety_email} depuis cette adresse e-mail avant cette date.}}\n\n– L’équipe {product_name}"
|
||||
"body": "Bonjour {username},\n\nVotre compte {product_name} est inactif depuis longtemps. Nous avons donc programmé sa suppression pour le {deletion_date}.\n\nPour conserver votre compte, connectez-vous avant cette date. {support_email, select, null {Si vous avez des questions, contactez les administrateurs de cette instance.} other {Si vous avez des questions, contactez {support_email}.}}\n\nL’équipe {product_name}"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"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à. {safety_email, select, null {Si vous n’êtes pas à l’origine de cette demande, ou si vous souhaitez conserver votre compte, contactez les administrateurs de cette instance avant cette date.} other {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_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\n}}Il s’agit d’une sanction importante. Les données de votre compte seront définitivement supprimées à la date prévue.\n\n{legalLinks, select,\n both {Veuillez consulter :\n- Conditions d’utilisation : {termsUrl}\n- Règles de la communauté : {guidelinesUrl}\n\n}\n terms {Veuillez consulter :\n- Conditions d’utilisation : {termsUrl}\n\n}\n guidelines {Veuillez consulter :\n- Règles de la communauté : {guidelinesUrl}\n\n}\n other {}\n}Procé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. {appeals_email, select, null {Contactez les administrateurs de cette instance.} other {É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}"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "Votre compte {product_name} a été temporairement suspendu",
|
||||
"body": "Bonjour {username},\n\nVotre compte {product_name} a été temporairement suspendu pour violation de nos conditions d’utilisation ou des règles de la communauté.\n\nDurée : {durationHours, plural, =1 {1 heure} other {# heures}}\nSuspension jusqu’au : {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select, null {} other {Motif : {reason}\n\n}}Vous ne pourrez pas accéder à votre compte pendant cette période.\n\n{legalLinks, select,\n both {Veuillez consulter :\n- Conditions d’utilisation : {termsUrl}\n- Règles de la communauté : {guidelinesUrl}\n\n}\n terms {Veuillez consulter :\n- Conditions d’utilisation : {termsUrl}\n\n}\n guidelines {Veuillez consulter :\n- Règles de la communauté : {guidelinesUrl}\n\n}\n other {}\n}Si vous estimez que cette décision est incorrecte ou injustifiée, vous pouvez déposer un recours. {appeals_email, select, null {Contactez les administrateurs de cette instance et expliquez clairement pourquoi vous contestez cette décision.} other {Écrivez à {appeals_email} depuis cette adresse e-mail et expliquez clairement pourquoi vous contestez cette décision.}} Nous examinerons votre recours et vous communiquerons notre décision.\n\n– L’équipe de sécurité de {product_name}"
|
||||
"body": "Bonjour {username},\n\nComme vous l’avez demandé, la suppression de votre compte {product_name} est programmée pour le {deletion_date}.\n\nPour conserver votre compte, connectez-vous avant le {deletion_date}. {safety_email, select, null {Si vous n’êtes pas à l’origine de cette demande, contactez les administrateurs de cette instance.} other {Si vous n’êtes pas à l’origine de cette demande, contactez {safety_email}.}}\n\nL’équipe {product_name}"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Merci pour votre don à {product_name}",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "Nous avons pris une décision concernant votre signalement sur {product_name}",
|
||||
"body": "Bonjour,\n\nNotre équipe de sécurité a examiné votre signalement au titre du règlement sur les services numériques (ID : {reportId}) et a pris une décision.{hasComment, select, yes {\n\nRéponse de l’équipe de sécurité :\n{publicComment}} other {}}\n\nNous ne communiquons pas le détail des mesures prises à l’encontre du compte d’une autre personne, car il s’agit de ses données personnelles.\n\nSi vous n’êtes pas d’accord avec cette décision, vous pouvez déposer un recours gratuitement dans un délai de 60 jours. {appeals_email, select, null {Contactez les administrateurs de cette instance, indiquez l’ID de votre signalement et expliquez pourquoi vous estimez que la décision est incorrecte.} other {Écrivez à {appeals_email} depuis cette adresse e-mail, indiquez l’ID de votre signalement et expliquez pourquoi vous estimez que la décision est incorrecte.}} Si vous êtes dans l’UE, vous pouvez également soumettre le litige à un organe certifié de règlement extrajudiciaire des litiges. Rien de tout cela n’affecte votre droit de saisir un tribunal.\n\n– L’équipe de sécurité de {product_name}"
|
||||
"body": "Bonjour,\n\nNous avons examiné votre signalement (ID : {reportId}).\n\n{outcome, select, removed {Nous avons supprimé le contenu que vous avez signalé, car il enfreignait nos règles.} restricted {Nous avons constaté que le compte que vous avez signalé enfreignait nos règles, et nous l’avons restreint. Nous ne communiquons pas les mesures prises contre un autre compte, car il s’agit de données personnelles de son titulaire.} action_taken {Nous avons constaté que ce que vous avez signalé enfreignait nos règles, et nous avons pris des mesures. Nous ne communiquons pas les mesures prises contre un autre compte, car il s’agit de données personnelles de son titulaire.} no_violation {Nous avons constaté que le contenu ou le compte que vous avez signalé n’enfreint pas nos règles, et nous n’avons donc rien changé.} other {Nous avons clôturé votre signalement.}}{hasComment, select, yes {\n\nNote de notre équipe : {publicComment}} other {}}{appeals_email, select, null {} other {\n\nSi vous n’êtes pas d’accord, écrivez à {appeals_email} dans un délai de 60 jours en indiquant l’ID du signalement. Si vous êtes dans l’UE, vous pouvez également soumettre la question à un organe de règlement extrajudiciaire des litiges certifié ou introduire une réclamation auprès du coordinateur pour les services numériques de votre pays.}}\n\nL’équipe {product_name}"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Vérifiez votre e-mail pour un signalement DSA",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Vérifiez votre adresse e-mail {product_name}",
|
||||
"body": "Bonjour {username},\n\nVérifiez l’adresse e-mail de votre compte {product_name} en cliquant sur le lien ci-dessous :\n\n{verifyUrl}\n\nSi vous n’avez pas créé de compte {product_name}, vous pouvez ignorer cet e-mail.\n\nCe lien est valable pendant 24 heures.\n\n– L’équipe {product_name}"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {Nous avons annulé une décision concernant votre compte {product_name}} other {Une restriction appliquée à votre compte {product_name} a pris fin}}",
|
||||
"body": "Bonjour {username},\n\n{outcome, select, reversed {{decision_id, select, null {Nous avons réexaminé une décision concernant votre compte et l’avons annulée.} other {Nous avons réexaminé la décision {decision_id} et l’avons annulée.}} Nous sommes désolés pour la gêne occasionnée.} other {{decision_id, select, null {Nous avons levé la restriction appliquée à votre compte.} other {Nous avons levé la restriction liée à la décision {decision_id}.}}}}{restores_access, select, yes { Vous pouvez de nouveau vous connecter et utiliser {product_name}.} other {}}{statement, select, null {} other {\n\nNote de notre équipe : {statement}}}{content_removed, select, yes {\n\nLe contenu que nous avons supprimé ne peut pas être restauré.} other {}}\n\nL’équipe {product_name}"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {Information concernant votre compte {product_name}} other {{kind_label, select, warning {Avertissement concernant votre compte {product_name}} suspension {Votre compte {product_name} est suspendu} ban {Votre compte {product_name} est banni} closure {Votre compte {product_name} est fermé} feature_limit {Certaines fonctionnalités de votre compte {product_name} sont limitées} content_removal {Nous avons supprimé des messages que vous avez envoyés sur {product_name}} profile_reset {Nous avons réinitialisé une partie de votre profil {product_name}} community_removal {Nous avons supprimé votre communauté sur {product_name}} bot {Information concernant votre bot sur {product_name}} other {Information concernant votre compte {product_name}}}}}",
|
||||
"body": "Bonjour {username},\n\n{summary}\n\n{effects}\n\nPourquoi\nRègle : {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nNote de notre équipe : {statement}}}\n\nComment nous avons décidé\n{how_we_decided}{appeals_email, select, null {} other {\n\nComment contester la décision\nSi vous pensez que nous nous sommes trompés, écrivez à {appeals_email} au plus tard le {appeal_deadline}. Utilisez si possible l’adresse e-mail de votre compte, et indiquez l’ID de la décision. La contestation est gratuite, et une personne examine chaque demande.{redress, select, null {} other {\n\n{redress}}}}}\n\nID de la décision : {decision_id}\n\nL’équipe {product_name}"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Les avantages de votre cadeau activé ont été retirés",
|
||||
"body": "Bonjour {username},\n\nUn code cadeau que vous avez utilisé avait été payé par une autre personne. Ce paiement a depuis été annulé à la suite d’une contestation bancaire.\n\nNous avons donc retiré les avantages ajoutés à votre compte lors de l’activation du cadeau.\n\n{support_email, select, null {Si vous pensez qu’il s’agit d’une erreur, contactez les administrateurs de cette instance et indiquez les informations dont vous disposez sur le code cadeau et sa date d’utilisation.} other {Si vous pensez qu’il s’agit d’une erreur, contactez notre équipe d’assistance et indiquez les informations dont vous disposez sur le code cadeau et sa date d’utilisation.}}\n\n– L’équipe {product_name}"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "Votre signalement sur {product_name} a été examiné",
|
||||
"body": "Bonjour {username},\n\nVotre signalement (ID : {reportId}) a été examiné par notre équipe de sécurité.{hasComment, select, yes {\n\nRéponse de l’équipe de sécurité :\n{publicComment}} other {}}\n\nMerci de nous aider à préserver la sécurité de {product_name} pour tous. Nous prenons tous les signalements au sérieux et apprécions votre contribution à la communauté.\n\n{safety_email, select, null {Pour toute question ou préoccupation concernant cette décision, contactez les administrateurs de cette instance.} other {Pour toute question ou préoccupation concernant cette décision, contactez {safety_email}.}}\n\n– L’équipe de sécurité de {product_name}"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"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.\n\nSuppression prévue : {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select, null {} other {Motif : {reason}\n\n}}Il s’agit d’une sanction importante. Les données de votre compte seront définitivement supprimées à la date prévue.\n\nSi vous estimez que cette décision est incorrecte, vous pouvez déposer un recours. {appeals_email, select, null {Contactez les administrateurs de cette instance.} other {Écrivez à {appeals_email} depuis cette adresse e-mail.}}\n\n– L’équipe de sécurité de {product_name}"
|
||||
"body": "Bonjour {username},\n\nNous avons examiné votre signalement (ID : {reportId}).\n\n{outcome, select, removed {Nous avons supprimé le contenu que vous avez signalé, car il enfreignait nos règles.} restricted {Nous avons constaté que le compte que vous avez signalé enfreignait nos règles, et nous l’avons restreint. Nous ne communiquons pas les mesures prises contre un autre compte, car il s’agit de données personnelles de son titulaire.} action_taken {Nous avons constaté que ce que vous avez signalé enfreignait nos règles, et nous avons pris des mesures. Nous ne communiquons pas les mesures prises contre un autre compte, car il s’agit de données personnelles de son titulaire.} no_violation {Nous avons constaté que le contenu ou le compte que vous avez signalé n’enfreint pas nos règles, et nous n’avons donc rien changé.} other {Nous avons clôturé votre signalement.}}{hasComment, select, yes {\n\nNote de notre équipe : {publicComment}} other {}}{appeals_email, select, null {} other {\n\nSi vous n’êtes pas d’accord, écrivez à {appeals_email} dans un délai de 60 jours en indiquant l’ID du signalement. Si vous êtes dans l’UE, vous pouvez également soumettre la question à un organe de règlement extrajudiciaire des litiges certifié ou introduire une réclamation auprès du coordinateur pour les services numériques de votre pays.}}\n\nL’équipe {product_name}"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "La suppression de votre compte {product_name} est programmée",
|
||||
"body": "Bonjour {username},\n\nVous avez demandé la suppression de votre compte {product_name}. Sa suppression définitive est prévue le :\n\n{deletionDate, date, full} à {deletionDate, time, short}\n\nSi vous n’êtes pas à l’origine de cette demande, connectez-vous à votre compte pour annuler la suppression. Nous vous recommandons également de changer votre mot de passe pour sécuriser votre compte.\n\n– L’équipe {product_name}"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "La suspension de votre compte {product_name} a été levée",
|
||||
"body": "Bonjour {username},\n\nBonne nouvelle : la suspension de votre compte {product_name} a été levée.\n\n{reason, select, null {} other {Motif : {reason}\n\n}}Vous pouvez maintenant vous reconnecter et utiliser {product_name} normalement.\n\n– L’équipe de sécurité de {product_name}"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"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, select, null {אם ברצונכם לשמור על החשבון, צרו קשר עם מנהלי המערכת של המופע הזה לפני תאריך זה.} other {אם ברצונכם לשמור על החשבון, צרו קשר עם {safety_email} מכתובת האימייל הזו לפני תאריך זה.}}\n\nצוות {product_name}"
|
||||
"body": "שלום {username},\n\nחשבון ה-{product_name} שלכם לא היה פעיל זמן רב, ולכן תזמנו את מחיקתו ל-{deletion_date}.\n\nכדי לשמור על החשבון, התחברו לפני התאריך הזה. {support_email, select, null {אם יש לכם שאלות, צרו קשר עם מנהלי המערכת של המופע הזה.} other {אם יש לכם שאלות, צרו קשר עם {support_email}.}}\n\nצוות {product_name}"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"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, select, null {אם לא ביקשתם זאת, או שברצונכם לשמור על החשבון, צרו קשר עם מנהלי המערכת של המופע הזה לפני תאריך זה.} other {אם לא ביקשתם זאת, או שברצונכם לשמור על החשבון, צרו קשר עם {safety_email} מכתובת האימייל הזו לפני תאריך זה.}}\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{legalLinks, select,\n both {אנא עיינו במסמכים הבאים:\n- תנאי השירות: {termsUrl}\n- הנחיות הקהילה: {guidelinesUrl}\n\n}\n terms {אנא עיינו במסמכים הבאים:\n- תנאי השירות: {termsUrl}\n\n}\n guidelines {אנא עיינו במסמכים הבאים:\n- הנחיות הקהילה: {guidelinesUrl}\n\n}\n other {}\n}תהליך ערעור:\nאם אתם חושבים שהחלטת האכיפה שגויה או בלתי מוצדקת, יש לכם 60 יום להגיש ערעור. {appeals_email, select, null {צרו קשר עם מנהלי המערכת של המופע הזה.} other {שלחו אימייל לכתובת {appeals_email} מכתובת האימייל הזו.}}\n\nבערעור:\n- הסבירו בבירור מדוע לדעתכם החלטת האכיפה שגויה או בלתי מוצדקת\n- צרפו כל ראיה או הקשר רלוונטיים\n\nחבר בצוות הבטיחות של {product_name} יבדוק את הערעור שלכם ועשוי להשהות את המחיקה הממתינה עד לקבלת החלטה סופית.\n\nצוות הבטיחות של {product_name}"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "חשבון ה-{product_name} שלכם הושעה זמנית",
|
||||
"body": "שלום {username},\n\nחשבון ה-{product_name} שלכם הושעה זמנית עקב הפרה של תנאי השירות או הנחיות הקהילה שלנו.\n\nמשך ההשעיה: {durationHours, plural,\n =1 {שעה אחת}\n other {# שעות}\n}\nמושעה עד: {bannedUntil, date, full} בשעה {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {סיבה: {reason}\n\n}\n}במהלך תקופה זו לא תהיה לכם גישה לחשבון.\n\n{legalLinks, select,\n both {אנא עיינו במסמכים הבאים:\n- תנאי השירות: {termsUrl}\n- הנחיות הקהילה: {guidelinesUrl}\n\n}\n terms {אנא עיינו במסמכים הבאים:\n- תנאי השירות: {termsUrl}\n\n}\n guidelines {אנא עיינו במסמכים הבאים:\n- הנחיות הקהילה: {guidelinesUrl}\n\n}\n other {}\n}אם אתם חושבים שהחלטת האכיפה שגויה או בלתי מוצדקת, תוכלו להגיש ערעור. {appeals_email, select, null {צרו קשר עם מנהלי המערכת של המופע הזה והסבירו בבירור מדוע לדעתכם ההחלטה שגויה.} other {שלחו אימייל לכתובת {appeals_email} מכתובת האימייל הזו והסבירו בבירור מדוע לדעתכם ההחלטה שגויה.}} נבדוק את הערעור ונעדכן אתכם בהחלטה שלנו.\n\nצוות הבטיחות של {product_name}"
|
||||
"body": "שלום {username},\n\nכפי שביקשתם, תזמנו את מחיקת חשבון ה-{product_name} שלכם ל-{deletion_date}.\n\nכדי לשמור על החשבון, התחברו לפני {deletion_date}. {safety_email, select, null {אם לא ביקשתם את זה, צרו קשר עם מנהלי המערכת של המופע הזה.} other {אם לא ביקשתם את זה, צרו קשר עם {safety_email}.}}\n\nצוות {product_name}"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "תודה על תרומתכם ל-{product_name}",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "קיבלנו החלטה בנוגע לדיווח שלכם ב-{product_name}",
|
||||
"body": "שלום,\n\nצוות הבטיחות שלנו בדק את הדיווח שלכם לפי חוק השירותים הדיגיטליים (מזהה: {reportId}) וקיבל החלטה.{hasComment, select, yes {\n\nתגובה מצוות הבטיחות:\n{publicComment}} other {}}\n\nאיננו משתפים פרטים על פעולות שננקטו נגד חשבון של אדם אחר, מכיוון שאלה נתונים אישיים שלו.\n\nאם אינכם מסכימים עם ההחלטה, תוכלו להגיש ערעור ללא תשלום בתוך 60 יום. {appeals_email, select, null {צרו קשר עם מנהלי המערכת של המופע הזה, ציינו את מזהה הדיווח והסבירו מדוע לדעתכם ההחלטה שגויה.} other {שלחו אימייל לכתובת {appeals_email} מכתובת האימייל הזו, ציינו את מזהה הדיווח והסבירו מדוע לדעתכם ההחלטה שגויה.}} אם אתם נמצאים באיחוד האירופי, תוכלו גם להפנות את המחלוקת לגוף מוסמך ליישוב סכסוכים מחוץ לבית המשפט. אין בכך כדי לפגוע בזכותכם לפנות לבית המשפט.\n\nצוות הבטיחות של {product_name}"
|
||||
"body": "שלום,\n\nבדקנו את הדיווח שלכם (מזהה: {reportId}).\n\n{outcome, select, removed {הסרנו את התוכן שדיווחתם עליו כי הוא הפר את הכללים שלנו.} restricted {מצאנו שהחשבון שדיווחתם עליו הפר את הכללים שלנו, והגבלנו אותו. אנחנו לא משתפים איזו פעולה נקטנו נגד חשבון אחר, כי זה מידע אישי של בעליו.} action_taken {מצאנו שמה שדיווחתם עליו הפר את הכללים שלנו, ונקטנו פעולה. אנחנו לא משתפים איזו פעולה נקטנו נגד חשבון אחר, כי זה מידע אישי של בעליו.} no_violation {מצאנו שהתוכן או החשבון שדיווחתם עליו לא מפר את הכללים שלנו, ולכן השארנו אותו כמו שהוא.} other {סגרנו את הדיווח שלכם.}}{hasComment, select, yes {\n\nהערה מהצוות שלנו: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nאם אתם לא מסכימים, שלחו אימייל אל {appeals_email} תוך 60 יום וציינו את מזהה הדיווח. אם אתם באיחוד האירופי, אפשר גם להפנות את העניין לגוף מוסמך ליישוב סכסוכים מחוץ לבית המשפט או להגיש תלונה לרכז השירותים הדיגיטליים במדינה שלכם.}}\n\nצוות {product_name}"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "אימות האימייל שלכם לדיווח DSA",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "אימות האימייל שלכם ב-{product_name}",
|
||||
"body": "שלום {username},\n\nאנא אמתו את כתובת האימייל של חשבון ה-{product_name} שלכם באמצעות לחיצה על הקישור למטה:\n\n{verifyUrl}\n\nאם לא יצרתם חשבון {product_name}, אתם יכולים להתעלם בבטחה מאימייל זה.\n\nקישור זה תקף למשך 24 שעות.\n\nצוות {product_name}"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {ביטלנו החלטה לגבי חשבון ה-{product_name} שלכם} other {הגבלה על חשבון ה-{product_name} שלכם הסתיימה}}",
|
||||
"body": "שלום {username},\n\n{outcome, select, reversed {{decision_id, select, null {בדקנו שוב החלטה לגבי החשבון שלכם וביטלנו אותה.} other {בדקנו שוב את החלטה {decision_id} וביטלנו אותה.}} אנחנו מתנצלים על אי הנוחות.} other {{decision_id, select, null {הסרנו את ההגבלה מהחשבון שלכם.} other {הסרנו את ההגבלה של החלטה {decision_id}.}}}}{restores_access, select, yes { אפשר שוב להתחבר ולהשתמש ב-{product_name}.} other {}}{statement, select, null {} other {\n\nהערה מהצוות שלנו: {statement}}}{content_removed, select, yes {\n\nאי אפשר לשחזר תוכן שהסרנו.} other {}}\n\nצוות {product_name}"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {עדכון לגבי חשבון ה-{product_name} שלכם} other {{kind_label, select, warning {אזהרה לגבי חשבון ה-{product_name} שלכם} suspension {חשבון ה-{product_name} שלכם הושעה} ban {חשבון ה-{product_name} שלכם נחסם} closure {חשבון ה-{product_name} שלכם נסגר} feature_limit {חלק מהתכונות בחשבון ה-{product_name} שלכם הוגבלו} content_removal {הסרנו הודעות ששלחתם ב-{product_name}} profile_reset {איפסנו חלקים מפרופיל ה-{product_name} שלכם} community_removal {הסרנו את הקהילה שלכם ב-{product_name}} bot {עדכון לגבי הבוט שלכם ב-{product_name}} other {עדכון לגבי חשבון ה-{product_name} שלכם}}}}",
|
||||
"body": "שלום {username},\n\n{summary}\n\n{effects}\n\nלמה\nכלל: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nהערה מהצוות שלנו: {statement}}}\n\nאיך החלטנו\n{how_we_decided}{appeals_email, select, null {} other {\n\nאיך לערער\nאם לדעתכם טעינו, שלחו אימייל אל {appeals_email} עד {appeal_deadline}. אם אפשר, כתבו מכתובת האימייל של החשבון שלכם, וציינו את מזהה ההחלטה. הערעור ללא תשלום, ואדם בודק כל ערעור.{redress, select, null {} other {\n\n{redress}}}}}\n\nמזהה החלטה: {decision_id}\n\nצוות {product_name}"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "ההטבות מהמתנה שמימשתם הוסרו",
|
||||
"body": "שלום {username},\n\nקוד מתנה שמימשתם שולם במקור על ידי מישהו אחר. התשלום הזה בוטל מאז (החזר חיוב).\n\nבשל כך, הסרנו את ההטבות שנוספו לחשבונכם כאשר מימשתם את המתנה.\n\n{support_email, select, null {אם אתם חושבים שזו טעות, אנא צרו קשר עם מנהלי המערכת של המופע הזה וצרפו כל פרט שידוע לכם לגבי קוד המתנה ומתי מימשתם אותו.} other {אם אתם חושבים שזו טעות, אנא צרו קשר עם צוות התמיכה שלנו וצרפו כל פרט שידוע לכם לגבי קוד המתנה ומתי מימשתם אותו.}}\n\nצוות {product_name}"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "הדיווח שלכם ב-{product_name} נבדק",
|
||||
"body": "שלום {username},\n\nהדיווח שלכם (מזהה: {reportId}) נבדק על ידי צוות הבטיחות שלנו.{hasComment, select, yes {\n\nתגובה מצוות הבטיחות:\n{publicComment}} other {}}\n\nתודה שעזרתם לשמור על {product_name} כמקום בטוח לכולם. אנו מתייחסים לכל הדיווחים ברצינות ומעריכים את תרומתכם לקהילה.\n\n{safety_email, select, null {אם יש לכם שאלות או חששות לגבי תוצאה זו, אנא צרו קשר עם מנהלי המערכת של המופע הזה.} other {אם יש לכם שאלות או חששות לגבי תוצאה זו, אנא צרו קשר עם {safety_email}.}}\n\nצוות הבטיחות של {product_name}"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"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אם אתם חושבים שהחלטת אכיפה זו שגויה, אתם יכולים להגיש ערעור. {appeals_email, select, null {צרו קשר עם מנהלי המערכת של המופע הזה.} other {שלחו אימייל לכתובת {appeals_email} מכתובת האימייל הזו.}}\n\nצוות הבטיחות של {product_name}"
|
||||
"body": "שלום {username},\n\nבדקנו את הדיווח שלכם (מזהה: {reportId}).\n\n{outcome, select, removed {הסרנו את התוכן שדיווחתם עליו כי הוא הפר את הכללים שלנו.} restricted {מצאנו שהחשבון שדיווחתם עליו הפר את הכללים שלנו, והגבלנו אותו. אנחנו לא משתפים איזו פעולה נקטנו נגד חשבון אחר, כי זה מידע אישי של בעליו.} action_taken {מצאנו שמה שדיווחתם עליו הפר את הכללים שלנו, ונקטנו פעולה. אנחנו לא משתפים איזו פעולה נקטנו נגד חשבון אחר, כי זה מידע אישי של בעליו.} no_violation {מצאנו שהתוכן או החשבון שדיווחתם עליו לא מפר את הכללים שלנו, ולכן השארנו אותו כמו שהוא.} other {סגרנו את הדיווח שלכם.}}{hasComment, select, yes {\n\nהערה מהצוות שלנו: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nאם אתם לא מסכימים, שלחו אימייל אל {appeals_email} תוך 60 יום וציינו את מזהה הדיווח. אם אתם באיחוד האירופי, אפשר גם להפנות את העניין לגוף מוסמך ליישוב סכסוכים מחוץ לבית המשפט או להגיש תלונה לרכז השירותים הדיגיטליים במדינה שלכם.}}\n\nצוות {product_name}"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "מחיקת חשבון ה-{product_name} שלכם מתוזמנת",
|
||||
"body": "שלום {username},\n\nביקשתם למחוק את חשבון ה-{product_name} שלכם. החשבון שלכם מתוכנן להימחק לצמיתות בתאריך:\n\n{deletionDate, date, full} בשעה {deletionDate, time, short}\n\nאם לא ביקשתם זאת, התחברו לחשבונכם כדי לבטל את המחיקה. אנו ממליצים גם לשנות את הסיסמה שלכם כדי לאבטח את החשבון.\n\nצוות {product_name}"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "השעיית חשבון ה-{product_name} שלכם בוטלה",
|
||||
"body": "שלום {username},\n\nחדשות טובות: השעיית חשבון ה-{product_name} שלכם בוטלה.\n\n{reason, select,\n null {}\n other {סיבה: {reason}\n\n}\n}כעת אתם יכולים להתחבר שוב ולהמשיך להשתמש ב-{product_name} כרגיל.\n\nצוות הבטיחות של {product_name}"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"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, select, null {अगर आप अपना अकाउंट रखना चाहते हैं, तो उस तारीख से पहले इस इंस्टेंस के एडमिनिस्ट्रेटर से संपर्क करें।} other {अगर आप अपना अकाउंट रखना चाहते हैं, तो उस तारीख से पहले इसी ईमेल पते से {safety_email} पर संपर्क करें।}}\n\n– {product_name} टीम"
|
||||
"body": "नमस्ते {username},\n\nआपका {product_name} अकाउंट लंबे समय से इनएक्टिव है, इसलिए हमने इसे {deletion_date} को डिलीट करने के लिए शेड्यूल कर दिया है।\n\nअपना अकाउंट रखने के लिए, उस तारीख से पहले साइन इन करें। {support_email, select, null {अगर आपके कोई सवाल हैं, तो इस इंस्टेंस के एडमिनिस्ट्रेटर से संपर्क करें।} other {अगर आपके कोई सवाल हैं, तो {support_email} से संपर्क करें।}}\n\n{product_name} टीम"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"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, select, null {अगर आपने इसका अनुरोध नहीं किया था, या आप अपना अकाउंट रखना चाहते हैं, तो उस तारीख से पहले इस इंस्टेंस के एडमिनिस्ट्रेटर से संपर्क करें।} other {अगर आपने इसका अनुरोध नहीं किया था, या आप अपना अकाउंट रखना चाहते हैं, तो उस तारीख से पहले इसी ईमेल पते से {safety_email} पर संपर्क करें।}}\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{legalLinks, select,\n both {कृपया देखें:\n- सेवा की शर्तें: {termsUrl}\n- कम्युनिटी गाइडलाइंस: {guidelinesUrl}\n\n}\n terms {कृपया देखें:\n- सेवा की शर्तें: {termsUrl}\n\n}\n guidelines {कृपया देखें:\n- कम्युनिटी गाइडलाइंस: {guidelinesUrl}\n\n}\n other {}\n}अपील प्रक्रिया:\nअगर आपको लगता है कि यह फैसला गलत या अनुचित था, तो आपके पास अपील सबमिट करने के लिए 60 दिन हैं। {appeals_email, select, null {इस इंस्टेंस के एडमिनिस्ट्रेटर से संपर्क करें।} other {इस ईमेल पते से {appeals_email} पर ईमेल करें।}}\n\nअपनी अपील में:\n- साफ़ तौर पर बताएं कि आपको क्यों लगता है कि यह फैसला गलत या अनुचित था\n- कोई भी प्रासंगिक सबूत या जानकारी दें\n\n{product_name} सेफ्टी टीम का एक सदस्य आपकी अपील का रिव्यू करेगा और अंतिम फैसला आने तक डिलीट होने की प्रक्रिया को रोक सकता है।\n\n– {product_name} सेफ्टी टीम"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "आपका {product_name} अकाउंट अस्थायी रूप से सस्पेंड कर दिया गया है",
|
||||
"body": "नमस्ते {username},\n\nआपके {product_name} अकाउंट को हमारी सेवा की शर्तों या कम्युनिटी गाइडलाइंस का उल्लंघन करने के लिए अस्थायी रूप से सस्पेंड कर दिया गया है।\n\nअवधि: {durationHours, plural,\n =1 {1 घंटा}\n other {# घंटे}\n}\nसस्पेंड रहेगा: {bannedUntil, date, full} {bannedUntil, time, short} तक\n\n{reason, select,\n null {}\n other {वजह: {reason}\n\n}\n}इस दौरान, आप अपने अकाउंट को एक्सेस नहीं कर पाएँगे।\n\n{legalLinks, select,\n both {कृपया देखें:\n- सेवा की शर्तें: {termsUrl}\n- कम्युनिटी गाइडलाइंस: {guidelinesUrl}\n\n}\n terms {कृपया देखें:\n- सेवा की शर्तें: {termsUrl}\n\n}\n guidelines {कृपया देखें:\n- कम्युनिटी गाइडलाइंस: {guidelinesUrl}\n\n}\n other {}\n}अगर आपको लगता है कि यह फैसला गलत या अनुचित था, तो आप अपील सबमिट कर सकते हैं। {appeals_email, select, null {इस इंस्टेंस के एडमिनिस्ट्रेटर से संपर्क करें और साफ़ तौर पर बताएं कि आपको क्यों लगता है कि फैसला गलत था।} other {इस ईमेल पते से {appeals_email} पर ईमेल करें और साफ़ तौर पर बताएं कि आपको क्यों लगता है कि फैसला गलत था।}} हम आपकी अपील का रिव्यू करेंगे और अपने फैसले के साथ जवाब देंगे।\n\n– {product_name} सेफ्टी टीम"
|
||||
"body": "नमस्ते {username},\n\nआपके कहने पर, हमने आपके {product_name} अकाउंट को {deletion_date} को डिलीट करने के लिए शेड्यूल कर दिया है।\n\nअपना अकाउंट रखने के लिए, {deletion_date} से पहले साइन इन करें। {safety_email, select, null {अगर आपने यह नहीं कहा था, तो इस इंस्टेंस के एडमिनिस्ट्रेटर से संपर्क करें।} other {अगर आपने यह नहीं कहा था, तो {safety_email} से संपर्क करें।}}\n\n{product_name} टीम"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "आपके {product_name} डोनेशन के लिए धन्यवाद",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "हमने आपकी {product_name} रिपोर्ट पर फैसला ले लिया है",
|
||||
"body": "नमस्ते,\n\nहमारी सेफ्टी टीम ने आपकी Digital Services Act रिपोर्ट (ID: {reportId}) का रिव्यू करके फैसला ले लिया है।{hasComment, select, yes {\n\nसेफ्टी टीम का जवाब:\n{publicComment}} other {}}\n\nहम किसी दूसरे व्यक्ति के अकाउंट पर की गई कार्रवाई की जानकारी शेयर नहीं करते, क्योंकि वह उनका निजी डेटा है।\n\nअगर आप इस फैसले से सहमत नहीं हैं, तो आप 60 दिनों के अंदर मुफ़्त में अपील कर सकते हैं। {appeals_email, select, null {इस इंस्टेंस के एडमिनिस्ट्रेटर से संपर्क करें, अपनी रिपोर्ट ID शामिल करें और बताएं कि आपको फैसला गलत क्यों लगता है।} other {इस ईमेल पते से {appeals_email} पर ईमेल करें, अपनी रिपोर्ट ID शामिल करें और बताएं कि आपको फैसला गलत क्यों लगता है।}} अगर आप EU में हैं, तो आप इस विवाद को अदालत के बाहर विवाद निपटाने वाली किसी प्रमाणित संस्था के पास भी ले जा सकते हैं। इनमें से किसी भी बात का आपके अदालत जाने के अधिकार पर कोई असर नहीं पड़ता।\n\n– {product_name} सेफ्टी टीम"
|
||||
"body": "नमस्ते,\n\nहमने आपकी रिपोर्ट (ID: {reportId}) रिव्यू कर ली है।\n\n{outcome, select, removed {आपने जिस कंटेंट की रिपोर्ट की थी, उसे हमने हटा दिया है क्योंकि उसने हमारे नियम तोड़े थे।} restricted {हमने पाया कि आपने जिस अकाउंट की रिपोर्ट की थी, उसने हमारे नियम तोड़े, और हमने उस पर पाबंदी लगा दी है। हम यह नहीं बताते कि किसी दूसरे अकाउंट पर हमने क्या कार्रवाई की, क्योंकि यह उनका निजी डेटा है।} action_taken {हमने पाया कि आपने जिसकी रिपोर्ट की थी, उसने हमारे नियम तोड़े, और हमने कार्रवाई की है। हम यह नहीं बताते कि किसी दूसरे अकाउंट पर हमने क्या कार्रवाई की, क्योंकि यह उनका निजी डेटा है।} no_violation {हमने पाया कि आपने जिस कंटेंट या अकाउंट की रिपोर्ट की थी, वह हमारे नियम नहीं तोड़ता, इसलिए हमने उसे वैसे ही रहने दिया है।} other {हमने आपकी रिपोर्ट बंद कर दी है।}}{hasComment, select, yes {\n\nहमारी टीम का नोट: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nअगर आप इससे सहमत नहीं हैं, तो 60 दिनों के अंदर {appeals_email} पर ईमेल करें और रिपोर्ट ID लिखें। अगर आप EU में हैं, तो आप यह मामला अदालत के बाहर विवाद निपटाने वाली किसी प्रमाणित संस्था के पास भी भेज सकते हैं, या अपने देश के डिजिटल सेवा समन्वयक से शिकायत कर सकते हैं।}}\n\n{product_name} टीम"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "DSA रिपोर्ट के लिए अपना ईमेल वेरिफ़ाई करें",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "अपना {product_name} ईमेल पता वेरिफ़ाई करें",
|
||||
"body": "नमस्ते {username},\n\nकृपया नीचे दिए गए लिंक पर क्लिक करके अपने {product_name} अकाउंट के लिए ईमेल पता वेरिफ़ाई करें:\n\n{verifyUrl}\n\nअगर आपने {product_name} अकाउंट नहीं बनाया है, तो आप इस ईमेल को बेझिझक अनदेखा कर सकते हैं।\n\nयह लिंक 24 घंटे तक मान्य है।\n\n– {product_name} टीम"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {हमने आपके {product_name} अकाउंट से जुड़ा एक फैसला वापस ले लिया है} other {आपके {product_name} अकाउंट पर लगी एक पाबंदी खत्म हो गई है}}",
|
||||
"body": "नमस्ते {username},\n\n{outcome, select, reversed {{decision_id, select, null {हमने आपके अकाउंट से जुड़े एक फैसले की दोबारा जाँच की और उसे वापस ले लिया।} other {हमने फैसले {decision_id} की दोबारा जाँच की और उसे वापस ले लिया।}} हुई परेशानी के लिए हमें खेद है।} other {{decision_id, select, null {हमने आपके अकाउंट से पाबंदी हटा दी है।} other {हमने फैसले {decision_id} से लगी पाबंदी हटा दी है।}}}}{restores_access, select, yes { आप फिर से साइन इन करके {product_name} इस्तेमाल कर सकते हैं।} other {}}{statement, select, null {} other {\n\nहमारी टीम का नोट: {statement}}}{content_removed, select, yes {\n\nहमने जो कंटेंट हटाया था, उसे वापस नहीं लाया जा सकता।} other {}}\n\n{product_name} टीम"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {आपके {product_name} अकाउंट के बारे में एक अपडेट} other {{kind_label, select, warning {आपके {product_name} अकाउंट के लिए एक चेतावनी} suspension {आपका {product_name} अकाउंट सस्पेंड कर दिया गया है} ban {आपका {product_name} अकाउंट बैन कर दिया गया है} closure {आपका {product_name} अकाउंट बंद कर दिया गया है} feature_limit {आपके {product_name} अकाउंट पर कुछ फ़ीचर सीमित कर दिए गए हैं} content_removal {हमने {product_name} पर आपके भेजे मैसेज हटा दिए हैं} profile_reset {हमने आपकी {product_name} प्रोफ़ाइल के कुछ हिस्से रीसेट कर दिए हैं} community_removal {हमने {product_name} पर आपकी कम्युनिटी हटा दी है} bot {{product_name} पर आपके बॉट के बारे में एक अपडेट} other {आपके {product_name} अकाउंट के बारे में एक अपडेट}}}}",
|
||||
"body": "नमस्ते {username},\n\n{summary}\n\n{effects}\n\nवजह\nनियम: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nहमारी टीम का नोट: {statement}}}\n\nहमने फैसला कैसे लिया\n{how_we_decided}{appeals_email, select, null {} other {\n\nअपील कैसे करें\nअगर आपको लगता है कि हमसे गलती हुई है, तो {appeal_deadline} तक {appeals_email} पर ईमेल करें। हो सके तो अपने अकाउंट वाले ईमेल एड्रेस से लिखें, और फैसला ID लिखें। अपील मुफ़्त है, और हर अपील को एक इंसान रिव्यू करता है।{redress, select, null {} other {\n\n{redress}}}}}\n\nफैसला ID: {decision_id}\n\n{product_name} टीम"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "आपके रिडीम किए गए गिफ्ट से मिलने वाले पर्क हटा दिए गए हैं",
|
||||
"body": "नमस्ते {username},\n\nआपने जो गिफ्ट कोड रिडीम किया था, उसका पेमेंट मूल रूप से किसी और ने किया था। उस पेमेंट को बाद में वापस ले लिया गया (चार्जबैक)।\n\nइस वजह से, हमने वे पर्क हटा दिए हैं जो गिफ्ट रिडीम करते समय आपके अकाउंट में जोड़े गए थे।\n\n{support_email, select, null {अगर आपको लगता है कि यह गलती है, तो कृपया इस इंस्टेंस के एडमिनिस्ट्रेटर से संपर्क करें और गिफ्ट कोड और उसे रिडीम करने के समय के बारे में आपके पास जो भी जानकारी है, वह शेयर करें।} other {अगर आपको लगता है कि यह गलती है, तो कृपया हमारी सपोर्ट टीम से संपर्क करें और गिफ्ट कोड और उसे रिडीम करने के समय के बारे में आपके पास जो भी जानकारी है, वह शेयर करें।}}\n\n– {product_name} टीम"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "आपकी {product_name} रिपोर्ट का रिव्यू कर लिया गया है",
|
||||
"body": "नमस्ते {username},\n\nआपकी रिपोर्ट (ID: {reportId}) का हमारी सेफ्टी टीम ने रिव्यू कर लिया है।{hasComment, select, yes {\n\nसेफ्टी टीम का जवाब:\n{publicComment}} other {}}\n\n{product_name} को सभी के लिए सुरक्षित रखने में मदद करने के लिए धन्यवाद। हम हर रिपोर्ट को गंभीरता से लेते हैं और कम्युनिटी में आपके योगदान की सराहना करते हैं।\n\n{safety_email, select, null {अगर आपको इस नतीजे के बारे में कोई सवाल या चिंता है, तो कृपया इस इंस्टेंस के एडमिनिस्ट्रेटर से संपर्क करें।} other {अगर आपको इस नतीजे के बारे में कोई सवाल या चिंता है, तो कृपया {safety_email} पर संपर्क करें।}}\n\n– {product_name} सेफ्टी टीम"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"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अगर आपको लगता है कि यह फैसला गलत था, तो आप अपील सबमिट कर सकते हैं। {appeals_email, select, null {इस इंस्टेंस के एडमिनिस्ट्रेटर से संपर्क करें।} other {इस ईमेल पते से {appeals_email} पर ईमेल करें।}}\n\n– {product_name} सेफ्टी टीम"
|
||||
"body": "नमस्ते {username},\n\nहमने आपकी रिपोर्ट (ID: {reportId}) रिव्यू कर ली है।\n\n{outcome, select, removed {आपने जिस कंटेंट की रिपोर्ट की थी, उसे हमने हटा दिया है क्योंकि उसने हमारे नियम तोड़े थे।} restricted {हमने पाया कि आपने जिस अकाउंट की रिपोर्ट की थी, उसने हमारे नियम तोड़े, और हमने उस पर पाबंदी लगा दी है। हम यह नहीं बताते कि किसी दूसरे अकाउंट पर हमने क्या कार्रवाई की, क्योंकि यह उनका निजी डेटा है।} action_taken {हमने पाया कि आपने जिसकी रिपोर्ट की थी, उसने हमारे नियम तोड़े, और हमने कार्रवाई की है। हम यह नहीं बताते कि किसी दूसरे अकाउंट पर हमने क्या कार्रवाई की, क्योंकि यह उनका निजी डेटा है।} no_violation {हमने पाया कि आपने जिस कंटेंट या अकाउंट की रिपोर्ट की थी, वह हमारे नियम नहीं तोड़ता, इसलिए हमने उसे वैसे ही रहने दिया है।} other {हमने आपकी रिपोर्ट बंद कर दी है।}}{hasComment, select, yes {\n\nहमारी टीम का नोट: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nअगर आप इससे सहमत नहीं हैं, तो 60 दिनों के अंदर {appeals_email} पर ईमेल करें और रिपोर्ट ID लिखें। अगर आप EU में हैं, तो आप यह मामला अदालत के बाहर विवाद निपटाने वाली किसी प्रमाणित संस्था के पास भी भेज सकते हैं, या अपने देश के डिजिटल सेवा समन्वयक से शिकायत कर सकते हैं।}}\n\n{product_name} टीम"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "आपके {product_name} अकाउंट का डिलीशन शेड्यूल कर दिया गया है",
|
||||
"body": "नमस्ते {username},\n\nआपने अपना {product_name} अकाउंट डिलीट करने का अनुरोध किया है। आपके अकाउंट को हमेशा के लिए डिलीट करने के लिए शेड्यूल कर दिया गया है:\n\n{deletionDate, date, full} को {deletionDate, time, short} पर\n\nअगर आपने इसका अनुरोध नहीं किया था, तो डिलीशन रद्द करने के लिए अपने अकाउंट में साइन इन करें। हम आपके अकाउंट को सुरक्षित करने के लिए अपना पासवर्ड बदलने की भी सलाह देते हैं।\n\n– {product_name} टीम"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "आपके {product_name} अकाउंट का सस्पेंशन हटा दिया गया है",
|
||||
"body": "नमस्ते {username},\n\nअच्छी खबर: आपके {product_name} अकाउंट का सस्पेंशन हटा दिया गया है।\n\n{reason, select,\n null {}\n other {वजह: {reason}\n\n}\n}अब आप फिर से लॉगिन कर सकते हैं और सामान्य रूप से {product_name} का उपयोग जारी रख सकते हैं।\n\n– {product_name} सेफ्टी टीम"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"subject": "Tvoj račun na {product_name}u bit će izbrisan zbog neaktivnosti",
|
||||
"body": "Pozdrav {username},\n\nTvoj račun na {product_name}u dugo je bio neaktivan, pa je zakazan za trajno brisanje:\n\n{deletionDate, date, full} u {deletionDate, time, short}{reason, select, null {} other {\n\nRazlog: {reason}}}\n\n{safety_email, select, null {Ako želiš zadržati račun, obrati se administratorima ove instance prije tog datuma.} other {Ako želiš zadržati račun, kontaktiraj {safety_email} s ove adrese e-pošte prije tog datuma.}}\n\n– Tim {product_name}a"
|
||||
"body": "Pozdrav {username},\n\nTvoj račun na platformi {product_name} dugo je bio neaktivan, pa smo zakazali njegovo brisanje za {deletion_date}.\n\nAko želiš zadržati račun, prijavi se prije tog datuma. {support_email, select, null {Ako imaš pitanja, obrati se administratorima ove instance.} other {Ako imaš pitanja, kontaktiraj {support_email}.}}\n\nTim platforme {product_name}"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"subject": "Brisanje tvog računa na {product_name}u je zakazano",
|
||||
"body": "Pozdrav {username},\n\nNa tvoj zahtjev, tvoj račun na {product_name}u zakazan je za trajno brisanje:\n\n{deletionDate, date, full} u {deletionDate, time, short}{reason, select, null {} other {\n\nRazlog: {reason}}}\n\nDo tada je tvoj račun zaključan. {safety_email, select, null {Ako ovaj zahtjev ne dolazi od tebe ili želiš zadržati račun, obrati se administratorima ove instance prije tog datuma.} other {Ako ovaj zahtjev ne dolazi od tebe ili želiš zadržati račun, kontaktiraj {safety_email} s ove adrese e-pošte prije tog datuma.}}\n\n– Tim {product_name}a"
|
||||
},
|
||||
"account_scheduled_deletion": {
|
||||
"subject": "Tvoj račun na {product_name}u bit će trajno izbrisan",
|
||||
"body": "Pozdrav {username},\n\nTvoj račun na {product_name}u zakazan je za trajno brisanje zbog kršenja naših Uvjeta pružanja usluge ili Smjernica zajednice.\n\nZakazano brisanje: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Razlog: {reason}\n\n}\n}Ovo je ozbiljna mjera. Podaci tvog računa bit će trajno izbrisani na zakazani datum.\n\n{legalLinks, select,\n both {Pregledaj:\n- Uvjeti pružanja usluge: {termsUrl}\n- Smjernice zajednice: {guidelinesUrl}\n\n}\n terms {Pregledaj:\n- Uvjeti pružanja usluge: {termsUrl}\n\n}\n guidelines {Pregledaj:\n- Smjernice zajednice: {guidelinesUrl}\n\n}\n other {}\n}Postupak žalbe:\nAko smatraš da je ova odluka bila netočna ili neopravdana, imaš 60 dana za podnošenje žalbe. {appeals_email, select, null {Obrati se administratorima ove instance.} other {Pošalji e-poštu na {appeals_email} s ove adrese e-pošte.}}\n\nU svojoj žalbi:\n- Jasno objasni zašto smatraš da je odluka bila netočna ili neopravdana\n- Navedi sve relevantne dokaze ili kontekst\n\nČlan Sigurnosnog tima {product_name}a pregledat će tvoju žalbu i može privremeno zaustaviti zakazano brisanje dok se ne donese konačna odluka.\n\n– Sigurnosni tim {product_name}a"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "Tvoj račun na {product_name}u privremeno je suspendiran",
|
||||
"body": "Pozdrav {username},\n\nTvoj račun na {product_name}u privremeno je suspendiran zbog kršenja naših Uvjeta pružanja usluge ili Smjernica zajednice.\n\nTrajanje: {durationHours, plural,\n =1 {1 sat}\n one {# sat}\n few {# sata}\n other {# sati}\n}\nSuspendiran do: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Razlog: {reason}\n\n}\n}Tijekom tog vremena nećeš moći pristupiti svom računu.\n\n{legalLinks, select,\n both {Pregledaj:\n- Uvjeti pružanja usluge: {termsUrl}\n- Smjernice zajednice: {guidelinesUrl}\n\n}\n terms {Pregledaj:\n- Uvjeti pružanja usluge: {termsUrl}\n\n}\n guidelines {Pregledaj:\n- Smjernice zajednice: {guidelinesUrl}\n\n}\n other {}\n}Ako smatraš da je ova odluka bila netočna ili neopravdana, možeš podnijeti žalbu. {appeals_email, select, null {Obrati se administratorima ove instance i jasno objasni zašto smatraš da je odluka bila netočna.} other {Pošalji e-poštu na {appeals_email} s ove adrese e-pošte i jasno objasni zašto smatraš da je odluka bila netočna.}} Pregledat ćemo tvoju žalbu i javiti ti našu odluku.\n\n– Sigurnosni tim {product_name}a"
|
||||
"body": "Pozdrav {username},\n\nNa tvoj zahtjev zakazali smo brisanje tvog računa na platformi {product_name} za {deletion_date}.\n\nAko želiš zadržati račun, prijavi se prije {deletion_date}. {safety_email, select, null {Ako zahtjev nije došao od tebe, obrati se administratorima ove instance.} other {Ako zahtjev nije došao od tebe, kontaktiraj {safety_email}.}}\n\nTim platforme {product_name}"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Hvala ti na donaciji za {product_name}",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "Donijeli smo odluku o tvojoj prijavi na platformi {product_name}",
|
||||
"body": "Pozdrav,\n\nNaš sigurnosni tim pregledao je tvoju prijavu prema Zakonu o digitalnim uslugama (ID: {reportId}) i donio odluku.{hasComment, select, yes {\n\nOdgovor sigurnosnog tima:\n{publicComment}} other {}}\n\nNe dijelimo pojedinosti o mjerama poduzetima protiv računa druge osobe jer su to njezini osobni podaci.\n\nAko se ne slažeš s ovom odlukom, možeš besplatno podnijeti žalbu u roku od 60 dana. {appeals_email, select, null {Obrati se administratorima ove instance, navedi ID svoje prijave i objasni zašto smatraš da je odluka pogrešna.} other {Pošalji e-poštu na {appeals_email} s ove adrese e-pošte, navedi ID svoje prijave i objasni zašto smatraš da je odluka pogrešna.}} Ako se nalaziš u EU-u, spor možeš uputiti i certificiranom tijelu za izvansudsko rješavanje sporova. Ništa od navedenog ne utječe na tvoje pravo da se obratiš sudu.\n\n– Sigurnosni tim {product_name}a"
|
||||
"body": "Pozdrav,\n\nPregledali smo tvoju prijavu (ID: {reportId}).\n\n{outcome, select, removed {Uklonili smo prijavljeni sadržaj jer je kršio naša pravila.} restricted {Utvrdili smo da je prijavljeni račun prekršio naša pravila i ograničili smo ga. Ne dijelimo koje smo mjere poduzeli protiv drugog računa jer su to osobni podaci njegova vlasnika.} action_taken {Utvrdili smo da je prijavljeni sadržaj ili račun prekršio naša pravila i poduzeli smo mjere. Ne dijelimo koje smo mjere poduzeli protiv drugog računa jer su to osobni podaci njegova vlasnika.} no_violation {Utvrdili smo da prijavljeni sadržaj ili račun ne krši naša pravila pa ništa nismo mijenjali.} other {Zatvorili smo tvoju prijavu.}}{hasComment, select, yes {\n\nNapomena našeg tima: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nAko se ne slažeš, pošalji e-poruku na {appeals_email} u roku od 60 dana i navedi ID prijave. Ako si u EU-u, slučaj možeš uputiti i certificiranom tijelu za izvansudsko rješavanje sporova ili podnijeti pritužbu koordinatoru za digitalne usluge u svojoj zemlji.}}\n\nTim platforme {product_name}"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Potvrdi svoju e-poštu za prijavu prema Zakonu o digitalnim uslugama",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Potvrdi svoju e-poštu za {product_name}",
|
||||
"body": "Pozdrav {username},\n\nPotvrdi adresu e-pošte za svoj račun na {product_name}u klikom na poveznicu ispod:\n\n{verifyUrl}\n\nAko račun na {product_name}u nije tvoj, možeš zanemariti ovu e-poštu.\n\nOva poveznica vrijedi 24 sata.\n\n– Tim {product_name}a"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {Poništili smo odluku o tvom računu na platformi {product_name}} other {Ograničenje tvog računa na platformi {product_name} je prestalo}}",
|
||||
"body": "Pozdrav {username},\n\n{outcome, select, reversed {{decision_id, select, null {Ponovno smo razmotrili odluku o tvom računu i poništili je.} other {Ponovno smo razmotrili odluku {decision_id} i poništili je.}} Ispričavamo se zbog neugodnosti.} other {{decision_id, select, null {Ukinuli smo ograničenje tvog računa.} other {Ukinuli smo ograničenje iz odluke {decision_id}.}}}}{restores_access, select, yes { Ponovno se možeš prijaviti i koristiti {product_name}.} other {}}{statement, select, null {} other {\n\nNapomena našeg tima: {statement}}}{content_removed, select, yes {\n\nUklonjeni sadržaj ne može se vratiti.} other {}}\n\nTim platforme {product_name}"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {Obavijest o tvom računu na platformi {product_name}} other {{kind_label, select, warning {Upozorenje u vezi s tvojim računom na platformi {product_name}} suspension {Tvoj račun na platformi {product_name} je suspendiran} ban {Tvoj račun na platformi {product_name} je trajno blokiran} closure {Tvoj račun na platformi {product_name} je zatvoren} feature_limit {Neke funkcije tvog računa na platformi {product_name} su ograničene} content_removal {Uklonili smo tvoje poruke na platformi {product_name}} profile_reset {Vratili smo dijelove tvog profila na platformi {product_name} na zadano} community_removal {Uklonili smo tvoju zajednicu na platformi {product_name}} bot {Obavijest o tvom botu na platformi {product_name}} other {Obavijest o tvom računu na platformi {product_name}}}}}",
|
||||
"body": "Pozdrav {username},\n\n{summary}\n\n{effects}\n\nZašto\nPravilo: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nNapomena našeg tima: {statement}}}\n\nKako smo odlučili\n{how_we_decided}{appeals_email, select, null {} other {\n\nKako uložiti žalbu\nAko misliš da smo pogriješili, pošalji e-poruku na {appeals_email} do {appeal_deadline}. Ako možeš, piši s adrese e-pošte povezane s tvojim računom i navedi ID odluke. Žalbe su besplatne i svaku pregledava čovjek.{redress, select, null {} other {\n\n{redress}}}}}\n\nID odluke: {decision_id}\n\nTim platforme {product_name}"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Pogodnosti tvog iskorištenog poklona su uklonjene",
|
||||
"body": "Pozdrav {username},\n\nPoklon kod iskorišten na tvom računu izvorno je platio netko drugi. To je plaćanje u međuvremenu poništeno (povrat sredstava putem banke).\n\nZbog toga smo uklonili pogodnosti koje su pri iskorištavanju poklona dodane tvom računu.\n\n{support_email, select, null {Ako misliš da je ovo pogreška, obrati se administratorima ove instance i navedi sve podatke koje imaš o poklon kodu i o vremenu njegova iskorištavanja.} other {Ako misliš da je ovo pogreška, kontaktiraj naš tim za podršku i navedi sve podatke koje imaš o poklon kodu i o vremenu njegova iskorištavanja.}}\n\n– Tim {product_name}a"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "Tvoja prijava na {product_name}u je pregledana",
|
||||
"body": "Pozdrav {username},\n\nTvoju prijavu (ID: {reportId}) pregledao je naš sigurnosni tim.{hasComment, select, yes {\n\nOdgovor sigurnosnog tima:\n{publicComment}} other {}}\n\nHvala ti što pomažeš da {product_name} bude siguran za sve. Sve prijave shvaćamo ozbiljno i cijenimo tvoj doprinos zajednici.\n\n{safety_email, select, null {Ako imaš bilo kakvih pitanja ili nedoumica u vezi s ovim ishodom, obrati se administratorima ove instance.} other {Ako imaš bilo kakvih pitanja ili nedoumica u vezi s ovim ishodom, kontaktiraj {safety_email}.}}\n\n– Sigurnosni tim {product_name}a"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"subject": "Tvoj račun na {product_name}u bit će trajno izbrisan",
|
||||
"body": "Pozdrav {username},\n\nTvoj račun na {product_name}u zakazan je za trajno brisanje.\n\nZakazano brisanje: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Razlog: {reason}\n\n}\n}Ovo je ozbiljna mjera. Podaci tvog računa bit će trajno izbrisani na zakazani datum.\n\nAko smatraš da je ova odluka bila netočna, možeš podnijeti žalbu. {appeals_email, select, null {Obrati se administratorima ove instance.} other {Pošalji e-poštu na {appeals_email} s ove adrese e-pošte.}}\n\n– Sigurnosni tim {product_name}a"
|
||||
"body": "Pozdrav {username},\n\nPregledali smo tvoju prijavu (ID: {reportId}).\n\n{outcome, select, removed {Uklonili smo prijavljeni sadržaj jer je kršio naša pravila.} restricted {Utvrdili smo da je prijavljeni račun prekršio naša pravila i ograničili smo ga. Ne dijelimo koje smo mjere poduzeli protiv drugog računa jer su to osobni podaci njegova vlasnika.} action_taken {Utvrdili smo da je prijavljeni sadržaj ili račun prekršio naša pravila i poduzeli smo mjere. Ne dijelimo koje smo mjere poduzeli protiv drugog računa jer su to osobni podaci njegova vlasnika.} no_violation {Utvrdili smo da prijavljeni sadržaj ili račun ne krši naša pravila pa ništa nismo mijenjali.} other {Zatvorili smo tvoju prijavu.}}{hasComment, select, yes {\n\nNapomena našeg tima: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nAko se ne slažeš, pošalji e-poruku na {appeals_email} u roku od 60 dana i navedi ID prijave. Ako si u EU-u, slučaj možeš uputiti i certificiranom tijelu za izvansudsko rješavanje sporova ili podnijeti pritužbu koordinatoru za digitalne usluge u svojoj zemlji.}}\n\nTim platforme {product_name}"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "Brisanje tvog računa na {product_name}u je zakazano",
|
||||
"body": "Pozdrav {username},\n\nPrimili smo tvoj zahtjev za brisanje računa na {product_name}u. Tvoj račun zakazan je za trajno brisanje:\n\n{deletionDate, date, full} u {deletionDate, time, short}\n\nAko ovaj zahtjev ne dolazi od tebe, prijavi se na svoj račun i otkaži brisanje. Također preporučujemo da promijeniš lozinku radi zaštite računa.\n\n– Tim {product_name}a"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "Suspenzija tvog računa na {product_name}u je ukinuta",
|
||||
"body": "Pozdrav {username},\n\nDobre vijesti: suspenzija tvog računa na {product_name}u je ukinuta.\n\n{reason, select,\n null {}\n other {Razlog: {reason}\n\n}\n}Sada se možeš ponovno prijaviti i nastaviti koristiti {product_name} kao i obično.\n\n– Sigurnosni tim {product_name}a"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"subject": "{product_name}-fiókodat inaktivitás miatt törölni fogjuk",
|
||||
"body": "Szia, {username}!\n\n{product_name}-fiókod hosszú ideje inaktív, ezért végleges törlése a következő időpontra van ütemezve:\n\n{deletionDate, date, full} {deletionDate, time, short}{reason, select, null {} other {\n\nIndok: {reason}}}\n\n{safety_email, select, null {Ha meg szeretnéd tartani a fiókodat, még az időpont előtt fordulj ennek a példánynak az adminisztrátoraihoz.} other {Ha meg szeretnéd tartani a fiókodat, még az időpont előtt írj erről az e-mail-címről erre a címre: {safety_email}.}}\n\n– {product_name} csapata"
|
||||
"body": "Szia, {username}!\n\n{product_name}-fiókod hosszú ideje inaktív, ezért ütemeztük a törlését erre az időpontra: {deletion_date}.\n\nHa meg szeretnéd tartani a fiókodat, jelentkezz be az időpont előtt. {support_email, select, null {Ha kérdésed van, fordulj ennek a példánynak az adminisztrátoraihoz.} other {Ha kérdésed van, írj erre a címre: {support_email}.}}\n\n{product_name} csapata"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"subject": "Ütemeztük {product_name}-fiókod törlését",
|
||||
"body": "Szia, {username}!\n\nKérésedre {product_name}-fiókod végleges törlése a következő időpontra van ütemezve:\n\n{deletionDate, date, full} {deletionDate, time, short}{reason, select, null {} other {\n\nIndok: {reason}}}\n\nAddig a fiókod zárolva van. {safety_email, select, null {Ha nem te kérted ezt, vagy meg szeretnéd tartani a fiókodat, még az időpont előtt fordulj ennek a példánynak az adminisztrátoraihoz.} other {Ha nem te kérted ezt, vagy meg szeretnéd tartani a fiókodat, még az időpont előtt írj erről az e-mail-címről erre a címre: {safety_email}.}}\n\n– {product_name} csapata"
|
||||
},
|
||||
"account_scheduled_deletion": {
|
||||
"subject": "{product_name}-fiókodat véglegesen törölni fogjuk",
|
||||
"body": "Szia, {username}!\n\n{product_name}-fiókod végleges törlését ütemeztük a szolgáltatási feltételeink vagy a közösségi irányelveink megsértése miatt.\n\nA törlés tervezett időpontja: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Indok: {reason}\n\n}\n}Ez súlyos moderációs intézkedés. Fiókod adatait a megadott időpontban véglegesen töröljük.\n\n{legalLinks, select,\n both {Kérjük, olvasd át az alábbiakat:\n- Szolgáltatási feltételek: {termsUrl}\n- Közösségi irányelvek: {guidelinesUrl}\n\n}\n terms {Kérjük, olvasd át az alábbiakat:\n- Szolgáltatási feltételek: {termsUrl}\n\n}\n guidelines {Kérjük, olvasd át az alábbiakat:\n- Közösségi irányelvek: {guidelinesUrl}\n\n}\n other {}\n}Fellebbezés:\nHa úgy gondolod, hogy ez a döntés hibás vagy indokolatlan volt, 60 napod van fellebbezni. {appeals_email, select, null {Fordulj ennek a példánynak az adminisztrátoraihoz.} other {Erről az e-mail-címről írj a következő címre: {appeals_email}.}}\n\nA fellebbezésben:\n- Világosan fejtsd ki, miért gondolod, hogy a döntés hibás vagy indokolatlan volt.\n- Add meg a kapcsolódó bizonyítékokat és a körülményeket.\n\nA {product_name} biztonsági csapatának egyik tagja elbírálja a fellebbezésedet, és a végleges döntésig felfüggesztheti a tervezett törlést.\n\n– {product_name} biztonsági csapata"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "{product_name}-fiókodat ideiglenesen felfüggesztettük",
|
||||
"body": "Szia, {username}!\n\n{product_name}-fiókodat ideiglenesen felfüggesztettük a szolgáltatási feltételeink vagy a közösségi irányelveink megsértése miatt.\n\nIdőtartam: {durationHours, plural,\n =1 {1 óra}\n other {# óra}\n}\nA felfüggesztés vége: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Indok: {reason}\n\n}\n}Ez idő alatt nem férsz hozzá a fiókodhoz.\n\n{legalLinks, select,\n both {Kérjük, olvasd át az alábbiakat:\n- Szolgáltatási feltételek: {termsUrl}\n- Közösségi irányelvek: {guidelinesUrl}\n\n}\n terms {Kérjük, olvasd át az alábbiakat:\n- Szolgáltatási feltételek: {termsUrl}\n\n}\n guidelines {Kérjük, olvasd át az alábbiakat:\n- Közösségi irányelvek: {guidelinesUrl}\n\n}\n other {}\n}Ha úgy gondolod, hogy ez a döntés hibás vagy indokolatlan volt, fellebbezhetsz. {appeals_email, select, null {Fordulj ennek a példánynak az adminisztrátoraihoz, és világosan fejtsd ki, miért tartod hibásnak a döntést.} other {Erről az e-mail-címről írj a következő címre: {appeals_email}, és világosan fejtsd ki, miért tartod hibásnak a döntést.}} Elbíráljuk a fellebbezésedet, és értesítünk a döntésünkről.\n\n– {product_name} biztonsági csapata"
|
||||
"body": "Szia, {username}!\n\nKérésedre ütemeztük {product_name}-fiókod törlését erre az időpontra: {deletion_date}.\n\nHa meg szeretnéd tartani a fiókodat, jelentkezz be {deletion_date} előtt. {safety_email, select, null {Ha nem te kérted ezt, fordulj ennek a példánynak az adminisztrátoraihoz.} other {Ha nem te kérted ezt, írj erre a címre: {safety_email}.}}\n\n{product_name} csapata"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Köszönjük, hogy adományoddal támogatod a {product_name} működését",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "Döntést hoztunk a {product_name} felületén tett bejelentésedről",
|
||||
"body": "Szia!\n\nBiztonsági csapatunk elbírálta a digitális szolgáltatásokról szóló rendelet szerinti bejelentésedet (azonosító: {reportId}), és döntést hozott.{hasComment, select, yes {\n\nA biztonsági csapat válasza:\n{publicComment}} other {}}\n\nMás személy fiókjával szemben tett intézkedések részleteit nem osztjuk meg, mert azok az ő személyes adatai.\n\nHa nem értesz egyet a döntéssel, 60 napon belül díjmentesen fellebbezhetsz. {appeals_email, select, null {Fordulj ennek a példánynak az adminisztrátoraihoz, add meg a bejelentésed azonosítóját, és fejtsd ki, miért tartod hibásnak a döntést.} other {Erről az e-mail-címről írj a következő címre: {appeals_email}, add meg a bejelentésed azonosítóját, és fejtsd ki, miért tartod hibásnak a döntést.}} Ha az EU-ban tartózkodsz, a vitát tanúsított peren kívüli vitarendezési testület elé is viheted. Mindez nem érinti azt a jogodat, hogy bírósághoz fordulj.\n\n– {product_name} biztonsági csapata"
|
||||
"body": "Szia!\n\nMegvizsgáltuk a bejelentésedet (azonosító: {reportId}).\n\n{outcome, select, removed {Eltávolítottuk a bejelentett tartalmat, mert megsértette a szabályainkat.} restricted {Megállapítottuk, hogy a bejelentett fiók megsértette a szabályainkat, ezért korlátoztuk. Azt nem osztjuk meg, milyen intézkedést hoztunk egy másik fiókkal szemben, mert az az illető személyes adata.} action_taken {Megállapítottuk, hogy amit bejelentettél, megsértette a szabályainkat, ezért intézkedtünk. Azt nem osztjuk meg, milyen intézkedést hoztunk egy másik fiókkal szemben, mert az az illető személyes adata.} no_violation {Megállapítottuk, hogy a bejelentett tartalom vagy fiók nem sérti a szabályainkat, ezért nem változtattunk rajta.} other {Lezártuk a bejelentésedet.}}{hasComment, select, yes {\n\nMegjegyzés a csapatunktól: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nHa nem értesz egyet, 60 napon belül írj a(z) {appeals_email} címre, és add meg a bejelentés azonosítóját. Ha az EU-ban élsz, az ügyet egy tanúsított peren kívüli vitarendezési testület elé is viheted, vagy panaszt tehetsz az országod digitális szolgáltatási koordinátoránál.}}\n\n{product_name} csapata"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Erősítsd meg e-mail-címedet a DSA-bejelentéshez",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Erősítsd meg {product_name}-fiókod e-mail-címét",
|
||||
"body": "Szia, {username}!\n\nErősítsd meg a {product_name}-fiókod e-mail-címét az alábbi linkre kattintva:\n\n{verifyUrl}\n\nHa nem hoztál létre {product_name}-fiókot, nyugodtan figyelmen kívül hagyhatod ezt az e-mailt.\n\nEz a link 24 óráig érvényes.\n\n– {product_name} csapata"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {Visszavontunk egy {product_name}-fiókodat érintő döntést} other {Véget ért egy {product_name}-fiókodat érintő korlátozás}}",
|
||||
"body": "Szia, {username}!\n\n{outcome, select, reversed {{decision_id, select, null {Újra megvizsgáltunk egy fiókodat érintő döntést, és visszavontuk.} other {Újra megvizsgáltuk a(z) {decision_id} azonosítójú döntést, és visszavontuk.}} Elnézést kérünk a kellemetlenségért.} other {{decision_id, select, null {Feloldottuk a fiókodat érintő korlátozást.} other {Feloldottuk a(z) {decision_id} azonosítójú döntésen alapuló korlátozást.}}}}{restores_access, select, yes { Újra bejelentkezhetsz, és használhatod a(z) {product_name} szolgáltatást.} other {}}{statement, select, null {} other {\n\nMegjegyzés a csapatunktól: {statement}}}{content_removed, select, yes {\n\nAz általunk eltávolított tartalmat nem lehet visszaállítani.} other {}}\n\n{product_name} csapata"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {Tájékoztatás {product_name}-fiókodról} other {{kind_label, select, warning {Figyelmeztetés {product_name}-fiókoddal kapcsolatban} suspension {{product_name}-fiókod fel van függesztve} ban {{product_name}-fiókod véglegesen fel van függesztve} closure {{product_name}-fiókod le van zárva} feature_limit {Néhány funkció korlátozva van {product_name}-fiókodon} content_removal {Eltávolítottunk üzeneteket, amelyeket a(z) {product_name} felületén küldtél} profile_reset {Visszaállítottuk {product_name}-profilod egyes részeit} community_removal {Eltávolítottuk a közösségedet a(z) {product_name} felületéről} bot {Tájékoztatás a botodról a(z) {product_name} felületén} other {Tájékoztatás {product_name}-fiókodról}}}}",
|
||||
"body": "Szia, {username}!\n\n{summary}\n\n{effects}\n\nMiért\nSzabály: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nMegjegyzés a csapatunktól: {statement}}}\n\nHogyan döntöttünk\n{how_we_decided}{appeals_email, select, null {} other {\n\nÍgy fellebbezhetsz\nHa úgy gondolod, hogy tévedtünk, írj a(z) {appeals_email} címre. Határidő: {appeal_deadline}. Ha tudsz, a fiókodhoz tartozó e-mail-címről írj, és add meg a döntésazonosítót. A fellebbezés ingyenes, és mindegyiket egy ember bírálja el.{redress, select, null {} other {\n\n{redress}}}}}\n\nDöntésazonosító: {decision_id}\n\n{product_name} csapata"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Megszűntek a beváltott ajándékodhoz tartozó előnyök",
|
||||
"body": "Szia, {username}!\n\nAz általad beváltott ajándékkódot eredetileg valaki más fizette ki. Ezt a fizetést azóta visszaterhelték (chargeback).\n\nEmiatt megszüntettük azokat az előnyöket, amelyeket az ajándék beváltásakor kapott a fiókod.\n\n{support_email, select, null {Ha úgy gondolod, hogy ez tévedés, fordulj ennek a példánynak az adminisztrátoraihoz, és add meg az ajándékkóddal és a beváltás időpontjával kapcsolatos ismert részleteket.} other {Ha úgy gondolod, hogy ez tévedés, vedd fel a kapcsolatot ügyfélszolgálatunkkal, és add meg az ajándékkóddal és a beváltás időpontjával kapcsolatos ismert részleteket.}}\n\n– {product_name} csapata"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "Elbíráltuk a {product_name} felületén tett bejelentésedet",
|
||||
"body": "Szia, {username}!\n\nBiztonsági csapatunk elbírálta a bejelentésedet (azonosító: {reportId}).{hasComment, select, yes {\n\nA biztonsági csapat válasza:\n{publicComment}} other {}}\n\nKöszönjük, hogy segítesz mindenki számára biztonságossá tenni a {product_name} közösségét. Minden bejelentést komolyan veszünk, és nagyra értékeljük, hogy segíted a közösséget.\n\n{safety_email, select, null {Ha kérdésed vagy aggályod van az eredménnyel kapcsolatban, fordulj ennek a példánynak az adminisztrátoraihoz.} other {Ha kérdésed vagy aggályod van az eredménnyel kapcsolatban, írj erre a címre: {safety_email}.}}\n\n– {product_name} biztonsági csapata"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"subject": "{product_name}-fiókodat véglegesen törölni fogjuk",
|
||||
"body": "Szia, {username}!\n\nÜtemeztük {product_name}-fiókod végleges törlését.\n\nA törlés tervezett időpontja: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Indok: {reason}\n\n}\n}Ez súlyos moderációs intézkedés. Fiókod adatait a megadott időpontban véglegesen töröljük.\n\nHa úgy gondolod, hogy ez a döntés hibás volt, fellebbezhetsz. {appeals_email, select, null {Fordulj ennek a példánynak az adminisztrátoraihoz.} other {Erről az e-mail-címről írj a következő címre: {appeals_email}.}}\n\n– {product_name} biztonsági csapata"
|
||||
"body": "Szia, {username}!\n\nMegvizsgáltuk a bejelentésedet (azonosító: {reportId}).\n\n{outcome, select, removed {Eltávolítottuk a bejelentett tartalmat, mert megsértette a szabályainkat.} restricted {Megállapítottuk, hogy a bejelentett fiók megsértette a szabályainkat, ezért korlátoztuk. Azt nem osztjuk meg, milyen intézkedést hoztunk egy másik fiókkal szemben, mert az az illető személyes adata.} action_taken {Megállapítottuk, hogy amit bejelentettél, megsértette a szabályainkat, ezért intézkedtünk. Azt nem osztjuk meg, milyen intézkedést hoztunk egy másik fiókkal szemben, mert az az illető személyes adata.} no_violation {Megállapítottuk, hogy a bejelentett tartalom vagy fiók nem sérti a szabályainkat, ezért nem változtattunk rajta.} other {Lezártuk a bejelentésedet.}}{hasComment, select, yes {\n\nMegjegyzés a csapatunktól: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nHa nem értesz egyet, 60 napon belül írj a(z) {appeals_email} címre, és add meg a bejelentés azonosítóját. Ha az EU-ban élsz, az ügyet egy tanúsított peren kívüli vitarendezési testület elé is viheted, vagy panaszt tehetsz az országod digitális szolgáltatási koordinátoránál.}}\n\n{product_name} csapata"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "Ütemeztük {product_name}-fiókod törlését",
|
||||
"body": "Szia, {username}!\n\nKérted a {product_name}-fiókod törlését. Fiókod végleges törlése a következő időpontra van ütemezve:\n\n{deletionDate, date, full} {deletionDate, time, short}\n\nHa nem te kérted ezt, jelentkezz be a fiókodba a törlés visszavonásához. Javasoljuk továbbá, hogy változtasd meg a jelszavadat a fiókod biztonságának megőrzése érdekében.\n\n– {product_name} csapata"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "Feloldottuk {product_name}-fiókod felfüggesztését",
|
||||
"body": "Szia, {username}!\n\nJó hír: feloldottuk {product_name}-fiókod felfüggesztését.\n\n{reason, select,\n null {}\n other {Indok: {reason}\n\n}\n}Most már újra bejelentkezhetsz, és a megszokott módon használhatod a {product_name} alkalmazást.\n\n– {product_name} biztonsági csapata"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"subject": "Akun {product_name} kamu akan dihapus karena tidak aktif",
|
||||
"body": "Halo {username},\n\nAkun {product_name} kamu sudah lama tidak aktif, sehingga dijadwalkan untuk dihapus permanen pada:\n\n{deletionDate, date, full} pukul {deletionDate, time, short}{reason, select, null {} other {\n\nAlasan: {reason}}}\n\n{safety_email, select, null {Jika kamu ingin mempertahankan akunmu, hubungi administrator instance ini sebelum tanggal tersebut.} other {Jika kamu ingin mempertahankan akunmu, hubungi {safety_email} dari alamat email ini sebelum tanggal tersebut.}}\n\n– Tim {product_name}"
|
||||
"body": "Halo {username},\n\nAkun {product_name} kamu sudah lama tidak aktif, jadi kami menjadwalkan penghapusannya pada {deletion_date}.\n\nUntuk mempertahankan akunmu, masuklah sebelum tanggal tersebut. {support_email, select, null {Jika kamu punya pertanyaan, hubungi administrator instance ini.} other {Jika kamu punya pertanyaan, hubungi {support_email}.}}\n\nTim {product_name}"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"subject": "Penghapusan akun {product_name} kamu telah dijadwalkan",
|
||||
"body": "Halo {username},\n\nSesuai permintaanmu, akun {product_name} kamu dijadwalkan untuk dihapus permanen pada:\n\n{deletionDate, date, full} pukul {deletionDate, time, short}{reason, select, null {} other {\n\nAlasan: {reason}}}\n\nAkunmu dikunci hingga saat itu. {safety_email, select, null {Jika kamu tidak meminta ini, atau ingin mempertahankan akunmu, hubungi administrator instance ini sebelum tanggal tersebut.} other {Jika kamu tidak meminta ini, atau ingin mempertahankan akunmu, hubungi {safety_email} dari alamat email ini sebelum tanggal tersebut.}}\n\n– Tim {product_name}"
|
||||
},
|
||||
"account_scheduled_deletion": {
|
||||
"subject": "Akun {product_name} kamu akan dihapus secara permanen",
|
||||
"body": "Halo {username},\n\nAkun {product_name} kamu telah dijadwalkan untuk dihapus permanen karena melanggar Ketentuan Layanan atau Pedoman Komunitas kami.\n\nPenghapusan terjadwal: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Alasan: {reason}\n\n}\n}Ini adalah tindakan penegakan yang serius. Data akunmu akan dihapus permanen pada tanggal yang dijadwalkan.\n\n{legalLinks, select,\n both {Silakan tinjau:\n- Ketentuan Layanan: {termsUrl}\n- Pedoman Komunitas: {guidelinesUrl}\n\n}\n terms {Silakan tinjau:\n- Ketentuan Layanan: {termsUrl}\n\n}\n guidelines {Silakan tinjau:\n- Pedoman Komunitas: {guidelinesUrl}\n\n}\n other {}\n}Proses banding:\nJika kamu yakin keputusan ini keliru atau tidak beralasan, kamu punya 60 hari untuk mengajukan banding. {appeals_email, select, null {Hubungi administrator instance ini.} other {Kirim email ke {appeals_email} dari alamat email ini.}}\n\nDalam bandingmu:\n- Uraikan dengan jelas kenapa kamu yakin keputusan ini keliru atau tidak beralasan\n- Berikan bukti atau konteks yang relevan\n\nAnggota Tim Keamanan {product_name} akan meninjau bandingmu dan bisa menunda penghapusan sampai keputusan akhir tercapai.\n\n– Tim Keamanan {product_name}"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "Akun {product_name} kamu telah ditangguhkan sementara",
|
||||
"body": "Halo {username},\n\nAkun {product_name} kamu telah ditangguhkan sementara karena melanggar Ketentuan Layanan atau Pedoman Komunitas kami.\n\nDurasi: {durationHours, plural,\n =1 {1 jam}\n other {# jam}\n}\nDitangguhkan hingga: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Alasan: {reason}\n\n}\n}Selama waktu ini, kamu tidak akan bisa mengakses akunmu.\n\n{legalLinks, select,\n both {Silakan tinjau:\n- Ketentuan Layanan: {termsUrl}\n- Pedoman Komunitas: {guidelinesUrl}\n\n}\n terms {Silakan tinjau:\n- Ketentuan Layanan: {termsUrl}\n\n}\n guidelines {Silakan tinjau:\n- Pedoman Komunitas: {guidelinesUrl}\n\n}\n other {}\n}Jika kamu yakin keputusan ini keliru atau tidak beralasan, kamu bisa mengajukan banding. {appeals_email, select, null {Hubungi administrator instance ini dan uraikan dengan jelas kenapa kamu yakin keputusan ini keliru.} other {Kirim email ke {appeals_email} dari alamat email ini dan uraikan dengan jelas kenapa kamu yakin keputusan ini keliru.}} Kami akan meninjau bandingmu dan menyampaikan keputusan kami.\n\n– Tim Keamanan {product_name}"
|
||||
"body": "Halo {username},\n\nSesuai permintaanmu, kami menjadwalkan penghapusan akun {product_name} kamu pada {deletion_date}.\n\nUntuk mempertahankan akunmu, masuklah sebelum {deletion_date}. {safety_email, select, null {Jika kamu tidak memintanya, hubungi administrator instance ini.} other {Jika kamu tidak memintanya, hubungi {safety_email}.}}\n\nTim {product_name}"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Terima kasih atas donasimu untuk {product_name}",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "Kami telah mengambil keputusan atas laporanmu di {product_name}",
|
||||
"body": "Halo,\n\nTim Keamanan kami telah meninjau laporan Digital Services Act kamu (ID: {reportId}) dan mengambil keputusan.{hasComment, select, yes {\n\nTanggapan dari Tim Keamanan:\n{publicComment}} other {}}\n\nKami tidak membagikan detail tindakan yang diambil terhadap akun orang lain, karena itu adalah data pribadi mereka.\n\nJika kamu tidak setuju dengan keputusan ini, kamu bisa mengajukan banding secara gratis dalam waktu 60 hari. {appeals_email, select, null {Hubungi administrator instance ini, sertakan ID laporanmu, dan jelaskan kenapa menurutmu keputusan ini keliru.} other {Kirim email ke {appeals_email} dari alamat email ini, sertakan ID laporanmu, dan jelaskan kenapa menurutmu keputusan ini keliru.}} Jika kamu berada di Uni Eropa, kamu juga bisa membawa sengketa ini ke badan penyelesaian sengketa di luar pengadilan yang bersertifikat. Semua ini tidak memengaruhi hakmu untuk menempuh jalur pengadilan.\n\n– Tim Keamanan {product_name}"
|
||||
"body": "Halo,\n\nKami telah meninjau laporanmu (ID: {reportId}).\n\n{outcome, select, removed {Kami menghapus konten yang kamu laporkan karena melanggar aturan kami.} restricted {Kami menemukan bahwa akun yang kamu laporkan melanggar aturan kami, dan kami telah membatasinya. Kami tidak membagikan tindakan apa yang kami ambil terhadap akun lain, karena itu adalah data pribadi pemiliknya.} action_taken {Kami menemukan bahwa yang kamu laporkan melanggar aturan kami, dan kami telah mengambil tindakan. Kami tidak membagikan tindakan apa yang kami ambil terhadap akun lain, karena itu adalah data pribadi pemiliknya.} no_violation {Kami menemukan bahwa konten atau akun yang kamu laporkan tidak melanggar aturan kami, jadi kami membiarkannya apa adanya.} other {Kami menutup laporanmu.}}{hasComment, select, yes {\n\nCatatan dari tim kami: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nJika kamu tidak setuju, kirim email ke {appeals_email} dalam 60 hari dan sertakan ID laporan. Jika kamu berada di UE, kamu juga bisa membawa hal ini ke badan penyelesaian sengketa di luar pengadilan yang tersertifikasi atau mengajukan pengaduan ke Koordinator Layanan Digital di negaramu.}}\n\nTim {product_name}"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Verifikasi emailmu untuk laporan DSA",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Verifikasi alamat email {product_name} kamu",
|
||||
"body": "Halo {username},\n\nSilakan verifikasi alamat email untuk akun {product_name} kamu dengan mengeklik tautan di bawah ini:\n\n{verifyUrl}\n\nJika kamu tidak membuat akun {product_name}, kamu bisa mengabaikan email ini dengan aman.\n\nTautan ini berlaku selama 24 jam.\n\n– Tim {product_name}"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {Kami membatalkan keputusan tentang akun {product_name} kamu} other {Pembatasan pada akun {product_name} kamu telah berakhir}}",
|
||||
"body": "Halo {username},\n\n{outcome, select, reversed {{decision_id, select, null {Kami meninjau ulang keputusan tentang akunmu dan membatalkannya.} other {Kami meninjau ulang keputusan {decision_id} dan membatalkannya.}} Kami mohon maaf atas ketidaknyamanannya.} other {{decision_id, select, null {Kami telah mencabut pembatasan pada akunmu.} other {Kami telah mencabut pembatasan dari keputusan {decision_id}.}}}}{restores_access, select, yes { Kamu bisa masuk dan menggunakan {product_name} lagi.} other {}}{statement, select, null {} other {\n\nCatatan dari tim kami: {statement}}}{content_removed, select, yes {\n\nKonten yang kami hapus tidak dapat dipulihkan.} other {}}\n\nTim {product_name}"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {Kabar terbaru tentang akun {product_name} kamu} other {{kind_label, select, warning {Peringatan tentang akun {product_name} kamu} suspension {Akun {product_name} kamu ditangguhkan} ban {Akun {product_name} kamu diblokir} closure {Akun {product_name} kamu ditutup} feature_limit {Beberapa fitur di akun {product_name} kamu dibatasi} content_removal {Kami menghapus pesan yang kamu kirim di {product_name}} profile_reset {Kami mengatur ulang sebagian profil {product_name} kamu} community_removal {Kami menghapus komunitasmu di {product_name}} bot {Kabar terbaru tentang botmu di {product_name}} other {Kabar terbaru tentang akun {product_name} kamu}}}}",
|
||||
"body": "Halo {username},\n\n{summary}\n\n{effects}\n\nAlasan\nAturan: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nCatatan dari tim kami: {statement}}}\n\nCara kami memutuskan\n{how_we_decided}{appeals_email, select, null {} other {\n\nCara mengajukan banding\nJika menurutmu kami keliru, kirim email ke {appeals_email} paling lambat {appeal_deadline}. Jika bisa, kirim dari alamat email akunmu, dan sertakan ID keputusan. Banding tidak dipungut biaya, dan setiap banding ditinjau oleh manusia.{redress, select, null {} other {\n\n{redress}}}}}\n\nID keputusan: {decision_id}\n\nTim {product_name}"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Manfaat dari hadiah yang kamu tukarkan telah dihapus",
|
||||
"body": "Halo {username},\n\nKode hadiah yang kamu tukarkan awalnya dibayar oleh orang lain. Pembayaran itu kemudian dibatalkan (chargeback).\n\nKarena itu, kami menghapus manfaat yang ditambahkan ke akunmu saat kamu menukarkan hadiah tersebut.\n\n{support_email, select, null {Jika kamu merasa ini keliru, silakan hubungi administrator instance ini dan sertakan detail apa pun yang kamu punya tentang kode hadiah itu dan kapan kamu menukarkannya.} other {Jika kamu merasa ini keliru, silakan hubungi tim dukungan kami dan sertakan detail apa pun yang kamu punya tentang kode hadiah itu dan kapan kamu menukarkannya.}}\n\n– Tim {product_name}"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "Laporanmu di {product_name} telah ditinjau",
|
||||
"body": "Halo {username},\n\nLaporanmu (ID: {reportId}) telah ditinjau oleh Tim Keamanan kami.{hasComment, select, yes {\n\nTanggapan dari Tim Keamanan:\n{publicComment}} other {}}\n\nTerima kasih telah membantu menjaga {product_name} tetap aman untuk semua orang. Kami menanggapi semua laporan dengan serius dan menghargai kontribusimu bagi komunitas.\n\n{safety_email, select, null {Jika kamu punya pertanyaan atau kekhawatiran tentang hasil ini, silakan hubungi administrator instance ini.} other {Jika kamu punya pertanyaan atau kekhawatiran tentang hasil ini, silakan hubungi {safety_email}.}}\n\n– Tim Keamanan {product_name}"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"subject": "Akun {product_name} kamu akan dihapus secara permanen",
|
||||
"body": "Halo {username},\n\nAkun {product_name} kamu telah dijadwalkan untuk dihapus permanen.\n\nPenghapusan terjadwal: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Alasan: {reason}\n\n}\n}Ini adalah tindakan penegakan yang serius. Data akunmu akan dihapus permanen pada tanggal yang dijadwalkan.\n\nJika kamu yakin keputusan ini keliru, kamu bisa mengajukan banding. {appeals_email, select, null {Hubungi administrator instance ini.} other {Kirim email ke {appeals_email} dari alamat email ini.}}\n\n– Tim Keamanan {product_name}"
|
||||
"body": "Halo {username},\n\nKami telah meninjau laporanmu (ID: {reportId}).\n\n{outcome, select, removed {Kami menghapus konten yang kamu laporkan karena melanggar aturan kami.} restricted {Kami menemukan bahwa akun yang kamu laporkan melanggar aturan kami, dan kami telah membatasinya. Kami tidak membagikan tindakan apa yang kami ambil terhadap akun lain, karena itu adalah data pribadi pemiliknya.} action_taken {Kami menemukan bahwa yang kamu laporkan melanggar aturan kami, dan kami telah mengambil tindakan. Kami tidak membagikan tindakan apa yang kami ambil terhadap akun lain, karena itu adalah data pribadi pemiliknya.} no_violation {Kami menemukan bahwa konten atau akun yang kamu laporkan tidak melanggar aturan kami, jadi kami membiarkannya apa adanya.} other {Kami menutup laporanmu.}}{hasComment, select, yes {\n\nCatatan dari tim kami: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nJika kamu tidak setuju, kirim email ke {appeals_email} dalam 60 hari dan sertakan ID laporan. Jika kamu berada di UE, kamu juga bisa membawa hal ini ke badan penyelesaian sengketa di luar pengadilan yang tersertifikasi atau mengajukan pengaduan ke Koordinator Layanan Digital di negaramu.}}\n\nTim {product_name}"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "Penghapusan akun {product_name} kamu telah dijadwalkan",
|
||||
"body": "Halo {username},\n\nKamu meminta agar akun {product_name} kamu dihapus. Akunmu dijadwalkan untuk dihapus permanen pada:\n\n{deletionDate, date, full} pukul {deletionDate, time, short}\n\nJika kamu tidak meminta ini, masuk ke akunmu untuk membatalkan penghapusan. Kami juga menyarankan kamu mengubah kata sandi agar akunmu tetap aman.\n\n– Tim {product_name}"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "Penangguhan akun {product_name} kamu telah dicabut",
|
||||
"body": "Halo {username},\n\nKabar baik: penangguhan akun {product_name} kamu telah dicabut.\n\n{reason, select,\n null {}\n other {Alasan: {reason}\n\n}\n}Kamu sekarang bisa masuk lagi dan terus menggunakan {product_name} seperti biasa.\n\n– Tim Keamanan {product_name}"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"subject": "Il tuo account {product_name} verrà eliminato per inattività",
|
||||
"body": "Ciao {username},\n\nIl tuo account {product_name} è inattivo da molto tempo, quindi verrà eliminato definitivamente in questa data:\n\n{deletionDate, date, full} alle {deletionDate, time, short}{reason, select, null {} other {\n\nMotivo: {reason}}}\n\n{safety_email, select, null {Se vuoi mantenere il tuo account, contatta gli amministratori di questa istanza prima di tale data.} other {Se vuoi mantenere il tuo account, contatta {safety_email} da questo indirizzo email prima di tale data.}}\n\n– Il team di {product_name}"
|
||||
"body": "Ciao {username},\n\nIl tuo account {product_name} è inattivo da molto tempo, quindi ne abbiamo programmato l'eliminazione per il {deletion_date}.\n\nPer mantenere il tuo account, accedi prima di tale data. {support_email, select, null {Se hai domande, contatta gli amministratori di questa istanza.} other {Se hai domande, contatta {support_email}.}}\n\nIl team di {product_name}"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"subject": "L'eliminazione del tuo account {product_name} è programmata",
|
||||
"body": "Ciao {username},\n\nCome da tua richiesta, il tuo account {product_name} verrà eliminato definitivamente in questa data:\n\n{deletionDate, date, full} alle {deletionDate, time, short}{reason, select, null {} other {\n\nMotivo: {reason}}}\n\nFino ad allora il tuo account è bloccato. {safety_email, select, null {Se non hai richiesto l'eliminazione o se vuoi mantenere il tuo account, contatta gli amministratori di questa istanza prima di tale data.} other {Se non hai richiesto l'eliminazione o se vuoi mantenere il tuo account, contatta {safety_email} da questo indirizzo email prima di tale data.}}\n\n– Il team di {product_name}"
|
||||
},
|
||||
"account_scheduled_deletion": {
|
||||
"subject": "Il tuo account {product_name} verrà eliminato definitivamente",
|
||||
"body": "Ciao {username},\n\nÈ stata programmata l’eliminazione definitiva del tuo account {product_name} a causa di violazioni dei nostri Termini di servizio o delle Linee guida della community.\n\nEliminazione programmata: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Motivo: {reason}\n\n}\n}Si tratta di un provvedimento grave. I dati del tuo account verranno eliminati definitivamente alla data programmata.\n\n{legalLinks, select,\n both {Consulta:\n- Termini di servizio: {termsUrl}\n- Linee guida della community: {guidelinesUrl}\n\n}\n terms {Consulta:\n- Termini di servizio: {termsUrl}\n\n}\n guidelines {Consulta:\n- Linee guida della community: {guidelinesUrl}\n\n}\n other {}\n}Procedura di ricorso:\nSe ritieni che questa decisione sia scorretta o ingiustificata, hai 60 giorni per presentare un ricorso. {appeals_email, select, null {Contatta gli amministratori di questa istanza.} other {Invia un'email a {appeals_email} da questo indirizzo email.}}\n\nNel tuo ricorso:\n- Spiega chiaramente perché ritieni che la decisione sia scorretta o ingiustificata\n- Fornisci qualsiasi prova o contesto pertinente\n\nUn membro del team per la sicurezza {product_name} esaminerà il tuo ricorso e potrebbe sospendere l’eliminazione programmata fino a quando non sarà stata raggiunta una decisione finale.\n\n– Il team per la sicurezza di {product_name}"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "Il tuo account {product_name} è stato temporaneamente sospeso",
|
||||
"body": "Ciao {username},\n\nIl tuo account {product_name} è stato temporaneamente sospeso per aver violato i nostri Termini di servizio o le Linee guida della community.\n\nDurata: {durationHours, plural,\n =1 {1 ora}\n other {# ore}\n}\nSospeso fino a: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Motivo: {reason}\n\n}\n}Durante questo periodo, non potrai accedere al tuo account.\n\n{legalLinks, select,\n both {Consulta:\n- Termini di servizio: {termsUrl}\n- Linee guida della community: {guidelinesUrl}\n\n}\n terms {Consulta:\n- Termini di servizio: {termsUrl}\n\n}\n guidelines {Consulta:\n- Linee guida della community: {guidelinesUrl}\n\n}\n other {}\n}Se ritieni che questa decisione sia scorretta o ingiustificata, puoi presentare un ricorso. {appeals_email, select, null {Contatta gli amministratori di questa istanza e spiega chiaramente perché ritieni che la decisione sia scorretta.} other {Invia un'email a {appeals_email} da questo indirizzo email e spiega chiaramente perché ritieni che la decisione sia scorretta.}} Esamineremo il tuo ricorso e risponderemo con la nostra decisione.\n\n– Il team per la sicurezza di {product_name}"
|
||||
"body": "Ciao {username},\n\nCome richiesto, abbiamo programmato l'eliminazione del tuo account {product_name} per il {deletion_date}.\n\nPer mantenere il tuo account, accedi prima del {deletion_date}. {safety_email, select, null {Se non l'hai richiesto tu, contatta gli amministratori di questa istanza.} other {Se non l'hai richiesto tu, contatta {safety_email}.}}\n\nIl team di {product_name}"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Grazie per la tua donazione a {product_name}",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "Abbiamo preso una decisione sulla tua segnalazione su {product_name}",
|
||||
"body": "Ciao,\n\nIl nostro team per la sicurezza ha esaminato la tua segnalazione ai sensi del Digital Services Act (ID: {reportId}) e ha preso una decisione.{hasComment, select, yes {\n\nRisposta dal team per la sicurezza:\n{publicComment}} other {}}\n\nNon condividiamo i dettagli delle misure adottate nei confronti dell'account di un'altra persona, perché si tratta dei suoi dati personali.\n\nSe non sei d'accordo con questa decisione, puoi presentare un ricorso gratuitamente entro 60 giorni. {appeals_email, select, null {Contatta gli amministratori di questa istanza, indica l'ID della tua segnalazione e spiega perché ritieni che la decisione sia scorretta.} other {Invia un'email a {appeals_email} da questo indirizzo email, indica l'ID della tua segnalazione e spiega perché ritieni che la decisione sia scorretta.}} Se ti trovi nell'UE, puoi anche sottoporre la controversia a un organismo certificato di risoluzione extragiudiziale delle controversie. Tutto questo non pregiudica il tuo diritto di rivolgerti a un tribunale.\n\n– Il team per la sicurezza di {product_name}"
|
||||
"body": "Ciao,\n\nAbbiamo esaminato la tua segnalazione (ID: {reportId}).\n\n{outcome, select, removed {Abbiamo rimosso il contenuto che hai segnalato perché violava le nostre regole.} restricted {Abbiamo verificato che l'account che hai segnalato violava le nostre regole e gli abbiamo applicato una restrizione. Non comunichiamo quali provvedimenti abbiamo preso nei confronti di un altro account, perché si tratta di dati personali del suo titolare.} action_taken {Abbiamo verificato che ciò che hai segnalato violava le nostre regole e abbiamo preso provvedimenti. Non comunichiamo quali provvedimenti abbiamo preso nei confronti di un altro account, perché si tratta di dati personali del suo titolare.} no_violation {Abbiamo verificato che il contenuto o l'account che hai segnalato non viola le nostre regole, quindi non abbiamo cambiato nulla.} other {Abbiamo chiuso la tua segnalazione.}}{hasComment, select, yes {\n\nNota del nostro team: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nSe non sei d'accordo, scrivi a {appeals_email} entro 60 giorni indicando l'ID della segnalazione. Se ti trovi nell'UE, puoi anche rivolgerti a un organismo certificato di risoluzione extragiudiziale delle controversie o presentare un reclamo al coordinatore dei servizi digitali del tuo paese.}}\n\nIl team di {product_name}"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Verifica la tua email per una segnalazione DSA",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Verifica il tuo indirizzo email di {product_name}",
|
||||
"body": "Ciao {username},\n\nVerifica l'indirizzo email del tuo account {product_name} cliccando sul link qui sotto:\n\n{verifyUrl}\n\nSe non hai creato un account {product_name}, puoi ignorare tranquillamente questa email.\n\nQuesto link è valido per 24 ore.\n\n– Il team di {product_name}"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {Abbiamo annullato una decisione sul tuo account {product_name}} other {Una restrizione sul tuo account {product_name} è terminata}}",
|
||||
"body": "Ciao {username},\n\n{outcome, select, reversed {{decision_id, select, null {Abbiamo riesaminato una decisione sul tuo account e l'abbiamo annullata.} other {Abbiamo riesaminato la decisione {decision_id} e l'abbiamo annullata.}} Ci scusiamo per il disagio.} other {{decision_id, select, null {Abbiamo revocato la restrizione sul tuo account.} other {Abbiamo revocato la restrizione della decisione {decision_id}.}}}}{restores_access, select, yes { Puoi di nuovo accedere e usare {product_name}.} other {}}{statement, select, null {} other {\n\nNota del nostro team: {statement}}}{content_removed, select, yes {\n\nI contenuti che abbiamo rimosso non possono essere ripristinati.} other {}}\n\nIl team di {product_name}"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {Un aggiornamento sul tuo account {product_name}} other {{kind_label, select, warning {Un avvertimento sul tuo account {product_name}} suspension {Il tuo account {product_name} è sospeso} ban {Il tuo account {product_name} è bannato} closure {Il tuo account {product_name} è chiuso} feature_limit {Alcune funzionalità del tuo account {product_name} sono limitate} content_removal {Abbiamo rimosso dei messaggi che hai inviato su {product_name}} profile_reset {Abbiamo reimpostato parte del tuo profilo {product_name}} community_removal {Abbiamo rimosso la tua community su {product_name}} bot {Un aggiornamento sul tuo bot su {product_name}} other {Un aggiornamento sul tuo account {product_name}}}}}",
|
||||
"body": "Ciao {username},\n\n{summary}\n\n{effects}\n\nPerché\nRegola: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nNota del nostro team: {statement}}}\n\nCome abbiamo deciso\n{how_we_decided}{appeals_email, select, null {} other {\n\nCome presentare ricorso\nSe pensi che abbiamo sbagliato, scrivi a {appeals_email} entro il {appeal_deadline}. Se puoi, scrivi dall'indirizzo email del tuo account e indica l'ID decisione. Il ricorso è gratuito e ogni richiesta viene esaminata da una persona.{redress, select, null {} other {\n\n{redress}}}}}\n\nID decisione: {decision_id}\n\nIl team di {product_name}"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "I vantaggi del tuo regalo riscattato sono stati rimossi",
|
||||
"body": "Ciao {username},\n\nUn codice regalo che hai riscattato era stato originariamente pagato da qualcun altro. Tale pagamento è stato successivamente annullato (un chargeback).\n\nPer questo motivo, abbiamo rimosso i vantaggi che erano stati aggiunti al tuo account quando hai riscattato il regalo.\n\n{support_email, select, null {Se ritieni che si tratti di un errore, contatta gli amministratori di questa istanza e includi tutti i dettagli che hai sul codice regalo e su quando lo hai riscattato.} other {Se ritieni che si tratti di un errore, contatta il nostro team di assistenza e includi tutti i dettagli che hai sul codice regalo e su quando lo hai riscattato.}}\n\n– Il team di {product_name}"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "La tua segnalazione {product_name} è stata esaminata",
|
||||
"body": "Ciao {username},\n\nLa tua segnalazione (ID: {reportId}) è stata esaminata dal nostro team per la sicurezza.{hasComment, select, yes {\n\nRisposta dal team per la sicurezza:\n{publicComment}} other {}}\n\nGrazie per aver contribuito a mantenere {product_name} sicuro per tutti. Prendiamo sul serio tutte le segnalazioni e apprezziamo il tuo contributo alla community.\n\n{safety_email, select, null {Se hai domande o preoccupazioni riguardo a questo esito, contatta gli amministratori di questa istanza.} other {Se hai domande o preoccupazioni riguardo a questo esito, contatta {safety_email}.}}\n\n– Il team per la sicurezza di {product_name}"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"subject": "Il tuo account {product_name} verrà eliminato definitivamente",
|
||||
"body": "Ciao {username},\n\nÈ stata programmata l’eliminazione definitiva del tuo account {product_name}.\n\nEliminazione programmata: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Motivo: {reason}\n\n}\n}Si tratta di un provvedimento grave. I dati del tuo account verranno eliminati definitivamente alla data programmata.\n\nSe ritieni che questa decisione sia scorretta, puoi presentare un ricorso. {appeals_email, select, null {Contatta gli amministratori di questa istanza.} other {Invia un'email a {appeals_email} da questo indirizzo email.}}\n\n– Il team per la sicurezza di {product_name}"
|
||||
"body": "Ciao {username},\n\nAbbiamo esaminato la tua segnalazione (ID: {reportId}).\n\n{outcome, select, removed {Abbiamo rimosso il contenuto che hai segnalato perché violava le nostre regole.} restricted {Abbiamo verificato che l'account che hai segnalato violava le nostre regole e gli abbiamo applicato una restrizione. Non comunichiamo quali provvedimenti abbiamo preso nei confronti di un altro account, perché si tratta di dati personali del suo titolare.} action_taken {Abbiamo verificato che ciò che hai segnalato violava le nostre regole e abbiamo preso provvedimenti. Non comunichiamo quali provvedimenti abbiamo preso nei confronti di un altro account, perché si tratta di dati personali del suo titolare.} no_violation {Abbiamo verificato che il contenuto o l'account che hai segnalato non viola le nostre regole, quindi non abbiamo cambiato nulla.} other {Abbiamo chiuso la tua segnalazione.}}{hasComment, select, yes {\n\nNota del nostro team: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nSe non sei d'accordo, scrivi a {appeals_email} entro 60 giorni indicando l'ID della segnalazione. Se ti trovi nell'UE, puoi anche rivolgerti a un organismo certificato di risoluzione extragiudiziale delle controversie o presentare un reclamo al coordinatore dei servizi digitali del tuo paese.}}\n\nIl team di {product_name}"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "L'eliminazione del tuo account {product_name} è programmata",
|
||||
"body": "Ciao {username},\n\nHai richiesto di eliminare il tuo account {product_name}. Il tuo account verrà eliminato definitivamente in questa data:\n\n{deletionDate, date, full} alle {deletionDate, time, short}\n\nSe non hai richiesto l'eliminazione, accedi al tuo account per annullarla. Ti consigliamo anche di cambiare la password per mettere in sicurezza il tuo account.\n\n– Il team di {product_name}"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "La sospensione del tuo account {product_name} è stata revocata",
|
||||
"body": "Ciao {username},\n\nBuone notizie: la sospensione del tuo account {product_name} è stata revocata.\n\n{reason, select,\n null {}\n other {Motivo: {reason}\n\n}\n}Ora puoi accedere di nuovo e continuare a usare {product_name} normalmente.\n\n– Il team per la sicurezza di {product_name}"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"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, select, null {アカウントを維持したい場合は、この日時までにこのインスタンスの管理者へご連絡ください。} other {アカウントを維持したい場合は、この日時までにこのメールアドレスから{safety_email}へご連絡ください。}}\n\n– {product_name}チーム"
|
||||
"body": "こんにちは、{username}さん\n\n{product_name}アカウントは長期間利用されていないため、{deletion_date}に削除するよう設定しました。\n\nアカウントを残したい場合は、その日より前にログインしてください。{support_email, select, null {ご質問がございましたら、このインスタンスの管理者にご連絡ください。} other {ご質問がございましたら、{support_email}までご連絡ください。}}\n\n{product_name}チーム"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"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, select, null {ご自身でリクエストしていない場合、またはアカウントを維持したい場合は、この日時までにこのインスタンスの管理者へご連絡ください。} other {ご自身でリクエストしていない場合、またはアカウントを維持したい場合は、この日時までにこのメールアドレスから{safety_email}へご連絡ください。}}\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{legalLinks, select,\n both {以下をご確認ください。\n- 利用規約: {termsUrl}\n- コミュニティガイドライン: {guidelinesUrl}\n\n}\n terms {以下をご確認ください。\n- 利用規約: {termsUrl}\n\n}\n guidelines {以下をご確認ください。\n- コミュニティガイドライン: {guidelinesUrl}\n\n}\n other {}\n}異議申し立て手続き:\nこの措置が不正確または不当であると思われる場合は、60日以内に異議申し立てを提出できます。{appeals_email, select, null {このインスタンスの管理者に連絡してください。} other {このメールアドレスから{appeals_email}までメールを送信してください。}}\n\n異議申し立てには、以下を記載してください。\n- 措置が不正確または不当であると考える理由の明確な説明\n- 関連する証拠や背景情報\n\n{product_name}安全チームのメンバーが異議申し立てを審査し、最終決定が下されるまで削除を一時停止する場合があります。\n\n– {product_name}安全チーム"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "{product_name}アカウントが一時的に停止されました",
|
||||
"body": "こんにちは、{username}さん\n\n{product_name}アカウントは、利用規約またはコミュニティガイドラインへの違反のため、一時的に停止されました。\n\n期間: {durationHours, plural,\n =1 {1時間}\n other {#時間}\n}\n停止解除日時: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {理由: {reason}\n\n}\n}この期間中、アカウントにアクセスすることはできません。\n\n{legalLinks, select,\n both {以下をご確認ください。\n- 利用規約: {termsUrl}\n- コミュニティガイドライン: {guidelinesUrl}\n\n}\n terms {以下をご確認ください。\n- 利用規約: {termsUrl}\n\n}\n guidelines {以下をご確認ください。\n- コミュニティガイドライン: {guidelinesUrl}\n\n}\n other {}\n}この措置が不正確または不当であると思われる場合は、異議申し立てを提出できます。{appeals_email, select, null {このインスタンスの管理者に連絡し、決定が不正確であると考える理由を明確に説明してください。} other {このメールアドレスから{appeals_email}までメールを送信し、決定が不正確であると考える理由を明確に説明してください。}}異議申し立てを審査し、決定をご連絡いたします。\n\n– {product_name}安全チーム"
|
||||
"body": "こんにちは、{username}さん\n\nご依頼のとおり、{product_name}アカウントを{deletion_date}に削除するよう設定しました。\n\nアカウントを残したい場合は、{deletion_date}より前にログインしてください。{safety_email, select, null {ご自身で依頼していない場合は、このインスタンスの管理者にご連絡ください。} other {ご自身で依頼していない場合は、{safety_email}までご連絡ください。}}\n\n{product_name}チーム"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "{product_name}へのご寄付ありがとうございます",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "{product_name}への報告について決定しました",
|
||||
"body": "こんにちは。\n\nデジタルサービス法に基づくご報告(ID: {reportId})を安全チームが審査し、決定を行いました。{hasComment, select, yes {\n\n安全チームからの回答:\n{publicComment}} other {}}\n\n他の方のアカウントに対して行った措置の詳細は、その方の個人データにあたるため、お伝えしていません。\n\nこの決定に同意できない場合は、60日以内に無料で異議申し立てを行えます。{appeals_email, select, null {このインスタンスの管理者に連絡し、報告IDを記載のうえ、決定が誤っていると考える理由を説明してください。} other {このメールアドレスから{appeals_email}までメールを送信し、報告IDを記載のうえ、決定が誤っていると考える理由を説明してください。}}EU域内にお住まいの場合は、認定を受けた裁判外紛争解決機関に紛争を付託することもできます。これらはいずれも、裁判所に訴える権利に影響しません。\n\n– {product_name}安全チーム"
|
||||
"body": "こんにちは。\n\nご報告(ID: {reportId})を確認しました。\n\n{outcome, select, removed {報告されたコンテンツはルールに違反していたため、削除しました。} restricted {報告されたアカウントがルールに違反していることを確認し、制限しました。他のアカウントに対してどのような措置をとったかは、その方の個人データにあたるため、お伝えしていません。} action_taken {報告された内容がルールに違反していることを確認し、措置をとりました。他のアカウントに対してどのような措置をとったかは、その方の個人データにあたるため、お伝えしていません。} no_violation {報告されたコンテンツまたはアカウントはルールに違反していないことを確認したため、そのままにしています。} other {ご報告の対応を終了しました。}}{hasComment, select, yes {\n\nチームからのコメント: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nこの結果に同意できない場合は、60日以内に報告IDを記載して{appeals_email}までメールでご連絡ください。EU域内の方は、認定を受けた裁判外紛争解決機関にこの件を付託すること、またはお住まいの国のデジタルサービス調整官(Digital Services Coordinator)に苦情を申し立てることもできます。}}\n\n{product_name}チーム"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "DSA報告のためのメールアドレスを確認",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "{product_name}のメールアドレスを確認",
|
||||
"body": "こんにちは、{username}さん\n\n以下のリンクをクリックして、{product_name}アカウントのメールアドレスを確認してください。\n\n{verifyUrl}\n\n{product_name}アカウントを作成していない場合は、このメールを無視して問題ありません。\n\nこのリンクは24時間有効です。\n\n– {product_name}チーム"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {{product_name}アカウントに関する決定を取り消しました} other {{product_name}アカウントの制限が終了しました}}",
|
||||
"body": "こんにちは、{username}さん\n\n{outcome, select, reversed {{decision_id, select, null {アカウントに関する決定を再検討し、取り消しました。} other {決定{decision_id}を再検討し、取り消しました。}}ご迷惑をおかけして申し訳ありません。} other {{decision_id, select, null {アカウントの制限を解除しました。} other {決定{decision_id}による制限を解除しました。}}}}{restores_access, select, yes {再びログインして{product_name}を利用できます。} other {}}{statement, select, null {} other {\n\nチームからのコメント: {statement}}}{content_removed, select, yes {\n\n削除したコンテンツは復元できません。} other {}}\n\n{product_name}チーム"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {{product_name}アカウントに関するお知らせ} other {{kind_label, select, warning {{product_name}アカウントに関する警告} suspension {{product_name}アカウントは一時停止されています} ban {{product_name}アカウントは永久停止されています} closure {{product_name}アカウントは閉鎖されています} feature_limit {{product_name}アカウントの一部の機能が制限されています} content_removal {{product_name}で送信したメッセージを削除しました} profile_reset {{product_name}プロフィールの一部をリセットしました} community_removal {{product_name}のあなたのコミュニティを削除しました} bot {{product_name}のボットに関するお知らせ} other {{product_name}アカウントに関するお知らせ}}}}",
|
||||
"body": "こんにちは、{username}さん\n\n{summary}\n\n{effects}\n\n理由\nルール: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nチームからのコメント: {statement}}}\n\n判断の経緯\n{how_we_decided}{appeals_email, select, null {} other {\n\n異議申し立ての方法\nこの判断が誤りだと思われる場合は、{appeal_deadline}までに{appeals_email}へメールでご連絡ください。可能であればアカウントに登録したメールアドレスから送信し、決定IDを記載してください。異議申し立ては無料で、一件ずつ担当者が審査します。{redress, select, null {} other {\n\n{redress}}}}}\n\n決定ID: {decision_id}\n\n{product_name}チーム"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "引き換えたギフトの特典が削除されました",
|
||||
"body": "こんにちは、{username}さん\n\n引き換えたギフトコードは、元は別の方が支払ったものです。その支払いはその後取り消されました(チャージバック)。\n\nそのため、ギフトを引き換えた際にアカウントに追加された特典を削除しました。\n\n{support_email, select, null {これが間違いであると思われる場合は、ギフトコードと引き換え時期に関する詳細を添えて、このインスタンスの管理者にお問い合わせください。} other {これが間違いであると思われる場合は、ギフトコードと引き換え時期に関する詳細を添えて、サポートチームにお問い合わせください。}}\n\n– {product_name}チーム"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "{product_name}への報告の審査が完了しました",
|
||||
"body": "こんにちは、{username}さん\n\nご報告いただいた内容(ID: {reportId})を安全チームが審査しました。{hasComment, select, yes {\n\n安全チームからの回答:\n{publicComment}} other {}}\n\n{product_name}をすべての人にとって安全に保つためのご協力ありがとうございます。すべての報告を真剣に受け止め、コミュニティへのご貢献に感謝します。\n\n{safety_email, select, null {この結果についてご質問やご不明な点がございましたら、このインスタンスの管理者にお問い合わせください。} other {この結果についてご質問やご不明な点がございましたら、{safety_email}までお問い合わせください。}}\n\n– {product_name}安全チーム"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"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この措置が不正確であると思われる場合は、異議申し立てを提出できます。{appeals_email, select, null {このインスタンスの管理者に連絡してください。} other {このメールアドレスから{appeals_email}までメールを送信してください。}}\n\n– {product_name}安全チーム"
|
||||
"body": "こんにちは、{username}さん\n\nご報告(ID: {reportId})を確認しました。\n\n{outcome, select, removed {報告されたコンテンツはルールに違反していたため、削除しました。} restricted {報告されたアカウントがルールに違反していることを確認し、制限しました。他のアカウントに対してどのような措置をとったかは、その方の個人データにあたるため、お伝えしていません。} action_taken {報告された内容がルールに違反していることを確認し、措置をとりました。他のアカウントに対してどのような措置をとったかは、その方の個人データにあたるため、お伝えしていません。} no_violation {報告されたコンテンツまたはアカウントはルールに違反していないことを確認したため、そのままにしています。} other {ご報告の対応を終了しました。}}{hasComment, select, yes {\n\nチームからのコメント: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nこの結果に同意できない場合は、60日以内に報告IDを記載して{appeals_email}までメールでご連絡ください。EU域内の方は、認定を受けた裁判外紛争解決機関にこの件を付託すること、またはお住まいの国のデジタルサービス調整官(Digital Services Coordinator)に苦情を申し立てることもできます。}}\n\n{product_name}チーム"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "{product_name}アカウントの削除が予定されています",
|
||||
"body": "こんにちは、{username}さん\n\n{product_name}アカウントの削除をリクエストされました。アカウントは以下の日時に完全に削除される予定です。\n\n{deletionDate, date, full} {deletionDate, time, short}\n\nご自身でリクエストしていない場合は、アカウントにログインして削除をキャンセルしてください。また、アカウントを保護するためにパスワードの変更をお勧めします。\n\n– {product_name}チーム"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "{product_name}アカウントの停止が解除されました",
|
||||
"body": "こんにちは、{username}さん\n\n良いお知らせです。{product_name}アカウントの停止が解除されました。\n\n{reason, select,\n null {}\n other {理由: {reason}\n\n}\n}これで再度ログインして、通常通り{product_name}を利用できます。\n\n– {product_name}安全チーム"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"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, select, null {계정을 유지하고 싶다면 해당 날짜 전에 이 인스턴스의 관리자에게 문의해 주세요.} other {계정을 유지하고 싶다면 해당 날짜 전에 이 이메일 주소에서 {safety_email}으로 문의해 주세요.}}\n\n– {product_name} 팀"
|
||||
"body": "안녕하세요, {username}님.\n\n{product_name} 계정이 오랫동안 사용되지 않아 {deletion_date}에 삭제하도록 예약했어요.\n\n계정을 유지하려면 그 날짜 전에 로그인해 주세요. {support_email, select, null {궁금한 점이 있다면 이 인스턴스의 관리자에게 문의해 주세요.} other {궁금한 점이 있다면 {support_email} 주소로 문의해 주세요.}}\n\n{product_name} 팀 드림"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"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, select, null {요청하지 않으셨거나 계정을 유지하고 싶다면 해당 날짜 전에 이 인스턴스의 관리자에게 문의해 주세요.} other {요청하지 않으셨거나 계정을 유지하고 싶다면 해당 날짜 전에 이 이메일 주소에서 {safety_email}으로 문의해 주세요.}}\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{legalLinks, select,\n both {다음 내용을 확인해 주세요:\n- 서비스 약관: {termsUrl}\n- 커뮤니티 가이드라인: {guidelinesUrl}\n\n}\n terms {다음 내용을 확인해 주세요:\n- 서비스 약관: {termsUrl}\n\n}\n guidelines {다음 내용을 확인해 주세요:\n- 커뮤니티 가이드라인: {guidelinesUrl}\n\n}\n other {}\n}이의 제기 절차:\n이 조치가 잘못되었거나 부당하다고 생각되면 60일 이내에 이의를 제기할 수 있어요. {appeals_email, select, null {이 인스턴스의 관리자에게 연락해 이의를 제기해 주세요.} other {이 이메일 주소에서 {appeals_email}으로 이메일을 보내 이의를 제기해 주세요.}}\n\n이의 제기 시에는:\n- 조치가 잘못되었거나 부당하다고 생각하는 이유를 명확하게 설명해 주세요.\n- 관련 증거 또는 맥락을 제공해 주세요.\n\n{product_name} 안전팀 담당자가 이의 제기를 검토하고 최종 결정이 내려질 때까지 삭제를 일시적으로 보류할 수 있어요.\n\n– {product_name} 안전팀"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "{product_name} 계정이 일시적으로 정지되었어요",
|
||||
"body": "안녕하세요, {username}님.\n\n서비스 약관 또는 커뮤니티 가이드라인 위반으로 인해 {product_name} 계정이 일시적으로 정지되었어요.\n\n기간: {durationHours, plural,\n =1 {1시간}\n other {#시간}\n}\n정지 해제 일시: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {사유: {reason}\n\n}\n}이 기간 동안에는 계정에 액세스할 수 없어요.\n\n{legalLinks, select,\n both {다음 내용을 확인해 주세요:\n- 서비스 약관: {termsUrl}\n- 커뮤니티 가이드라인: {guidelinesUrl}\n\n}\n terms {다음 내용을 확인해 주세요:\n- 서비스 약관: {termsUrl}\n\n}\n guidelines {다음 내용을 확인해 주세요:\n- 커뮤니티 가이드라인: {guidelinesUrl}\n\n}\n other {}\n}이 조치가 잘못되었거나 부당하다고 생각되면 이의를 제기할 수 있어요. {appeals_email, select, null {이 인스턴스의 관리자에게 연락해 결정이 잘못되었다고 생각하는 이유를 명확하게 설명해 주세요.} other {이 이메일 주소에서 {appeals_email}으로 이메일을 보내 결정이 잘못되었다고 생각하는 이유를 명확하게 설명해 주세요.}} 이의 제기를 검토한 후 결과를 알려 드릴 예정이에요.\n\n– {product_name} 안전팀"
|
||||
"body": "안녕하세요, {username}님.\n\n요청하신 대로 {product_name} 계정을 {deletion_date}에 삭제하도록 예약했어요.\n\n계정을 유지하려면 {deletion_date} 전에 로그인해 주세요. {safety_email, select, null {직접 요청하지 않았다면 이 인스턴스의 관리자에게 문의해 주세요.} other {직접 요청하지 않았다면 {safety_email} 주소로 문의해 주세요.}}\n\n{product_name} 팀 드림"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "{product_name} 후원에 감사드려요",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "{product_name} 신고에 대한 결정이 내려졌어요",
|
||||
"body": "안녕하세요,\n\n안전팀에서 디지털 서비스법 신고(ID: {reportId})를 검토하고 결정을 내렸어요.{hasComment, select, yes {\n\n안전팀의 답변:\n{publicComment}} other {}}\n\n다른 사람의 계정에 취한 조치의 세부 내용은 그 사람의 개인 정보이므로 공유하지 않아요.\n\n이 결정에 동의하지 않는다면 60일 이내에 무료로 이의를 제기할 수 있어요. {appeals_email, select, null {이 인스턴스의 관리자에게 연락해 신고 ID를 적고 결정이 잘못되었다고 생각하는 이유를 설명해 주세요.} other {이 이메일 주소에서 {appeals_email}으로 이메일을 보내 신고 ID를 적고 결정이 잘못되었다고 생각하는 이유를 설명해 주세요.}} EU에 거주하고 있다면 인증된 법원 외 분쟁 해결 기관에 분쟁을 회부할 수도 있어요. 이 내용은 법원에 소송을 제기할 권리에 영향을 주지 않아요.\n\n– {product_name} 안전팀"
|
||||
"body": "안녕하세요.\n\n접수하신 신고(ID: {reportId})를 검토했어요.\n\n{outcome, select, removed {신고하신 콘텐츠가 규칙을 위반해 삭제했어요.} restricted {신고하신 계정이 규칙을 위반한 것을 확인하고 제한했어요. 다른 계정에 어떤 조치를 했는지는 그 사람의 개인 정보이므로 공유하지 않아요.} action_taken {신고하신 내용이 규칙을 위반한 것을 확인하고 조치했어요. 다른 계정에 어떤 조치를 했는지는 그 사람의 개인 정보이므로 공유하지 않아요.} no_violation {신고하신 콘텐츠나 계정이 규칙을 위반하지 않은 것으로 확인되어 그대로 두었어요.} other {신고 처리를 종료했어요.}}{hasComment, select, yes {\n\n팀에서 남긴 메모: {publicComment}} other {}}{appeals_email, select, null {} other {\n\n이 결과에 동의하지 않는다면 60일 이내에 신고 ID를 적어 {appeals_email} 주소로 이메일을 보내 주세요. EU에 있다면 인증된 재판 외 분쟁 해결 기관에 이 건을 회부하거나, 거주 국가의 디지털 서비스 조정관에게 민원을 제기할 수도 있어요.}}\n\n{product_name} 팀 드림"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "DSA 신고를 위한 이메일 인증",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "{product_name} 이메일 주소 인증",
|
||||
"body": "안녕하세요, {username}님.\n\n아래 링크를 클릭하여 {product_name} 계정의 이메일 주소를 인증해 주세요:\n\n{verifyUrl}\n\n{product_name} 계정을 생성하지 않으셨다면 이 이메일을 무시해도 괜찮아요.\n\n이 링크는 24시간 동안 유효해요.\n\n– {product_name} 팀"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {{product_name} 계정에 대한 결정을 취소했어요} other {{product_name} 계정의 제한이 종료되었어요}}",
|
||||
"body": "안녕하세요, {username}님.\n\n{outcome, select, reversed {{decision_id, select, null {계정에 대한 결정을 다시 검토하고 취소했어요.} other {{decision_id} 결정을 다시 검토하고 취소했어요.}} 불편을 드려 죄송해요.} other {{decision_id, select, null {계정의 제한을 해제했어요.} other {{decision_id} 결정에 따른 제한을 해제했어요.}}}}{restores_access, select, yes { 이제 다시 로그인해 {product_name} 서비스를 이용할 수 있어요.} other {}}{statement, select, null {} other {\n\n팀에서 남긴 메모: {statement}}}{content_removed, select, yes {\n\n삭제된 콘텐츠는 복원할 수 없어요.} other {}}\n\n{product_name} 팀 드림"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {{product_name} 계정 관련 안내} other {{kind_label, select, warning {{product_name} 계정에 대한 경고} suspension {{product_name} 계정이 일시 정지되었어요} ban {{product_name} 계정이 영구 정지되었어요} closure {{product_name} 계정이 폐쇄되었어요} feature_limit {{product_name} 계정의 일부 기능이 제한되었어요} content_removal {{product_name}에서 보낸 메시지를 삭제했어요} profile_reset {{product_name} 프로필의 일부를 초기화했어요} community_removal {{product_name}에서 회원님의 커뮤니티를 삭제했어요} bot {{product_name} 봇 관련 안내} other {{product_name} 계정 관련 안내}}}}",
|
||||
"body": "안녕하세요, {username}님.\n\n{summary}\n\n{effects}\n\n사유\n규칙: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\n팀에서 남긴 메모: {statement}}}\n\n결정 과정\n{how_we_decided}{appeals_email, select, null {} other {\n\n이의 제기 방법\n결정이 잘못되었다고 생각하면 {appeal_deadline}까지 {appeals_email} 주소로 이메일을 보내 주세요. 가능하면 계정에 등록된 이메일 주소로 보내고, 결정 ID를 적어 주세요. 이의 제기는 무료이며, 모든 이의 제기는 담당자가 직접 검토해요.{redress, select, null {} other {\n\n{redress}}}}}\n\n결정 ID: {decision_id}\n\n{product_name} 팀 드림"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "사용하신 선물 혜택이 제거되었어요",
|
||||
"body": "안녕하세요, {username}님.\n\n사용하신 선물 코드는 다른 사용자가 결제한 것이었어요. 해당 결제가 취소(차지백)되었어요.\n\n이로 인해 선물을 사용했을 때 계정에 추가되었던 혜택이 제거되었어요.\n\n{support_email, select, null {실수라고 생각되면 이 인스턴스의 관리자에게 문의하여 선물 코드 및 사용 시기에 대한 자세한 정보를 알려 주세요.} other {실수라고 생각되면 고객지원팀에 문의하여 선물 코드 및 사용 시기에 대한 자세한 정보를 알려 주세요.}}\n\n– {product_name} 팀"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "{product_name} 신고 검토가 완료되었어요",
|
||||
"body": "안녕하세요, {username}님.\n\n접수하신 신고(ID: {reportId})를 안전팀에서 검토했어요.{hasComment, select, yes {\n\n안전팀의 답변:\n{publicComment}} other {}}\n\n모두를 위해 {product_name}를 안전하게 유지하는 데 도움을 주셔서 감사해요. 모든 신고를 진지하게 검토하고 있으며, 커뮤니티에 기여해 주셔서 감사해요.\n\n{safety_email, select, null {이 결과에 대해 궁금한 점이나 우려 사항이 있다면 이 인스턴스의 관리자에게 문의해 주세요.} other {이 결과에 대해 궁금한 점이나 우려 사항이 있다면 {safety_email}으로 문의해 주세요.}}\n\n– {product_name} 안전팀"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"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이 조치가 잘못되었다고 생각되면 이의를 제기할 수 있어요. {appeals_email, select, null {이 인스턴스의 관리자에게 연락해 이의를 제기해 주세요.} other {이 이메일 주소에서 {appeals_email}으로 이메일을 보내 이의를 제기해 주세요.}}\n\n– {product_name} 안전팀"
|
||||
"body": "안녕하세요, {username}님.\n\n접수하신 신고(ID: {reportId})를 검토했어요.\n\n{outcome, select, removed {신고하신 콘텐츠가 규칙을 위반해 삭제했어요.} restricted {신고하신 계정이 규칙을 위반한 것을 확인하고 제한했어요. 다른 계정에 어떤 조치를 했는지는 그 사람의 개인 정보이므로 공유하지 않아요.} action_taken {신고하신 내용이 규칙을 위반한 것을 확인하고 조치했어요. 다른 계정에 어떤 조치를 했는지는 그 사람의 개인 정보이므로 공유하지 않아요.} no_violation {신고하신 콘텐츠나 계정이 규칙을 위반하지 않은 것으로 확인되어 그대로 두었어요.} other {신고 처리를 종료했어요.}}{hasComment, select, yes {\n\n팀에서 남긴 메모: {publicComment}} other {}}{appeals_email, select, null {} other {\n\n이 결과에 동의하지 않는다면 60일 이내에 신고 ID를 적어 {appeals_email} 주소로 이메일을 보내 주세요. EU에 있다면 인증된 재판 외 분쟁 해결 기관에 이 건을 회부하거나, 거주 국가의 디지털 서비스 조정관에게 민원을 제기할 수도 있어요.}}\n\n{product_name} 팀 드림"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "{product_name} 계정 삭제가 예정되었어요",
|
||||
"body": "안녕하세요, {username}님.\n\n{product_name} 계정 삭제를 요청하셨어요. 계정은 다음 날짜에 영구적으로 삭제될 예정이에요:\n\n{deletionDate, date, full} {deletionDate, time, short}\n\n요청하지 않으셨다면 계정에 로그인하여 삭제를 취소해 주세요. 계정을 보호하기 위해 비밀번호를 변경하는 것을 권장해요.\n\n– {product_name} 팀"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "{product_name} 계정 정지가 해제되었어요",
|
||||
"body": "안녕하세요, {username}님.\n\n좋은 소식이에요: {product_name} 계정 정지가 해제되었어요.\n\n{reason, select,\n null {}\n other {사유: {reason}\n\n}\n}이제 다시 로그인하여 평소처럼 {product_name}를 계속 사용할 수 있어요.\n\n– {product_name} 안전팀"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"subject": "Tavo {product_name} paskyra bus ištrinta dėl neaktyvumo",
|
||||
"body": "Sveiki, {username},\n\nTavo {product_name} paskyra ilgą laiką buvo neaktyvi, todėl ją numatyta visam laikui ištrinti:\n\n{deletionDate, date, full} {deletionDate, time, short}{reason, select, null {} other {\n\nPriežastis: {reason}}}\n\n{safety_email, select, null {Jei nori išsaugoti paskyrą, iki šios datos susisiek su šios instancijos administratoriais.} other {Jei nori išsaugoti paskyrą, iki šios datos parašyk adresu {safety_email} iš šio el. pašto adreso.}}\n\n– {product_name} komanda"
|
||||
"body": "Sveiki, {username},\n\nTavo {product_name} paskyra ilgą laiką buvo neaktyvi, todėl numatėme ją ištrinti {deletion_date}.\n\nJei nori išsaugoti paskyrą, prisijunk iki šios datos. {support_email, select, null {Jei turi klausimų, susisiek su šios instancijos administratoriais.} other {Jei turi klausimų, parašyk adresu {support_email}.}}\n\n{product_name} komanda"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"subject": "Tavo {product_name} paskyros ištrynimas numatytas",
|
||||
"body": "Sveiki, {username},\n\nTavo prašymu {product_name} paskyrą numatyta visam laikui ištrinti:\n\n{deletionDate, date, full} {deletionDate, time, short}{reason, select, null {} other {\n\nPriežastis: {reason}}}\n\nIki tol tavo paskyra užrakinta. {safety_email, select, null {Jei to neprašei arba nori išsaugoti paskyrą, iki šios datos susisiek su šios instancijos administratoriais.} other {Jei to neprašei arba nori išsaugoti paskyrą, iki šios datos parašyk adresu {safety_email} iš šio el. pašto adreso.}}\n\n– {product_name} komanda"
|
||||
},
|
||||
"account_scheduled_deletion": {
|
||||
"subject": "Tavo {product_name} paskyra bus visam laikui ištrinta",
|
||||
"body": "Sveiki, {username},\n\nTavo {product_name} paskyrą numatyta visam laikui ištrinti dėl mūsų paslaugų teikimo sąlygų arba bendruomenės gairių pažeidimų.\n\nNumatytas ištrynimas: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Priežastis: {reason}\n\n}\n}Tai rimta priemonė. Tavo paskyros duomenys numatytą dieną bus visam laikui ištrinti.\n\n{legalLinks, select,\n both {Peržiūrėk:\n- Paslaugų teikimo sąlygos: {termsUrl}\n- Bendruomenės gairės: {guidelinesUrl}\n\n}\n terms {Peržiūrėk:\n- Paslaugų teikimo sąlygos: {termsUrl}\n\n}\n guidelines {Peržiūrėk:\n- Bendruomenės gairės: {guidelinesUrl}\n\n}\n other {}\n}Apeliacijos procesas:\nJei manai, kad šis sprendimas neteisingas arba nepagrįstas, apeliaciją gali pateikti per 60 dienų. {appeals_email, select, null {Susisiek su šios instancijos administratoriais.} other {Iš šio el. pašto adreso siųsk el. laišką adresu {appeals_email}.}}\n\nSavo apeliacijoje:\n- Aiškiai paaiškink, kodėl manai, kad sprendimas neteisingas arba nepagrįstas\n- Pateik visus susijusius įrodymus ar kontekstą\n\n{product_name} saugos komandos narys peržiūrės tavo apeliaciją ir gali sustabdyti numatytą ištrynimą, kol bus priimtas galutinis sprendimas.\n\n– {product_name} saugos komanda"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "Tavo {product_name} paskyra laikinai sustabdyta",
|
||||
"body": "Sveiki, {username},\n\nTavo {product_name} paskyra laikinai sustabdyta dėl mūsų paslaugų teikimo sąlygų arba bendruomenės gairių pažeidimo.\n\nTrukmė: {durationHours, plural,\n one {# valanda} few {# valandos}\n many {# valandos} other {# valandų}\n}\nSustabdyta iki: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Priežastis: {reason}\n\n}\n}Šiuo laikotarpiu negalėsi pasiekti savo paskyros.\n\n{legalLinks, select,\n both {Peržiūrėk:\n- Paslaugų teikimo sąlygos: {termsUrl}\n- Bendruomenės gairės: {guidelinesUrl}\n\n}\n terms {Peržiūrėk:\n- Paslaugų teikimo sąlygos: {termsUrl}\n\n}\n guidelines {Peržiūrėk:\n- Bendruomenės gairės: {guidelinesUrl}\n\n}\n other {}\n}Jei manai, kad šis sprendimas neteisingas arba nepagrįstas, gali pateikti apeliaciją. {appeals_email, select, null {Susisiek su šios instancijos administratoriais ir aiškiai paaiškink, kodėl manai, kad sprendimas neteisingas.} other {Iš šio el. pašto adreso siųsk el. laišką adresu {appeals_email} ir aiškiai paaiškink, kodėl manai, kad sprendimas neteisingas.}} Peržiūrėsime tavo apeliaciją ir pranešime savo sprendimą.\n\n– {product_name} saugos komanda"
|
||||
"body": "Sveiki, {username},\n\nKaip prašei, numatėme ištrinti tavo {product_name} paskyrą {deletion_date}.\n\nJei nori išsaugoti paskyrą, prisijunk iki {deletion_date}. {safety_email, select, null {Jei to neprašei, susisiek su šios instancijos administratoriais.} other {Jei to neprašei, parašyk adresu {safety_email}.}}\n\n{product_name} komanda"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Dėkojame už tavo {product_name} paramą",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "Priėmėme sprendimą dėl tavo {product_name} pranešimo",
|
||||
"body": "Sveiki,\n\nMūsų saugos komanda peržiūrėjo tavo pranešimą pagal Skaitmeninių paslaugų aktą (ID: {reportId}) ir priėmė sprendimą.{hasComment, select, yes {\n\nSaugos komandos atsakymas:\n{publicComment}} other {}}\n\nNesidalijame informacija apie veiksmus, kurių imtasi dėl kito asmens paskyros, nes tai yra jo asmens duomenys.\n\nJei nesutinki su šiuo sprendimu, per 60 dienų gali nemokamai pateikti apeliaciją. {appeals_email, select, null {Susisiek su šios instancijos administratoriais, nurodyk savo pranešimo ID ir paaiškink, kodėl manai, kad sprendimas neteisingas.} other {Iš šio el. pašto adreso siųsk el. laišką adresu {appeals_email}, nurodyk savo pranešimo ID ir paaiškink, kodėl manai, kad sprendimas neteisingas.}} Jei esi ES, ginčą taip pat gali perduoti sertifikuotai neteisminio ginčų sprendimo įstaigai. Visa tai neturi įtakos tavo teisei kreiptis į teismą.\n\n– {product_name} saugos komanda"
|
||||
"body": "Sveiki,\n\nPeržiūrėjome tavo pranešimą (ID: {reportId}).\n\n{outcome, select, removed {Pašalinome turinį, apie kurį pranešei, nes jis pažeidė mūsų taisykles.} restricted {Nustatėme, kad paskyra, apie kurią pranešei, pažeidė mūsų taisykles, ir ją apribojome. Neatskleidžiame, kokių veiksmų ėmėmės prieš kitą paskyrą, nes tai yra jos savininko asmens duomenys.} action_taken {Nustatėme, kad tai, apie ką pranešei, pažeidė mūsų taisykles, ir ėmėmės veiksmų. Neatskleidžiame, kokių veiksmų ėmėmės prieš kitą paskyrą, nes tai yra jos savininko asmens duomenys.} no_violation {Nustatėme, kad turinys ar paskyra, apie kuriuos pranešei, nepažeidžia mūsų taisyklių, todėl nieko nekeitėme.} other {Uždarėme tavo pranešimą.}}{hasComment, select, yes {\n\nMūsų komandos pastaba: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nJei nesutinki, per 60 dienų parašyk adresu {appeals_email} ir nurodyk pranešimo ID. Jei esi ES, taip pat gali kreiptis į sertifikuotą neteisminio ginčų sprendimo įstaigą arba pateikti skundą savo šalies skaitmeninių paslaugų koordinatoriui.}}\n\n{product_name} komanda"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Patvirtink savo el. paštą DSA pranešimui pateikti",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Patvirtink savo {product_name} el. pašto adresą",
|
||||
"body": "Sveiki, {username},\n\nSpustelėk žemiau esančią nuorodą ir patvirtink savo {product_name} paskyros el. pašto adresą:\n\n{verifyUrl}\n\nJei {product_name} paskyros nekūrei, gali saugiai ignoruoti šį el. laišką.\n\nŠi nuoroda galioja 24 valandas.\n\n– {product_name} komanda"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {Atšaukėme sprendimą dėl tavo {product_name} paskyros} other {Tavo {product_name} paskyros apribojimas baigėsi}}",
|
||||
"body": "Sveiki, {username},\n\n{outcome, select, reversed {{decision_id, select, null {Dar kartą peržiūrėjome sprendimą dėl tavo paskyros ir jį atšaukėme.} other {Dar kartą peržiūrėjome sprendimą {decision_id} ir jį atšaukėme.}} Atsiprašome už nepatogumus.} other {{decision_id, select, null {Panaikinome tavo paskyros apribojimą.} other {Panaikinome apribojimą, taikytą pagal sprendimą {decision_id}.}}}}{restores_access, select, yes { Vėl gali prisijungti ir naudotis {product_name}.} other {}}{statement, select, null {} other {\n\nMūsų komandos pastaba: {statement}}}{content_removed, select, yes {\n\nMūsų pašalinto turinio atkurti negalima.} other {}}\n\n{product_name} komanda"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {Informacija apie tavo {product_name} paskyrą} other {{kind_label, select, warning {Įspėjimas dėl tavo {product_name} paskyros} suspension {Tavo {product_name} paskyra sustabdyta} ban {Tavo {product_name} paskyra visam laikui sustabdyta} closure {Tavo {product_name} paskyra uždaryta} feature_limit {Kai kurios tavo {product_name} paskyros funkcijos apribotos} content_removal {Pašalinome tavo {product_name} platformoje išsiųstas žinutes} profile_reset {Iš naujo nustatėme kai kurias tavo {product_name} profilio dalis} community_removal {Pašalinome tavo bendruomenę iš {product_name}} bot {Informacija apie tavo botą {product_name} platformoje} other {Informacija apie tavo {product_name} paskyrą}}}}",
|
||||
"body": "Sveiki, {username},\n\n{summary}\n\n{effects}\n\nKodėl\nTaisyklė: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nMūsų komandos pastaba: {statement}}}\n\nKaip priėmėme sprendimą\n{how_we_decided}{appeals_email, select, null {} other {\n\nKaip apskųsti sprendimą\nJei manai, kad suklydome, parašyk adresu {appeals_email} iki {appeal_deadline}. Jei gali, rašyk iš savo paskyros el. pašto adreso ir nurodyk sprendimo ID. Skundai nagrinėjami nemokamai, o kiekvieną jų peržiūri žmogus.{redress, select, null {} other {\n\n{redress}}}}}\n\nSprendimo ID: {decision_id}\n\n{product_name} komanda"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Privilegijos, gautos už išpirktą dovaną, pašalintos",
|
||||
"body": "Sveiki, {username},\n\nDovanos kodą, kurį išpirkai, iš pradžių apmokėjo kitas asmuo. Tas mokėjimas vėliau buvo atšauktas (lėšų grąžinimas).\n\nDėl to pašalinome privilegijas, kurios buvo pridėtos prie tavo paskyros, kai išpirkai dovaną.\n\n{support_email, select, null {Jei manai, kad tai klaida, susisiek su šios instancijos administratoriais ir nurodyk visą turimą informaciją apie dovanos kodą ir apie tai, kada jį išpirkai.} other {Jei manai, kad tai klaida, susisiek su mūsų palaikymo komanda ir nurodyk visą turimą informaciją apie dovanos kodą ir apie tai, kada jį išpirkai.}}\n\n– {product_name} komanda"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "Tavo {product_name} pranešimas peržiūrėtas",
|
||||
"body": "Sveiki, {username},\n\nTavo pranešimą (ID: {reportId}) peržiūrėjo mūsų saugos komanda.{hasComment, select, yes {\n\nSaugos komandos atsakymas:\n{publicComment}} other {}}\n\nDėkojame, kad padedi užtikrinti {product_name} saugumą visiems. Visus pranešimus vertiname rimtai ir esame dėkingi už tavo indėlį į bendruomenę.\n\n{safety_email, select, null {Jei turi klausimų ar abejonių dėl šio sprendimo, susisiek su šios instancijos administratoriais.} other {Jei turi klausimų ar abejonių dėl šio sprendimo, rašyk adresu {safety_email}.}}\n\n– {product_name} saugos komanda"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"subject": "Tavo {product_name} paskyra bus visam laikui ištrinta",
|
||||
"body": "Sveiki, {username},\n\nTavo {product_name} paskyrą numatyta visam laikui ištrinti.\n\nNumatytas ištrynimas: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Priežastis: {reason}\n\n}\n}Tai rimta priemonė. Tavo paskyros duomenys numatytą dieną bus visam laikui ištrinti.\n\nJei manai, kad šis sprendimas neteisingas, gali pateikti apeliaciją. {appeals_email, select, null {Susisiek su šios instancijos administratoriais.} other {Iš šio el. pašto adreso siųsk el. laišką adresu {appeals_email}.}}\n\n– {product_name} saugos komanda"
|
||||
"body": "Sveiki, {username},\n\nPeržiūrėjome tavo pranešimą (ID: {reportId}).\n\n{outcome, select, removed {Pašalinome turinį, apie kurį pranešei, nes jis pažeidė mūsų taisykles.} restricted {Nustatėme, kad paskyra, apie kurią pranešei, pažeidė mūsų taisykles, ir ją apribojome. Neatskleidžiame, kokių veiksmų ėmėmės prieš kitą paskyrą, nes tai yra jos savininko asmens duomenys.} action_taken {Nustatėme, kad tai, apie ką pranešei, pažeidė mūsų taisykles, ir ėmėmės veiksmų. Neatskleidžiame, kokių veiksmų ėmėmės prieš kitą paskyrą, nes tai yra jos savininko asmens duomenys.} no_violation {Nustatėme, kad turinys ar paskyra, apie kuriuos pranešei, nepažeidžia mūsų taisyklių, todėl nieko nekeitėme.} other {Uždarėme tavo pranešimą.}}{hasComment, select, yes {\n\nMūsų komandos pastaba: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nJei nesutinki, per 60 dienų parašyk adresu {appeals_email} ir nurodyk pranešimo ID. Jei esi ES, taip pat gali kreiptis į sertifikuotą neteisminio ginčų sprendimo įstaigą arba pateikti skundą savo šalies skaitmeninių paslaugų koordinatoriui.}}\n\n{product_name} komanda"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "Tavo {product_name} paskyros ištrynimas numatytas",
|
||||
"body": "Sveiki, {username},\n\nPaprašei ištrinti savo {product_name} paskyrą. Tavo paskyrą numatyta visam laikui ištrinti:\n\n{deletionDate, date, full} {deletionDate, time, short}\n\nJei to neprašei, prisijunk prie savo paskyros ir atšauk ištrynimą. Taip pat rekomenduojame pakeisti slaptažodį, kad apsaugotum savo paskyrą.\n\n– {product_name} komanda"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "Tavo {product_name} paskyros sustabdymas panaikintas",
|
||||
"body": "Sveiki, {username},\n\nGeros naujienos: tavo {product_name} paskyros sustabdymas panaikintas.\n\n{reason, select,\n null {}\n other {Priežastis: {reason}\n\n}\n}Dabar gali vėl prisijungti ir toliau naudotis {product_name} kaip įprasta.\n\n– {product_name} saugos komanda"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"subject": "Je {product_name}-account wordt verwijderd wegens inactiviteit",
|
||||
"body": "Hallo {username},\n\nJe {product_name}-account is al lange tijd inactief en staat daarom gepland voor permanente verwijdering op:\n\n{deletionDate, date, full} om {deletionDate, time, short}{reason, select, null {} other {\n\nReden: {reason}}}\n\n{safety_email, select, null {Als je je account wilt behouden, neem dan vóór die datum contact op met de beheerders van deze instantie.} other {Als je je account wilt behouden, neem dan vóór die datum vanaf dit e-mailadres contact op met {safety_email}.}}\n\n– Het team van {product_name}"
|
||||
"body": "Hallo {username},\n\nJe {product_name}-account is al lange tijd inactief, dus we hebben het ingepland voor verwijdering op {deletion_date}.\n\nLog vóór die datum in als je je account wilt houden. {support_email, select, null {Als je vragen hebt, neem dan contact op met de beheerders van deze instantie.} other {Als je vragen hebt, neem dan contact op met {support_email}.}}\n\nHet team van {product_name}"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"subject": "De verwijdering van je {product_name}-account staat gepland",
|
||||
"body": "Hallo {username},\n\nZoals je hebt verzocht, staat je {product_name}-account gepland voor permanente verwijdering op:\n\n{deletionDate, date, full} om {deletionDate, time, short}{reason, select, null {} other {\n\nReden: {reason}}}\n\nJe account is tot die tijd vergrendeld. {safety_email, select, null {Als je dit niet hebt aangevraagd of je account wilt behouden, neem dan vóór die datum contact op met de beheerders van deze instantie.} other {Als je dit niet hebt aangevraagd of je account wilt behouden, neem dan vóór die datum vanaf dit e-mailadres contact op met {safety_email}.}}\n\n– Het team van {product_name}"
|
||||
},
|
||||
"account_scheduled_deletion": {
|
||||
"subject": "Je {product_name}-account wordt permanent verwijderd",
|
||||
"body": "Hallo {username},\n\nJe {product_name}-account staat gepland voor permanente verwijdering vanwege schendingen van onze Gebruiksvoorwaarden of Communityrichtlijnen.\n\nGeplande verwijdering: {deletionDate, date, full} om {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Reden: {reason}\n\n}\n}Dit is een ingrijpende handhavingsmaatregel. Je accountgegevens worden permanent verwijderd op de geplande datum.\n\n{legalLinks, select,\n both {Bekijk ook:\n- Gebruiksvoorwaarden: {termsUrl}\n- Communityrichtlijnen: {guidelinesUrl}\n\n}\n terms {Bekijk ook:\n- Gebruiksvoorwaarden: {termsUrl}\n\n}\n guidelines {Bekijk ook:\n- Communityrichtlijnen: {guidelinesUrl}\n\n}\n other {}\n}Bezwaarprocedure:\nAls je van mening bent dat deze handhavingsbeslissing onjuist of ongerechtvaardigd was, heb je 60 dagen de tijd om bezwaar te maken. {appeals_email, select, null {Neem contact op met de beheerders van deze instantie.} other {Stuur een e-mail naar {appeals_email} vanaf dit e-mailadres.}}\n\nVermeld in je bezwaar:\n- Een duidelijke uitleg waarom je van mening bent dat de handhavingsbeslissing onjuist of ongerechtvaardigd was\n- Alle relevante bewijzen of context\n\nEen medewerker van het veiligheidsteam van {product_name} zal je bezwaar beoordelen en kan de geplande verwijdering uitstellen totdat een definitieve beslissing is genomen.\n\n– Het veiligheidsteam van {product_name}"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "Je {product_name}-account is tijdelijk geschorst",
|
||||
"body": "Hallo {username},\n\nJe {product_name}-account is tijdelijk geschorst wegens schending van onze Gebruiksvoorwaarden of Communityrichtlijnen.\n\nDuur: {durationHours, plural,\n =1 {1 uur}\n other {# uur}\n}\nGeschorst tot: {bannedUntil, date, full} om {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Reden: {reason}\n\n}\n}Gedurende deze periode heb je geen toegang tot je account.\n\n{legalLinks, select,\n both {Bekijk ook:\n- Gebruiksvoorwaarden: {termsUrl}\n- Communityrichtlijnen: {guidelinesUrl}\n\n}\n terms {Bekijk ook:\n- Gebruiksvoorwaarden: {termsUrl}\n\n}\n guidelines {Bekijk ook:\n- Communityrichtlijnen: {guidelinesUrl}\n\n}\n other {}\n}Als je van mening bent dat deze handhavingsbeslissing onjuist of ongerechtvaardigd was, kun je bezwaar maken. {appeals_email, select, null {Neem contact op met de beheerders van deze instantie en leg duidelijk uit waarom je van mening bent dat de beslissing onjuist was.} other {Stuur een e-mail naar {appeals_email} vanaf dit e-mailadres en leg duidelijk uit waarom je van mening bent dat de beslissing onjuist was.}} We zullen je bezwaar beoordelen en reageren met onze beslissing.\n\n– Het veiligheidsteam van {product_name}"
|
||||
"body": "Hallo {username},\n\nZoals je hebt gevraagd, hebben we je {product_name}-account ingepland voor verwijdering op {deletion_date}.\n\nLog vóór {deletion_date} in als je je account wilt houden. {safety_email, select, null {Als je hier niet om hebt gevraagd, neem dan contact op met de beheerders van deze instantie.} other {Als je hier niet om hebt gevraagd, neem dan contact op met {safety_email}.}}\n\nHet team van {product_name}"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Bedankt voor je {product_name}-donatie",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "We hebben een beslissing genomen over je melding op {product_name}",
|
||||
"body": "Hallo,\n\nOns veiligheidsteam heeft je melding op grond van de Digital Services Act (ID: {reportId}) beoordeeld en een beslissing genomen.{hasComment, select, yes {\n\nReactie van het veiligheidsteam:\n{publicComment}} other {}}\n\nWe delen geen details over maatregelen tegen het account van iemand anders, omdat dat persoonsgegevens van die persoon zijn.\n\nAls je het niet eens bent met deze beslissing, kun je binnen 60 dagen kosteloos bezwaar maken. {appeals_email, select, null {Neem contact op met de beheerders van deze instantie, vermeld je meldings-ID en leg uit waarom je vindt dat de beslissing onjuist is.} other {Stuur een e-mail naar {appeals_email} vanaf dit e-mailadres, vermeld je meldings-ID en leg uit waarom je vindt dat de beslissing onjuist is.}} Als je in de EU bent, kun je het geschil ook voorleggen aan een gecertificeerd orgaan voor buitengerechtelijke geschillenbeslechting. Dit alles doet niets af aan je recht om naar de rechter te stappen.\n\n– Het veiligheidsteam van {product_name}"
|
||||
"body": "Hallo,\n\nWe hebben je melding (ID: {reportId}) beoordeeld.\n\n{outcome, select, removed {We hebben de inhoud die je hebt gemeld verwijderd, omdat die onze regels overtrad.} restricted {We hebben vastgesteld dat het account dat je hebt gemeld onze regels heeft overtreden, en we hebben het beperkt. We delen niet welke maatregel we tegen een ander account hebben genomen, omdat dat persoonsgegevens van die persoon zijn.} action_taken {We hebben vastgesteld dat wat je hebt gemeld onze regels heeft overtreden, en we hebben maatregelen genomen. We delen niet welke maatregel we tegen een ander account hebben genomen, omdat dat persoonsgegevens van die persoon zijn.} no_violation {We hebben vastgesteld dat de inhoud of het account dat je hebt gemeld onze regels niet overtreedt, dus we hebben niets veranderd.} other {We hebben je melding afgesloten.}}{hasComment, select, yes {\n\nOpmerking van ons team: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nAls je het er niet mee eens bent, stuur dan binnen 60 dagen een e-mail naar {appeals_email} en vermeld het meldings-ID. Als je in de EU bent, kun je dit ook voorleggen aan een gecertificeerd orgaan voor buitengerechtelijke geschillenbeslechting of een klacht indienen bij de digitaledienstencoördinator in je land.}}\n\nHet team van {product_name}"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Verifieer je e-mailadres voor een DSA-melding",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Verifieer je e-mailadres op {product_name}",
|
||||
"body": "Hallo {username},\n\nVerifieer het e-mailadres van je {product_name}-account door op de onderstaande link te klikken:\n\n{verifyUrl}\n\nAls je geen {product_name}-account hebt aangemaakt, kun je deze e-mail veilig negeren.\n\nDeze link is 24 uur geldig.\n\n– Het team van {product_name}"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {We hebben een beslissing over je {product_name}-account teruggedraaid} other {Een beperking op je {product_name}-account is afgelopen}}",
|
||||
"body": "Hallo {username},\n\n{outcome, select, reversed {{decision_id, select, null {We hebben een beslissing over je account opnieuw bekeken en teruggedraaid.} other {We hebben beslissing {decision_id} opnieuw bekeken en teruggedraaid.}} Sorry voor het ongemak.} other {{decision_id, select, null {We hebben de beperking op je account opgeheven.} other {We hebben de beperking uit beslissing {decision_id} opgeheven.}}}}{restores_access, select, yes { Je kunt weer inloggen en {product_name} gebruiken.} other {}}{statement, select, null {} other {\n\nOpmerking van ons team: {statement}}}{content_removed, select, yes {\n\nInhoud die we hebben verwijderd, kan niet worden hersteld.} other {}}\n\nHet team van {product_name}"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {Een update over je {product_name}-account} other {{kind_label, select, warning {Een waarschuwing over je {product_name}-account} suspension {Je {product_name}-account is geschorst} ban {Je {product_name}-account is verbannen} closure {Je {product_name}-account is gesloten} feature_limit {Sommige functies van je {product_name}-account zijn beperkt} content_removal {We hebben berichten verwijderd die je op {product_name} hebt gestuurd} profile_reset {We hebben delen van je {product_name}-profiel teruggezet} community_removal {We hebben je community op {product_name} verwijderd} bot {Een update over je bot op {product_name}} other {Een update over je {product_name}-account}}}}",
|
||||
"body": "Hallo {username},\n\n{summary}\n\n{effects}\n\nWaarom\nRegel: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nOpmerking van ons team: {statement}}}\n\nZo hebben we besloten\n{how_we_decided}{appeals_email, select, null {} other {\n\nZo maak je bezwaar\nAls je denkt dat we een fout hebben gemaakt, stuur dan uiterlijk {appeal_deadline} een e-mail naar {appeals_email}. Stuur die zo mogelijk vanaf het e-mailadres van je account en vermeld het beslissings-ID. Bezwaar maken is gratis en elk bezwaar wordt door een persoon beoordeeld.{redress, select, null {} other {\n\n{redress}}}}}\n\nBeslissings-ID: {decision_id}\n\nHet team van {product_name}"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "De voordelen van je ingewisselde cadeau zijn verwijderd",
|
||||
"body": "Hallo {username},\n\nEen cadeaucode die je hebt ingewisseld, is oorspronkelijk door iemand anders betaald. Die betaling is sindsdien teruggedraaid (een terugboeking).\n\nHierdoor hebben we de voordelen verwijderd die aan je account zijn toegevoegd toen je het cadeau inwisselde.\n\n{support_email, select, null {Als je denkt dat dit een fout is, neem dan contact op met de beheerders van deze instantie en vermeld alle details die je hebt over de cadeaucode en wanneer je deze hebt ingewisseld.} other {Als je denkt dat dit een fout is, neem dan contact op met ons supportteam en vermeld alle details die je hebt over de cadeaucode en wanneer je deze hebt ingewisseld.}}\n\n– Het team van {product_name}"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "Je melding op {product_name} is beoordeeld",
|
||||
"body": "Hallo {username},\n\nJe melding (ID: {reportId}) is beoordeeld door ons veiligheidsteam.{hasComment, select, yes {\n\nReactie van het veiligheidsteam:\n{publicComment}} other {}}\n\nBedankt dat je helpt {product_name} veilig te houden voor iedereen. We nemen alle meldingen serieus en waarderen je bijdrage aan de community.\n\n{safety_email, select, null {Als je vragen of opmerkingen hebt over deze uitkomst, neem dan contact op met de beheerders van deze instantie.} other {Als je vragen of opmerkingen hebt over deze uitkomst, neem dan contact op met {safety_email}.}}\n\n– Het veiligheidsteam van {product_name}"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"subject": "Je {product_name}-account wordt permanent verwijderd",
|
||||
"body": "Hallo {username},\n\nJe {product_name}-account staat gepland voor permanente verwijdering.\n\nGeplande verwijdering: {deletionDate, date, full} om {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Reden: {reason}\n\n}\n}Dit is een ingrijpende handhavingsmaatregel. Je accountgegevens worden permanent verwijderd op de geplande datum.\n\nAls je van mening bent dat deze handhavingsbeslissing onjuist was, kun je bezwaar maken. {appeals_email, select, null {Neem contact op met de beheerders van deze instantie.} other {Stuur een e-mail naar {appeals_email} vanaf dit e-mailadres.}}\n\n– Het veiligheidsteam van {product_name}"
|
||||
"body": "Hallo {username},\n\nWe hebben je melding (ID: {reportId}) beoordeeld.\n\n{outcome, select, removed {We hebben de inhoud die je hebt gemeld verwijderd, omdat die onze regels overtrad.} restricted {We hebben vastgesteld dat het account dat je hebt gemeld onze regels heeft overtreden, en we hebben het beperkt. We delen niet welke maatregel we tegen een ander account hebben genomen, omdat dat persoonsgegevens van die persoon zijn.} action_taken {We hebben vastgesteld dat wat je hebt gemeld onze regels heeft overtreden, en we hebben maatregelen genomen. We delen niet welke maatregel we tegen een ander account hebben genomen, omdat dat persoonsgegevens van die persoon zijn.} no_violation {We hebben vastgesteld dat de inhoud of het account dat je hebt gemeld onze regels niet overtreedt, dus we hebben niets veranderd.} other {We hebben je melding afgesloten.}}{hasComment, select, yes {\n\nOpmerking van ons team: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nAls je het er niet mee eens bent, stuur dan binnen 60 dagen een e-mail naar {appeals_email} en vermeld het meldings-ID. Als je in de EU bent, kun je dit ook voorleggen aan een gecertificeerd orgaan voor buitengerechtelijke geschillenbeslechting of een klacht indienen bij de digitaledienstencoördinator in je land.}}\n\nHet team van {product_name}"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "De verwijdering van je {product_name}-account staat gepland",
|
||||
"body": "Hallo {username},\n\nJe hebt verzocht om je {product_name}-account te verwijderen. Je account staat gepland voor permanente verwijdering op:\n\n{deletionDate, date, full} om {deletionDate, time, short}\n\nAls je dit niet hebt aangevraagd, log dan in op je account om de verwijdering te annuleren. We raden ook aan om je wachtwoord te wijzigen om je account te beveiligen.\n\n– Het team van {product_name}"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "De schorsing van je {product_name}-account is opgeheven",
|
||||
"body": "Hallo {username},\n\nGoed nieuws: de schorsing van je {product_name}-account is opgeheven.\n\n{reason, select,\n null {}\n other {Reden: {reason}\n\n}\n}Je kunt nu weer inloggen en {product_name} normaal blijven gebruiken.\n\n– Het veiligheidsteam van {product_name}"
|
||||
}
|
||||
}
|
||||
+14
-22
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"subject": "{product_name}-kontoen din vil bli slettet på grunn av inaktivitet",
|
||||
"body": "Hei {username},\n\n{product_name}-kontoen din har vært inaktiv lenge, så den er planlagt for permanent sletting:\n\n{deletionDate, date, full} kl. {deletionDate, time, short}{reason, select, null {} other {\n\nÅrsak: {reason}}}\n\n{safety_email, select, null {Hvis du vil beholde kontoen din, kan du kontakte administratorene av denne instansen før denne datoen.} other {Hvis du vil beholde kontoen din, kan du kontakte {safety_email} fra denne e-postadressen før denne datoen.}}\n\n– {product_name}-teamet"
|
||||
"body": "Hei {username},\n\n{product_name}-kontoen din har vært inaktiv lenge, så vi har planlagt sletting av kontoen den {deletion_date}.\n\nHvis du vil beholde kontoen din, logger du på før den datoen. {support_email, select, null {Hvis du har spørsmål, kontakt administratorene av denne instansen.} other {Hvis du har spørsmål, kontakt {support_email}.}}\n\n{product_name}-teamet"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"subject": "Slettingen av {product_name}-kontoen din er planlagt",
|
||||
"body": "Hei {username},\n\nSom du ba om, er {product_name}-kontoen din planlagt for permanent sletting:\n\n{deletionDate, date, full} kl. {deletionDate, time, short}{reason, select, null {} other {\n\nÅrsak: {reason}}}\n\nKontoen din er låst frem til da. {safety_email, select, null {Hvis du ikke ba om dette, eller hvis du vil beholde kontoen din, kan du kontakte administratorene av denne instansen før denne datoen.} other {Hvis du ikke ba om dette, eller hvis du vil beholde kontoen din, kan du kontakte {safety_email} fra denne e-postadressen før denne datoen.}}\n\n– {product_name}-teamet"
|
||||
},
|
||||
"account_scheduled_deletion": {
|
||||
"subject": "{product_name}-kontoen din blir permanent slettet",
|
||||
"body": "Hei {username},\n\nVi har planlagt permanent sletting av {product_name}-kontoen din på grunn av brudd på våre vilkår for bruk eller retningslinjer for fellesskapet.\n\nPlanlagt sletting: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Årsak: {reason}\n\n}\n}Dette er et alvorlig tiltak. Kontodataene dine blir permanent slettet på den planlagte datoen.\n\n{legalLinks, select,\n both {Les gjennom:\n- Vilkår for bruk: {termsUrl}\n- Retningslinjer for fellesskapet: {guidelinesUrl}\n\n}\n terms {Les gjennom:\n- Vilkår for bruk: {termsUrl}\n\n}\n guidelines {Les gjennom:\n- Retningslinjer for fellesskapet: {guidelinesUrl}\n\n}\n other {}\n}Klageprosessen:\nHvis du mener avgjørelsen var feil eller uberettiget, har du 60 dager på deg til å sende inn en klage. {appeals_email, select, null {Kontakt administratorene av denne instansen.} other {Send en e-post til {appeals_email} fra denne e-postadressen.}}\n\nI klagen din:\n- Forklar tydelig hvorfor du mener avgjørelsen var feil eller uberettiget\n- Legg ved relevante bevis eller opplysninger\n\nEt medlem av sikkerhetsteamet hos {product_name} vil vurdere klagen din og kan utsette slettingen til en endelig avgjørelse er tatt.\n\n– Sikkerhetsteamet hos {product_name}"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "{product_name}-kontoen din er midlertidig suspendert",
|
||||
"body": "Hei {username},\n\n{product_name}-kontoen din er midlertidig suspendert på grunn av brudd på våre vilkår for bruk eller retningslinjer for fellesskapet.\n\nVarighet: {durationHours, plural,\n =1 {1 time}\n other {# timer}\n}\nSuspendert til: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Årsak: {reason}\n\n}\n}Du har ikke tilgang til kontoen din i denne perioden.\n\n{legalLinks, select,\n both {Les gjennom:\n- Vilkår for bruk: {termsUrl}\n- Retningslinjer for fellesskapet: {guidelinesUrl}\n\n}\n terms {Les gjennom:\n- Vilkår for bruk: {termsUrl}\n\n}\n guidelines {Les gjennom:\n- Retningslinjer for fellesskapet: {guidelinesUrl}\n\n}\n other {}\n}Hvis du mener avgjørelsen var feil eller uberettiget, kan du sende inn en klage. {appeals_email, select, null {Kontakt administratorene av denne instansen, og forklar tydelig hvorfor du mener avgjørelsen var feil.} other {Send en e-post til {appeals_email} fra denne e-postadressen, og forklar tydelig hvorfor du mener avgjørelsen var feil.}} Vi vil vurdere klagen din og gi deg beskjed om avgjørelsen vår.\n\n– Sikkerhetsteamet hos {product_name}"
|
||||
"body": "Hei {username},\n\nSom du ba om, har vi planlagt sletting av {product_name}-kontoen din den {deletion_date}.\n\nHvis du vil beholde kontoen din, logger du på før {deletion_date}. {safety_email, select, null {Hvis du ikke ba om dette, kontakt administratorene av denne instansen.} other {Hvis du ikke ba om dette, kontakt {safety_email}.}}\n\n{product_name}-teamet"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Takk for bidraget ditt til {product_name}",
|
||||
@@ -28,8 +20,8 @@
|
||||
"body": "Hei,\n\nKlikk på lenken nedenfor for å få tilgang til giverportalen din:\n\n{manageUrl}\n\nI portalen kan du administrere abonnementer, laste ned fakturaer og se bidragshistorikken din.\n\nDenne lenken utløper {expiresAt, date, full} kl. {expiresAt, time, short}.\n\nHvis du ikke ba om denne lenken, kan du trygt ignorere denne e-posten.\n\n– {product_name}-teamet"
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "Vi har tatt en avgjørelse om {product_name}-rapporten din",
|
||||
"body": "Hei,\n\nSikkerhetsteamet vårt har gjennomgått rapporten din i henhold til Digital Services Act (ID: {reportId}) og tatt en avgjørelse.{hasComment, select, yes {\n\nSvar fra sikkerhetsteamet:\n{publicComment}} other {}}\n\nVi deler ikke detaljer om tiltak mot en annen persons konto, fordi det er personopplysningene til den personen.\n\nHvis du er uenig i avgjørelsen, kan du klage gratis innen 60 dager. {appeals_email, select, null {Kontakt administratorene av denne instansen, oppgi rapport-ID-en din, og forklar hvorfor du mener avgjørelsen er feil.} other {Send en e-post til {appeals_email} fra denne e-postadressen, oppgi rapport-ID-en din, og forklar hvorfor du mener avgjørelsen er feil.}} Hvis du befinner deg i EU, kan du også bringe tvisten inn for et sertifisert organ for utenrettslig tvisteløsning. Ingenting av dette påvirker retten din til å gå til domstolene.\n\n– Sikkerhetsteamet hos {product_name}"
|
||||
"subject": "Vi har tatt en avgjørelse om rapporten din på {product_name}",
|
||||
"body": "Hei,\n\nVi har gått gjennom rapporten din (ID: {reportId}).\n\n{outcome, select, removed {Vi har fjernet innholdet du rapporterte, fordi det brøt reglene våre.} restricted {Vi fant ut at kontoen du rapporterte, brøt reglene våre, og vi har begrenset den. Vi oppgir ikke hvilke tiltak vi har iverksatt mot en annen konto, fordi det er personopplysningene til den personen.} action_taken {Vi fant ut at det du rapporterte, brøt reglene våre, og vi har grepet inn. Vi oppgir ikke hvilke tiltak vi har iverksatt mot en annen konto, fordi det er personopplysningene til den personen.} no_violation {Vi fant ut at innholdet eller kontoen du rapporterte, ikke bryter reglene våre, så vi har ikke gjort noe.} other {Vi har lukket rapporten din.}}{hasComment, select, yes {\n\nMerknad fra teamet vårt: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nHvis du er uenig, send en e-post til {appeals_email} innen 60 dager og oppgi rapport-ID-en. Hvis du er i EU, kan du også bringe saken inn for et sertifisert organ for utenrettslig tvisteløsning eller klage til koordinatoren for digitale tjenester i landet ditt.}}\n\n{product_name}-teamet"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Bekreft e-postadressen din for en DSA-rapport",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Bekreft e-postadressen din for {product_name}",
|
||||
"body": "Hei {username},\n\nBekreft e-postadressen til {product_name}-kontoen din ved å klikke på lenken nedenfor:\n\n{verifyUrl}\n\nHvis du ikke opprettet en {product_name}-konto, kan du trygt se bort fra denne e-posten.\n\nDenne lenken er gyldig i 24 timer.\n\n– {product_name}-teamet"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {Vi har omgjort en avgjørelse om {product_name}-kontoen din} other {En begrensning på {product_name}-kontoen din er opphevet}}",
|
||||
"body": "Hei {username},\n\n{outcome, select, reversed {{decision_id, select, null {Vi har sett på en avgjørelse om kontoen din på nytt og omgjort den.} other {Vi har sett på avgjørelse {decision_id} på nytt og omgjort den.}} Vi beklager bryderiet.} other {{decision_id, select, null {Vi har opphevet begrensningen på kontoen din.} other {Vi har opphevet begrensningen fra avgjørelse {decision_id}.}}}}{restores_access, select, yes { Du kan logge på og bruke {product_name} igjen.} other {}}{statement, select, null {} other {\n\nMerknad fra teamet vårt: {statement}}}{content_removed, select, yes {\n\nInnhold vi har fjernet, kan ikke gjenopprettes.} other {}}\n\n{product_name}-teamet"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {Informasjon om {product_name}-kontoen din} other {{kind_label, select, warning {En advarsel om {product_name}-kontoen din} suspension {{product_name}-kontoen din er suspendert} ban {{product_name}-kontoen din er utestengt} closure {{product_name}-kontoen din er stengt} feature_limit {Noen funksjoner er begrenset på {product_name}-kontoen din} content_removal {Vi har fjernet meldinger du sendte på {product_name}} profile_reset {Vi har tilbakestilt deler av {product_name}-profilen din} community_removal {Vi har fjernet fellesskapet ditt på {product_name}} bot {Informasjon om boten din på {product_name}} other {Informasjon om {product_name}-kontoen din}}}}",
|
||||
"body": "Hei {username},\n\n{summary}\n\n{effects}\n\nHvorfor\nRegel: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nMerknad fra teamet vårt: {statement}}}\n\nSlik tok vi avgjørelsen\n{how_we_decided}{appeals_email, select, null {} other {\n\nSlik klager du\nHvis du mener at vi tok feil, send en e-post til {appeals_email} innen {appeal_deadline}. Skriv fra e-postadressen på kontoen din hvis du kan, og oppgi avgjørelses-ID-en. Det er gratis å klage, og en person går gjennom hver klage.{redress, select, null {} other {\n\n{redress}}}}}\n\nAvgjørelses-ID: {decision_id}\n\n{product_name}-teamet"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Fordelene fra gaven du løste inn, er fjernet",
|
||||
"body": "Hei {username},\n\nDu løste inn en gavekode som opprinnelig var betalt av noen andre. Betalingen har senere blitt tilbakeført.\n\nDerfor har vi fjernet fordelene som ble lagt til kontoen din da du løste inn gaven.\n\n{support_email, select, null {Hvis du mener dette er en feil, kan du kontakte administratorene av denne instansen. Ta med opplysningene du har om gavekoden og når du løste den inn.} other {Hvis du mener dette er en feil, kan du kontakte kundestøtten vår. Ta med opplysningene du har om gavekoden og når du løste den inn.}}\n\n– {product_name}-teamet"
|
||||
@@ -84,19 +84,11 @@
|
||||
"body": "Hei,\n\nVi har mottatt rapporten din i henhold til Digital Services Act om {targetKind, select, message {en melding} user {en konto} guild {et fellesskap} other {innhold}} på {product_name}.\n\nRapport-ID: {reportId}\n\nSikkerhetsteamet vårt går gjennom rapporten din, og vi sender deg en e-post på denne adressen når vi har tatt en avgjørelse. Ta vare på denne e-posten.\n\n– Sikkerhetsteamet hos {product_name}"
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "{product_name}-rapporten din er gjennomgått",
|
||||
"body": "Hei {username},\n\nRapporten din (ID: {reportId}) er gjennomgått av sikkerhetsteamet vårt.{hasComment, select, yes {\n\nSvar fra sikkerhetsteamet:\n{publicComment}} other {}}\n\nTakk for at du bidrar til å holde {product_name} trygt for alle. Vi tar alle rapporter på alvor og setter pris på bidraget ditt til fellesskapet.\n\n{safety_email, select, null {Hvis du har spørsmål eller er bekymret for utfallet, kan du kontakte administratorene av denne instansen.} other {Hvis du har spørsmål eller er bekymret for utfallet, kan du kontakte {safety_email}.}}\n\n– Sikkerhetsteamet hos {product_name}"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"subject": "{product_name}-kontoen din blir permanent slettet",
|
||||
"body": "Hei {username},\n\n{product_name}-kontoen din er planlagt for permanent sletting.\n\nPlanlagt sletting: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Årsak: {reason}\n\n}\n}Dette er et alvorlig tiltak. Kontodataene dine blir permanent slettet på den planlagte datoen.\n\nHvis du mener avgjørelsen var feil, kan du sende inn en klage. {appeals_email, select, null {Kontakt administratorene av denne instansen.} other {Send en e-post til {appeals_email} fra denne e-postadressen.}}\n\n– Sikkerhetsteamet hos {product_name}"
|
||||
"subject": "Rapporten din på {product_name} er gjennomgått",
|
||||
"body": "Hei {username},\n\nVi har gått gjennom rapporten din (ID: {reportId}).\n\n{outcome, select, removed {Vi har fjernet innholdet du rapporterte, fordi det brøt reglene våre.} restricted {Vi fant ut at kontoen du rapporterte, brøt reglene våre, og vi har begrenset den. Vi oppgir ikke hvilke tiltak vi har iverksatt mot en annen konto, fordi det er personopplysningene til den personen.} action_taken {Vi fant ut at det du rapporterte, brøt reglene våre, og vi har grepet inn. Vi oppgir ikke hvilke tiltak vi har iverksatt mot en annen konto, fordi det er personopplysningene til den personen.} no_violation {Vi fant ut at innholdet eller kontoen du rapporterte, ikke bryter reglene våre, så vi har ikke gjort noe.} other {Vi har lukket rapporten din.}}{hasComment, select, yes {\n\nMerknad fra teamet vårt: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nHvis du er uenig, send en e-post til {appeals_email} innen 60 dager og oppgi rapport-ID-en. Hvis du er i EU, kan du også bringe saken inn for et sertifisert organ for utenrettslig tvisteløsning eller klage til koordinatoren for digitale tjenester i landet ditt.}}\n\n{product_name}-teamet"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "Slettingen av {product_name}-kontoen din er planlagt",
|
||||
"body": "Hei {username},\n\nDu ba om å slette {product_name}-kontoen din. Kontoen din er planlagt for permanent sletting:\n\n{deletionDate, date, full} kl. {deletionDate, time, short}\n\nHvis du ikke ba om dette, kan du logge inn på kontoen din for å avbryte slettingen. Vi anbefaler også å endre passordet ditt for å sikre kontoen din.\n\n– {product_name}-teamet"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "Suspensjonen av {product_name}-kontoen din er opphevet",
|
||||
"body": "Hei {username},\n\nGode nyheter! Suspensjonen av {product_name}-kontoen din er opphevet.\n\n{reason, select,\n null {}\n other {Årsak: {reason}\n\n}\n}Du kan nå logge inn igjen og fortsette å bruke {product_name} som normalt.\n\n– Sikkerhetsteamet hos {product_name}"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"subject": "Twoje konto {product_name} zostanie usunięte z powodu braku aktywności",
|
||||
"body": "Witaj {username},\n\nTwoje konto {product_name} od dawna jest nieaktywne, dlatego zostało zaplanowane do trwałego usunięcia. Termin usunięcia:\n\n{deletionDate, date, full} o {deletionDate, time, short}{reason, select, null {} other {\n\nPowód: {reason}}}\n\n{safety_email, select, null {Jeśli chcesz zachować konto, skontaktuj się z administratorami tej instancji przed tym terminem.} other {Jeśli chcesz zachować konto, skontaktuj się z {safety_email} z tego adresu e-mail przed tym terminem.}}\n\n– Zespół {product_name}"
|
||||
"body": "Witaj {username},\n\nTwoje konto {product_name} od dawna jest nieaktywne, dlatego zaplanowaliśmy jego usunięcie na dzień {deletion_date}.\n\nAby zachować konto, zaloguj się przed tym dniem. {support_email, select, null {Jeśli masz pytania, skontaktuj się z administratorami tej instancji.} other {Jeśli masz pytania, napisz na adres {support_email}.}}\n\nZespół {product_name}"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"subject": "Usunięcie Twojego konta {product_name} zostało zaplanowane",
|
||||
"body": "Witaj {username},\n\nZgodnie z Twoją prośbą zaplanowaliśmy trwałe usunięcie konta {product_name}. Termin usunięcia:\n\n{deletionDate, date, full} o {deletionDate, time, short}{reason, select, null {} other {\n\nPowód: {reason}}}\n\nDo tego czasu Twoje konto jest zablokowane. {safety_email, select, null {Jeśli nie prosiłeś o to lub chcesz zachować konto, skontaktuj się z administratorami tej instancji przed tym terminem.} other {Jeśli nie prosiłeś o to lub chcesz zachować konto, skontaktuj się z {safety_email} z tego adresu e-mail przed tym terminem.}}\n\n– Zespół {product_name}"
|
||||
},
|
||||
"account_scheduled_deletion": {
|
||||
"subject": "Twoje konto {product_name} zostanie trwale usunięte",
|
||||
"body": "Witaj {username},\n\nZaplanowaliśmy trwałe usunięcie Twojego konta {product_name} z powodu naruszenia naszych warunków korzystania z usługi lub zasad społeczności.\n\nZaplanowane usunięcie: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Powód: {reason}\n\n}\n}Jest to poważna sankcja. Dane Twojego konta zostaną usunięte na stałe w zaplanowanym terminie.\n\n{legalLinks, select,\n both {Sprawdź:\n- Warunki korzystania z usługi: {termsUrl}\n- Zasady społeczności: {guidelinesUrl}\n\n}\n terms {Sprawdź:\n- Warunki korzystania z usługi: {termsUrl}\n\n}\n guidelines {Sprawdź:\n- Zasady społeczności: {guidelinesUrl}\n\n}\n other {}\n}Proces odwoławczy:\nJeśli uważasz, że ta decyzja była błędna lub nieuzasadniona, masz 60 dni na złożenie odwołania. {appeals_email, select, null {Skontaktuj się z administratorami tej instancji.} other {Wyślij e-mail na adres {appeals_email} z tego adresu e-mail.}}\n\nW swoim odwołaniu:\n- Wyjaśnij jasno, dlaczego uważasz, że decyzja była błędna lub nieuzasadniona\n- Przedstaw wszelkie istotne dowody lub kontekst\n\nCzłonek zespołu ds. bezpieczeństwa {product_name} rozpatrzy Twoje odwołanie i może wstrzymać usunięcie do czasu podjęcia ostatecznej decyzji.\n\n– Zespół ds. bezpieczeństwa {product_name}"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "Twoje konto {product_name} zostało tymczasowo zawieszone",
|
||||
"body": "Witaj {username},\n\nTwoje konto {product_name} zostało tymczasowo zawieszone za naruszenie naszych warunków korzystania z usługi lub zasad społeczności.\n\nCzas trwania: {durationHours, plural,\n one {1 godzina} few {# godziny}\n many {# godzin} other {# godziny}\n}\nZawieszone do: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Powód: {reason}\n\n}\n}W tym czasie nie będziesz mieć dostępu do swojego konta.\n\n{legalLinks, select,\n both {Sprawdź:\n- Warunki korzystania z usługi: {termsUrl}\n- Zasady społeczności: {guidelinesUrl}\n\n}\n terms {Sprawdź:\n- Warunki korzystania z usługi: {termsUrl}\n\n}\n guidelines {Sprawdź:\n- Zasady społeczności: {guidelinesUrl}\n\n}\n other {}\n}Jeśli uważasz, że ta decyzja była błędna lub nieuzasadniona, możesz złożyć odwołanie. {appeals_email, select, null {Skontaktuj się z administratorami tej instancji i jasno wyjaśnij, dlaczego uważasz, że decyzja była błędna.} other {Wyślij e-mail na adres {appeals_email} z tego adresu e-mail i jasno wyjaśnij, dlaczego uważasz, że decyzja była błędna.}} Rozpatrzymy Twoje odwołanie i poinformujemy Cię o naszej decyzji.\n\n– Zespół ds. bezpieczeństwa {product_name}"
|
||||
"body": "Witaj {username},\n\nZgodnie z Twoją prośbą zaplanowaliśmy usunięcie Twojego konta {product_name} na dzień {deletion_date}.\n\nAby zachować konto, zaloguj się przed dniem {deletion_date}. {safety_email, select, null {Jeśli ta prośba nie pochodzi od Ciebie, skontaktuj się z administratorami tej instancji.} other {Jeśli ta prośba nie pochodzi od Ciebie, napisz na adres {safety_email}.}}\n\nZespół {product_name}"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Dziękujemy za darowiznę dla {product_name}",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "Podjęliśmy decyzję w sprawie Twojego zgłoszenia w {product_name}",
|
||||
"body": "Witaj,\n\nNasz zespół ds. bezpieczeństwa rozpatrzył Twoje zgłoszenie w ramach aktu o usługach cyfrowych (ID: {reportId}) i podjął decyzję.{hasComment, select, yes {\n\nOdpowiedź od zespołu ds. bezpieczeństwa:\n{publicComment}} other {}}\n\nNie udostępniamy szczegółów działań podjętych wobec konta innej osoby, ponieważ są to jej dane osobowe.\n\nJeśli nie zgadzasz się z tą decyzją, możesz bezpłatnie złożyć odwołanie w ciągu 60 dni. {appeals_email, select, null {Skontaktuj się z administratorami tej instancji, podaj ID zgłoszenia i wyjaśnij, dlaczego uważasz, że decyzja jest błędna.} other {Wyślij e-mail na adres {appeals_email} z tego adresu e-mail, podaj ID zgłoszenia i wyjaśnij, dlaczego uważasz, że decyzja jest błędna.}} Jeśli przebywasz w UE, możesz też skierować spór do certyfikowanego organu pozasądowego rozstrzygania sporów. Nic z tego nie wpływa na Twoje prawo do skierowania sprawy do sądu.\n\n– Zespół ds. bezpieczeństwa {product_name}"
|
||||
"body": "Dzień dobry,\n\nRozpatrzyliśmy Twoje zgłoszenie (ID: {reportId}).\n\n{outcome, select, removed {Usunęliśmy zgłoszone treści, ponieważ naruszały nasze zasady.} restricted {Ustaliliśmy, że zgłoszone konto naruszyło nasze zasady, i nałożyliśmy na nie ograniczenia. Nie informujemy, jakie działania podjęliśmy wobec innego konta, ponieważ są to dane osobowe jego właściciela.} action_taken {Ustaliliśmy, że zgłoszona treść lub konto naruszyły nasze zasady, i podjęliśmy działania. Nie informujemy, jakie działania podjęliśmy wobec innego konta, ponieważ są to dane osobowe jego właściciela.} no_violation {Ustaliliśmy, że zgłoszone treści lub konto nie naruszają naszych zasad, więc pozostawiliśmy je bez zmian.} other {Zamknęliśmy Twoje zgłoszenie.}}{hasComment, select, yes {\n\nKomentarz naszego zespołu: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nJeśli się nie zgadzasz, napisz na adres {appeals_email} w ciągu 60 dni i podaj identyfikator zgłoszenia. Jeśli jesteś w UE, możesz też skierować sprawę do certyfikowanego organu pozasądowego rozstrzygania sporów lub złożyć skargę do koordynatora ds. usług cyfrowych w swoim kraju.}}\n\nZespół {product_name}"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Zweryfikuj adres e-mail na potrzeby zgłoszenia DSA",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Zweryfikuj adres e-mail w {product_name}",
|
||||
"body": "Witaj {username},\n\nZweryfikuj adres e-mail swojego konta {product_name}, klikając poniższy link:\n\n{verifyUrl}\n\nJeśli nie utworzyłeś konta {product_name}, możesz bezpiecznie zignorować tę wiadomość e-mail.\n\nTen link jest ważny przez 24 godziny.\n\n– Zespół {product_name}"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {Cofnęliśmy decyzję dotyczącą Twojego konta {product_name}} other {Ograniczenie Twojego konta {product_name} przestało obowiązywać}}",
|
||||
"body": "Witaj {username},\n\n{outcome, select, reversed {{decision_id, select, null {Ponownie przeanalizowaliśmy decyzję dotyczącą Twojego konta i ją cofnęliśmy.} other {Ponownie przeanalizowaliśmy decyzję {decision_id} i ją cofnęliśmy.}} Przepraszamy za kłopot.} other {{decision_id, select, null {Zdjęliśmy ograniczenie z Twojego konta.} other {Zdjęliśmy ograniczenie nałożone decyzją {decision_id}.}}}}{restores_access, select, yes { Możesz znowu logować się i korzystać z {product_name}.} other {}}{statement, select, null {} other {\n\nKomentarz naszego zespołu: {statement}}}{content_removed, select, yes {\n\nUsuniętych przez nas treści nie można przywrócić.} other {}}\n\nZespół {product_name}"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {Informacja o Twoim koncie {product_name}} other {{kind_label, select, warning {Ostrzeżenie dotyczące Twojego konta {product_name}} suspension {Twoje konto {product_name} zostało zawieszone} ban {Twoje konto {product_name} zostało zablokowane} closure {Twoje konto {product_name} zostało zamknięte} feature_limit {Niektóre funkcje Twojego konta {product_name} są ograniczone} content_removal {Usunęliśmy Twoje wiadomości w {product_name}} profile_reset {Zresetowaliśmy część Twojego profilu {product_name}} community_removal {Usunęliśmy Twoją społeczność w {product_name}} bot {Informacja o Twoim bocie w {product_name}} other {Informacja o Twoim koncie {product_name}}}}}",
|
||||
"body": "Witaj {username},\n\n{summary}\n\n{effects}\n\nDlaczego\nZasada: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nKomentarz naszego zespołu: {statement}}}\n\nJak podjęliśmy decyzję\n{how_we_decided}{appeals_email, select, null {} other {\n\nJak się odwołać\nJeśli uważasz, że popełniliśmy błąd, napisz na adres {appeals_email} do {appeal_deadline}. Jeśli możesz, napisz z adresu e-mail przypisanego do Twojego konta i podaj identyfikator decyzji. Odwołania są bezpłatne, a każde z nich rozpatruje człowiek.{redress, select, null {} other {\n\n{redress}}}}}\n\nIdentyfikator decyzji: {decision_id}\n\nZespół {product_name}"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Usunięto bonusy z wykorzystanego prezentu",
|
||||
"body": "Witaj {username},\n\nKod podarunkowy wykorzystany na Twoim koncie został pierwotnie opłacony przez inną osobę. Płatność ta została następnie cofnięta (chargeback).\n\nZ tego powodu usunęliśmy bonusy, które zostały dodane do Twojego konta po wykorzystaniu prezentu.\n\n{support_email, select, null {Jeśli uważasz, że to pomyłka, skontaktuj się z administratorami tej instancji i podaj wszelkie szczegóły dotyczące kodu podarunkowego oraz daty jego wykorzystania.} other {Jeśli uważasz, że to pomyłka, skontaktuj się z naszym zespołem wsparcia i podaj wszelkie szczegóły dotyczące kodu podarunkowego oraz daty jego wykorzystania.}}\n\n– Zespół {product_name}"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "Twoje zgłoszenie w {product_name} zostało rozpatrzone",
|
||||
"body": "Witaj {username},\n\nTwoje zgłoszenie (ID: {reportId}) zostało rozpatrzone przez nasz zespół ds. bezpieczeństwa.{hasComment, select, yes {\n\nOdpowiedź od zespołu ds. bezpieczeństwa:\n{publicComment}} other {}}\n\nDziękujemy za pomoc w utrzymaniu bezpieczeństwa {product_name} dla wszystkich. Traktujemy wszystkie zgłoszenia poważnie i doceniamy Twój wkład w społeczność.\n\n{safety_email, select, null {Jeśli masz pytania lub wątpliwości dotyczące tej decyzji, skontaktuj się z administratorami tej instancji.} other {Jeśli masz pytania lub wątpliwości dotyczące tej decyzji, skontaktuj się z {safety_email}.}}\n\n– Zespół ds. bezpieczeństwa {product_name}"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"subject": "Twoje konto {product_name} zostanie trwale usunięte",
|
||||
"body": "Witaj {username},\n\nZaplanowaliśmy trwałe usunięcie Twojego konta {product_name}.\n\nZaplanowane usunięcie: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Powód: {reason}\n\n}\n}Jest to poważna sankcja. Dane Twojego konta zostaną usunięte na stałe w zaplanowanym terminie.\n\nJeśli uważasz, że ta decyzja była błędna, możesz złożyć odwołanie. {appeals_email, select, null {Skontaktuj się z administratorami tej instancji.} other {Wyślij e-mail na adres {appeals_email} z tego adresu e-mail.}}\n\n– Zespół ds. bezpieczeństwa {product_name}"
|
||||
"body": "Witaj {username},\n\nRozpatrzyliśmy Twoje zgłoszenie (ID: {reportId}).\n\n{outcome, select, removed {Usunęliśmy zgłoszone treści, ponieważ naruszały nasze zasady.} restricted {Ustaliliśmy, że zgłoszone konto naruszyło nasze zasady, i nałożyliśmy na nie ograniczenia. Nie informujemy, jakie działania podjęliśmy wobec innego konta, ponieważ są to dane osobowe jego właściciela.} action_taken {Ustaliliśmy, że zgłoszona treść lub konto naruszyły nasze zasady, i podjęliśmy działania. Nie informujemy, jakie działania podjęliśmy wobec innego konta, ponieważ są to dane osobowe jego właściciela.} no_violation {Ustaliliśmy, że zgłoszone treści lub konto nie naruszają naszych zasad, więc pozostawiliśmy je bez zmian.} other {Zamknęliśmy Twoje zgłoszenie.}}{hasComment, select, yes {\n\nKomentarz naszego zespołu: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nJeśli się nie zgadzasz, napisz na adres {appeals_email} w ciągu 60 dni i podaj identyfikator zgłoszenia. Jeśli jesteś w UE, możesz też skierować sprawę do certyfikowanego organu pozasądowego rozstrzygania sporów lub złożyć skargę do koordynatora ds. usług cyfrowych w swoim kraju.}}\n\nZespół {product_name}"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "Usunięcie Twojego konta {product_name} zostało zaplanowane",
|
||||
"body": "Witaj {username},\n\nOtrzymaliśmy Twoją prośbę o usunięcie konta {product_name}. Termin trwałego usunięcia konta:\n\n{deletionDate, date, full} o {deletionDate, time, short}\n\nJeśli nie prosiłeś o to, zaloguj się na swoje konto, aby anulować usunięcie. Zalecamy również zmianę hasła w celu zabezpieczenia konta.\n\n– Zespół {product_name}"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "Zawieszenie Twojego konta {product_name} zostało zniesione",
|
||||
"body": "Witaj {username},\n\nDobre wieści: zawieszenie Twojego konta {product_name} zostało zniesione.\n\n{reason, select,\n null {}\n other {Powód: {reason}\n\n}\n}Możesz teraz ponownie się zalogować i kontynuować korzystanie z {product_name} jak zwykle.\n\n– Zespół ds. bezpieczeństwa {product_name}"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"subject": "A sua conta do {product_name} será excluída devido à inatividade",
|
||||
"body": "Olá, {username},\n\nSua conta do {product_name} está inativa há muito tempo, por isso está agendada para exclusão permanente em:\n\n{deletionDate, date, full} às {deletionDate, time, short}{reason, select, null {} other {\n\nMotivo: {reason}}}\n\n{safety_email, select, null {Se quiser manter sua conta, entre em contato com os administradores desta instância antes dessa data.} other {Se quiser manter sua conta, entre em contato com {safety_email} usando este endereço de e-mail antes dessa data.}}\n\n– Equipe do {product_name}"
|
||||
"body": "Olá, {username},\n\nSua conta do {product_name} está inativa há muito tempo, por isso agendamos a exclusão dela para {deletion_date}.\n\nPara manter sua conta, entre antes dessa data. {support_email, select, null {Se tiver dúvidas, entre em contato com os administradores desta instância.} other {Se tiver dúvidas, entre em contato com {support_email}.}}\n\nEquipe do {product_name}"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"subject": "A exclusão da sua conta do {product_name} está agendada",
|
||||
"body": "Olá, {username},\n\nConforme solicitado, sua conta do {product_name} está agendada para exclusão permanente em:\n\n{deletionDate, date, full} às {deletionDate, time, short}{reason, select, null {} other {\n\nMotivo: {reason}}}\n\nSua conta permanecerá bloqueada até lá. {safety_email, select, null {Se você não solicitou isso, ou se quiser manter sua conta, entre em contato com os administradores desta instância antes dessa data.} other {Se você não solicitou isso, ou se quiser manter sua conta, entre em contato com {safety_email} usando este endereço de e-mail antes dessa data.}}\n\n– Equipe do {product_name}"
|
||||
},
|
||||
"account_scheduled_deletion": {
|
||||
"subject": "Sua conta do {product_name} será excluída permanentemente",
|
||||
"body": "Olá, {username},\n\nA exclusão permanente da sua conta do {product_name} foi agendada devido a violações de nossos Termos de serviço ou Diretrizes da comunidade.\n\nExclusão agendada: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Motivo: {reason}\n\n}\n}Esta é uma medida séria. Os dados da sua conta serão excluídos permanentemente na data agendada.\n\n{legalLinks, select,\n both {Por favor, revise:\n- Termos de serviço: {termsUrl}\n- Diretrizes da comunidade: {guidelinesUrl}\n\n}\n terms {Por favor, revise:\n- Termos de serviço: {termsUrl}\n\n}\n guidelines {Por favor, revise:\n- Diretrizes da comunidade: {guidelinesUrl}\n\n}\n other {}\n}Processo de recurso:\nSe você acredita que esta decisão foi incorreta ou injustificada, você tem 60 dias para enviar um recurso. {appeals_email, select, null {Entre em contato com os administradores desta instância.} other {Envie um e-mail para {appeals_email} a partir deste endereço de e-mail.}}\n\nEm seu recurso:\n- Explique claramente por que você acredita que a decisão foi incorreta ou injustificada\n- Forneça quaisquer evidências ou contexto relevantes\n\nUm membro da equipe de segurança do {product_name} revisará seu recurso e poderá pausar a exclusão pendente até que uma decisão final seja tomada.\n\n– equipe de segurança do {product_name}"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "Sua conta do {product_name} foi suspensa temporariamente",
|
||||
"body": "Olá, {username},\n\nSua conta do {product_name} foi suspensa temporariamente por violar nossos Termos de serviço ou Diretrizes da comunidade.\n\nDuração: {durationHours, plural,\n =1 {1 hora} other {# horas}}\nSuspensa até: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {} other {Motivo: {reason}\n\n}}Durante este período, você não poderá acessar sua conta.\n\n{legalLinks, select,\n both {Por favor, revise:\n- Termos de serviço: {termsUrl}\n- Diretrizes da comunidade: {guidelinesUrl}\n\n}\n terms {Por favor, revise:\n- Termos de serviço: {termsUrl}\n\n}\n guidelines {Por favor, revise:\n- Diretrizes da comunidade: {guidelinesUrl}\n\n}\n other {}\n}Se você acredita que esta decisão foi incorreta ou injustificada, você pode enviar um recurso. {appeals_email, select, null {Entre em contato com os administradores desta instância e explique claramente por que você acredita que a decisão foi incorreta.} other {Envie um e-mail para {appeals_email} a partir deste endereço de e-mail e explique claramente por que você acredita que a decisão foi incorreta.}} Revisaremos seu recurso e responderemos com nossa decisão.\n\n– equipe de segurança do {product_name}"
|
||||
"body": "Olá, {username},\n\nConforme você pediu, agendamos a exclusão da sua conta do {product_name} para {deletion_date}.\n\nPara manter sua conta, entre antes de {deletion_date}. {safety_email, select, null {Se você não pediu isso, entre em contato com os administradores desta instância.} other {Se você não pediu isso, entre em contato com {safety_email}.}}\n\nEquipe do {product_name}"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Agradecemos pela sua doação ao {product_name}",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "Tomamos uma decisão sobre sua denúncia no {product_name}",
|
||||
"body": "Olá,\n\nNossa equipe de segurança analisou sua denúncia nos termos da Lei dos Serviços Digitais (ID: {reportId}) e tomou uma decisão.{hasComment, select, yes {\n\nResposta da equipe de segurança:\n{publicComment}} other {}}\n\nNão compartilhamos detalhes de medidas tomadas contra a conta de outra pessoa, pois são dados pessoais dela.\n\nSe você não concordar com esta decisão, pode enviar um recurso gratuitamente em até 60 dias. {appeals_email, select, null {Entre em contato com os administradores desta instância, inclua o ID da sua denúncia e explique por que você acredita que a decisão está incorreta.} other {Envie um e-mail para {appeals_email} a partir deste endereço de e-mail, inclua o ID da sua denúncia e explique por que você acredita que a decisão está incorreta.}} Se você estiver na UE, também pode encaminhar a disputa a um órgão certificado de resolução extrajudicial de litígios. Nada disso afeta seu direito de recorrer à Justiça.\n\n– equipe de segurança do {product_name}"
|
||||
"body": "Olá,\n\nAnalisamos sua denúncia (ID: {reportId}).\n\n{outcome, select, removed {Removemos o conteúdo que você denunciou porque ele violava nossas regras.} restricted {Constatamos que a conta que você denunciou violava nossas regras e aplicamos uma restrição a ela. Não informamos que medida tomamos contra outra conta, porque isso é um dado pessoal do titular.} action_taken {Constatamos que o que você denunciou violava nossas regras e tomamos uma medida. Não informamos que medida tomamos contra outra conta, porque isso é um dado pessoal do titular.} no_violation {Constatamos que o conteúdo ou a conta que você denunciou não viola nossas regras, então não fizemos nenhuma alteração.} other {Encerramos sua denúncia.}}{hasComment, select, yes {\n\nNota da nossa equipe: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nSe você discordar, envie um e-mail para {appeals_email} em até 60 dias informando o ID da denúncia. Se você estiver na UE, também pode submeter a questão a um organismo de resolução extrajudicial de litígios certificado ou apresentar uma reclamação ao coordenador dos serviços digitais do seu país.}}\n\nEquipe do {product_name}"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Verifique seu e-mail para enviar uma denúncia nos termos do DSA",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Verifique o endereço de e-mail da sua conta do {product_name}",
|
||||
"body": "Olá, {username},\n\nPor favor, verifique o endereço de e-mail da sua conta do {product_name} clicando no link abaixo:\n\n{verifyUrl}\n\nSe você não criou uma conta do {product_name}, pode ignorar este e-mail com segurança.\n\nEste link é válido por 24 horas.\n\n– Equipe do {product_name}"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {Revertemos uma decisão sobre sua conta do {product_name}} other {Uma restrição na sua conta do {product_name} terminou}}",
|
||||
"body": "Olá, {username},\n\n{outcome, select, reversed {{decision_id, select, null {Analisamos novamente uma decisão sobre sua conta e a revertemos.} other {Analisamos novamente a decisão {decision_id} e a revertemos.}} Pedimos desculpas pelo transtorno.} other {{decision_id, select, null {Removemos a restrição da sua conta.} other {Removemos a restrição da decisão {decision_id}.}}}}{restores_access, select, yes { Você já pode entrar e usar o {product_name} novamente.} other {}}{statement, select, null {} other {\n\nNota da nossa equipe: {statement}}}{content_removed, select, yes {\n\nO conteúdo que removemos não pode ser restaurado.} other {}}\n\nEquipe do {product_name}"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {Uma atualização sobre sua conta do {product_name}} other {{kind_label, select, warning {Um aviso sobre sua conta do {product_name}} suspension {Sua conta do {product_name} está suspensa} ban {Sua conta do {product_name} está banida} closure {Sua conta do {product_name} foi encerrada} feature_limit {Alguns recursos da sua conta do {product_name} estão limitados} content_removal {Removemos mensagens que você enviou no {product_name}} profile_reset {Redefinimos partes do seu perfil do {product_name}} community_removal {Removemos sua comunidade no {product_name}} bot {Uma atualização sobre seu bot no {product_name}} other {Uma atualização sobre sua conta do {product_name}}}}}",
|
||||
"body": "Olá, {username},\n\n{summary}\n\n{effects}\n\nPor quê\nRegra: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nNota da nossa equipe: {statement}}}\n\nComo decidimos\n{how_we_decided}{appeals_email, select, null {} other {\n\nComo recorrer\nSe você acha que erramos, envie um e-mail para {appeals_email} até {appeal_deadline}. Se puder, escreva do endereço de e-mail da sua conta e informe o ID da decisão. O recurso é gratuito, e uma pessoa analisa cada pedido.{redress, select, null {} other {\n\n{redress}}}}}\n\nID da decisão: {decision_id}\n\nEquipe do {product_name}"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Os benefícios do presente que você resgatou foram removidos",
|
||||
"body": "Olá, {username},\n\nUm código de presente que você resgatou foi originalmente pago por outra pessoa. Esse pagamento foi revertido (um estorno).\n\nPor causa disso, removemos os benefícios que foram adicionados à sua conta quando você resgatou o presente.\n\n{support_email, select, null {Se você acha que isso é um erro, entre em contato com os administradores desta instância e inclua quaisquer detalhes que você tenha sobre o código do presente e quando você o resgatou.} other {Se você acha que isso é um erro, entre em contato com nossa equipe de suporte e inclua quaisquer detalhes que você tenha sobre o código do presente e quando você o resgatou.}}\n\n– Equipe do {product_name}"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "Sua denúncia no {product_name} foi analisada",
|
||||
"body": "Olá, {username},\n\nSua denúncia (ID: {reportId}) foi analisada por nossa equipe de segurança.{hasComment, select, yes {\n\nResposta da equipe de segurança:\n{publicComment}} other {}}\n\nAgradecemos por ajudar a manter o {product_name} seguro para todos. Levamos todas as denúncias a sério e agradecemos sua contribuição para a comunidade.\n\n{safety_email, select, null {Se você tiver alguma dúvida ou preocupação sobre este resultado, entre em contato com os administradores desta instância.} other {Se você tiver alguma dúvida ou preocupação sobre este resultado, entre em contato com {safety_email}.}}\n\n– equipe de segurança do {product_name}"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"subject": "Sua conta do {product_name} será excluída permanentemente",
|
||||
"body": "Olá, {username},\n\nA exclusão permanente da sua conta do {product_name} foi agendada.\n\nExclusão agendada: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Motivo: {reason}\n\n}\n}Esta é uma medida séria. Os dados da sua conta serão excluídos permanentemente na data agendada.\n\nSe você acredita que esta decisão foi incorreta, você pode enviar um recurso. {appeals_email, select, null {Entre em contato com os administradores desta instância.} other {Envie um e-mail para {appeals_email} a partir deste endereço de e-mail.}}\n\n– equipe de segurança do {product_name}"
|
||||
"body": "Olá, {username},\n\nAnalisamos sua denúncia (ID: {reportId}).\n\n{outcome, select, removed {Removemos o conteúdo que você denunciou porque ele violava nossas regras.} restricted {Constatamos que a conta que você denunciou violava nossas regras e aplicamos uma restrição a ela. Não informamos que medida tomamos contra outra conta, porque isso é um dado pessoal do titular.} action_taken {Constatamos que o que você denunciou violava nossas regras e tomamos uma medida. Não informamos que medida tomamos contra outra conta, porque isso é um dado pessoal do titular.} no_violation {Constatamos que o conteúdo ou a conta que você denunciou não viola nossas regras, então não fizemos nenhuma alteração.} other {Encerramos sua denúncia.}}{hasComment, select, yes {\n\nNota da nossa equipe: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nSe você discordar, envie um e-mail para {appeals_email} em até 60 dias informando o ID da denúncia. Se você estiver na UE, também pode submeter a questão a um organismo de resolução extrajudicial de litígios certificado ou apresentar uma reclamação ao coordenador dos serviços digitais do seu país.}}\n\nEquipe do {product_name}"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "A exclusão da sua conta do {product_name} está agendada",
|
||||
"body": "Olá, {username},\n\nVocê solicitou a exclusão da sua conta do {product_name}. Sua conta está agendada para exclusão permanente em:\n\n{deletionDate, date, full} às {deletionDate, time, short}\n\nSe você não solicitou isso, entre em sua conta para cancelar a exclusão. Também recomendamos alterar sua senha para proteger sua conta.\n\n– Equipe do {product_name}"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "A suspensão da sua conta do {product_name} foi encerrada",
|
||||
"body": "Olá, {username},\n\nBoas notícias: a suspensão da sua conta do {product_name} foi encerrada.\n\n{reason, select,\n null {}\n other {Motivo: {reason}\n\n}\n}Agora você pode entrar novamente e continuar usando o {product_name} normalmente.\n\n– equipe de segurança do {product_name}"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"subject": "Contul tău {product_name} va fi șters din cauza inactivității",
|
||||
"body": "Salut {username},\n\nContul tău {product_name} a fost inactiv o perioadă lungă, așa că este programat pentru ștergere permanentă la:\n\n{deletionDate, date, full} la ora {deletionDate, time, short}{reason, select, null {} other {\n\nMotiv: {reason}}}\n\n{safety_email, select, null {Dacă vrei să îți păstrezi contul, contactează administratorii acestei instanțe înainte de această dată.} other {Dacă vrei să îți păstrezi contul, scrie la {safety_email} de pe această adresă de e-mail înainte de această dată.}}\n\n– Echipa {product_name}"
|
||||
"body": "Salut {username},\n\nContul tău {product_name} a fost inactiv o perioadă lungă, așa că am programat ștergerea lui pentru {deletion_date}.\n\nCa să îți păstrezi contul, conectează-te înainte de această dată. {support_email, select, null {Dacă ai întrebări, contactează administratorii acestei instanțe.} other {Dacă ai întrebări, contactează {support_email}.}}\n\nEchipa {product_name}"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"subject": "Ștergerea contului {product_name} a fost programată",
|
||||
"body": "Salut {username},\n\nConform solicitării tale, contul tău {product_name} este programat pentru ștergere permanentă la:\n\n{deletionDate, date, full} la ora {deletionDate, time, short}{reason, select, null {} other {\n\nMotiv: {reason}}}\n\nPână atunci, contul tău este blocat. {safety_email, select, null {Dacă nu ai solicitat acest lucru sau vrei să îți păstrezi contul, contactează administratorii acestei instanțe înainte de această dată.} other {Dacă nu ai solicitat acest lucru sau vrei să îți păstrezi contul, scrie la {safety_email} de pe această adresă de e-mail înainte de această dată.}}\n\n– Echipa {product_name}"
|
||||
},
|
||||
"account_scheduled_deletion": {
|
||||
"subject": "Contul tău {product_name} va fi șters permanent",
|
||||
"body": "Salut {username},\n\nContul tău {product_name} a fost programat pentru ștergere permanentă din cauza încălcării Termenilor și condițiilor sau a regulilor comunității.\n\nȘtergere programată: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Motiv: {reason}\n\n}\n}Aceasta este o măsură disciplinară serioasă. Datele contului tău vor fi șterse permanent la data programată.\n\n{legalLinks, select,\n both {Te rugăm să consulți:\n- Termeni și condiții: {termsUrl}\n- Regulile comunității: {guidelinesUrl}\n\n}\n terms {Te rugăm să consulți:\n- Termeni și condiții: {termsUrl}\n\n}\n guidelines {Te rugăm să consulți:\n- Regulile comunității: {guidelinesUrl}\n\n}\n other {}\n}Procesul de contestare:\nDacă crezi că această decizie a fost incorectă sau nejustificată, ai la dispoziție 60 de zile pentru a depune o contestație. {appeals_email, select, null {Contactează administratorii acestei instanțe.} other {Trimite un e-mail la {appeals_email} de la această adresă.}}\n\nÎn contestația ta:\n- Explică clar de ce crezi că decizia a fost incorectă sau nejustificată\n- Adaugă orice dovadă sau context relevant\n\nUn membru al echipei de siguranță {product_name} va analiza contestația ta și poate suspenda ștergerea programată până la luarea unei decizii finale.\n\n– Echipa de siguranță {product_name}"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "Contul tău {product_name} a fost suspendat temporar",
|
||||
"body": "Salut {username},\n\nContul tău {product_name} a fost suspendat temporar pentru încălcarea Termenilor și condițiilor sau a regulilor comunității.\n\nDurată: {durationHours, plural,\n one {1 oră}\n few {# ore}\n other {# de ore}\n}\nSuspendat până la: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Motiv: {reason}\n\n}\n}În acest timp, nu vei putea accesa contul.\n\n{legalLinks, select,\n both {Te rugăm să consulți:\n- Termeni și condiții: {termsUrl}\n- Regulile comunității: {guidelinesUrl}\n\n}\n terms {Te rugăm să consulți:\n- Termeni și condiții: {termsUrl}\n\n}\n guidelines {Te rugăm să consulți:\n- Regulile comunității: {guidelinesUrl}\n\n}\n other {}\n}Dacă crezi că această decizie a fost incorectă sau nejustificată, poți depune o contestație. {appeals_email, select, null {Contactează administratorii acestei instanțe și explică clar de ce crezi că decizia a fost incorectă.} other {Trimite un e-mail la {appeals_email} de la această adresă și explică clar de ce crezi că decizia a fost incorectă.}} Vom analiza contestația ta și îți vom comunica decizia noastră.\n\n– Echipa de siguranță {product_name}"
|
||||
"body": "Salut {username},\n\nAșa cum ai cerut, am programat ștergerea contului tău {product_name} pentru {deletion_date}.\n\nCa să îți păstrezi contul, conectează-te înainte de {deletion_date}. {safety_email, select, null {Dacă nu ai cerut acest lucru, contactează administratorii acestei instanțe.} other {Dacă nu ai cerut acest lucru, contactează {safety_email}.}}\n\nEchipa {product_name}"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Îți mulțumim pentru donația către {product_name}",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "Am luat o decizie cu privire la raportul tău de pe {product_name}",
|
||||
"body": "Salut,\n\nEchipa noastră de siguranță a analizat raportul tău conform Digital Services Act (ID: {reportId}) și a luat o decizie.{hasComment, select, yes {\n\nRăspuns de la echipa de siguranță:\n{publicComment}} other {}}\n\nNu oferim detalii despre măsurile luate împotriva contului altei persoane, deoarece acestea sunt datele sale personale.\n\nDacă nu ești de acord cu această decizie, poți depune gratuit o contestație în termen de 60 de zile. {appeals_email, select, null {Contactează administratorii acestei instanțe, include ID-ul raportului tău și explică de ce crezi că decizia este incorectă.} other {Trimite un e-mail la {appeals_email} de la această adresă, include ID-ul raportului tău și explică de ce crezi că decizia este incorectă.}} Dacă te afli în UE, poți, de asemenea, să înaintezi litigiul unui organism certificat de soluționare extrajudiciară a litigiilor. Nimic din toate acestea nu îți afectează dreptul de a te adresa instanței.\n\n– Echipa de siguranță {product_name}"
|
||||
"body": "Salut,\n\nAm analizat raportul tău (ID: {reportId}).\n\n{outcome, select, removed {Am eliminat conținutul raportat de tine, deoarece încălca regulile noastre.} restricted {Am constatat că contul raportat de tine a încălcat regulile noastre și l-am restricționat. Nu dezvăluim ce măsuri am luat împotriva altui cont, deoarece acestea sunt date personale ale titularului său.} action_taken {Am constatat că ceea ce ai raportat a încălcat regulile noastre și am luat măsuri. Nu dezvăluim ce măsuri am luat împotriva altui cont, deoarece acestea sunt date personale ale titularului său.} no_violation {Am constatat că conținutul sau contul raportat de tine nu încalcă regulile noastre, așa că nu am schimbat nimic.} other {Am închis raportul tău.}}{hasComment, select, yes {\n\nNotă de la echipa noastră: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nDacă nu ești de acord, scrie la {appeals_email} în termen de 60 de zile și include ID-ul raportului. Dacă te afli în UE, poți să te adresezi și unui organism certificat de soluționare extrajudiciară a litigiilor sau să depui o plângere la coordonatorul serviciilor digitale din țara ta.}}\n\nEchipa {product_name}"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Verifică-ți e-mailul pentru un raport DSA",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Verifică-ți adresa de e-mail pentru {product_name}",
|
||||
"body": "Salut {username},\n\nTe rugăm să verifici adresa de e-mail a contului tău {product_name} apăsând pe linkul de mai jos:\n\n{verifyUrl}\n\nDacă nu ai creat un cont {product_name}, poți ignora în siguranță acest e-mail.\n\nAcest link este valabil 24 de ore.\n\n– Echipa {product_name}"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {Am anulat o decizie privind contul tău {product_name}} other {O restricție asupra contului tău {product_name} s-a încheiat}}",
|
||||
"body": "Salut {username},\n\n{outcome, select, reversed {{decision_id, select, null {Am reanalizat o decizie privind contul tău și am anulat-o.} other {Am reanalizat decizia {decision_id} și am anulat-o.}} Ne pare rău pentru neplăcerile create.} other {{decision_id, select, null {Am ridicat restricția asupra contului tău.} other {Am ridicat restricția din decizia {decision_id}.}}}}{restores_access, select, yes { Te poți conecta din nou și poți folosi {product_name}.} other {}}{statement, select, null {} other {\n\nNotă de la echipa noastră: {statement}}}{content_removed, select, yes {\n\nConținutul pe care l-am eliminat nu poate fi restaurat.} other {}}\n\nEchipa {product_name}"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {O actualizare despre contul tău {product_name}} other {{kind_label, select, warning {Un avertisment privind contul tău {product_name}} suspension {Contul tău {product_name} este suspendat} ban {Contul tău {product_name} este suspendat permanent} closure {Contul tău {product_name} este închis} feature_limit {Unele funcții ale contului tău {product_name} sunt limitate} content_removal {Am eliminat mesaje pe care le-ai trimis pe {product_name}} profile_reset {Am resetat unele părți ale profilului tău {product_name}} community_removal {Am eliminat comunitatea ta de pe {product_name}} bot {O actualizare despre botul tău de pe {product_name}} other {O actualizare despre contul tău {product_name}}}}}",
|
||||
"body": "Salut {username},\n\n{summary}\n\n{effects}\n\nDe ce\nRegulă: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nNotă de la echipa noastră: {statement}}}\n\nCum am decis\n{how_we_decided}{appeals_email, select, null {} other {\n\nCum poți contesta decizia\nDacă crezi că am greșit, scrie la {appeals_email} până la {appeal_deadline}. Dacă poți, scrie de pe adresa de e-mail asociată contului și include ID-ul deciziei. Contestațiile sunt gratuite, iar fiecare este analizată de o persoană.{redress, select, null {} other {\n\n{redress}}}}}\n\nID decizie: {decision_id}\n\nEchipa {product_name}"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Beneficiile din cadoul pe care l-ai valorificat au fost eliminate",
|
||||
"body": "Salut {username},\n\nUn cod cadou pe care l-ai valorificat a fost plătit inițial de altcineva. De atunci, plata respectivă a fost retrasă (o anulare a plății).\n\nDin această cauză, am eliminat beneficiile care au fost adăugate contului tău atunci când ai valorificat cadoul.\n\n{support_email, select, null {Dacă crezi că este o greșeală, te rugăm să contactezi administratorii acestei instanțe și să incluzi orice detalii ai despre codul cadou și despre momentul în care l-ai valorificat.} other {Dacă crezi că este o greșeală, te rugăm să contactezi asistența și să incluzi orice detalii ai despre codul cadou și despre momentul în care l-ai valorificat.}}\n\n– Echipa {product_name}"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "Raportul tău de pe {product_name} a fost analizat",
|
||||
"body": "Salut {username},\n\nRaportul tău (ID: {reportId}) a fost analizat de echipa noastră de siguranță.{hasComment, select, yes {\n\nRăspuns de la echipa de siguranță:\n{publicComment}} other {}}\n\nÎți mulțumim că ne ajuți să păstrăm {product_name} în siguranță pentru toată lumea. Luăm în serios toate rapoartele și apreciem contribuția ta la comunitate.\n\n{safety_email, select, null {Dacă ai întrebări sau nelămuriri cu privire la acest rezultat, te rugăm să contactezi administratorii acestei instanțe.} other {Dacă ai întrebări sau nelămuriri cu privire la acest rezultat, te rugăm să scrii la {safety_email}.}}\n\n– Echipa de siguranță {product_name}"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"subject": "Contul tău {product_name} va fi șters permanent",
|
||||
"body": "Salut {username},\n\nContul tău {product_name} a fost programat pentru ștergere permanentă.\n\nȘtergere programată: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Motiv: {reason}\n\n}\n}Aceasta este o măsură disciplinară serioasă. Datele contului tău vor fi șterse permanent la data programată.\n\nDacă crezi că această decizie a fost incorectă, poți depune o contestație. {appeals_email, select, null {Contactează administratorii acestei instanțe.} other {Trimite un e-mail la {appeals_email} de la această adresă.}}\n\n– Echipa de siguranță {product_name}"
|
||||
"body": "Salut {username},\n\nAm analizat raportul tău (ID: {reportId}).\n\n{outcome, select, removed {Am eliminat conținutul raportat de tine, deoarece încălca regulile noastre.} restricted {Am constatat că contul raportat de tine a încălcat regulile noastre și l-am restricționat. Nu dezvăluim ce măsuri am luat împotriva altui cont, deoarece acestea sunt date personale ale titularului său.} action_taken {Am constatat că ceea ce ai raportat a încălcat regulile noastre și am luat măsuri. Nu dezvăluim ce măsuri am luat împotriva altui cont, deoarece acestea sunt date personale ale titularului său.} no_violation {Am constatat că conținutul sau contul raportat de tine nu încalcă regulile noastre, așa că nu am schimbat nimic.} other {Am închis raportul tău.}}{hasComment, select, yes {\n\nNotă de la echipa noastră: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nDacă nu ești de acord, scrie la {appeals_email} în termen de 60 de zile și include ID-ul raportului. Dacă te afli în UE, poți să te adresezi și unui organism certificat de soluționare extrajudiciară a litigiilor sau să depui o plângere la coordonatorul serviciilor digitale din țara ta.}}\n\nEchipa {product_name}"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "Ștergerea contului {product_name} a fost programată",
|
||||
"body": "Salut {username},\n\nAi solicitat ștergerea contului tău {product_name}. Contul tău este programat pentru ștergere permanentă la:\n\n{deletionDate, date, full} la ora {deletionDate, time, short}\n\nDacă nu ai solicitat acest lucru, conectează-te la contul tău pentru a anula ștergerea. De asemenea, îți recomandăm să-ți schimbi parola pentru a-ți securiza contul.\n\n– Echipa {product_name}"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "Suspendarea contului {product_name} a fost ridicată",
|
||||
"body": "Salut {username},\n\nVești bune: suspendarea contului tău {product_name} a fost ridicată.\n\n{reason, select,\n null {}\n other {Motiv: {reason}\n\n}\n}Acum te poți conecta din nou și poți continua să folosești {product_name} în mod normal.\n\n– Echipa de siguranță {product_name}"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"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, select, null {Если хочешь сохранить аккаунт, свяжись с администраторами этого экземпляра до этой даты.} other {Если хочешь сохранить аккаунт, напиши на {safety_email} с этого адреса электронной почты до этой даты.}}\n\n– Команда {product_name}"
|
||||
"body": "Привет, {username},\n\nТвой аккаунт {product_name} долгое время был неактивен, поэтому мы запланировали его удаление на {deletion_date}.\n\nЧтобы сохранить аккаунт, войди в него до этой даты. {support_email, select, null {Если у тебя есть вопросы, свяжись с администраторами этого экземпляра.} other {Если у тебя есть вопросы, напиши на {support_email}.}}\n\nКоманда {product_name}"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"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, select, null {Если ты не запрашивал удаление или хочешь сохранить аккаунт, свяжись с администраторами этого экземпляра до этой даты.} other {Если ты не запрашивал удаление или хочешь сохранить аккаунт, напиши на {safety_email} с этого адреса электронной почты до этой даты.}}\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{legalLinks, select,\n both {Пожалуйста, ознакомься:\n- Условия использования: {termsUrl}\n- Правила сообщества: {guidelinesUrl}\n\n}\n terms {Пожалуйста, ознакомься:\n- Условия использования: {termsUrl}\n\n}\n guidelines {Пожалуйста, ознакомься:\n- Правила сообщества: {guidelinesUrl}\n\n}\n other {}\n}Процесс апелляции:\nЕсли ты считаешь, что это решение было неверным или необоснованным, у тебя есть 60 дней, чтобы подать апелляцию. {appeals_email, select, null {Свяжись с администраторами этого экземпляра.} other {Отправь письмо на {appeals_email} с этого адреса электронной почты.}}\n\nВ своей апелляции:\n- Чётко объясни, почему ты считаешь, что решение было неверным или необоснованным\n- Предоставь любые соответствующие доказательства или контекст\n\nСотрудник команды безопасности {product_name} рассмотрит твою апелляцию и может приостановить запланированное удаление до принятия окончательного решения.\n\n– Команда безопасности {product_name}"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "Твой аккаунт {product_name} временно заблокирован",
|
||||
"body": "Привет, {username},\n\nТвой аккаунт {product_name} временно заблокирован за нарушение наших Условий использования или Правил сообщества.\n\nДлительность: {durationHours, plural,\n one {# час}\n few {# часа}\n many {# часов}\n other {# часа}\n}\nЗаблокирован до: {bannedUntil, date, full} в {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Причина: {reason}\n\n}\n}В течение этого времени ты не сможешь получить доступ к своему аккаунту.\n\n{legalLinks, select,\n both {Пожалуйста, ознакомься:\n- Условия использования: {termsUrl}\n- Правила сообщества: {guidelinesUrl}\n\n}\n terms {Пожалуйста, ознакомься:\n- Условия использования: {termsUrl}\n\n}\n guidelines {Пожалуйста, ознакомься:\n- Правила сообщества: {guidelinesUrl}\n\n}\n other {}\n}Если ты считаешь, что это решение было неверным или необоснованным, ты можешь подать апелляцию. {appeals_email, select, null {Свяжись с администраторами этого экземпляра и чётко объясни, почему ты считаешь решение неверным.} other {Отправь письмо на {appeals_email} с этого адреса электронной почты и чётко объясни, почему ты считаешь решение неверным.}} Мы рассмотрим твою апелляцию и сообщим о нашем решении.\n\n– Команда безопасности {product_name}"
|
||||
"body": "Привет, {username},\n\nПо твоей просьбе мы запланировали удаление твоего аккаунта {product_name} на {deletion_date}.\n\nЧтобы сохранить аккаунт, войди в него до {deletion_date}. {safety_email, select, null {Если запрос был не от тебя, свяжись с администраторами этого экземпляра.} other {Если запрос был не от тебя, напиши на {safety_email}.}}\n\nКоманда {product_name}"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Спасибо за твоё пожертвование для {product_name}",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "Мы приняли решение по твоей жалобе в {product_name}",
|
||||
"body": "Привет,\n\nНаша команда безопасности рассмотрела твою жалобу по Закону о цифровых услугах (ID: {reportId}) и приняла решение.{hasComment, select, yes {\n\nОтвет команды безопасности:\n{publicComment}} other {}}\n\nМы не раскрываем подробности мер, принятых в отношении аккаунта другого человека, так как это его персональные данные.\n\nЕсли ты не согласен с этим решением, ты можешь бесплатно подать апелляцию в течение 60 дней. {appeals_email, select, null {Свяжись с администраторами этого экземпляра, укажи ID жалобы и объясни, почему считаешь решение неверным.} other {Отправь письмо на {appeals_email} с этого адреса электронной почты, укажи ID жалобы и объясни, почему считаешь решение неверным.}} Если ты находишься в ЕС, ты также можешь передать спор в сертифицированный орган по внесудебному урегулированию споров. Всё это не влияет на твоё право обратиться в суд.\n\n– Команда безопасности {product_name}"
|
||||
"body": "Привет!\n\nМы рассмотрели твою жалобу (ID: {reportId}).\n\n{outcome, select, removed {Мы удалили контент, указанный в твоей жалобе, потому что он нарушал наши правила.} restricted {Мы установили, что аккаунт, указанный в твоей жалобе, нарушил наши правила, и ограничили его. Мы не сообщаем, какие меры приняты в отношении другого аккаунта, потому что это персональные данные его владельца.} action_taken {Мы установили, что указанное в твоей жалобе нарушило наши правила, и приняли меры. Мы не сообщаем, какие меры приняты в отношении другого аккаунта, потому что это персональные данные его владельца.} no_violation {Мы установили, что контент или аккаунт, указанный в твоей жалобе, не нарушает наши правила, поэтому оставили всё как есть.} other {Мы закрыли твою жалобу.}}{hasComment, select, yes {\n\nКомментарий нашей команды: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nЕсли решение кажется тебе неверным, напиши на {appeals_email} в течение 60 дней и укажи ID жалобы. Если ты находишься в ЕС, ты также можешь передать спор в сертифицированный орган внесудебного разрешения споров или подать жалобу координатору цифровых услуг в своей стране.}}\n\nКоманда {product_name}"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Подтверди свой адрес электронной почты для жалобы по DSA",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Подтверди адрес электронной почты для {product_name}",
|
||||
"body": "Привет, {username},\n\nПожалуйста, подтверди адрес электронной почты своего аккаунта {product_name}, нажав на ссылку ниже:\n\n{verifyUrl}\n\nЕсли ты не создавал аккаунт {product_name}, можешь смело проигнорировать это письмо.\n\nЭта ссылка действительна в течение 24 часов.\n\n– Команда {product_name}"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {Мы отменили решение по твоему аккаунту {product_name}} other {Ограничение твоего аккаунта {product_name} больше не действует}}",
|
||||
"body": "Привет, {username},\n\n{outcome, select, reversed {{decision_id, select, null {Мы повторно рассмотрели решение по твоему аккаунту и отменили его.} other {Мы повторно рассмотрели решение {decision_id} и отменили его.}} Приносим извинения за неудобства.} other {{decision_id, select, null {Мы сняли ограничение с твоего аккаунта.} other {Мы сняли ограничение, введённое решением {decision_id}.}}}}{restores_access, select, yes { Ты снова можешь входить в аккаунт и пользоваться {product_name}.} other {}}{statement, select, null {} other {\n\nКомментарий нашей команды: {statement}}}{content_removed, select, yes {\n\nУдалённый контент восстановить нельзя.} other {}}\n\nКоманда {product_name}"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {Информация о твоём аккаунте {product_name}} other {{kind_label, select, warning {Предупреждение по твоему аккаунту {product_name}} suspension {Твой аккаунт {product_name} временно заблокирован} ban {Твой аккаунт {product_name} заблокирован} closure {Твой аккаунт {product_name} закрыт} feature_limit {Некоторые функции твоего аккаунта {product_name} ограничены} content_removal {Мы удалили твои сообщения в {product_name}} profile_reset {Мы сбросили часть данных твоего профиля {product_name}} community_removal {Мы удалили твоё сообщество в {product_name}} bot {Информация о твоём боте в {product_name}} other {Информация о твоём аккаунте {product_name}}}}}",
|
||||
"body": "Привет, {username},\n\n{summary}\n\n{effects}\n\nПочему\nПравило: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nКомментарий нашей команды: {statement}}}\n\nКак мы приняли решение\n{how_we_decided}{appeals_email, select, null {} other {\n\nКак обжаловать решение\nЕсли ты считаешь, что мы ошиблись, напиши на {appeals_email} до {appeal_deadline}. По возможности пиши с адреса электронной почты, привязанного к твоему аккаунту, и укажи ID решения. Апелляции бесплатны, и каждую из них рассматривает человек.{redress, select, null {} other {\n\n{redress}}}}}\n\nID решения: {decision_id}\n\nКоманда {product_name}"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Привилегии за активированный подарок были удалены",
|
||||
"body": "Привет, {username},\n\nПодарочный код, который ты активировал, изначально был оплачен другим человеком. Этот платёж был впоследствии отменён (возвратный платёж).\n\nИз-за этого мы удалили привилегии, которые были добавлены в твой аккаунт при активации подарка.\n\n{support_email, select, null {Если ты считаешь, что это ошибка, пожалуйста, свяжись с администраторами этого экземпляра и укажи все имеющиеся детали о подарочном коде и дате его активации.} other {Если ты считаешь, что это ошибка, пожалуйста, свяжись с нашей службой поддержки и укажи все имеющиеся детали о подарочном коде и дате его активации.}}\n\n– Команда {product_name}"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "Твоя жалоба в {product_name} рассмотрена",
|
||||
"body": "Привет, {username},\n\nТвоя жалоба (ID: {reportId}) была рассмотрена нашей командой безопасности.{hasComment, select, yes {\n\nОтвет команды безопасности:\n{publicComment}} other {}}\n\nСпасибо за помощь в обеспечении безопасности {product_name} для всех. Мы серьёзно относимся ко всем жалобам и ценим твой вклад в сообщество.\n\n{safety_email, select, null {Если у тебя есть вопросы или сомнения по поводу этого результата, пожалуйста, свяжись с администраторами этого экземпляра.} other {Если у тебя есть вопросы или сомнения по поводу этого результата, пожалуйста, напиши на {safety_email}.}}\n\n– Команда безопасности {product_name}"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"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Если ты считаешь, что это решение было неверным, ты можешь подать апелляцию. {appeals_email, select, null {Свяжись с администраторами этого экземпляра.} other {Отправь письмо на {appeals_email} с этого адреса электронной почты.}}\n\n– Команда безопасности {product_name}"
|
||||
"body": "Привет, {username},\n\nМы рассмотрели твою жалобу (ID: {reportId}).\n\n{outcome, select, removed {Мы удалили контент, указанный в твоей жалобе, потому что он нарушал наши правила.} restricted {Мы установили, что аккаунт, указанный в твоей жалобе, нарушил наши правила, и ограничили его. Мы не сообщаем, какие меры приняты в отношении другого аккаунта, потому что это персональные данные его владельца.} action_taken {Мы установили, что указанное в твоей жалобе нарушило наши правила, и приняли меры. Мы не сообщаем, какие меры приняты в отношении другого аккаунта, потому что это персональные данные его владельца.} no_violation {Мы установили, что контент или аккаунт, указанный в твоей жалобе, не нарушает наши правила, поэтому оставили всё как есть.} other {Мы закрыли твою жалобу.}}{hasComment, select, yes {\n\nКомментарий нашей команды: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nЕсли решение кажется тебе неверным, напиши на {appeals_email} в течение 60 дней и укажи ID жалобы. Если ты находишься в ЕС, ты также можешь передать спор в сертифицированный орган внесудебного разрешения споров или подать жалобу координатору цифровых услуг в своей стране.}}\n\nКоманда {product_name}"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "Запланировано удаление твоего аккаунта {product_name}",
|
||||
"body": "Привет, {username},\n\nТы запросил удаление своего аккаунта {product_name}. Безвозвратное удаление запланировано на:\n\n{deletionDate, date, full} в {deletionDate, time, short}\n\nЕсли ты не запрашивал это, войди в свой аккаунт, чтобы отменить удаление. Также рекомендуем сменить пароль, чтобы защитить свой аккаунт.\n\n– Команда {product_name}"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "Блокировка твоего аккаунта {product_name} снята",
|
||||
"body": "Привет, {username},\n\nХорошие новости: блокировка твоего аккаунта {product_name} снята.\n\n{reason, select,\n null {}\n other {Причина: {reason}\n\n}\n}Теперь ты можешь снова войти в аккаунт и продолжить пользоваться {product_name} как обычно.\n\n– Команда безопасности {product_name}"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"subject": "Ditt {product_name}-konto kommer att raderas på grund av inaktivitet",
|
||||
"body": "Hej {username},\n\nDitt {product_name}-konto har varit inaktivt länge, så permanent radering av kontot är planerad till följande tidpunkt:\n\n{deletionDate, date, full} kl. {deletionDate, time, short}{reason, select, null {} other {\n\nAnledning: {reason}}}\n\n{safety_email, select, null {Om du vill behålla ditt konto, kontakta administratörerna för den här instansen före det datumet.} other {Om du vill behålla ditt konto, kontakta {safety_email} från den här e-postadressen före det datumet.}}\n\n– {product_name}-teamet"
|
||||
"body": "Hej {username},\n\nDitt {product_name}-konto har varit inaktivt länge, så vi har schemalagt radering av kontot den {deletion_date}.\n\nLogga in före det datumet om du vill behålla ditt konto. {support_email, select, null {Om du har frågor, kontakta administratörerna för den här instansen.} other {Om du har frågor, kontakta {support_email}.}}\n\n{product_name}-teamet"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"subject": "Raderingen av ditt {product_name}-konto är schemalagd",
|
||||
"body": "Hej {username},\n\nSom du har begärt är permanent radering av ditt {product_name}-konto planerad till följande tidpunkt:\n\n{deletionDate, date, full} kl. {deletionDate, time, short}{reason, select, null {} other {\n\nAnledning: {reason}}}\n\nDitt konto är låst fram till dess. {safety_email, select, null {Om du inte begärde detta, eller om du vill behålla ditt konto, kontakta administratörerna för den här instansen före det datumet.} other {Om du inte begärde detta, eller om du vill behålla ditt konto, kontakta {safety_email} från den här e-postadressen före det datumet.}}\n\n– {product_name}-teamet"
|
||||
},
|
||||
"account_scheduled_deletion": {
|
||||
"subject": "Ditt {product_name}-konto kommer att raderas permanent",
|
||||
"body": "Hej {username},\n\nDitt {product_name}-konto har schemalagts för permanent radering på grund av brott mot våra användarvillkor eller communityriktlinjer.\n\nSchemalagd radering: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Anledning: {reason}\n\n}\n}Detta är en allvarlig åtgärd. Dina kontodata kommer att raderas permanent på det schemalagda datumet.\n\n{legalLinks, select,\n both {Granska:\n- Användarvillkor: {termsUrl}\n- Communityriktlinjer: {guidelinesUrl}\n\n}\n terms {Granska:\n- Användarvillkor: {termsUrl}\n\n}\n guidelines {Granska:\n- Communityriktlinjer: {guidelinesUrl}\n\n}\n other {}\n}Överklagandeprocess:\nOm du anser att detta beslut var felaktigt eller obefogat har du 60 dagar på dig att överklaga. {appeals_email, select, null {Kontakta administratörerna för den här instansen.} other {Mejla {appeals_email} från den här e-postadressen.}}\n\nI ditt överklagande:\n- Förklara tydligt varför du anser att beslutet var felaktigt eller obefogat\n- Bifoga relevanta bevis eller bakgrundsinformation\n\nEn medarbetare i säkerhetsteamet på {product_name} kommer att granska ditt överklagande och kan pausa den planerade raderingen tills ett slutgiltigt beslut har fattats.\n\n– Säkerhetsteamet på {product_name}"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "Ditt {product_name}-konto har tillfälligt stängts av",
|
||||
"body": "Hej {username},\n\nDitt {product_name}-konto har tillfälligt stängts av på grund av brott mot våra användarvillkor eller communityriktlinjer.\n\nVaraktighet: {durationHours, plural,\n =1 {1 timme}\n other {# timmar}\n}\nAvstängningen upphör: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Anledning: {reason}\n\n}\n}Under denna tid kommer du inte att kunna komma åt ditt konto.\n\n{legalLinks, select,\n both {Granska:\n- Användarvillkor: {termsUrl}\n- Communityriktlinjer: {guidelinesUrl}\n\n}\n terms {Granska:\n- Användarvillkor: {termsUrl}\n\n}\n guidelines {Granska:\n- Communityriktlinjer: {guidelinesUrl}\n\n}\n other {}\n}Om du anser att detta beslut var felaktigt eller obefogat kan du överklaga. {appeals_email, select, null {Kontakta administratörerna för den här instansen och förklara tydligt varför du anser att beslutet var felaktigt.} other {Kontakta vårt supportteam på {appeals_email} från den här e-postadressen och förklara tydligt varför du anser att beslutet var felaktigt.}} Vi kommer att granska ditt överklagande och svara med vårt beslut.\n\n– Säkerhetsteamet på {product_name}"
|
||||
"body": "Hej {username},\n\nSom du begärde har vi schemalagt radering av ditt {product_name}-konto den {deletion_date}.\n\nLogga in före den {deletion_date} om du vill behålla ditt konto. {safety_email, select, null {Om du inte har begärt detta, kontakta administratörerna för den här instansen.} other {Om du inte har begärt detta, kontakta {safety_email}.}}\n\n{product_name}-teamet"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Tack för din donation till {product_name}",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "Vi har fattat ett beslut om din anmälan på {product_name}",
|
||||
"body": "Hej,\n\nVårt säkerhetsteam har granskat din anmälan enligt förordningen om digitala tjänster (ID: {reportId}) och fattat ett beslut.{hasComment, select, yes {\n\nSvar från säkerhetsteamet:\n{publicComment}} other {}}\n\nVi delar inte uppgifter om åtgärder mot någon annans konto, eftersom det är den personens personuppgifter.\n\nOm du inte håller med om beslutet kan du överklaga kostnadsfritt inom 60 dagar. {appeals_email, select, null {Kontakta administratörerna för den här instansen, ange ditt anmälnings-ID och förklara varför du anser att beslutet är felaktigt.} other {Mejla {appeals_email} från den här e-postadressen, ange ditt anmälnings-ID och förklara varför du anser att beslutet är felaktigt.}} Om du befinner dig i EU kan du också hänskjuta tvisten till ett certifierat organ för tvistlösning utanför domstol. Inget av detta påverkar din rätt att vända dig till domstol.\n\n– Säkerhetsteamet på {product_name}"
|
||||
"body": "Hej,\n\nVi har granskat din anmälan (ID: {reportId}).\n\n{outcome, select, removed {Vi har tagit bort innehållet som du anmälde eftersom det bröt mot våra regler.} restricted {Vi har kommit fram till att kontot som du anmälde bröt mot våra regler, och vi har begränsat det. Vi berättar inte vilken åtgärd vi har vidtagit mot ett annat konto, eftersom det är den personens personuppgifter.} action_taken {Vi har kommit fram till att det du anmälde bröt mot våra regler, och vi har vidtagit åtgärder. Vi berättar inte vilken åtgärd vi har vidtagit mot ett annat konto, eftersom det är den personens personuppgifter.} no_violation {Vi har kommit fram till att innehållet eller kontot som du anmälde inte bryter mot våra regler, så vi har inte gjort något.} other {Vi har avslutat din anmälan.}}{hasComment, select, yes {\n\nKommentar från vårt team: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nOm du inte håller med kan du mejla {appeals_email} inom 60 dagar och ange anmälnings-ID:t. Om du befinner dig i EU kan du också hänskjuta ärendet till ett certifierat organ för tvistlösning utanför domstol eller klaga hos samordnaren för digitala tjänster i ditt land.}}\n\n{product_name}-teamet"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Verifiera din e-postadress för en DSA-anmälan",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Verifiera din e-postadress på {product_name}",
|
||||
"body": "Hej {username},\n\nVerifiera e-postadressen för ditt {product_name}-konto genom att klicka på länken nedan:\n\n{verifyUrl}\n\nOm du inte skapade ett {product_name}-konto kan du tryggt ignorera det här mejlet.\n\nDen här länken är giltig i 24 timmar.\n\n– {product_name}-teamet"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {Vi har upphävt ett beslut om ditt {product_name}-konto} other {En begränsning på ditt {product_name}-konto har upphört}}",
|
||||
"body": "Hej {username},\n\n{outcome, select, reversed {{decision_id, select, null {Vi har tittat på ett beslut om ditt konto igen och upphävt det.} other {Vi har tittat på beslut {decision_id} igen och upphävt det.}} Vi ber om ursäkt för besväret.} other {{decision_id, select, null {Vi har tagit bort begränsningen på ditt konto.} other {Vi har tagit bort begränsningen från beslut {decision_id}.}}}}{restores_access, select, yes { Du kan logga in och använda {product_name} igen.} other {}}{statement, select, null {} other {\n\nKommentar från vårt team: {statement}}}{content_removed, select, yes {\n\nInnehåll som vi har tagit bort kan inte återställas.} other {}}\n\n{product_name}-teamet"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {Information om ditt {product_name}-konto} other {{kind_label, select, warning {En varning om ditt {product_name}-konto} suspension {Ditt {product_name}-konto är avstängt} ban {Ditt {product_name}-konto är bannlyst} closure {Ditt {product_name}-konto är stängt} feature_limit {Vissa funktioner är begränsade på ditt {product_name}-konto} content_removal {Vi har tagit bort meddelanden som du skickade på {product_name}} profile_reset {Vi har återställt delar av din {product_name}-profil} community_removal {Vi har tagit bort din community på {product_name}} bot {Information om din bot på {product_name}} other {Information om ditt {product_name}-konto}}}}",
|
||||
"body": "Hej {username},\n\n{summary}\n\n{effects}\n\nVarför\nRegel: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nKommentar från vårt team: {statement}}}\n\nSå fattade vi beslutet\n{how_we_decided}{appeals_email, select, null {} other {\n\nSå överklagar du\nOm du tror att vi har gjort fel kan du mejla {appeals_email} senast {appeal_deadline}. Skriv från e-postadressen på ditt konto om du kan, och ange besluts-ID:t. Det är gratis att överklaga, och en person granskar varje överklagande.{redress, select, null {} other {\n\n{redress}}}}}\n\nBesluts-ID: {decision_id}\n\n{product_name}-teamet"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Förmånerna från din inlösta present har tagits bort",
|
||||
"body": "Hej {username},\n\nEn presentkod du löste in betalades ursprungligen av någon annan. Den betalningen har sedan dess återförts genom en kortreklamation (chargeback).\n\nPå grund av detta har vi tagit bort de förmåner som lades till ditt konto när du löste in presenten.\n\n{support_email, select, null {Om du tror att detta är ett misstag, kontakta administratörerna för den här instansen och ta med den information du har om presentkoden och när du löste in den.} other {Om du tror att detta är ett misstag, kontakta vårt supportteam och ta med den information du har om presentkoden och när du löste in den.}}\n\n– {product_name}-teamet"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "Din anmälan på {product_name} har granskats",
|
||||
"body": "Hej {username},\n\nDin anmälan (ID: {reportId}) har granskats av vårt säkerhetsteam.{hasComment, select, yes {\n\nSvar från säkerhetsteamet:\n{publicComment}} other {}}\n\nTack för att du hjälper till att göra {product_name} tryggt för alla. Vi tar alla anmälningar på allvar och uppskattar ditt bidrag till communityn.\n\n{safety_email, select, null {Om du har några frågor eller funderingar kring detta resultat, kontakta administratörerna för den här instansen.} other {Om du har några frågor eller funderingar kring detta resultat, kontakta oss på {safety_email}.}}\n\n– Säkerhetsteamet på {product_name}"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"subject": "Ditt {product_name}-konto kommer att raderas permanent",
|
||||
"body": "Hej {username},\n\nDitt {product_name}-konto har schemalagts för permanent radering.\n\nSchemalagd radering: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Anledning: {reason}\n\n}\n}Detta är en allvarlig åtgärd. Dina kontodata kommer att raderas permanent på det schemalagda datumet.\n\nOm du anser att detta beslut var felaktigt kan du överklaga. {appeals_email, select, null {Kontakta administratörerna för den här instansen.} other {Mejla {appeals_email} från den här e-postadressen.}}\n\n– Säkerhetsteamet på {product_name}"
|
||||
"body": "Hej {username},\n\nVi har granskat din anmälan (ID: {reportId}).\n\n{outcome, select, removed {Vi har tagit bort innehållet som du anmälde eftersom det bröt mot våra regler.} restricted {Vi har kommit fram till att kontot som du anmälde bröt mot våra regler, och vi har begränsat det. Vi berättar inte vilken åtgärd vi har vidtagit mot ett annat konto, eftersom det är den personens personuppgifter.} action_taken {Vi har kommit fram till att det du anmälde bröt mot våra regler, och vi har vidtagit åtgärder. Vi berättar inte vilken åtgärd vi har vidtagit mot ett annat konto, eftersom det är den personens personuppgifter.} no_violation {Vi har kommit fram till att innehållet eller kontot som du anmälde inte bryter mot våra regler, så vi har inte gjort något.} other {Vi har avslutat din anmälan.}}{hasComment, select, yes {\n\nKommentar från vårt team: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nOm du inte håller med kan du mejla {appeals_email} inom 60 dagar och ange anmälnings-ID:t. Om du befinner dig i EU kan du också hänskjuta ärendet till ett certifierat organ för tvistlösning utanför domstol eller klaga hos samordnaren för digitala tjänster i ditt land.}}\n\n{product_name}-teamet"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "Raderingen av ditt {product_name}-konto är schemalagd",
|
||||
"body": "Hej {username},\n\nDu har begärt att ditt {product_name}-konto ska raderas. Permanent radering av kontot är planerad till följande tidpunkt:\n\n{deletionDate, date, full} kl. {deletionDate, time, short}\n\nOm du inte begärde detta, logga in på ditt konto för att avbryta raderingen. Vi rekommenderar också att du ändrar ditt lösenord för att säkra ditt konto.\n\n– {product_name}-teamet"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "Avstängningen av ditt {product_name}-konto har hävts",
|
||||
"body": "Hej {username},\n\nGoda nyheter: avstängningen av ditt {product_name}-konto har hävts.\n\n{reason, select,\n null {}\n other {Anledning: {reason}\n\n}\n}Du kan nu logga in igen och fortsätta använda {product_name} som vanligt.\n\n– Säkerhetsteamet på {product_name}"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"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, select, null {หากคุณต้องการเก็บบัญชีไว้ โปรดติดต่อผู้ดูแลระบบของอินสแตนซ์นี้ก่อนวันดังกล่าว} other {หากคุณต้องการเก็บบัญชีไว้ โปรดติดต่อ {safety_email} จากที่อยู่อีเมลนี้ก่อนวันดังกล่าว}}\n\n– ทีม {product_name}"
|
||||
"body": "สวัสดี {username},\n\nบัญชี {product_name} ของคุณไม่มีการใช้งานมาเป็นเวลานาน เราจึงกำหนดให้ลบบัญชีในวันที่ {deletion_date}\n\nหากต้องการเก็บบัญชีไว้ โปรดลงชื่อเข้าใช้ก่อนวันดังกล่าว {support_email, select, null {หากมีคำถาม โปรดติดต่อผู้ดูแลระบบของอินสแตนซ์นี้} other {หากมีคำถาม โปรดติดต่อ {support_email}}}\n\nทีม {product_name}"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"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, select, null {หากคุณไม่ได้ร้องขอ หรือต้องการเก็บบัญชีไว้ โปรดติดต่อผู้ดูแลระบบของอินสแตนซ์นี้ก่อนวันดังกล่าว} other {หากคุณไม่ได้ร้องขอ หรือต้องการเก็บบัญชีไว้ โปรดติดต่อ {safety_email} จากที่อยู่อีเมลนี้ก่อนวันดังกล่าว}}\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{legalLinks, select,\n both {โปรดตรวจสอบ:\n- ข้อกำหนดในการให้บริการ: {termsUrl}\n- แนวทางปฏิบัติของคอมมูนิตี้: {guidelinesUrl}\n\n}\n terms {โปรดตรวจสอบ:\n- ข้อกำหนดในการให้บริการ: {termsUrl}\n\n}\n guidelines {โปรดตรวจสอบ:\n- แนวทางปฏิบัติของคอมมูนิตี้: {guidelinesUrl}\n\n}\n other {}\n}กระบวนการอุทธรณ์:\nหากคุณเชื่อว่าการตัดสินใจนี้ไม่ถูกต้องหรือไม่ยุติธรรม คุณมีเวลา 60 วันในการยื่นอุทธรณ์ {appeals_email, select, null {ติดต่อผู้ดูแลระบบของอินสแตนซ์นี้} other {ส่งอีเมลไปที่ {appeals_email} จากที่อยู่อีเมลนี้}}\n\nในการอุทธรณ์ของคุณ:\n- อธิบายให้ชัดเจนว่าทำไมคุณถึงเชื่อว่าการตัดสินใจนั้นไม่ถูกต้องหรือไม่ยุติธรรม\n- แนบหลักฐานหรือบริบทที่เกี่ยวข้อง\n\nสมาชิกของทีมความปลอดภัย {product_name} จะตรวจสอบคำอุทธรณ์ของคุณและอาจระงับการลบที่รอดำเนินการไว้จนกว่าจะมีการตัดสินใจขั้นสุดท้าย\n\n– ทีมความปลอดภัย {product_name}"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "บัญชี {product_name} ของคุณถูกระงับชั่วคราว",
|
||||
"body": "สวัสดี {username},\n\nบัญชี {product_name} ของคุณถูกระงับชั่วคราวเนื่องจากละเมิดข้อกำหนดในการให้บริการหรือแนวทางปฏิบัติของคอมมูนิตี้ของเรา\n\nระยะเวลา: {durationHours, plural,\n =1 {1 ชั่วโมง}\n other {# ชั่วโมง}\n}\nระงับจนถึง: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {เหตุผล: {reason}\n\n}\n}ในช่วงเวลานี้ คุณจะไม่สามารถเข้าถึงบัญชีของคุณได้\n\n{legalLinks, select,\n both {โปรดตรวจสอบ:\n- ข้อกำหนดในการให้บริการ: {termsUrl}\n- แนวทางปฏิบัติของคอมมูนิตี้: {guidelinesUrl}\n\n}\n terms {โปรดตรวจสอบ:\n- ข้อกำหนดในการให้บริการ: {termsUrl}\n\n}\n guidelines {โปรดตรวจสอบ:\n- แนวทางปฏิบัติของคอมมูนิตี้: {guidelinesUrl}\n\n}\n other {}\n}หากคุณเชื่อว่าการตัดสินใจนี้ไม่ถูกต้องหรือไม่ยุติธรรม คุณสามารถยื่นอุทธรณ์ได้ {appeals_email, select, null {ติดต่อผู้ดูแลระบบของอินสแตนซ์นี้และอธิบายให้ชัดเจนว่าทำไมคุณถึงเชื่อว่าการตัดสินใจนั้นไม่ถูกต้อง} other {ส่งอีเมลไปที่ {appeals_email} จากที่อยู่อีเมลนี้และอธิบายให้ชัดเจนว่าทำไมคุณถึงเชื่อว่าการตัดสินใจนั้นไม่ถูกต้อง}} เราจะตรวจสอบคำอุทธรณ์ของคุณและตอบกลับด้วยการตัดสินใจของเรา\n\n– ทีมความปลอดภัย {product_name}"
|
||||
"body": "สวัสดี {username},\n\nตามที่คุณขอ เราได้กำหนดให้ลบบัญชี {product_name} ของคุณในวันที่ {deletion_date}\n\nหากต้องการเก็บบัญชีไว้ โปรดลงชื่อเข้าใช้ก่อนวันที่ {deletion_date} {safety_email, select, null {หากคุณไม่ได้ขอเรื่องนี้ โปรดติดต่อผู้ดูแลระบบของอินสแตนซ์นี้} other {หากคุณไม่ได้ขอเรื่องนี้ โปรดติดต่อ {safety_email}}}\n\nทีม {product_name}"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "ขอบคุณสำหรับการบริจาคให้ {product_name}",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "เราได้ตัดสินใจเกี่ยวกับรายงานของคุณใน {product_name} แล้ว",
|
||||
"body": "สวัสดี,\n\nทีมความปลอดภัยของเราได้ตรวจสอบรายงานตามกฎหมายบริการดิจิทัล (Digital Services Act) ของคุณ (ID: {reportId}) และตัดสินใจแล้ว{hasComment, select, yes {\n\nการตอบกลับจากทีมความปลอดภัย:\n{publicComment}} other {}}\n\nเราไม่เปิดเผยรายละเอียดการดำเนินการกับบัญชีของบุคคลอื่น เนื่องจากเป็นข้อมูลส่วนบุคคลของบุคคลนั้น\n\nหากคุณไม่เห็นด้วยกับการตัดสินใจนี้ คุณสามารถยื่นอุทธรณ์ได้โดยไม่มีค่าใช้จ่ายภายใน 60 วัน {appeals_email, select, null {ติดต่อผู้ดูแลระบบของอินสแตนซ์นี้ ระบุ ID รายงานของคุณ และอธิบายว่าทำไมคุณถึงคิดว่าการตัดสินใจนั้นไม่ถูกต้อง} other {ส่งอีเมลไปที่ {appeals_email} จากที่อยู่อีเมลนี้ ระบุ ID รายงานของคุณ และอธิบายว่าทำไมคุณถึงคิดว่าการตัดสินใจนั้นไม่ถูกต้อง}} หากคุณอยู่ในสหภาพยุโรป คุณยังสามารถส่งข้อพิพาทไปยังหน่วยงานระงับข้อพิพาทนอกศาลที่ได้รับการรับรองได้ ทั้งหมดนี้ไม่กระทบต่อสิทธิ์ของคุณในการฟ้องร้องต่อศาล\n\n– ทีมความปลอดภัย {product_name}"
|
||||
"body": "สวัสดี,\n\nเราได้ตรวจสอบรายงานของคุณแล้ว (รหัส: {reportId})\n\n{outcome, select, removed {เราได้ลบเนื้อหาที่คุณรายงานแล้ว เนื่องจากเนื้อหานั้นละเมิดกฎของเรา} restricted {เราพบว่าบัญชีที่คุณรายงานละเมิดกฎของเรา และเราได้จำกัดบัญชีนั้นแล้ว เราไม่เปิดเผยว่าได้ดำเนินการใดกับบัญชีอื่น เนื่องจากเป็นข้อมูลส่วนบุคคลของเจ้าของบัญชี} action_taken {เราพบว่าสิ่งที่คุณรายงานละเมิดกฎของเรา และเราได้ดำเนินการแล้ว เราไม่เปิดเผยว่าได้ดำเนินการใดกับบัญชีอื่น เนื่องจากเป็นข้อมูลส่วนบุคคลของเจ้าของบัญชี} no_violation {เราพบว่าเนื้อหาหรือบัญชีที่คุณรายงานไม่ได้ละเมิดกฎของเรา จึงปล่อยไว้ตามเดิม} other {เราได้ปิดรายงานของคุณแล้ว}}{hasComment, select, yes {\n\nหมายเหตุจากทีมของเรา: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nหากคุณไม่เห็นด้วย โปรดส่งอีเมลไปที่ {appeals_email} ภายใน 60 วัน และระบุรหัสรายงาน หากคุณอยู่ในสหภาพยุโรป คุณยังสามารถส่งเรื่องนี้ไปยังหน่วยงานระงับข้อพิพาทนอกศาลที่ได้รับการรับรอง หรือร้องเรียนต่อผู้ประสานงานบริการดิจิทัลในประเทศของคุณได้}}\n\nทีม {product_name}"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "ยืนยันอีเมลสำหรับการรายงาน DSA",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "ยืนยันที่อยู่อีเมลสำหรับ {product_name}",
|
||||
"body": "สวัสดี {username},\n\nโปรดยืนยันที่อยู่อีเมลสำหรับบัญชี {product_name} ของคุณโดยคลิกที่ลิงก์ด้านล่าง:\n\n{verifyUrl}\n\nหากคุณไม่ได้สร้างบัญชี {product_name} คุณสามารถเพิกเฉยต่ออีเมลนี้ได้เลย\n\nลิงก์นี้ใช้ได้เป็นเวลา 24 ชั่วโมง\n\n– ทีม {product_name}"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {เรากลับคำตัดสินเกี่ยวกับบัญชี {product_name} ของคุณแล้ว} other {ข้อจำกัดบนบัญชี {product_name} ของคุณสิ้นสุดแล้ว}}",
|
||||
"body": "สวัสดี {username},\n\n{outcome, select, reversed {{decision_id, select, null {เราได้ทบทวนคำตัดสินเกี่ยวกับบัญชีของคุณอีกครั้งและกลับคำตัดสินนั้นแล้ว} other {เราได้ทบทวนคำตัดสิน {decision_id} อีกครั้งและกลับคำตัดสินนั้นแล้ว}} ขออภัยในความไม่สะดวก} other {{decision_id, select, null {เราได้ยกเลิกข้อจำกัดบนบัญชีของคุณแล้ว} other {เราได้ยกเลิกข้อจำกัดจากคำตัดสิน {decision_id} แล้ว}}}}{restores_access, select, yes { คุณสามารถลงชื่อเข้าใช้และใช้งาน {product_name} ได้อีกครั้ง} other {}}{statement, select, null {} other {\n\nหมายเหตุจากทีมของเรา: {statement}}}{content_removed, select, yes {\n\nเนื้อหาที่เราลบไปแล้วไม่สามารถกู้คืนได้} other {}}\n\nทีม {product_name}"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {ข้อมูลอัปเดตเกี่ยวกับบัญชี {product_name} ของคุณ} other {{kind_label, select, warning {คำเตือนเกี่ยวกับบัญชี {product_name} ของคุณ} suspension {บัญชี {product_name} ของคุณถูกระงับชั่วคราว} ban {บัญชี {product_name} ของคุณถูกแบน} closure {บัญชี {product_name} ของคุณถูกปิด} feature_limit {ฟีเจอร์บางอย่างในบัญชี {product_name} ของคุณถูกจำกัด} content_removal {เราได้ลบข้อความที่คุณส่งใน {product_name}} profile_reset {เราได้รีเซ็ตบางส่วนของโปรไฟล์ {product_name} ของคุณ} community_removal {เราได้ลบคอมมูนิตี้ของคุณใน {product_name}} bot {ข้อมูลอัปเดตเกี่ยวกับบอทของคุณใน {product_name}} other {ข้อมูลอัปเดตเกี่ยวกับบัญชี {product_name} ของคุณ}}}}",
|
||||
"body": "สวัสดี {username},\n\n{summary}\n\n{effects}\n\nเหตุผล\nกฎ: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nหมายเหตุจากทีมของเรา: {statement}}}\n\nวิธีที่เราตัดสิน\n{how_we_decided}{appeals_email, select, null {} other {\n\nวิธีอุทธรณ์\nหากคุณคิดว่าเราตัดสินผิด โปรดส่งอีเมลไปที่ {appeals_email} ภายใน {appeal_deadline} หากเป็นไปได้ โปรดส่งจากอีเมลที่ใช้กับบัญชีของคุณ และระบุรหัสคำตัดสิน การอุทธรณ์ไม่มีค่าใช้จ่าย และเจ้าหน้าที่จะตรวจสอบทุกคำอุทธรณ์ด้วยตนเอง{redress, select, null {} other {\n\n{redress}}}}}\n\nรหัสคำตัดสิน: {decision_id}\n\nทีม {product_name}"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "สิทธิประโยชน์จากของขวัญที่คุณแลกรับถูกนำออกแล้ว",
|
||||
"body": "สวัสดี {username},\n\nรหัสของขวัญที่คุณแลกรับนั้นเดิมทีชำระโดยผู้อื่น การชำระเงินดังกล่าวถูกยกเลิกในภายหลัง (การเรียกเก็บเงินคืน)\n\nด้วยเหตุนี้ เราจึงลบสิทธิประโยชน์ที่เพิ่มเข้าบัญชีของคุณตอนที่คุณแลกรับของขวัญ\n\n{support_email, select, null {หากคุณคิดว่านี่เป็นข้อผิดพลาด โปรดติดต่อผู้ดูแลระบบของอินสแตนซ์นี้และระบุรายละเอียดใด ๆ ที่คุณมีเกี่ยวกับรหัสของขวัญและเวลาที่คุณแลกรับ} other {หากคุณคิดว่านี่เป็นข้อผิดพลาด โปรดติดต่อทีมสนับสนุนของเราและระบุรายละเอียดใด ๆ ที่คุณมีเกี่ยวกับรหัสของขวัญและเวลาที่คุณแลกรับ}}\n\n– ทีม {product_name}"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "รายงานของคุณใน {product_name} ได้รับการตรวจสอบแล้ว",
|
||||
"body": "สวัสดี {username},\n\nทีมความปลอดภัยของเราได้ตรวจสอบรายงานของคุณ (ID: {reportId}) แล้ว{hasComment, select, yes {\n\nการตอบกลับจากทีมความปลอดภัย:\n{publicComment}} other {}}\n\nขอบคุณที่ช่วยให้ {product_name} ปลอดภัยสำหรับทุกคน เราให้ความสำคัญกับรายงานทั้งหมดและขอขอบคุณสำหรับการมีส่วนร่วมของคุณในคอมมูนิตี้\n\n{safety_email, select, null {หากคุณมีคำถามหรือข้อกังวลใด ๆ เกี่ยวกับผลลัพธ์นี้ โปรดติดต่อผู้ดูแลระบบของอินสแตนซ์นี้} other {หากคุณมีคำถามหรือข้อกังวลใด ๆ เกี่ยวกับผลลัพธ์นี้ โปรดติดต่อ {safety_email}}}\n\n– ทีมความปลอดภัย {product_name}"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"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หากคุณเชื่อว่าการตัดสินใจนี้ไม่ถูกต้อง คุณสามารถยื่นอุทธรณ์ได้ {appeals_email, select, null {ติดต่อผู้ดูแลระบบของอินสแตนซ์นี้} other {ส่งอีเมลไปที่ {appeals_email} จากที่อยู่อีเมลนี้}}\n\n– ทีมความปลอดภัย {product_name}"
|
||||
"body": "สวัสดี {username},\n\nเราได้ตรวจสอบรายงานของคุณแล้ว (รหัส: {reportId})\n\n{outcome, select, removed {เราได้ลบเนื้อหาที่คุณรายงานแล้ว เนื่องจากเนื้อหานั้นละเมิดกฎของเรา} restricted {เราพบว่าบัญชีที่คุณรายงานละเมิดกฎของเรา และเราได้จำกัดบัญชีนั้นแล้ว เราไม่เปิดเผยว่าได้ดำเนินการใดกับบัญชีอื่น เนื่องจากเป็นข้อมูลส่วนบุคคลของเจ้าของบัญชี} action_taken {เราพบว่าสิ่งที่คุณรายงานละเมิดกฎของเรา และเราได้ดำเนินการแล้ว เราไม่เปิดเผยว่าได้ดำเนินการใดกับบัญชีอื่น เนื่องจากเป็นข้อมูลส่วนบุคคลของเจ้าของบัญชี} no_violation {เราพบว่าเนื้อหาหรือบัญชีที่คุณรายงานไม่ได้ละเมิดกฎของเรา จึงปล่อยไว้ตามเดิม} other {เราได้ปิดรายงานของคุณแล้ว}}{hasComment, select, yes {\n\nหมายเหตุจากทีมของเรา: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nหากคุณไม่เห็นด้วย โปรดส่งอีเมลไปที่ {appeals_email} ภายใน 60 วัน และระบุรหัสรายงาน หากคุณอยู่ในสหภาพยุโรป คุณยังสามารถส่งเรื่องนี้ไปยังหน่วยงานระงับข้อพิพาทนอกศาลที่ได้รับการรับรอง หรือร้องเรียนต่อผู้ประสานงานบริการดิจิทัลในประเทศของคุณได้}}\n\nทีม {product_name}"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "บัญชี {product_name} ของคุณถูกกำหนดให้ลบแล้ว",
|
||||
"body": "สวัสดี {username},\n\nคุณได้ร้องขอให้ลบบัญชี {product_name} ของคุณ บัญชีนี้ถูกกำหนดให้ลบอย่างถาวรในวันที่:\n\n{deletionDate, date, full} เวลา {deletionDate, time, short}\n\nหากคุณไม่ได้ร้องขอ ให้เข้าสู่ระบบบัญชีของคุณเพื่อยกเลิกการลบ เราขอแนะนำให้คุณเปลี่ยนรหัสผ่านด้วยเพื่อรักษาความปลอดภัยบัญชี\n\n– ทีม {product_name}"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "การระงับบัญชี {product_name} ของคุณถูกยกเลิกแล้ว",
|
||||
"body": "สวัสดี {username},\n\nข่าวดี: การระงับบัญชี {product_name} ของคุณถูกยกเลิกแล้ว\n\n{reason, select,\n null {}\n other {เหตุผล: {reason}\n\n}\n}ตอนนี้คุณสามารถเข้าสู่ระบบอีกครั้งและใช้งาน {product_name} ได้ตามปกติ\n\n– ทีมความปลอดภัย {product_name}"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"subject": "{product_name} hesabın hareketsizlik nedeniyle silinecek",
|
||||
"body": "Merhaba {username},\n\n{product_name} hesabın uzun süredir hareketsiz olduğu için şu tarihte kalıcı olarak silinmek üzere planlandı:\n\n{deletionDate, date, full} {deletionDate, time, short}{reason, select, null {} other {\n\nSebep: {reason}}}\n\n{safety_email, select, null {Hesabını korumak istiyorsan bu tarihten önce bu kurulumun yöneticileriyle iletişime geç.} other {Hesabını korumak istiyorsan bu tarihten önce bu e-posta adresinden {safety_email} adresine ulaş.}}\n\n– {product_name} Ekibi"
|
||||
"body": "Merhaba {username},\n\n{product_name} hesabın uzun süredir hareketsiz olduğu için {deletion_date} tarihinde silinmek üzere planladık.\n\nHesabını korumak istiyorsan bu tarihten önce giriş yap. {support_email, select, null {Soruların varsa bu kurulumun yöneticileriyle iletişime geç.} other {Soruların varsa {support_email} adresiyle iletişime geç.}}\n\n{product_name} ekibi"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"subject": "{product_name} hesabının silinmesi planlandı",
|
||||
"body": "Merhaba {username},\n\nTalebin doğrultusunda {product_name} hesabın şu tarihte kalıcı olarak silinmek üzere planlandı:\n\n{deletionDate, date, full} {deletionDate, time, short}{reason, select, null {} other {\n\nSebep: {reason}}}\n\nHesabın o zamana kadar kilitli. {safety_email, select, null {Bunu sen talep etmediysen ya da hesabını korumak istiyorsan bu tarihten önce bu kurulumun yöneticileriyle iletişime geç.} other {Bunu sen talep etmediysen ya da hesabını korumak istiyorsan bu tarihten önce bu e-posta adresinden {safety_email} adresine ulaş.}}\n\n– {product_name} Ekibi"
|
||||
},
|
||||
"account_scheduled_deletion": {
|
||||
"subject": "{product_name} hesabın kalıcı olarak silinecek",
|
||||
"body": "Merhaba {username},\n\nHizmet Şartlarımızı veya Topluluk Kurallarımızı ihlal ettiğin için {product_name} hesabın kalıcı olarak silinmek üzere planlandı.\n\nPlanlanan silme tarihi: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Sebep: {reason}\n\n}\n}Bu, ciddi bir yaptırım işlemidir. Hesap verilerin planlanan tarihte kalıcı olarak silinecektir.\n\n{legalLinks, select,\n both {Lütfen şunları incele:\n- Hizmet Şartları: {termsUrl}\n- Topluluk Kuralları: {guidelinesUrl}\n\n}\n terms {Lütfen şunları incele:\n- Hizmet Şartları: {termsUrl}\n\n}\n guidelines {Lütfen şunları incele:\n- Topluluk Kuralları: {guidelinesUrl}\n\n}\n other {}\n}İtiraz süreci:\nBu yaptırım kararının yanlış veya haksız olduğuna inanıyorsan, itiraz göndermek için 60 günün var. {appeals_email, select, null {Bu kurulumun yöneticileriyle iletişime geç.} other {Bu e-posta adresinden {appeals_email} adresine e-posta gönder.}}\n\nİtirazında:\n- Yaptırım kararının neden yanlış veya haksız olduğunu düşündüğünü açıkça anlat\n- İlgili kanıtı veya bağlamı paylaş\n\n{product_name} Güvenlik Ekibinden bir üye itirazını inceleyecek ve nihai karar verilene kadar bekleyen silme işlemini duraklatabilir.\n\n– {product_name} Güvenlik Ekibi"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "{product_name} hesabın geçici olarak askıya alındı",
|
||||
"body": "Merhaba {username},\n\nHizmet Şartlarımızı veya Topluluk Kurallarımızı ihlal ettiğin için {product_name} hesabını geçici olarak askıya aldık.\n\nSüre: {durationHours, plural,\n =1 {1 saat}\n other {# saat}\n}\nŞu tarihe kadar askıda: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Sebep: {reason}\n\n}\n}Bu süre boyunca hesabına erişemeyeceksin.\n\n{legalLinks, select,\n both {Lütfen şunları incele:\n- Hizmet Şartları: {termsUrl}\n- Topluluk Kuralları: {guidelinesUrl}\n\n}\n terms {Lütfen şunları incele:\n- Hizmet Şartları: {termsUrl}\n\n}\n guidelines {Lütfen şunları incele:\n- Topluluk Kuralları: {guidelinesUrl}\n\n}\n other {}\n}Bu yaptırım kararının yanlış veya haksız olduğuna inanıyorsan, itiraz gönderebilirsin. {appeals_email, select, null {Bu kurulumun yöneticileriyle iletişime geç ve kararın neden yanlış olduğunu düşündüğünü açıkça anlat.} other {Bu e-posta adresinden {appeals_email} adresine e-posta gönder ve kararın neden yanlış olduğunu düşündüğünü açıkça anlat.}} İtirazını inceleyip kararımızı sana bildireceğiz.\n\n– {product_name} Güvenlik Ekibi"
|
||||
"body": "Merhaba {username},\n\nİsteğin üzerine {product_name} hesabını {deletion_date} tarihinde silinmek üzere planladık.\n\nHesabını korumak istiyorsan {deletion_date} tarihinden önce giriş yap. {safety_email, select, null {Bunu sen istemediysen bu kurulumun yöneticileriyle iletişime geç.} other {Bunu sen istemediysen {safety_email} adresiyle iletişime geç.}}\n\n{product_name} ekibi"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "{product_name}'a yaptığın bağış için teşekkürler",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "{product_name} raporun hakkında karar verdik",
|
||||
"body": "Merhaba,\n\nGüvenlik Ekibimiz Dijital Hizmetler Yasası raporunu (Kimlik: {reportId}) inceledi ve bir karar verdi.{hasComment, select, yes {\n\nGüvenlik Ekibinin yanıtı:\n{publicComment}} other {}}\n\nBaşka bir kişinin hesabına uygulanan işlemlerin ayrıntılarını paylaşmıyoruz çünkü bunlar o kişinin kişisel verileridir.\n\nBu karara katılmıyorsan 60 gün içinde ücretsiz olarak itiraz edebilirsin. {appeals_email, select, null {Bu kurulumun yöneticileriyle iletişime geç, rapor kimliğini ekle ve kararın neden yanlış olduğunu düşündüğünü açıkla.} other {Bu e-posta adresinden {appeals_email} adresine e-posta gönder, rapor kimliğini ekle ve kararın neden yanlış olduğunu düşündüğünü açıkla.}} AB'deysen anlaşmazlığı sertifikalı bir mahkeme dışı uyuşmazlık çözüm kuruluşuna da götürebilirsin. Bunların hiçbiri mahkemeye başvurma hakkını etkilemez.\n\n– {product_name} Güvenlik Ekibi"
|
||||
"body": "Merhaba,\n\nRaporunu inceledik (Kimlik: {reportId}).\n\n{outcome, select, removed {Bildirdiğin içeriği kurallarımızı ihlal ettiği için kaldırdık.} restricted {Bildirdiğin hesabın kurallarımızı ihlal ettiğini tespit ettik ve hesabı kısıtladık. Başka bir hesaba karşı hangi işlemi yaptığımızı paylaşmıyoruz, çünkü bu bilgi o kişinin kişisel verisidir.} action_taken {Bildirdiğin şeyin kurallarımızı ihlal ettiğini tespit ettik ve işlem yaptık. Başka bir hesaba karşı hangi işlemi yaptığımızı paylaşmıyoruz, çünkü bu bilgi o kişinin kişisel verisidir.} no_violation {Bildirdiğin içeriğin veya hesabın kurallarımızı ihlal etmediğini tespit ettik, bu yüzden herhangi bir değişiklik yapmadık.} other {Raporunu kapattık.}}{hasComment, select, yes {\n\nEkibimizden not: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nKatılmıyorsan 60 gün içinde {appeals_email} adresine e-posta gönder ve rapor kimliğini belirt. AB içindeysen, konuyu sertifikalı bir mahkeme dışı uyuşmazlık çözüm kuruluşuna da taşıyabilir veya ülkendeki Dijital Hizmetler Koordinatörüne şikâyette bulunabilirsin.}}\n\n{product_name} ekibi"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "DSA raporu için e-postanı doğrula",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "{product_name} e-posta adresini doğrula",
|
||||
"body": "Merhaba {username},\n\n{product_name} hesabının e-posta adresini aşağıdaki bağlantıya tıklayarak doğrula:\n\n{verifyUrl}\n\nBir {product_name} hesabı oluşturmadıysan, bu e-postayı güvenle yok sayabilirsin.\n\nBu bağlantı 24 saat geçerlidir.\n\n– {product_name} Ekibi"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {{product_name} hesabınla ilgili bir kararı geri aldık} other {{product_name} hesabındaki bir kısıtlama sona erdi}}",
|
||||
"body": "Merhaba {username},\n\n{outcome, select, reversed {{decision_id, select, null {Hesabınla ilgili bir kararı yeniden inceledik ve geri aldık.} other {{decision_id} kimlikli kararı yeniden inceledik ve geri aldık.}} Yaşattığımız sorun için özür dileriz.} other {{decision_id, select, null {Hesabındaki kısıtlamayı kaldırdık.} other {{decision_id} kimlikli karardan kaynaklanan kısıtlamayı kaldırdık.}}}}{restores_access, select, yes { Artık yeniden giriş yapıp {product_name} hizmetini kullanabilirsin.} other {}}{statement, select, null {} other {\n\nEkibimizden not: {statement}}}{content_removed, select, yes {\n\nKaldırdığımız içerik geri yüklenemez.} other {}}\n\n{product_name} ekibi"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {{product_name} hesabınla ilgili bir güncelleme} other {{kind_label, select, warning {{product_name} hesabınla ilgili bir uyarı} suspension {{product_name} hesabın askıya alındı} ban {{product_name} hesabın kalıcı olarak askıya alındı} closure {{product_name} hesabın kapatıldı} feature_limit {{product_name} hesabındaki bazı özellikler kısıtlandı} content_removal {{product_name} üzerinde gönderdiğin mesajları kaldırdık} profile_reset {{product_name} profilinin bazı bölümlerini sıfırladık} community_removal {{product_name} üzerindeki topluluğunu kaldırdık} bot {{product_name} üzerindeki botunla ilgili bir güncelleme} other {{product_name} hesabınla ilgili bir güncelleme}}}}",
|
||||
"body": "Merhaba {username},\n\n{summary}\n\n{effects}\n\nNeden\nKural: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nEkibimizden not: {statement}}}\n\nNasıl karar verdik\n{how_we_decided}{appeals_email, select, null {} other {\n\nNasıl itiraz edebilirsin\nHata yaptığımızı düşünüyorsan en geç {appeal_deadline} tarihine kadar {appeals_email} adresine e-posta gönder. Mümkünse hesabındaki e-posta adresinden yaz ve karar kimliğini belirt. İtirazlar ücretsizdir ve her birini bir insan inceler.{redress, select, null {} other {\n\n{redress}}}}}\n\nKarar kimliği: {decision_id}\n\n{product_name} ekibi"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Kullandığın hediyenin avantajları kaldırıldı",
|
||||
"body": "Merhaba {username},\n\nKullandığın bir hediye kodunun ödemesi başlangıçta başka biri tarafından yapılmıştı. Bu ödeme daha sonra geri alındı (ters ibraz).\n\nBu nedenle, hediyeyi kullandığında hesabına eklenen avantajlar kaldırıldı.\n\n{support_email, select, null {Bunun bir hata olduğunu düşünüyorsan, lütfen bu kurulumun yöneticileriyle iletişime geç ve hediye koduyla ilgili sahip olduğun tüm detayları ve ne zaman kullandığını belirt.} other {Bunun bir hata olduğunu düşünüyorsan, lütfen destek ekibimizle iletişime geç ve hediye koduyla ilgili sahip olduğun tüm detayları ve ne zaman kullandığını belirt.}}\n\n– {product_name} Ekibi"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "{product_name} raporun incelendi",
|
||||
"body": "Merhaba {username},\n\nRaporun (Kimlik: {reportId}) Güvenlik Ekibimiz tarafından incelendi.{hasComment, select, yes {\n\nGüvenlik Ekibinin yanıtı:\n{publicComment}} other {}}\n\n{product_name}'ı herkes için güvenli tutmaya yardımcı olduğun için teşekkür ederiz. Tüm raporları ciddiye alıyoruz ve topluluğa katkını takdir ediyoruz.\n\n{safety_email, select, null {Bu sonuç hakkında herhangi bir sorun veya endişen varsa, lütfen bu kurulumun yöneticileriyle iletişime geç.} other {Bu sonuç hakkında herhangi bir sorun veya endişen varsa, lütfen {safety_email} adresine ulaş.}}\n\n– {product_name} Güvenlik Ekibi"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"subject": "{product_name} hesabın kalıcı olarak silinecek",
|
||||
"body": "Merhaba {username},\n\n{product_name} hesabın kalıcı olarak silinmek üzere planlandı.\n\nPlanlanan silme tarihi: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Sebep: {reason}\n\n}\n}Bu, ciddi bir yaptırım işlemidir. Hesap verilerin planlanan tarihte kalıcı olarak silinecektir.\n\nBu yaptırım kararının yanlış olduğuna inanıyorsan, itiraz gönderebilirsin. {appeals_email, select, null {Bu kurulumun yöneticileriyle iletişime geç.} other {Bu e-posta adresinden {appeals_email} adresine e-posta gönder.}}\n\n– {product_name} Güvenlik Ekibi"
|
||||
"body": "Merhaba {username},\n\nRaporunu inceledik (Kimlik: {reportId}).\n\n{outcome, select, removed {Bildirdiğin içeriği kurallarımızı ihlal ettiği için kaldırdık.} restricted {Bildirdiğin hesabın kurallarımızı ihlal ettiğini tespit ettik ve hesabı kısıtladık. Başka bir hesaba karşı hangi işlemi yaptığımızı paylaşmıyoruz, çünkü bu bilgi o kişinin kişisel verisidir.} action_taken {Bildirdiğin şeyin kurallarımızı ihlal ettiğini tespit ettik ve işlem yaptık. Başka bir hesaba karşı hangi işlemi yaptığımızı paylaşmıyoruz, çünkü bu bilgi o kişinin kişisel verisidir.} no_violation {Bildirdiğin içeriğin veya hesabın kurallarımızı ihlal etmediğini tespit ettik, bu yüzden herhangi bir değişiklik yapmadık.} other {Raporunu kapattık.}}{hasComment, select, yes {\n\nEkibimizden not: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nKatılmıyorsan 60 gün içinde {appeals_email} adresine e-posta gönder ve rapor kimliğini belirt. AB içindeysen, konuyu sertifikalı bir mahkeme dışı uyuşmazlık çözüm kuruluşuna da taşıyabilir veya ülkendeki Dijital Hizmetler Koordinatörüne şikâyette bulunabilirsin.}}\n\n{product_name} ekibi"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "{product_name} hesabının silinmesi planlandı",
|
||||
"body": "Merhaba {username},\n\n{product_name} hesabını silme talebinde bulundun. Hesabın şu tarihte kalıcı olarak silinmek üzere planlandı:\n\n{deletionDate, date, full} {deletionDate, time, short}\n\nBunu sen talep etmediysen, silme işlemini iptal etmek için hesabına giriş yap. Ayrıca hesabını güvence altına almak için şifreni değiştirmeni öneririz.\n\n– {product_name} Ekibi"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "{product_name} hesabının askıya alınması kaldırıldı",
|
||||
"body": "Merhaba {username},\n\nİyi haber: {product_name} hesabının askıya alınması kaldırıldı.\n\n{reason, select,\n null {}\n other {Sebep: {reason}\n\n}\n}Artık tekrar giriş yapabilir ve {product_name}'ı normal şekilde kullanmaya devam edebilirsin.\n\n– {product_name} Güvenlik Ekibi"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"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, select, null {Якщо хочеш зберегти акаунт, звернися до адміністраторів цього екземпляра до цієї дати.} other {Якщо хочеш зберегти акаунт, напиши на {safety_email} з цієї електронної адреси до цієї дати.}}\n\n– Команда {product_name}"
|
||||
"body": "Привіт, {username},\n\nТвій акаунт {product_name} тривалий час був неактивним, тому ми запланували його видалення на {deletion_date}.\n\nЩоб зберегти акаунт, увійди в нього до цієї дати. {support_email, select, null {Якщо маєш запитання, звернися до адміністраторів цього екземпляра.} other {Якщо маєш запитання, напиши на {support_email}.}}\n\nКоманда {product_name}"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"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, select, null {Якщо цей запит не від тебе або ти хочеш зберегти акаунт, звернися до адміністраторів цього екземпляра до цієї дати.} other {Якщо цей запит не від тебе або ти хочеш зберегти акаунт, напиши на {safety_email} з цієї електронної адреси до цієї дати.}}\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{legalLinks, select,\n both {Будь ласка, переглянь:\n- Умови використання: {termsUrl}\n- Правила спільноти: {guidelinesUrl}\n\n}\n terms {Будь ласка, переглянь:\n- Умови використання: {termsUrl}\n\n}\n guidelines {Будь ласка, переглянь:\n- Правила спільноти: {guidelinesUrl}\n\n}\n other {}\n}Процес апеляції:\nЯкщо ти вважаєш, що це рішення було неправильним або необґрунтованим, у тебе є 60 днів, щоб подати апеляцію. {appeals_email, select, null {Звернися до адміністраторів цього екземпляра.} other {Надішли електронний лист на {appeals_email} з цієї електронної адреси.}}\n\nУ твоїй апеляції:\n- Чітко поясни, чому ти вважаєш, що рішення було неправильним або необґрунтованим\n- Надай будь-які відповідні докази або контекст\n\nКоманда безпеки {product_name} розгляне твою апеляцію та може призупинити видалення до ухвалення остаточного рішення.\n\n– Команда безпеки {product_name}"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "Твій акаунт {product_name} тимчасово призупинено",
|
||||
"body": "Привіт, {username},\n\nТвій акаунт {product_name} тимчасово призупинено за порушення наших Умов використання або Правил спільноти.\n\nТривалість: {durationHours, plural,\n one {# година}\n few {# години}\n many {# годин}\n other {# години}\n}\nПризупинено до: {bannedUntil, date, full} о {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Причина: {reason}\n\n}\n}Протягом цього часу ти не зможеш отримати доступ до свого акаунта.\n\n{legalLinks, select,\n both {Будь ласка, переглянь:\n- Умови використання: {termsUrl}\n- Правила спільноти: {guidelinesUrl}\n\n}\n terms {Будь ласка, переглянь:\n- Умови використання: {termsUrl}\n\n}\n guidelines {Будь ласка, переглянь:\n- Правила спільноти: {guidelinesUrl}\n\n}\n other {}\n}Якщо ти вважаєш, що це рішення було неправильним або необґрунтованим, ти можеш подати апеляцію. {appeals_email, select, null {Звернися до адміністраторів цього екземпляра та чітко поясни, чому ти вважаєш рішення неправильним.} other {Надішли електронний лист на {appeals_email} з цієї електронної адреси та чітко поясни, чому ти вважаєш рішення неправильним.}} Ми розглянемо твою апеляцію та повідомимо про своє рішення.\n\n– Команда безпеки {product_name}"
|
||||
"body": "Привіт, {username},\n\nНа твоє прохання ми запланували видалення твого акаунта {product_name} на {deletion_date}.\n\nЩоб зберегти акаунт, увійди в нього до {deletion_date}. {safety_email, select, null {Якщо це був не твій запит, звернися до адміністраторів цього екземпляра.} other {Якщо це був не твій запит, напиши на {safety_email}.}}\n\nКоманда {product_name}"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Дякуємо за твою пожертву для {product_name}",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "Ми ухвалили рішення щодо твого звіту в {product_name}",
|
||||
"body": "Привіт,\n\nНаша команда безпеки розглянула твій звіт за Законом про цифрові послуги (ID: {reportId}) та ухвалила рішення.{hasComment, select, yes {\n\nВідповідь команди безпеки:\n{publicComment}} other {}}\n\nМи не розголошуємо подробиць заходів, ужитих щодо акаунта іншої людини, оскільки це її персональні дані.\n\nЯкщо ти не погоджуєшся з цим рішенням, ти можеш безкоштовно подати апеляцію протягом 60 днів. {appeals_email, select, null {Звернися до адміністраторів цього екземпляра, вкажи ID свого звіту та поясни, чому вважаєш рішення неправильним.} other {Надішли електронний лист на {appeals_email} з цієї електронної адреси, вкажи ID свого звіту та поясни, чому вважаєш рішення неправильним.}} Якщо ти перебуваєш у ЄС, ти також можеш передати спір до сертифікованого органу позасудового врегулювання спорів. Усе це не впливає на твоє право звернутися до суду.\n\n– Команда безпеки {product_name}"
|
||||
"body": "Привіт!\n\nМи розглянули твою скаргу (ID: {reportId}).\n\n{outcome, select, removed {Ми видалили вміст, указаний у твоїй скарзі, бо він порушував наші правила.} restricted {Ми встановили, що акаунт, указаний у твоїй скарзі, порушив наші правила, і обмежили його. Ми не повідомляємо, яких заходів ужили щодо іншого акаунта, бо це персональні дані його власника.} action_taken {Ми встановили, що вказане у твоїй скарзі порушило наші правила, і вжили заходів. Ми не повідомляємо, яких заходів ужили щодо іншого акаунта, бо це персональні дані його власника.} no_violation {Ми встановили, що вміст або акаунт, указаний у твоїй скарзі, не порушує наших правил, тому залишили все як є.} other {Ми закрили твою скаргу.}}{hasComment, select, yes {\n\nКоментар нашої команди: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nЯкщо рішення здається тобі неправильним, напиши на {appeals_email} протягом 60 днів і вкажи ID скарги. Якщо ти перебуваєш у ЄС, ти також можеш передати спір до сертифікованого органу позасудового врегулювання спорів або подати скаргу координатору цифрових послуг у своїй країні.}}\n\nКоманда {product_name}"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Підтвердь свою електронну адресу для звіту DSA",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Підтвердь свою електронну адресу для {product_name}",
|
||||
"body": "Привіт, {username},\n\nБудь ласка, підтвердь електронну адресу свого акаунта {product_name}, натиснувши посилання нижче:\n\n{verifyUrl}\n\nЯкщо акаунт {product_name} не твій, можеш сміливо проігнорувати цей лист.\n\nЦе посилання діє протягом 24 годин.\n\n– Команда {product_name}"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {Ми скасували рішення щодо твого акаунта {product_name}} other {Обмеження твого акаунта {product_name} більше не діє}}",
|
||||
"body": "Привіт, {username},\n\n{outcome, select, reversed {{decision_id, select, null {Ми повторно розглянули рішення щодо твого акаунта і скасували його.} other {Ми повторно розглянули рішення {decision_id} і скасували його.}} Просимо вибачення за незручності.} other {{decision_id, select, null {Ми зняли обмеження з твого акаунта.} other {Ми зняли обмеження, накладене рішенням {decision_id}.}}}}{restores_access, select, yes { Ти знову можеш входити в акаунт і користуватися {product_name}.} other {}}{statement, select, null {} other {\n\nКоментар нашої команди: {statement}}}{content_removed, select, yes {\n\nВидалений вміст відновити не можна.} other {}}\n\nКоманда {product_name}"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {Інформація про твій акаунт {product_name}} other {{kind_label, select, warning {Попередження щодо твого акаунта {product_name}} suspension {Твій акаунт {product_name} тимчасово заблоковано} ban {Твій акаунт {product_name} заблоковано} closure {Твій акаунт {product_name} закрито} feature_limit {Деякі функції твого акаунта {product_name} обмежено} content_removal {Ми видалили твої повідомлення в {product_name}} profile_reset {Ми скинули частину даних твого профілю {product_name}} community_removal {Ми видалили твою спільноту в {product_name}} bot {Інформація про твого бота в {product_name}} other {Інформація про твій акаунт {product_name}}}}}",
|
||||
"body": "Привіт, {username},\n\n{summary}\n\n{effects}\n\nЧому\nПравило: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nКоментар нашої команди: {statement}}}\n\nЯк ми ухвалили рішення\n{how_we_decided}{appeals_email, select, null {} other {\n\nЯк оскаржити рішення\nЯкщо ти вважаєш, що ми помилилися, напиши на {appeals_email} до {appeal_deadline}. За можливості пиши з електронної адреси, прив'язаної до твого акаунта, і вкажи ID рішення. Апеляції безкоштовні, і кожну з них розглядає людина.{redress, select, null {} other {\n\n{redress}}}}}\n\nID рішення: {decision_id}\n\nКоманда {product_name}"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Видалено переваги активованого подарунка",
|
||||
"body": "Привіт, {username},\n\nПодарунковий код, активований на твоєму акаунті, спочатку оплатила інша людина. Цей платіж згодом скасовано (повернення коштів).\n\nЧерез це видалено переваги, які були додані до твого акаунта під час активації подарунка.\n\n{support_email, select, null {Якщо ти вважаєш, що це помилка, будь ласка, зв'яжися з адміністраторами цього екземпляра та надай будь-які деталі про подарунковий код та час його активації.} other {Якщо ти вважаєш, що це помилка, будь ласка, зв'яжися з нашою службою підтримки та надай будь-які деталі про подарунковий код та час його активації.}}\n\n– Команда {product_name}"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "Твій звіт у {product_name} розглянуто",
|
||||
"body": "Привіт, {username},\n\nНаша команда безпеки розглянула твій звіт (ID: {reportId}).{hasComment, select, yes {\n\nВідповідь команди безпеки:\n{publicComment}} other {}}\n\nДякуємо, що допомагаєш зберігати {product_name} безпечним для всіх. Ми серйозно ставимося до кожного звіту та цінуємо твій внесок у спільноту.\n\n{safety_email, select, null {Якщо у тебе є запитання чи зауваження щодо цього рішення, звернися до адміністраторів цього екземпляра.} other {Якщо у тебе є запитання чи зауваження щодо цього рішення, напиши на {safety_email}.}}\n\n– Команда безпеки {product_name}"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"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Якщо ти вважаєш, що це рішення було неправильним, ти можеш подати апеляцію. {appeals_email, select, null {Звернися до адміністраторів цього екземпляра.} other {Надішли електронний лист на {appeals_email} з цієї електронної адреси.}}\n\n– Команда безпеки {product_name}"
|
||||
"body": "Привіт, {username},\n\nМи розглянули твою скаргу (ID: {reportId}).\n\n{outcome, select, removed {Ми видалили вміст, указаний у твоїй скарзі, бо він порушував наші правила.} restricted {Ми встановили, що акаунт, указаний у твоїй скарзі, порушив наші правила, і обмежили його. Ми не повідомляємо, яких заходів ужили щодо іншого акаунта, бо це персональні дані його власника.} action_taken {Ми встановили, що вказане у твоїй скарзі порушило наші правила, і вжили заходів. Ми не повідомляємо, яких заходів ужили щодо іншого акаунта, бо це персональні дані його власника.} no_violation {Ми встановили, що вміст або акаунт, указаний у твоїй скарзі, не порушує наших правил, тому залишили все як є.} other {Ми закрили твою скаргу.}}{hasComment, select, yes {\n\nКоментар нашої команди: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nЯкщо рішення здається тобі неправильним, напиши на {appeals_email} протягом 60 днів і вкажи ID скарги. Якщо ти перебуваєш у ЄС, ти також можеш передати спір до сертифікованого органу позасудового врегулювання спорів або подати скаргу координатору цифрових послуг у своїй країні.}}\n\nКоманда {product_name}"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "Видалення акаунта {product_name} заплановано",
|
||||
"body": "Привіт, {username},\n\nМи отримали твій запит на видалення акаунта {product_name}. Остаточне видалення твого акаунта заплановано на:\n\n{deletionDate, date, full} о {deletionDate, time, short}\n\nЯкщо цей запит не від тебе, увійди до свого акаунта, щоб скасувати видалення. Ми також рекомендуємо змінити пароль, щоб захистити акаунт.\n\n– Команда {product_name}"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "Призупинення твого акаунта {product_name} скасовано",
|
||||
"body": "Привіт, {username},\n\nГарні новини: призупинення твого акаунта {product_name} скасовано.\n\n{reason, select,\n null {}\n other {Причина: {reason}\n\n}\n}Тепер ти можеш знову увійти та продовжувати користуватися {product_name} як зазвичай.\n\n– Команда безпеки {product_name}"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"subject": "Tài khoản {product_name} của bạn sẽ bị xóa do không hoạt động",
|
||||
"body": "Xin chào {username},\n\nTài khoản {product_name} của bạn đã không hoạt động trong thời gian dài, vì vậy tài khoản được lên lịch xóa vĩnh viễn vào:\n\n{deletionDate, date, full} lúc {deletionDate, time, short}{reason, select, null {} other {\n\nLý do: {reason}}}\n\n{safety_email, select, null {Nếu bạn muốn giữ tài khoản, hãy liên hệ quản trị viên của máy chủ này trước ngày đó.} other {Nếu bạn muốn giữ tài khoản, hãy liên hệ {safety_email} từ địa chỉ email này trước ngày đó.}}\n\n– Đội ngũ {product_name}"
|
||||
"body": "Xin chào {username},\n\nTài khoản {product_name} của bạn đã không hoạt động trong thời gian dài, vì vậy chúng tôi đã lên lịch xóa tài khoản vào {deletion_date}.\n\nĐể giữ lại tài khoản, hãy đăng nhập trước ngày đó. {support_email, select, null {Nếu bạn có câu hỏi, hãy liên hệ quản trị viên của máy chủ này.} other {Nếu bạn có câu hỏi, hãy liên hệ {support_email}.}}\n\nĐội ngũ {product_name}"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"subject": "Tài khoản {product_name} của bạn đã được lên lịch xóa",
|
||||
"body": "Xin chào {username},\n\nTheo yêu cầu của bạn, tài khoản {product_name} của bạn được lên lịch xóa vĩnh viễn vào:\n\n{deletionDate, date, full} lúc {deletionDate, time, short}{reason, select, null {} other {\n\nLý do: {reason}}}\n\nTài khoản của bạn sẽ bị khóa cho đến lúc đó. {safety_email, select, null {Nếu bạn không yêu cầu điều này, hoặc bạn muốn giữ tài khoản, hãy liên hệ quản trị viên của máy chủ này trước ngày đó.} other {Nếu bạn không yêu cầu điều này, hoặc bạn muốn giữ tài khoản, hãy liên hệ {safety_email} từ địa chỉ email này trước ngày đó.}}\n\n– Đội ngũ {product_name}"
|
||||
},
|
||||
"account_scheduled_deletion": {
|
||||
"subject": "Tài khoản {product_name} của bạn sẽ bị xóa vĩnh viễn",
|
||||
"body": "Xin chào {username},\n\nTài khoản {product_name} của bạn đã được lên lịch xóa vĩnh viễn do vi phạm Điều khoản dịch vụ hoặc Nguyên tắc cộng đồng của chúng tôi.\n\nThời gian xóa theo lịch: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Lý do: {reason}\n\n}\n}Đây là một biện pháp xử lý nghiêm trọng. Dữ liệu tài khoản của bạn sẽ bị xóa vĩnh viễn vào ngày đã lên lịch.\n\n{legalLinks, select,\n both {Vui lòng xem lại:\n- Điều khoản dịch vụ: {termsUrl}\n- Nguyên tắc cộng đồng: {guidelinesUrl}\n\n}\n terms {Vui lòng xem lại:\n- Điều khoản dịch vụ: {termsUrl}\n\n}\n guidelines {Vui lòng xem lại:\n- Nguyên tắc cộng đồng: {guidelinesUrl}\n\n}\n other {}\n}Quy trình khiếu nại:\nNếu bạn cho rằng quyết định này không chính xác hoặc không hợp lý, bạn có 60 ngày để gửi khiếu nại. {appeals_email, select, null {Liên hệ quản trị viên của máy chủ này.} other {Gửi email đến {appeals_email} từ địa chỉ email này.}}\n\nTrong khiếu nại của bạn:\n- Giải thích rõ ràng lý do bạn cho rằng quyết định này không chính xác hoặc không hợp lý\n- Cung cấp bằng chứng hoặc thông tin liên quan nếu có\n\nMột thành viên của Đội ngũ An toàn {product_name} sẽ xem xét khiếu nại của bạn và có thể tạm dừng việc xóa đang chờ xử lý cho đến khi có quyết định cuối cùng.\n\n– Đội ngũ An toàn {product_name}"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "Tài khoản {product_name} của bạn đã bị đình chỉ tạm thời",
|
||||
"body": "Xin chào {username},\n\nTài khoản {product_name} của bạn đã bị đình chỉ tạm thời do vi phạm Điều khoản dịch vụ hoặc Nguyên tắc cộng đồng của chúng tôi.\n\nThời gian: {durationHours, plural,\n =1 {1 giờ}\n other {# giờ}\n}\nBị đình chỉ cho đến: {bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {Lý do: {reason}\n\n}\n}Trong thời gian này, bạn sẽ không thể truy cập tài khoản của mình.\n\n{legalLinks, select,\n both {Vui lòng xem lại:\n- Điều khoản dịch vụ: {termsUrl}\n- Nguyên tắc cộng đồng: {guidelinesUrl}\n\n}\n terms {Vui lòng xem lại:\n- Điều khoản dịch vụ: {termsUrl}\n\n}\n guidelines {Vui lòng xem lại:\n- Nguyên tắc cộng đồng: {guidelinesUrl}\n\n}\n other {}\n}Nếu bạn cho rằng quyết định này không chính xác hoặc không hợp lý, bạn có thể gửi khiếu nại. {appeals_email, select, null {Liên hệ quản trị viên của máy chủ này và giải thích rõ ràng lý do bạn cho rằng quyết định đó không chính xác.} other {Gửi email đến {appeals_email} từ địa chỉ email này và giải thích rõ ràng lý do bạn cho rằng quyết định đó không chính xác.}} Chúng tôi sẽ xem xét khiếu nại của bạn và thông báo quyết định của chúng tôi cho bạn.\n\n– Đội ngũ An toàn {product_name}"
|
||||
"body": "Xin chào {username},\n\nTheo yêu cầu của bạn, chúng tôi đã lên lịch xóa tài khoản {product_name} của bạn vào {deletion_date}.\n\nĐể giữ lại tài khoản, hãy đăng nhập trước {deletion_date}. {safety_email, select, null {Nếu bạn không yêu cầu việc này, hãy liên hệ quản trị viên của máy chủ này.} other {Nếu bạn không yêu cầu việc này, hãy liên hệ {safety_email}.}}\n\nĐội ngũ {product_name}"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "Cảm ơn bạn đã ủng hộ {product_name}",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "Chúng tôi đã có quyết định về báo cáo của bạn trên {product_name}",
|
||||
"body": "Xin chào,\n\nĐội ngũ An toàn của chúng tôi đã xem xét báo cáo theo Đạo luật Dịch vụ kỹ thuật số của bạn (ID: {reportId}) và đã đưa ra quyết định.{hasComment, select, yes {\n\nPhản hồi từ Đội ngũ An toàn:\n{publicComment}} other {}}\n\nChúng tôi không chia sẻ chi tiết về biện pháp áp dụng với tài khoản của người khác, vì đó là dữ liệu cá nhân của họ.\n\nNếu không đồng ý với quyết định này, bạn có thể gửi khiếu nại miễn phí trong vòng 60 ngày. {appeals_email, select, null {Liên hệ quản trị viên của máy chủ này, ghi rõ ID báo cáo của bạn và giải thích lý do bạn cho rằng quyết định đó không chính xác.} other {Gửi email đến {appeals_email} từ địa chỉ email này, ghi rõ ID báo cáo của bạn và giải thích lý do bạn cho rằng quyết định đó không chính xác.}} Nếu bạn ở EU, bạn cũng có thể đưa tranh chấp đến một cơ quan giải quyết tranh chấp ngoài tòa án đã được chứng nhận. Những điều này không ảnh hưởng đến quyền khởi kiện ra tòa của bạn.\n\n– Đội ngũ An toàn {product_name}"
|
||||
"body": "Xin chào,\n\nChúng tôi đã xem xét báo cáo của bạn (ID: {reportId}).\n\n{outcome, select, removed {Chúng tôi đã xóa nội dung bạn báo cáo vì nội dung đó vi phạm quy tắc của chúng tôi.} restricted {Chúng tôi xác định tài khoản bạn báo cáo đã vi phạm quy tắc của chúng tôi và đã hạn chế tài khoản đó. Chúng tôi không chia sẻ biện pháp đã áp dụng với tài khoản khác, vì đó là dữ liệu cá nhân của họ.} action_taken {Chúng tôi xác định những gì bạn báo cáo đã vi phạm quy tắc của chúng tôi và đã có biện pháp xử lý. Chúng tôi không chia sẻ biện pháp đã áp dụng với tài khoản khác, vì đó là dữ liệu cá nhân của họ.} no_violation {Chúng tôi xác định nội dung hoặc tài khoản bạn báo cáo không vi phạm quy tắc của chúng tôi, vì vậy chúng tôi giữ nguyên.} other {Chúng tôi đã đóng báo cáo của bạn.}}{hasComment, select, yes {\n\nGhi chú từ đội ngũ của chúng tôi: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nNếu bạn không đồng ý, hãy gửi email đến {appeals_email} trong vòng 60 ngày và ghi kèm ID báo cáo. Nếu bạn ở EU, bạn cũng có thể đưa việc này đến một cơ quan giải quyết tranh chấp ngoài tòa án đã được chứng nhận hoặc khiếu nại với Điều phối viên Dịch vụ Kỹ thuật số tại quốc gia của bạn.}}\n\nĐội ngũ {product_name}"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "Xác minh email của bạn cho báo cáo DSA",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "Xác minh địa chỉ email của bạn trên {product_name}",
|
||||
"body": "Xin chào {username},\n\nVui lòng xác minh địa chỉ email cho tài khoản {product_name} của bạn bằng cách nhấp vào liên kết bên dưới:\n\n{verifyUrl}\n\nNếu bạn không tạo tài khoản {product_name}, bạn có thể bỏ qua email này.\n\nLiên kết này có hiệu lực trong 24 giờ.\n\n– Đội ngũ {product_name}"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {Chúng tôi đã đảo ngược một quyết định về tài khoản {product_name} của bạn} other {Một hạn chế trên tài khoản {product_name} của bạn đã kết thúc}}",
|
||||
"body": "Xin chào {username},\n\n{outcome, select, reversed {{decision_id, select, null {Chúng tôi đã xem xét lại một quyết định về tài khoản của bạn và đã đảo ngược quyết định đó.} other {Chúng tôi đã xem xét lại quyết định {decision_id} và đã đảo ngược quyết định đó.}} Chúng tôi xin lỗi vì sự phiền toái này.} other {{decision_id, select, null {Chúng tôi đã gỡ bỏ hạn chế trên tài khoản của bạn.} other {Chúng tôi đã gỡ bỏ hạn chế từ quyết định {decision_id}.}}}}{restores_access, select, yes { Bạn có thể đăng nhập và sử dụng {product_name} trở lại.} other {}}{statement, select, null {} other {\n\nGhi chú từ đội ngũ của chúng tôi: {statement}}}{content_removed, select, yes {\n\nNội dung chúng tôi đã xóa không thể khôi phục.} other {}}\n\nĐội ngũ {product_name}"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {Thông báo về tài khoản {product_name} của bạn} other {{kind_label, select, warning {Cảnh báo về tài khoản {product_name} của bạn} suspension {Tài khoản {product_name} của bạn bị đình chỉ} ban {Tài khoản {product_name} của bạn bị cấm} closure {Tài khoản {product_name} của bạn đã bị đóng} feature_limit {Một số tính năng trên tài khoản {product_name} của bạn bị giới hạn} content_removal {Chúng tôi đã xóa tin nhắn bạn gửi trên {product_name}} profile_reset {Chúng tôi đã đặt lại một phần hồ sơ {product_name} của bạn} community_removal {Chúng tôi đã xóa cộng đồng của bạn trên {product_name}} bot {Thông báo về bot của bạn trên {product_name}} other {Thông báo về tài khoản {product_name} của bạn}}}}",
|
||||
"body": "Xin chào {username},\n\n{summary}\n\n{effects}\n\nLý do\nQuy tắc: {rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\nGhi chú từ đội ngũ của chúng tôi: {statement}}}\n\nCách chúng tôi quyết định\n{how_we_decided}{appeals_email, select, null {} other {\n\nCách kháng nghị\nNếu bạn cho rằng chúng tôi đã sai, hãy gửi email đến {appeals_email} chậm nhất vào {appeal_deadline}. Nếu có thể, hãy gửi từ địa chỉ email của tài khoản và ghi kèm ID quyết định. Kháng nghị là miễn phí và mỗi kháng nghị đều được một người trong đội ngũ của chúng tôi xem xét.{redress, select, null {} other {\n\n{redress}}}}}\n\nID quyết định: {decision_id}\n\nĐội ngũ {product_name}"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "Các đặc quyền từ quà tặng đã đổi của bạn đã bị gỡ bỏ",
|
||||
"body": "Xin chào {username},\n\nMã quà tặng bạn đã đổi vốn do người khác thanh toán. Khoản thanh toán đó sau đó đã bị đảo ngược (chargeback).\n\nVì lý do này, chúng tôi đã gỡ bỏ các đặc quyền đã được thêm vào tài khoản của bạn khi bạn đổi quà tặng.\n\n{support_email, select, null {Nếu bạn cho rằng đây là nhầm lẫn, vui lòng liên hệ quản trị viên của máy chủ này và cung cấp bất kỳ chi tiết nào bạn có về mã quà tặng và thời điểm bạn đổi mã đó.} other {Nếu bạn cho rằng đây là nhầm lẫn, vui lòng liên hệ đội ngũ hỗ trợ của chúng tôi và cung cấp bất kỳ chi tiết nào bạn có về mã quà tặng và thời điểm bạn đổi mã đó.}}\n\n– Đội ngũ {product_name}"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "Báo cáo {product_name} của bạn đã được xem xét",
|
||||
"body": "Xin chào {username},\n\nBáo cáo của bạn (ID: {reportId}) đã được Đội ngũ An toàn của chúng tôi xem xét.{hasComment, select, yes {\n\nPhản hồi từ Đội ngũ An toàn:\n{publicComment}} other {}}\n\nCảm ơn bạn đã giúp giữ cho {product_name} an toàn cho mọi người. Chúng tôi xem xét nghiêm túc mọi báo cáo và đánh giá cao đóng góp của bạn cho cộng đồng.\n\n{safety_email, select, null {Nếu bạn có bất kỳ câu hỏi hoặc lo ngại nào về kết quả này, vui lòng liên hệ quản trị viên của máy chủ này.} other {Nếu bạn có bất kỳ câu hỏi hoặc lo ngại nào về kết quả này, vui lòng liên hệ {safety_email}.}}\n\n– Đội ngũ An toàn {product_name}"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"subject": "Tài khoản {product_name} của bạn sẽ bị xóa vĩnh viễn",
|
||||
"body": "Xin chào {username},\n\nTài khoản {product_name} của bạn đã được lên lịch xóa vĩnh viễn.\n\nThời gian xóa theo lịch: {deletionDate, date, full} {deletionDate, time, short}\n\n{reason, select,\n null {}\n other {Lý do: {reason}\n\n}\n}Đây là một biện pháp xử lý nghiêm trọng. Dữ liệu tài khoản của bạn sẽ bị xóa vĩnh viễn vào ngày đã lên lịch.\n\nNếu bạn cho rằng quyết định này không chính xác, bạn có thể gửi khiếu nại. {appeals_email, select, null {Liên hệ quản trị viên của máy chủ này.} other {Gửi email đến {appeals_email} từ địa chỉ email này.}}\n\n– Đội ngũ An toàn {product_name}"
|
||||
"body": "Xin chào {username},\n\nChúng tôi đã xem xét báo cáo của bạn (ID: {reportId}).\n\n{outcome, select, removed {Chúng tôi đã xóa nội dung bạn báo cáo vì nội dung đó vi phạm quy tắc của chúng tôi.} restricted {Chúng tôi xác định tài khoản bạn báo cáo đã vi phạm quy tắc của chúng tôi và đã hạn chế tài khoản đó. Chúng tôi không chia sẻ biện pháp đã áp dụng với tài khoản khác, vì đó là dữ liệu cá nhân của họ.} action_taken {Chúng tôi xác định những gì bạn báo cáo đã vi phạm quy tắc của chúng tôi và đã có biện pháp xử lý. Chúng tôi không chia sẻ biện pháp đã áp dụng với tài khoản khác, vì đó là dữ liệu cá nhân của họ.} no_violation {Chúng tôi xác định nội dung hoặc tài khoản bạn báo cáo không vi phạm quy tắc của chúng tôi, vì vậy chúng tôi giữ nguyên.} other {Chúng tôi đã đóng báo cáo của bạn.}}{hasComment, select, yes {\n\nGhi chú từ đội ngũ của chúng tôi: {publicComment}} other {}}{appeals_email, select, null {} other {\n\nNếu bạn không đồng ý, hãy gửi email đến {appeals_email} trong vòng 60 ngày và ghi kèm ID báo cáo. Nếu bạn ở EU, bạn cũng có thể đưa việc này đến một cơ quan giải quyết tranh chấp ngoài tòa án đã được chứng nhận hoặc khiếu nại với Điều phối viên Dịch vụ Kỹ thuật số tại quốc gia của bạn.}}\n\nĐội ngũ {product_name}"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "Tài khoản {product_name} của bạn đã được lên lịch xóa",
|
||||
"body": "Xin chào {username},\n\nBạn đã yêu cầu xóa tài khoản {product_name} của mình. Tài khoản của bạn được lên lịch xóa vĩnh viễn vào:\n\n{deletionDate, date, full} lúc {deletionDate, time, short}\n\nNếu bạn không yêu cầu điều này, hãy đăng nhập vào tài khoản của bạn để hủy việc xóa. Chúng tôi cũng khuyên bạn nên thay đổi mật khẩu để bảo mật tài khoản của mình.\n\n– Đội ngũ {product_name}"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "Lệnh đình chỉ tài khoản {product_name} của bạn đã được gỡ bỏ",
|
||||
"body": "Xin chào {username},\n\nTin tốt: lệnh đình chỉ tài khoản {product_name} của bạn đã được gỡ bỏ.\n\n{reason, select,\n null {}\n other {Lý do: {reason}\n\n}\n}Bây giờ bạn có thể đăng nhập lại và tiếp tục sử dụng {product_name} như bình thường.\n\n– Đội ngũ An toàn {product_name}"
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -5,19 +5,11 @@
|
||||
},
|
||||
"account_deletion_scheduled_inactivity": {
|
||||
"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, select, null {如果你想保留账号,请在该日期前联系此实例的管理员。} other {如果你想保留账号,请在该日期前使用此邮箱地址联系 {safety_email}。}}\n\n– {product_name} 团队"
|
||||
"body": "你好 {username},\n\n你的 {product_name} 账号已长期未活动,因此我们已安排在 {deletion_date} 删除该账号。\n\n如果想保留账号,请在该日期前登录。{support_email, select, null {如有疑问,请联系此实例的管理员。} other {如有疑问,请联系 {support_email}。}}\n\n{product_name} 团队"
|
||||
},
|
||||
"account_deletion_scheduled_requested": {
|
||||
"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, select, null {如果你未请求此操作,或者想保留账号,请在该日期前联系此实例的管理员。} other {如果你未请求此操作,或者想保留账号,请在该日期前使用此邮箱地址联系 {safety_email}。}}\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{legalLinks, select,\n both {请查阅:\n- 服务条款:{termsUrl}\n- 社区准则:{guidelinesUrl}\n\n}\n terms {请查阅:\n- 服务条款:{termsUrl}\n\n}\n guidelines {请查阅:\n- 社区准则:{guidelinesUrl}\n\n}\n other {}\n}申诉流程:\n如果你认为此处罚决定不正确或不合理,你有 60 天时间提交申诉。{appeals_email, select, null {请联系此实例的管理员。} other {请使用此邮箱地址发送邮件至 {appeals_email}。}}\n\n在你的申诉中:\n- 清楚说明你认为处罚决定不正确或不合理的原因\n- 提供任何相关证据或背景信息\n\n{product_name} 安全团队成员将审核你的申诉,并可能在做出最终决定前暂停此次删除。\n\n– {product_name} 安全团队"
|
||||
},
|
||||
"account_temp_banned": {
|
||||
"subject": "你的 {product_name} 账号已被暂时封禁",
|
||||
"body": "你好 {username},\n\n你的 {product_name} 账号因违反我们的《服务条款》或《社区准则》已被暂时封禁。\n\n封禁时长:{durationHours, plural,\n =1 {1 小时}\n other {# 小时}\n}\n封禁至:{bannedUntil, date, full} {bannedUntil, time, short}\n\n{reason, select,\n null {}\n other {原因:{reason}\n\n}\n}在此期间,你将无法访问你的账号。\n\n{legalLinks, select,\n both {请查阅:\n- 服务条款:{termsUrl}\n- 社区准则:{guidelinesUrl}\n\n}\n terms {请查阅:\n- 服务条款:{termsUrl}\n\n}\n guidelines {请查阅:\n- 社区准则:{guidelinesUrl}\n\n}\n other {}\n}如果你认为此处罚决定不正确或不合理,你可以提交申诉。{appeals_email, select, null {请联系此实例的管理员,并清楚说明你认为该决定不正确的原因。} other {请使用此邮箱地址发送邮件至 {appeals_email},并清楚说明你认为该决定不正确的原因。}}我们会审核你的申诉,并将决定回复你。\n\n– {product_name} 安全团队"
|
||||
"body": "你好 {username},\n\n根据你的请求,我们已安排在 {deletion_date} 删除你的 {product_name} 账号。\n\n如果想保留账号,请在 {deletion_date} 前登录。{safety_email, select, null {如果你没有提出此请求,请联系此实例的管理员。} other {如果你没有提出此请求,请联系 {safety_email}。}}\n\n{product_name} 团队"
|
||||
},
|
||||
"donation_confirmation": {
|
||||
"subject": "感谢你的 {product_name} 捐赠",
|
||||
@@ -29,7 +21,7 @@
|
||||
},
|
||||
"dsa_report_resolved": {
|
||||
"subject": "我们已对你的 {product_name} 举报做出决定",
|
||||
"body": "你好,\n\n我们的安全团队已审核你根据《数字服务法》提交的举报(ID:{reportId})并做出决定。{hasComment, select, yes {\n\n安全团队的回复:\n{publicComment}} other {}}\n\n我们不会透露对他人账号所采取措施的详情,因为这属于对方的个人数据。\n\n如果你不同意此决定,可以在 60 天内免费提交申诉。{appeals_email, select, null {请联系此实例的管理员,注明你的举报 ID,并说明你认为该决定不正确的原因。} other {请使用此邮箱地址发送邮件至 {appeals_email},注明你的举报 ID,并说明你认为该决定不正确的原因。}}如果你身在欧盟,还可以将争议提交给经认证的庭外争议解决机构。以上内容均不影响你向法院提起诉讼的权利。\n\n– {product_name} 安全团队"
|
||||
"body": "你好,\n\n我们已审核你的举报(ID:{reportId})。\n\n{outcome, select, removed {你举报的内容违反了我们的规则,我们已将其删除。} restricted {我们确认你举报的账号违反了我们的规则,并已对其进行限制。我们不会透露对其他账号采取了什么措施,因为这属于对方的个人数据。} action_taken {我们确认你举报的对象违反了我们的规则,并已采取措施。我们不会透露对其他账号采取了什么措施,因为这属于对方的个人数据。} no_violation {我们确认你举报的内容或账号没有违反我们的规则,因此未做处理。} other {我们已结束处理你的举报。}}{hasComment, select, yes {\n\n团队留言:{publicComment}} other {}}{appeals_email, select, null {} other {\n\n如果你不同意,请在 60 天内发送邮件至 {appeals_email},并附上举报 ID。如果你身在欧盟,还可以将此事提交给经认证的庭外争议解决机构,或向你所在国家的数字服务协调员投诉。}}\n\n{product_name} 团队"
|
||||
},
|
||||
"dsa_report_verification": {
|
||||
"subject": "验证邮箱以提交 DSA 举报",
|
||||
@@ -51,6 +43,14 @@
|
||||
"subject": "验证你的 {product_name} 邮箱地址",
|
||||
"body": "你好 {username},\n\n请点击下方链接验证你的 {product_name} 账号邮箱地址:\n\n{verifyUrl}\n\n如果你未创建 {product_name} 账号,可以放心忽略此邮件。\n\n此链接 24 小时内有效。\n\n– {product_name} 团队"
|
||||
},
|
||||
"enforcement_ended": {
|
||||
"subject": "{outcome, select, reversed {我们撤销了关于你的 {product_name} 账号的一项决定} other {你的 {product_name} 账号的限制已结束}}",
|
||||
"body": "你好 {username},\n\n{outcome, select, reversed {{decision_id, select, null {我们重新审视了关于你账号的一项决定,并已将其撤销。} other {我们重新审视了决定 {decision_id},并已将其撤销。}}给你带来不便,我们深表歉意。} other {{decision_id, select, null {我们已解除你账号上的限制。} other {我们已解除决定 {decision_id} 中的限制。}}}}{restores_access, select, yes {你可以重新登录并使用 {product_name} 了。} other {}}{statement, select, null {} other {\n\n团队留言:{statement}}}{content_removed, select, yes {\n\n我们删除的内容无法恢复。} other {}}\n\n{product_name} 团队"
|
||||
},
|
||||
"enforcement_notice": {
|
||||
"subject": "{neutral, select, yes {关于你的 {product_name} 账号的通知} other {{kind_label, select, warning {关于你的 {product_name} 账号的警告} suspension {你的 {product_name} 账号已被暂时封禁} ban {你的 {product_name} 账号已被永久封禁} closure {你的 {product_name} 账号已关闭} feature_limit {你的 {product_name} 账号的部分功能已受限} content_removal {我们删除了你在 {product_name} 上发送的消息} profile_reset {我们重置了你的 {product_name} 个人资料的部分内容} community_removal {我们删除了你在 {product_name} 上的社区} bot {关于你在 {product_name} 上的机器人的通知} other {关于你的 {product_name} 账号的通知}}}}",
|
||||
"body": "你好 {username},\n\n{summary}\n\n{effects}\n\n原因\n规则:{rule_title}{rule_url, select, null {} other { ({rule_url})}}\n{facts}{statement, select, null {} other {\n\n团队留言:{statement}}}\n\n我们如何做出决定\n{how_we_decided}{appeals_email, select, null {} other {\n\n如何申诉\n如果你认为我们判断有误,请最迟于 {appeal_deadline} 发送邮件至 {appeals_email}。如果可以,请使用你账号绑定的邮箱地址发送,并附上决定 ID。申诉免费,每一项申诉都由专人审核。{redress, select, null {} other {\n\n{redress}}}}}\n\n决定 ID:{decision_id}\n\n{product_name} 团队"
|
||||
},
|
||||
"gift_chargeback_notification": {
|
||||
"subject": "已移除你兑换礼品所获得的福利",
|
||||
"body": "你好 {username},\n\n你兑换的礼品码最初由他人支付。该付款现已被撤销(退单)。\n\n因此,我们已移除你在兑换礼品时添加到你账号的福利。\n\n{support_email, select, null {如果你认为此操作有误,请联系此实例的管理员,并提供有关礼品码以及兑换时间的任何详细信息。} other {如果你认为此操作有误,请联系我们的支持团队,并提供有关礼品码以及兑换时间的任何详细信息。}}\n\n– {product_name} 团队"
|
||||
@@ -85,18 +85,10 @@
|
||||
},
|
||||
"report_resolved": {
|
||||
"subject": "你的 {product_name} 举报已完成审核",
|
||||
"body": "你好 {username},\n\n你的举报(ID:{reportId})已由我们的安全团队完成审核。{hasComment, select, yes {\n\n安全团队的回复:\n{publicComment}} other {}}\n\n感谢你帮助维护 {product_name} 的安全环境。我们会认真对待每一份举报,也感谢你为社区做出的贡献。\n\n{safety_email, select, null {如果你对此结果有任何疑问或意见,请联系此实例的管理员。} other {如果你对此结果有任何疑问或意见,请联系 {safety_email}。}}\n\n– {product_name} 安全团队"
|
||||
},
|
||||
"scheduled_deletion_notification": {
|
||||
"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如果你认为此处罚决定不正确,可以提交申诉。{appeals_email, select, null {请联系此实例的管理员。} other {请使用此邮箱地址发送邮件至 {appeals_email}。}}\n\n– {product_name} 安全团队"
|
||||
"body": "你好 {username},\n\n我们已审核你的举报(ID:{reportId})。\n\n{outcome, select, removed {你举报的内容违反了我们的规则,我们已将其删除。} restricted {我们确认你举报的账号违反了我们的规则,并已对其进行限制。我们不会透露对其他账号采取了什么措施,因为这属于对方的个人数据。} action_taken {我们确认你举报的对象违反了我们的规则,并已采取措施。我们不会透露对其他账号采取了什么措施,因为这属于对方的个人数据。} no_violation {我们确认你举报的内容或账号没有违反我们的规则,因此未做处理。} other {我们已结束处理你的举报。}}{hasComment, select, yes {\n\n团队留言:{publicComment}} other {}}{appeals_email, select, null {} other {\n\n如果你不同意,请在 60 天内发送邮件至 {appeals_email},并附上举报 ID。如果你身在欧盟,还可以将此事提交给经认证的庭外争议解决机构,或向你所在国家的数字服务协调员投诉。}}\n\n{product_name} 团队"
|
||||
},
|
||||
"self_deletion_scheduled": {
|
||||
"subject": "你的 {product_name} 账号已安排删除",
|
||||
"body": "你好 {username},\n\n你已申请删除你的 {product_name} 账号。账号将于以下时间永久删除:\n\n{deletionDate, date, full} {deletionDate, time, short}\n\n如果你未请求此操作,请登录账号取消删除。建议同时更改密码以保护账号安全。\n\n– {product_name} 团队"
|
||||
},
|
||||
"unban_notification": {
|
||||
"subject": "你的 {product_name} 账号已解除封禁",
|
||||
"body": "你好 {username},\n\n好消息:你的 {product_name} 账号已解除封禁。\n\n{reason, select,\n null {}\n other {原因:{reason}\n\n}\n}你现在可以重新登录,继续正常使用 {product_name}。\n\n– {product_name} 安全团队"
|
||||
}
|
||||
}
|
||||
Loaded 100 of 734 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user