Merge branch 'develop' into feat/embeds
This commit is contained in:
commit
c9e6fab8ea
307 changed files with 26403 additions and 22279 deletions
|
@ -35,7 +35,7 @@ port: 3000
|
|||
db:
|
||||
host: localhost
|
||||
port: 5432
|
||||
|
||||
#ssl: false
|
||||
# Database name
|
||||
db: calckey
|
||||
|
||||
|
@ -48,7 +48,9 @@ db:
|
|||
|
||||
# Extra Connection options
|
||||
#extra:
|
||||
# ssl: true
|
||||
# ssl:
|
||||
# host: localhost
|
||||
# rejectUnauthorized: false
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Redis configuration └─────────────────────────────────────
|
||||
|
@ -56,10 +58,14 @@ db:
|
|||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
#tls:
|
||||
# host: localhost
|
||||
# rejectUnauthorized: false
|
||||
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||
#pass: example-pass
|
||||
#prefix: example-prefix
|
||||
#db: 1
|
||||
#user: default
|
||||
|
||||
# Please configure either MeiliSearch *or* Sonic.
|
||||
# If both MeiliSearch and Sonic configurations are present, MeiliSearch will take precedence.
|
||||
|
|
|
@ -10,8 +10,12 @@ packages/backend/.idea/vcs.xml
|
|||
|
||||
# Node.js
|
||||
node_modules
|
||||
**/node_modules
|
||||
report.*.json
|
||||
|
||||
# Rust
|
||||
packages/backend/native-utils/target/*
|
||||
|
||||
# Cypress
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
|
@ -24,9 +28,6 @@ coverage
|
|||
!/.config/example.yml
|
||||
!/.config/docker_example.env
|
||||
|
||||
#docker dev config
|
||||
/dev/docker-compose.yml
|
||||
|
||||
# misskey
|
||||
built
|
||||
db
|
||||
|
@ -46,3 +47,4 @@ packages/backend/assets/instance.css
|
|||
# dockerignore custom
|
||||
.git
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"eslint.packageManager": "pnpm",
|
||||
"workspace.workspaceFolderCheckCwd": false
|
||||
}
|
30
CALCKEY.md
30
CALCKEY.md
|
@ -1,5 +1,8 @@
|
|||
# All the changes to Calckey from stock Misskey
|
||||
|
||||
> **Warning**
|
||||
> This list is incomplete. Please check the [Releases](https://codeberg.org/calckey/calckey/releases) and [Changelog](https://codeberg.org/calckey/calckey/src/branch/develop/CHANGELOG.md) for a more complete list of changes. There have been [>4000 commits (laggy link)](https://codeberg.org/calckey/calckey/compare/700a7110f7e34f314b070987aa761c451ec34efc...develop) since we forked Misskey!
|
||||
|
||||
## Planned
|
||||
|
||||
- Stucture
|
||||
|
@ -8,31 +11,25 @@
|
|||
- Rewrite backend in Rust and [Rocket](https://rocket.rs/)
|
||||
- Use [Magic RegExP](https://regexp.dev/) for RegEx 🦄
|
||||
- Function
|
||||
- User "choices" (recommended users) like Mastodon and Soapbox
|
||||
- User "choices" (recommended users) and featured hashtags like Mastodon and Soapbox
|
||||
- Join Reason system like Mastodon/Pleroma
|
||||
- Option to publicize server blocks
|
||||
- Build flag to remove NSFW/AI stuff
|
||||
- Filter notifications by user
|
||||
- Exclude self from antenna
|
||||
- More antenna options
|
||||
- Groups
|
||||
- Form
|
||||
- MFM button
|
||||
- Personal notes for all accounts
|
||||
- Fully revamp non-logged-in screen
|
||||
- Lookup/details for post/file/server
|
||||
- [Rat mode?](https://stop.voring.me/notes/933fx97bmd)
|
||||
|
||||
## Work in progress
|
||||
|
||||
- Weblate project
|
||||
- Customizable max note length
|
||||
- Link verification
|
||||
- Better Messaging UI
|
||||
- Better API Documentation
|
||||
- Remote follow button
|
||||
- Admin custom CSS
|
||||
- Add back time machine (jump to date)
|
||||
- Improve accesibility
|
||||
- Timeline filters
|
||||
- Events
|
||||
- Fully revamp non-logged-in screen
|
||||
|
||||
## Implemented
|
||||
|
||||
|
@ -73,7 +70,6 @@
|
|||
- Raw server info only for moderators
|
||||
- New spinner animation
|
||||
- Spinner instead of "Loading..."
|
||||
- SearchX instead of Google
|
||||
- Always signToActivityPubGet
|
||||
- Spacing on group items
|
||||
- Quotes have solid border
|
||||
|
@ -108,10 +104,6 @@
|
|||
- More antenna options
|
||||
- New dashboard
|
||||
- Backfill follower counts
|
||||
- Improved emoji licensing
|
||||
- This feature was ported from Misskey.
|
||||
- https://github.com/misskey-dev/misskey/commit/8ae9d2eaa8b0842671558370f787902e94b7f5a3: enhance: カスタム絵文字にライセンス情報を付与できるように
|
||||
- https://github.com/misskey-dev/misskey/commit/ed51209172441927d24339f0759a5badbee3c9b6: 絵文字のライセンスを表示できるように
|
||||
- Compile time compression
|
||||
- Sonic search
|
||||
- Popular color schemes, including Nord, Gruvbox, and Catppuccin
|
||||
|
@ -125,10 +117,14 @@
|
|||
- Focus trapping and button labels
|
||||
- Meilisearch with filters
|
||||
- Post editing
|
||||
- Display remaining time on rate-limits
|
||||
- Proper 2FA input dialog
|
||||
- Let moderators see moderation nodes
|
||||
- Non-mangled unicode emojis
|
||||
- Skin tone selection support
|
||||
|
||||
## Implemented (remote)
|
||||
|
||||
|
||||
- MissV: [fix Misskey Forkbomb](https://code.vtopia.live/Vtopia/MissV/commit/40b23c070bd4adbb3188c73546c6c625138fb3c1)
|
||||
- [Make showing ads optional](https://github.com/misskey-dev/misskey/pull/8996)
|
||||
- [Tapping avatar in mobile opens account modal](https://github.com/misskey-dev/misskey/pull/9056)
|
||||
|
|
9736
CHANGELOG.md
9736
CHANGELOG.md
File diff suppressed because it is too large
Load diff
46
Dockerfile
46
Dockerfile
|
@ -1,10 +1,19 @@
|
|||
## Install dev and compilation dependencies, build files
|
||||
FROM node:19-alpine as build
|
||||
FROM alpine:3.18 as build
|
||||
WORKDIR /calckey
|
||||
|
||||
# Install compilation dependencies
|
||||
RUN apk update
|
||||
RUN apk add --no-cache --no-progress git alpine-sdk python3 rust cargo vips
|
||||
RUN apk add --no-cache --no-progress git alpine-sdk python3 nodejs-current npm rust cargo vips
|
||||
|
||||
# Copy only the cargo dependency-related files first, to cache efficiently
|
||||
COPY packages/backend/native-utils/Cargo.toml packages/backend/native-utils/Cargo.toml
|
||||
COPY packages/backend/native-utils/Cargo.lock packages/backend/native-utils/Cargo.lock
|
||||
COPY packages/backend/native-utils/src/lib.rs packages/backend/native-utils/src/
|
||||
COPY packages/backend/native-utils/migration/Cargo.toml packages/backend/native-utils/migration/Cargo.toml
|
||||
COPY packages/backend/native-utils/migration/src/lib.rs packages/backend/native-utils/migration/src/
|
||||
|
||||
# Install cargo dependencies
|
||||
RUN cargo fetch --locked --manifest-path /calckey/packages/backend/native-utils/Cargo.toml
|
||||
|
||||
# Copy only the dependency-related files first, to cache efficiently
|
||||
COPY package.json pnpm*.yaml ./
|
||||
|
@ -16,27 +25,31 @@ COPY packages/backend/native-utils/package.json packages/backend/native-utils/pa
|
|||
COPY packages/backend/native-utils/npm/linux-x64-musl/package.json packages/backend/native-utils/npm/linux-x64-musl/package.json
|
||||
COPY packages/backend/native-utils/npm/linux-arm64-musl/package.json packages/backend/native-utils/npm/linux-arm64-musl/package.json
|
||||
|
||||
# Configure corepack and pnpm
|
||||
RUN corepack enable
|
||||
RUN corepack prepare pnpm@latest --activate
|
||||
# Configure corepack and pnpm, and install dev mode dependencies for compilation
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate && pnpm i --frozen-lockfile
|
||||
|
||||
# Install dev mode dependencies for compilation
|
||||
RUN pnpm i --frozen-lockfile
|
||||
# Copy in the rest of the native-utils rust files
|
||||
COPY packages/backend/native-utils/.cargo packages/backend/native-utils/.cargo
|
||||
COPY packages/backend/native-utils/build.rs packages/backend/native-utils/
|
||||
COPY packages/backend/native-utils/src packages/backend/native-utils/src/
|
||||
COPY packages/backend/native-utils/migration/src packages/backend/native-utils/migration/src/
|
||||
|
||||
# Copy in the rest of the files, to compile from TS to JS
|
||||
# Compile native-utils
|
||||
RUN pnpm run --filter native-utils build
|
||||
|
||||
# Copy in the rest of the files to compile
|
||||
COPY . ./
|
||||
RUN pnpm run build
|
||||
RUN env NODE_ENV=production sh -c "pnpm run --filter '!native-utils' build && pnpm run gulp"
|
||||
|
||||
# Trim down the dependencies to only the prod deps
|
||||
# Trim down the dependencies to only those for production
|
||||
RUN pnpm i --prod --frozen-lockfile
|
||||
|
||||
|
||||
## Runtime container
|
||||
FROM node:19-alpine
|
||||
FROM alpine:3.18
|
||||
WORKDIR /calckey
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache --no-progress tini ffmpeg vips-dev zip unzip rust cargo
|
||||
RUN apk add --no-cache --no-progress tini ffmpeg vips-dev zip unzip nodejs-current
|
||||
|
||||
COPY . ./
|
||||
|
||||
|
@ -52,8 +65,9 @@ COPY --from=build /calckey/built /calckey/built
|
|||
COPY --from=build /calckey/packages/backend/built /calckey/packages/backend/built
|
||||
COPY --from=build /calckey/packages/backend/assets/instance.css /calckey/packages/backend/assets/instance.css
|
||||
COPY --from=build /calckey/packages/backend/native-utils/built /calckey/packages/backend/native-utils/built
|
||||
COPY --from=build /calckey/packages/backend/native-utils/target /calckey/packages/backend/native-utils/target
|
||||
|
||||
RUN corepack enable
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
ENV NODE_ENV=production
|
||||
VOLUME "/calckey/files"
|
||||
ENTRYPOINT [ "/sbin/tini", "--" ]
|
||||
CMD [ "pnpm", "run", "migrateandstart" ]
|
||||
|
|
26
README.md
26
README.md
|
@ -49,17 +49,26 @@
|
|||
|
||||
# 🥂 Links
|
||||
|
||||
- 💸 OpenCollective: <https://opencollective.com/Calckey>
|
||||
- 💸 Liberapay: <https://liberapay.com/ThatOneCalculator>
|
||||
### Want to get involved? Great!
|
||||
|
||||
- If you have the means to, [donations](https://opencollective.com/Calckey) are a great way to keep us going.
|
||||
- If you know how to program in TypeScript, Vue, or Rust, read the [contributing](./CONTRIBUTING.md) document.
|
||||
- If you know a non-English language, translating Calckey on [Weblate](https://hosted.weblate.org/engage/calckey/) help bring Calckey to more people. No technical experience needed!
|
||||
- Want to write/report about us, have any professional inquiries, or just have questions to ask? Contact us [here!](https://calckey.org/contact/)
|
||||
|
||||
### All links
|
||||
|
||||
- 🌐 Homepage: <https://calckey.org>
|
||||
- 💸 Donations:
|
||||
- OpenCollective: <https://opencollective.com/Calckey>
|
||||
- Liberapay: <https://liberapay.com/ThatOneCalculator>
|
||||
- Donate publicly to get your name on the Patron list!
|
||||
- 🚢 Flagship server: <https://calckey.social>
|
||||
- 📣 Official account: <https://i.calckey.cloud/@calckey>
|
||||
- 💁 Matrix support room: <https://matrix.to/#/#calckey:matrix.fedibird.com>
|
||||
- 📜 Server list: <https://calckey.fediverse.observer/list>
|
||||
- 📖 JoinFediverse Wiki: <https://joinfediverse.wiki/What_is_Calckey%3F>
|
||||
- 🐋 Docker Hub: <https://hub.docker.com/r/thatonecalculator/calckey>
|
||||
- 📣 Official account: <https://i.calckey.cloud/@calckey>
|
||||
- 📜 Server list: <https://calckey.org/join>
|
||||
- ✍️ Weblate: <https://hosted.weblate.org/engage/calckey/>
|
||||
- 📦 Yunohost: <https://github.com/YunoHost-Apps/calckey_ynh>
|
||||
- ️️📬 Contact: <https://calckey.org/contact/>
|
||||
|
||||
# 🌠 Getting started
|
||||
|
||||
|
@ -86,6 +95,7 @@ If you have access to a server that supports one of the sources below, I recomme
|
|||
- 🍀 Nginx (recommended)
|
||||
- 🦦 Caddy
|
||||
- 🪶 Apache
|
||||
- ⚡ [libvips](https://www.libvips.org/)
|
||||
|
||||
### 😗 Optional dependencies
|
||||
|
||||
|
@ -97,7 +107,7 @@ If you have access to a server that supports one of the sources below, I recomme
|
|||
|
||||
### 🏗️ Build dependencies
|
||||
|
||||
- 🦀 At least [Rust](https://www.rust-lang.org/) v1.65.0
|
||||
- 🦀 At least [Rust](https://www.rust-lang.org/) v1.68.0
|
||||
- 🦬 C/C++ compiler & build tools
|
||||
- `build-essential` on Debian/Ubuntu Linux
|
||||
- `base-devel` on Arch Linux
|
||||
|
|
|
@ -137,7 +137,9 @@ db:
|
|||
|
||||
# Extra Connection options
|
||||
#extra:
|
||||
# ssl: true
|
||||
# ssl:
|
||||
# host: localhost
|
||||
# rejectUnauthorized: false
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Redis configuration └─────────────────────────────────────
|
||||
|
@ -153,6 +155,10 @@ redis:
|
|||
pass: {{ .Values.redis.auth.password | quote }}
|
||||
#prefix: example-prefix
|
||||
#db: 1
|
||||
#user: default
|
||||
#tls:
|
||||
# host: localhost
|
||||
# rejectUnauthorized: false
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Sonic configuration └─────────────────────────────────────
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { defineConfig } from 'cypress'
|
||||
import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
// We've imported your old cypress plugins here.
|
||||
// You may want to clean this up later by importing these.
|
||||
setupNodeEvents(on, config) {
|
||||
return require('./cypress/plugins/index.js')(on, config)
|
||||
},
|
||||
baseUrl: 'http://localhost:61812',
|
||||
},
|
||||
})
|
||||
e2e: {
|
||||
// We've imported your old cypress plugins here.
|
||||
// You may want to clean this up later by importing these.
|
||||
setupNodeEvents(on, config) {
|
||||
return require("./cypress/plugins/index.js")(on, config);
|
||||
},
|
||||
baseUrl: "http://localhost:61812",
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
describe('Before setup instance', () => {
|
||||
describe("Before setup instance", () => {
|
||||
beforeEach(() => {
|
||||
cy.resetState();
|
||||
});
|
||||
|
@ -9,31 +9,31 @@ describe('Before setup instance', () => {
|
|||
cy.wait(1000);
|
||||
});
|
||||
|
||||
it('successfully loads', () => {
|
||||
cy.visit('/');
|
||||
});
|
||||
it("successfully loads", () => {
|
||||
cy.visit("/");
|
||||
});
|
||||
|
||||
it('setup instance', () => {
|
||||
cy.visit('/');
|
||||
it("setup instance", () => {
|
||||
cy.visit("/");
|
||||
|
||||
cy.intercept('POST', '/api/admin/accounts/create').as('signup');
|
||||
|
||||
cy.get('[data-cy-admin-username] input').type('admin');
|
||||
cy.get('[data-cy-admin-password] input').type('admin1234');
|
||||
cy.get('[data-cy-admin-ok]').click();
|
||||
cy.intercept("POST", "/api/admin/accounts/create").as("signup");
|
||||
|
||||
cy.get("[data-cy-admin-username] input").type("admin");
|
||||
cy.get("[data-cy-admin-password] input").type("admin1234");
|
||||
cy.get("[data-cy-admin-ok]").click();
|
||||
|
||||
// なぜか動かない
|
||||
//cy.wait('@signup').should('have.property', 'response.statusCode');
|
||||
cy.wait('@signup');
|
||||
});
|
||||
cy.wait("@signup");
|
||||
});
|
||||
});
|
||||
|
||||
describe('After setup instance', () => {
|
||||
describe("After setup instance", () => {
|
||||
beforeEach(() => {
|
||||
cy.resetState();
|
||||
|
||||
// インスタンス初期セットアップ
|
||||
cy.registerUser('admin', 'pass', true);
|
||||
cy.registerUser("admin", "pass", true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -42,34 +42,34 @@ describe('After setup instance', () => {
|
|||
cy.wait(1000);
|
||||
});
|
||||
|
||||
it('successfully loads', () => {
|
||||
cy.visit('/');
|
||||
});
|
||||
it("successfully loads", () => {
|
||||
cy.visit("/");
|
||||
});
|
||||
|
||||
it('signup', () => {
|
||||
cy.visit('/');
|
||||
it("signup", () => {
|
||||
cy.visit("/");
|
||||
|
||||
cy.intercept('POST', '/api/signup').as('signup');
|
||||
cy.intercept("POST", "/api/signup").as("signup");
|
||||
|
||||
cy.get('[data-cy-signup]').click();
|
||||
cy.get('[data-cy-signup-username] input').type('alice');
|
||||
cy.get('[data-cy-signup-password] input').type('alice1234');
|
||||
cy.get('[data-cy-signup-password-retype] input').type('alice1234');
|
||||
cy.get('[data-cy-signup-submit]').click();
|
||||
cy.get("[data-cy-signup]").click();
|
||||
cy.get("[data-cy-signup-username] input").type("alice");
|
||||
cy.get("[data-cy-signup-password] input").type("alice1234");
|
||||
cy.get("[data-cy-signup-password-retype] input").type("alice1234");
|
||||
cy.get("[data-cy-signup-submit]").click();
|
||||
|
||||
cy.wait('@signup');
|
||||
});
|
||||
cy.wait("@signup");
|
||||
});
|
||||
});
|
||||
|
||||
describe('After user signup', () => {
|
||||
describe("After user signup", () => {
|
||||
beforeEach(() => {
|
||||
cy.resetState();
|
||||
|
||||
// インスタンス初期セットアップ
|
||||
cy.registerUser('admin', 'pass', true);
|
||||
cy.registerUser("admin", "pass", true);
|
||||
|
||||
// ユーザー作成
|
||||
cy.registerUser('alice', 'alice1234');
|
||||
cy.registerUser("alice", "alice1234");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -78,51 +78,53 @@ describe('After user signup', () => {
|
|||
cy.wait(1000);
|
||||
});
|
||||
|
||||
it('successfully loads', () => {
|
||||
cy.visit('/');
|
||||
});
|
||||
it("successfully loads", () => {
|
||||
cy.visit("/");
|
||||
});
|
||||
|
||||
it('signin', () => {
|
||||
cy.visit('/');
|
||||
it("signin", () => {
|
||||
cy.visit("/");
|
||||
|
||||
cy.intercept('POST', '/api/signin').as('signin');
|
||||
cy.intercept("POST", "/api/signin").as("signin");
|
||||
|
||||
cy.get('[data-cy-signin]').click();
|
||||
cy.get('[data-cy-signin-username] input').type('alice');
|
||||
cy.get("[data-cy-signin]").click();
|
||||
cy.get("[data-cy-signin-username] input").type("alice");
|
||||
// Enterキーでサインインできるかの確認も兼ねる
|
||||
cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
|
||||
cy.get("[data-cy-signin-password] input").type("alice1234{enter}");
|
||||
|
||||
cy.wait('@signin');
|
||||
});
|
||||
cy.wait("@signin");
|
||||
});
|
||||
|
||||
it('suspend', function() {
|
||||
cy.request('POST', '/api/admin/suspend-user', {
|
||||
it("suspend", function () {
|
||||
cy.request("POST", "/api/admin/suspend-user", {
|
||||
i: this.admin.token,
|
||||
userId: this.alice.id,
|
||||
});
|
||||
|
||||
cy.visit('/');
|
||||
cy.visit("/");
|
||||
|
||||
cy.get('[data-cy-signin]').click();
|
||||
cy.get('[data-cy-signin-username] input').type('alice');
|
||||
cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
|
||||
cy.get("[data-cy-signin]").click();
|
||||
cy.get("[data-cy-signin-username] input").type("alice");
|
||||
cy.get("[data-cy-signin-password] input").type("alice1234{enter}");
|
||||
|
||||
// TODO: cypressにブラウザの言語指定できる機能が実装され次第英語のみテストするようにする
|
||||
cy.contains(/アカウントが凍結されています|This account has been suspended due to/gi);
|
||||
cy.contains(
|
||||
/アカウントが凍結されています|This account has been suspended due to/gi,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('After user singed in', () => {
|
||||
describe("After user singed in", () => {
|
||||
beforeEach(() => {
|
||||
cy.resetState();
|
||||
|
||||
// インスタンス初期セットアップ
|
||||
cy.registerUser('admin', 'pass', true);
|
||||
cy.registerUser("admin", "pass", true);
|
||||
|
||||
// ユーザー作成
|
||||
cy.registerUser('alice', 'alice1234');
|
||||
cy.registerUser("alice", "alice1234");
|
||||
|
||||
cy.login('alice', 'alice1234');
|
||||
cy.login("alice", "alice1234");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -131,17 +133,17 @@ describe('After user singed in', () => {
|
|||
cy.wait(1000);
|
||||
});
|
||||
|
||||
it('successfully loads', () => {
|
||||
cy.get('[data-cy-open-post-form]').should('be.visible');
|
||||
});
|
||||
it("successfully loads", () => {
|
||||
cy.get("[data-cy-open-post-form]").should("be.visible");
|
||||
});
|
||||
|
||||
it('note', () => {
|
||||
cy.get('[data-cy-open-post-form]').click();
|
||||
cy.get('[data-cy-post-form-text]').type('Hello, Misskey!');
|
||||
cy.get('[data-cy-open-post-form-submit]').click();
|
||||
it("note", () => {
|
||||
cy.get("[data-cy-open-post-form]").click();
|
||||
cy.get("[data-cy-post-form-text]").type("Hello, Misskey!");
|
||||
cy.get("[data-cy-open-post-form-submit]").click();
|
||||
|
||||
cy.contains('Hello, Misskey!');
|
||||
});
|
||||
cy.contains("Hello, Misskey!");
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: 投稿フォームの公開範囲指定のテスト
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
describe('After user signed in', () => {
|
||||
describe("After user signed in", () => {
|
||||
beforeEach(() => {
|
||||
cy.resetState();
|
||||
cy.viewport('macbook-16');
|
||||
cy.viewport("macbook-16");
|
||||
// インスタンス初期セットアップ
|
||||
cy.registerUser('admin', 'pass', true);
|
||||
cy.registerUser("admin", "pass", true);
|
||||
|
||||
// ユーザー作成
|
||||
cy.registerUser('alice', 'alice1234');
|
||||
cy.registerUser("alice", "alice1234");
|
||||
|
||||
cy.login('alice', 'alice1234');
|
||||
cy.login("alice", "alice1234");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -17,47 +17,47 @@ describe('After user signed in', () => {
|
|||
cy.wait(1000);
|
||||
});
|
||||
|
||||
it('widget edit toggle is visible', () => {
|
||||
cy.get('.mk-widget-edit').should('be.visible');
|
||||
});
|
||||
it("widget edit toggle is visible", () => {
|
||||
cy.get(".mk-widget-edit").should("be.visible");
|
||||
});
|
||||
|
||||
it('widget select should be visible in edit mode', () => {
|
||||
cy.get('.mk-widget-edit').click();
|
||||
cy.get('.mk-widget-select').should('be.visible');
|
||||
});
|
||||
it("widget select should be visible in edit mode", () => {
|
||||
cy.get(".mk-widget-edit").click();
|
||||
cy.get(".mk-widget-select").should("be.visible");
|
||||
});
|
||||
|
||||
it('first widget should be removed', () => {
|
||||
cy.get('.mk-widget-edit').click();
|
||||
cy.get('.customize-container:first-child .remove._button').click();
|
||||
cy.get('.customize-container').should('have.length', 2);
|
||||
it("first widget should be removed", () => {
|
||||
cy.get(".mk-widget-edit").click();
|
||||
cy.get(".customize-container:first-child .remove._button").click();
|
||||
cy.get(".customize-container").should("have.length", 2);
|
||||
});
|
||||
|
||||
function buildWidgetTest(widgetName) {
|
||||
it(`${widgetName} widget should get added`, () => {
|
||||
cy.get('.mk-widget-edit').click();
|
||||
cy.get('.mk-widget-select select').select(widgetName, { force: true });
|
||||
cy.get('.bg._modalBg.transparent').click({ multiple: true, force: true });
|
||||
cy.get('.mk-widget-add').click({ force: true });
|
||||
cy.get(`.mkw-${widgetName}`).should('exist');
|
||||
cy.get(".mk-widget-edit").click();
|
||||
cy.get(".mk-widget-select select").select(widgetName, { force: true });
|
||||
cy.get(".bg._modalBg.transparent").click({ multiple: true, force: true });
|
||||
cy.get(".mk-widget-add").click({ force: true });
|
||||
cy.get(`.mkw-${widgetName}`).should("exist");
|
||||
});
|
||||
}
|
||||
|
||||
buildWidgetTest('memo');
|
||||
buildWidgetTest('notifications');
|
||||
buildWidgetTest('timeline');
|
||||
buildWidgetTest('calendar');
|
||||
buildWidgetTest('rss');
|
||||
buildWidgetTest('trends');
|
||||
buildWidgetTest('clock');
|
||||
buildWidgetTest('activity');
|
||||
buildWidgetTest('photos');
|
||||
buildWidgetTest('digitalClock');
|
||||
buildWidgetTest('federation');
|
||||
buildWidgetTest('postForm');
|
||||
buildWidgetTest('slideshow');
|
||||
buildWidgetTest('serverMetric');
|
||||
buildWidgetTest('onlineUsers');
|
||||
buildWidgetTest('jobQueue');
|
||||
buildWidgetTest('button');
|
||||
buildWidgetTest('aiscript');
|
||||
buildWidgetTest("memo");
|
||||
buildWidgetTest("notifications");
|
||||
buildWidgetTest("timeline");
|
||||
buildWidgetTest("calendar");
|
||||
buildWidgetTest("rss");
|
||||
buildWidgetTest("trends");
|
||||
buildWidgetTest("clock");
|
||||
buildWidgetTest("activity");
|
||||
buildWidgetTest("photos");
|
||||
buildWidgetTest("digitalClock");
|
||||
buildWidgetTest("federation");
|
||||
buildWidgetTest("postForm");
|
||||
buildWidgetTest("slideshow");
|
||||
buildWidgetTest("serverMetric");
|
||||
buildWidgetTest("onlineUsers");
|
||||
buildWidgetTest("jobQueue");
|
||||
buildWidgetTest("button");
|
||||
buildWidgetTest("aiscript");
|
||||
});
|
||||
|
|
|
@ -16,6 +16,6 @@
|
|||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
};
|
||||
|
|
|
@ -24,32 +24,34 @@
|
|||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
|
||||
Cypress.Commands.add('resetState', () => {
|
||||
cy.window(win => {
|
||||
win.indexedDB.deleteDatabase('keyval-store');
|
||||
Cypress.Commands.add("resetState", () => {
|
||||
cy.window((win) => {
|
||||
win.indexedDB.deleteDatabase("keyval-store");
|
||||
});
|
||||
cy.request('POST', '/api/reset-db').as('reset');
|
||||
cy.get('@reset').its('status').should('equal', 204);
|
||||
cy.request("POST", "/api/reset-db").as("reset");
|
||||
cy.get("@reset").its("status").should("equal", 204);
|
||||
cy.reload(true);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => {
|
||||
const route = isAdmin ? '/api/admin/accounts/create' : '/api/signup';
|
||||
Cypress.Commands.add("registerUser", (username, password, isAdmin = false) => {
|
||||
const route = isAdmin ? "/api/admin/accounts/create" : "/api/signup";
|
||||
|
||||
cy.request('POST', route, {
|
||||
cy.request("POST", route, {
|
||||
username: username,
|
||||
password: password,
|
||||
}).its('body').as(username);
|
||||
})
|
||||
.its("body")
|
||||
.as(username);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('login', (username, password) => {
|
||||
cy.visit('/');
|
||||
Cypress.Commands.add("login", (username, password) => {
|
||||
cy.visit("/");
|
||||
|
||||
cy.intercept('POST', '/api/signin').as('signin');
|
||||
cy.intercept("POST", "/api/signin").as("signin");
|
||||
|
||||
cy.get('[data-cy-signin]').click();
|
||||
cy.get('[data-cy-signin-username] input').type(username);
|
||||
cy.get('[data-cy-signin-password] input').type(`${password}{enter}`);
|
||||
cy.get("[data-cy-signin]").click();
|
||||
cy.get("[data-cy-signin-username] input").type(username);
|
||||
cy.get("[data-cy-signin-password] input").type(`${password}{enter}`);
|
||||
|
||||
cy.wait('@signin').as('signedIn');
|
||||
cy.wait("@signin").as("signedIn");
|
||||
});
|
||||
|
|
|
@ -14,19 +14,21 @@
|
|||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
import "./commands";
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
|
||||
Cypress.on('uncaught:exception', (err, runnable) => {
|
||||
if ([
|
||||
// Chrome
|
||||
'ResizeObserver loop limit exceeded',
|
||||
Cypress.on("uncaught:exception", (err, runnable) => {
|
||||
if (
|
||||
[
|
||||
// Chrome
|
||||
"ResizeObserver loop limit exceeded",
|
||||
|
||||
// Firefox
|
||||
'ResizeObserver loop completed with undelivered notifications',
|
||||
].some(msg => err.message.includes(msg))) {
|
||||
// Firefox
|
||||
"ResizeObserver loop completed with undelivered notifications",
|
||||
].some((msg) => err.message.includes(msg))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -19,8 +19,6 @@ services:
|
|||
environment:
|
||||
NODE_ENV: production
|
||||
volumes:
|
||||
- ./.cargo-cache:/root/.cargo
|
||||
- ./.cargo-target:/calckey/packages/backend/native-utils/target
|
||||
- ./files:/calckey/files
|
||||
- ./.config:/calckey/.config:ro
|
||||
|
||||
|
|
5
docs/api-doc.md
Normal file
5
docs/api-doc.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# API Documentation
|
||||
|
||||
You can find interactive API documentation at any Calckey instance. https://calckey.social/api-doc
|
||||
|
||||
You can also find auto-generated documentation for calckey-js [here](../packages/calckey-js/markdown/calckey-js.md).
|
|
@ -1,18 +1,28 @@
|
|||
name: Bug Report
|
||||
name: 🐛 Bug Report
|
||||
about: File a bug report
|
||||
title: "[Bug]: "
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: 💁 Support Matrix
|
||||
url: https://matrix.to/#/%23calckey:matrix.fedibird.com
|
||||
about: Having trouble with deployment? Ask the support chat.
|
||||
- name: 🔒 Resposible Disclosure
|
||||
url: https://codeberg.org/calckey/calckey/src/branch/develop/SECURITY.md
|
||||
about: Found a security vulnerability? Please disclose it responsibly.
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
💖 Thanks for taking the time to fill out this bug report!
|
||||
💁 Having trouble with deployment? [Ask the support chat.](https://matrix.to/#/%23calckey:matrix.fedibird.com)
|
||||
🔒 Found a security vulnerability? [Please disclose it responsibly.](https://codeberg.org/calckey/calckey/src/branch/develop/SECURITY.md)
|
||||
🤝 By submitting this issue, you agree to follow our [Contribution Guidelines.](https://codeberg.org/calckey/calckey/src/branch/develop/CONTRIBUTING.md)
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Please give us a brief description of what happened.
|
||||
placeholder: Tell us what you see!
|
||||
value: "A bug happened!"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
@ -21,7 +31,6 @@ body:
|
|||
label: What did you expect to happen?
|
||||
description: Please give us a brief description of what you expected to happen.
|
||||
placeholder: Tell us what you wish happened!
|
||||
value: "Instead of x, y should happen instead!"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
@ -29,7 +38,7 @@ body:
|
|||
attributes:
|
||||
label: Version
|
||||
description: What version of calckey is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information.
|
||||
placeholder: Calckey Version 13.0.4
|
||||
placeholder: v13.1.4.1
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
@ -37,15 +46,26 @@ body:
|
|||
attributes:
|
||||
label: Instance
|
||||
description: What instance of calckey are you using?
|
||||
placeholder: stop.voring.me
|
||||
placeholder: calckey.social
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
id: issue-type
|
||||
attributes:
|
||||
label: What browser are you using?
|
||||
label: What type of issue is this?
|
||||
description: If this happens on your device and has to do with the user interface, it's client-side. If this happens on either with the API or the backend, or you got a server-side error in the client, it's server-side.
|
||||
multiple: false
|
||||
options:
|
||||
- Client-side
|
||||
- Server-side
|
||||
- Other (Please Specify)
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What browser are you using? (Client-side issues only)
|
||||
multiple: false
|
||||
options:
|
||||
- N/A
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Brave
|
||||
|
@ -54,6 +74,50 @@ body:
|
|||
- Safari
|
||||
- Microsoft Edge
|
||||
- Other (Please Specify)
|
||||
- type: dropdown
|
||||
id: device
|
||||
attributes:
|
||||
label: What operating system are you using? (Client-side issues only)
|
||||
multiple: false
|
||||
options:
|
||||
- N/A
|
||||
- Windows
|
||||
- MacOS
|
||||
- Linux
|
||||
- Android
|
||||
- iOS
|
||||
- Other (Please Specify)
|
||||
- type: dropdown
|
||||
id: deplotment-method
|
||||
attributes:
|
||||
label: How do you deploy Calckey on your server? (Server-side issues only)
|
||||
multiple: false
|
||||
options:
|
||||
- N/A
|
||||
- Manual
|
||||
- Ubuntu Install Script
|
||||
- Docker Compose
|
||||
- Docker Prebuilt Image
|
||||
- Helm Chart
|
||||
- YunoHost
|
||||
- AUR Package
|
||||
- Other (Please Specify)
|
||||
- type: dropdown
|
||||
id: operating-system
|
||||
attributes:
|
||||
label: What operating system are you using? (Server-side issues only)
|
||||
multiple: false
|
||||
options:
|
||||
- N/A
|
||||
- Ubuntu >= 22.04
|
||||
- Ubuntu < 22.04
|
||||
- Debian
|
||||
- Arch
|
||||
- RHEL (CentOS/AlmaLinux/Rocky Linux)
|
||||
- FreeBSD
|
||||
- OpenBSD
|
||||
- Android
|
||||
- Other (Please Specify)
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
|
|
|
@ -1,18 +1,28 @@
|
|||
name: Feature Request
|
||||
name: ✨ Feature Request
|
||||
about: Request a Feature
|
||||
title: "[Feature]: "
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: 💁 Support Matrix
|
||||
url: https://matrix.to/#/%23calckey:matrix.fedibird.com
|
||||
about: Having trouble with deployment? Ask the support chat.
|
||||
- name: 🔒 Resposible Disclosure
|
||||
url: https://codeberg.org/calckey/calckey/src/branch/develop/SECURITY.md
|
||||
about: Found a security vulnerability? Please disclose it responsibly.
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this feature request!
|
||||
💖 Thanks for taking the time to fill out this feature request!
|
||||
💁 Having trouble with deployment? [Ask the support chat.](https://matrix.to/#/%23calckey:matrix.fedibird.com)
|
||||
🔒 Found a security vulnerability? [Please disclose it responsibly.](https://codeberg.org/calckey/calckey/src/branch/develop/SECURITY.md)
|
||||
🤝 By submitting this issue, you agree to follow our [Contribution Guidelines.](https://codeberg.org/calckey/calckey/src/branch/develop/CONTRIBUTING.md)
|
||||
- type: textarea
|
||||
id: what-feature
|
||||
attributes:
|
||||
label: What feature would you like implemented?
|
||||
description: Please give us a brief description of what you'd like.
|
||||
placeholder: Tell us what you want!
|
||||
value: "x feature would be great!"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
@ -21,7 +31,6 @@ body:
|
|||
label: Why should we add this feature?
|
||||
description: Please give us a brief description of why your feature is important.
|
||||
placeholder: Tell us why you want this feature!
|
||||
value: "x feature is super useful because y!"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
@ -29,7 +38,7 @@ body:
|
|||
attributes:
|
||||
label: Version
|
||||
description: What version of calckey is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information.
|
||||
placeholder: Calckey Version 13.0.4
|
||||
placeholder: Calckey Version 13.1.4.1
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
@ -37,7 +46,7 @@ body:
|
|||
attributes:
|
||||
label: Instance
|
||||
description: What instance of calckey are you using?
|
||||
placeholder: stop.voring.me
|
||||
placeholder: calckey.social
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
|
|
|
@ -136,7 +136,7 @@ smtpUser: "Nom d'usuari"
|
|||
smtpPass: "Contrasenya"
|
||||
user: "Usuari"
|
||||
searchByGoogle: "Cercar"
|
||||
file: "Fitxers"
|
||||
file: "Fitxer"
|
||||
_email:
|
||||
_follow:
|
||||
title: "Tens un nou seguidor"
|
||||
|
@ -325,11 +325,31 @@ _2fa:
|
|||
per protegir encara més el vostre compte.
|
||||
step4: A partir d'ara, qualsevol intent d'inici de sessió futur demanarà aquest
|
||||
token d'inici de sessió.
|
||||
registerSecurityKey: Registra una clau de seguretat
|
||||
registerSecurityKey: Registrar una clau de seguretat o d'accés
|
||||
step1: En primer lloc, instal·la una aplicació d'autenticació (com ara {a} o {b})
|
||||
al dispositiu.
|
||||
step2: A continuació, escaneja el codi QR que es mostra en aquesta pantalla.
|
||||
step3: Introdueix el token que t'ha proporcionat l'aplicació per finalitzar la configuració.
|
||||
step3Title: Introduïu un codi d'autenticació
|
||||
chromePasskeyNotSupported: Les claus de pas de Chrome actualment no s'admeten.
|
||||
securityKeyName: Introduïu un nom de clau
|
||||
removeKey: Suprimeix la clau de seguretat
|
||||
removeKeyConfirm: Vols suprimir la clau {name}?
|
||||
renewTOTP: Tornar a configurar l'aplicació d'autenticació
|
||||
renewTOTPOk: Reconfigurar
|
||||
renewTOTPCancel: Cancel·lar
|
||||
step2Click: Fer clic en aquest codi QR us permetrà registrar 2FA a la vostra clau
|
||||
de seguretat o aplicació d'autenticació del telèfon.
|
||||
securityKeyNotSupported: El vostre navegador no admet claus de seguretat.
|
||||
registerTOTPBeforeKey: Configureu una aplicació d'autenticació per registrar una
|
||||
clau de seguretat o de passi.
|
||||
tapSecurityKey: Si us plau, seguiu el vostre navegador per registrar la clau de
|
||||
seguretat o d'accés
|
||||
renewTOTPConfirm: Això farà que els codis de verificació de l'aplicació anterior
|
||||
deixin de funcionar
|
||||
whyTOTPOnlyRenew: L’aplicació d’autenticació no es pot eliminar sempre que es hi
|
||||
hagi una clau de seguretat registrada.
|
||||
token: Token 2FA
|
||||
_widgets:
|
||||
notifications: "Notificacions"
|
||||
timeline: "Línia de temps"
|
||||
|
@ -366,12 +386,13 @@ _cw:
|
|||
chars: '{count} caràcters'
|
||||
_visibility:
|
||||
followers: "Seguidors"
|
||||
publicDescription: La teva publicació serà visible per a tots els usuaris
|
||||
publicDescription: La teva publicació serà visible per a totes les línies de temps
|
||||
públiques
|
||||
localOnly: Només Local
|
||||
specified: Directe
|
||||
home: Sense llistar
|
||||
homeDescription: Publica només a la línea de temps local
|
||||
followersDescription: Fes visible només per als teus seguidors
|
||||
followersDescription: Fes visible només per als teus seguidors i usuaris mencionats
|
||||
specifiedDescription: Fer visible només per a usuaris determinats
|
||||
public: Públic
|
||||
localOnlyDescription: No és visible per als usuaris remots
|
||||
|
@ -988,7 +1009,7 @@ avoidMultiCaptchaConfirm: Fent servir diferents sistemes de Captcha pot causar i
|
|||
antennas: Antenes
|
||||
enableEmojiReactions: Activa reaccions amb emojis
|
||||
blockThisInstance: Bloqueja aquest servidor
|
||||
registration: Registre
|
||||
registration: Registra't
|
||||
showEmojisInReactionNotifications: Mostra els emojis a les notificacions de les reaccions
|
||||
renoteMute: Silencia els impulsos
|
||||
renoteUnmute: Treu el silenci als impulsos
|
||||
|
@ -1035,7 +1056,7 @@ recentlyRegisteredUsers: Usuaris registrats fa poc
|
|||
recentlyDiscoveredUsers: Nous suaris descoberts
|
||||
administrator: Administrador
|
||||
token: Token
|
||||
registerSecurityKey: Registra una clau de seguretat
|
||||
registerSecurityKey: Registreu una clau de seguretat
|
||||
securityKeyName: Nom clau
|
||||
lastUsed: Feta servir per última vegada
|
||||
unregister: Anul·lar el registre
|
||||
|
@ -1580,6 +1601,8 @@ _aboutMisskey:
|
|||
morePatrons: També agraïm el suport de molts altres ajudants que no figuren aquí.
|
||||
Gràcies! 🥰
|
||||
patrons: Mecenes de Calckey
|
||||
patronsList: Llistats cronològicament, no per la quantitat donada. Fes una donació
|
||||
amb l'enllaç de dalt per veure el teu nom aquí!
|
||||
unknown: Desconegut
|
||||
pageLikesCount: Nombre de pàgines amb M'agrada
|
||||
youAreRunningUpToDateClient: Estás fent servir la versió del client més nova.
|
||||
|
@ -1649,9 +1672,9 @@ popout: Apareixa
|
|||
volume: Volum
|
||||
objectStorageUseSSLDesc: Desactiva això si no fas servir HTTPS per les connexions
|
||||
API
|
||||
objectStorageUseProxy: Conectarse mitjançant un Proxy
|
||||
objectStorageUseProxy: Connectar-se mitjançant un Proxy
|
||||
objectStorageUseProxyDesc: Desactiva això si no faràs servir un servidor Proxy per
|
||||
conexions API
|
||||
conexions amb l'API
|
||||
objectStorageSetPublicRead: Fixar com a "public-read" al pujar
|
||||
serverLogs: Registres del servidor
|
||||
deleteAll: Esborrar tot
|
||||
|
@ -1767,7 +1790,7 @@ createNew: Crear una nova
|
|||
optional: Opcional
|
||||
jumpToSpecifiedDate: Vés a una data concreta
|
||||
showingPastTimeline: Ara es mostra un línea de temps antiga
|
||||
clear: Tornar
|
||||
clear: Netejar
|
||||
markAllAsRead: Marcar tot com a llegit
|
||||
recentPosts: Pàgines recents
|
||||
noMaintainerInformationWarning: La informació de l'administrador no està configurada.
|
||||
|
@ -2012,11 +2035,12 @@ _auth:
|
|||
shareAccessAsk: Estàs segur que vols autoritzar aquesta aplicació per accedir al
|
||||
teu compte?
|
||||
shareAccess: Vols autoritzar "{name}" per accedir a aquest compte?
|
||||
permissionAsk: Aquesta aplicació sol·licita els següents permisos
|
||||
permissionAsk: 'Aquesta aplicació sol·licita els següents permisos:'
|
||||
callback: Tornant a l'aplicació
|
||||
denied: Accés denegat
|
||||
pleaseGoBack: Si us plau, torneu a l'aplicació
|
||||
copyAsk: Posa el següent codi d'autorització a l'aplicació
|
||||
copyAsk: "Posa el següent codi d'autorització a l'aplicació:"
|
||||
allPermissions: Accés complet al compte
|
||||
_weekday:
|
||||
wednesday: Dimecres
|
||||
saturday: Dissabte
|
||||
|
@ -2041,7 +2065,7 @@ _relayStatus:
|
|||
rejected: Rebutjat
|
||||
deleted: Eliminat
|
||||
editNote: Edita la nota
|
||||
edited: Editat
|
||||
edited: 'Editat a {date} {time}'
|
||||
findOtherInstance: Cercar un altre servidor
|
||||
signupsDisabled: Actualment, les inscripcions en aquest servidor estan desactivades,
|
||||
però sempre podeu registrar-vos en un altre servidor. Si teniu un codi d'invitació
|
||||
|
@ -2060,11 +2084,7 @@ _experiments:
|
|||
alpha: Alfa
|
||||
beta: Beta
|
||||
release: Publicà
|
||||
enablePostEditing: Activà l'edició de publicacions
|
||||
title: Experiments
|
||||
postEditingCaption: Mostra l'opció perquè els usuaris editin les seves publicacions
|
||||
mitjançant el menú d'opcions de publicació, i permet rebre publicacions editades
|
||||
d'altres servidors.
|
||||
enablePostImports: Activar l'importació de publicacions
|
||||
postImportsCaption: Permet els usuaris importar publicacions desde comptes a Calckey,
|
||||
Misskey, Mastodon, Akkoma i Pleroma. Pot fer que el servidor vagi més lent durant
|
||||
|
@ -2107,3 +2127,18 @@ _filters:
|
|||
image: Imatge
|
||||
video: Vídeo
|
||||
audio: Àudio
|
||||
_dialog:
|
||||
charactersExceeded: "S'han superat el màxim de caràcters! Actual: {current}/Límit:
|
||||
{max}"
|
||||
charactersBelow: 'No hi ha caràcters suficients! Corrent: {current}/Limit: {min}'
|
||||
removeReaction: Elimina la teva reacció
|
||||
reactionPickerSkinTone: To de pell d'emoji preferit
|
||||
alt: ALT
|
||||
_skinTones:
|
||||
light: Clar
|
||||
mediumLight: Clar Mitx
|
||||
medium: Mitx
|
||||
mediumDark: Fosc Mitx
|
||||
dark: Fosc
|
||||
yellow: Groc
|
||||
swipeOnMobile: Permet lliscar entre pàgines
|
||||
|
|
|
@ -963,7 +963,7 @@ disablingTimelinesInfo: Administrátoři a moderátoři budou vždy mít příst
|
|||
časovým osám, i pokud jsou vypnuté.
|
||||
deleted: Vymazáno
|
||||
editNote: Upravit poznámku
|
||||
edited: Upraveno
|
||||
edited: 'Upraveno dne {date} {time}'
|
||||
silencedInstancesDescription: Vypište hostnames instancí, které chcete ztlumit. Účty
|
||||
v uvedených instancích jsou považovány za "ztlumené", mohou pouze zadávat požadavky
|
||||
na sledování a nemohou zmiňovat místní účty, pokud nejsou sledovány. Na blokované
|
||||
|
|
|
@ -83,7 +83,7 @@ deleteAndEditConfirm: Er du sikker på at du vil slet denne opslag og ændre det
|
|||
vil tabe alle reaktioner, forstærkninger og svarer indenfor denne opslag.
|
||||
editNote: Ændre note
|
||||
deleted: Slettet
|
||||
edited: Ændret
|
||||
edited: 'Ændret den {date} {time}'
|
||||
sendMessage: Send en besked
|
||||
youShouldUpgradeClient: Til at vise denne side, vær sød at refresh til at opdatere
|
||||
din brugerenhed.
|
||||
|
|
|
@ -693,8 +693,8 @@ abuseReported: "Deine Meldung wurde versendet. Vielen Dank."
|
|||
reporter: "Melder"
|
||||
reporteeOrigin: "Herkunft des Gemeldeten"
|
||||
reporterOrigin: "Herkunft des Meldenden"
|
||||
forwardReport: "Einen Meldung zusätzlich an den mit-beteiligten Server senden"
|
||||
forwardReportIsAnonymous: "Anstelle Ihres Nutzerkontos wird ein anonymes Systemkonto
|
||||
forwardReport: "Meldung auch an den mit-beteiligten Server weiterleiten"
|
||||
forwardReportIsAnonymous: "Anstelle deines Nutzerkontos wird ein anonymes Systemkonto
|
||||
als Hinweisgeber auf dem mit-beteiligten Server angezeigt."
|
||||
send: "Senden"
|
||||
abuseMarkAsResolved: "Meldung als gelöst markieren"
|
||||
|
@ -812,7 +812,7 @@ useReactionPickerForContextMenu: "Reaktionsauswahl durch Rechtsklick öffnen"
|
|||
typingUsers: "{users} ist/sind am schreiben"
|
||||
jumpToSpecifiedDate: "Zu bestimmtem Datum springen"
|
||||
showingPastTimeline: "Es wird eine alte Timeline angezeigt"
|
||||
clear: "Zurückkehren"
|
||||
clear: "Leeren"
|
||||
markAllAsRead: "Alle als gelesen markieren"
|
||||
goBack: "Zurück"
|
||||
unlikeConfirm: "\"Gefällt mir\" wirklich entfernen?"
|
||||
|
@ -1189,6 +1189,8 @@ _mfm:
|
|||
stop: MFM anhalten
|
||||
warn: MFM können schnell bewegte oder anderweitig auffallende Animationen enthalten
|
||||
alwaysPlay: Alle animierten MFM immer automatisch abspielen
|
||||
advancedDescription: Wenn diese Funktion deaktiviert ist, können nur einfache Formatierungen
|
||||
vorgenommen werden, es sei denn, animiertes MFM ist aktiviert
|
||||
_instanceTicker:
|
||||
none: "Nie anzeigen"
|
||||
remote: "Für Nutzer eines anderen Servers anzeigen"
|
||||
|
@ -1356,8 +1358,8 @@ _tutorial:
|
|||
der/die auf diesem Server registriert ist."
|
||||
step5_5: "Die Social-Timeline {icon} ist eine Kombination aus der Home-Timeline
|
||||
und der Local-Timeline."
|
||||
step5_6: "In der {icon} \"Favoriten\"-Timeline können sie Beiträge von Servern sehen,
|
||||
die von den Server-Administratoren vorgeschlagen werden."
|
||||
step5_6: "In der Empfohlen-Timeline {icon} kannst du Posts sehen, die von den Admins
|
||||
vorgeschlagen wurden."
|
||||
step5_7: "In der {icon} Global-Timeline können Sie Beiträge von allen verknüpften
|
||||
Servern aus dem Fediverse sehen."
|
||||
step6_1: "Also, was ist das hier?"
|
||||
|
@ -1383,6 +1385,25 @@ _2fa:
|
|||
securityKeyInfo: "Du kannst neben Fingerabdruck- oder PIN-Authentifizierung auf
|
||||
deinem Gerät auch Anmeldung mit Hilfe eines FIDO2-kompatiblen Hardware-Sicherheitsschlüssels
|
||||
einrichten."
|
||||
step3Title: Gib deinen Authentifizierungscode ein
|
||||
renewTOTPOk: Neu konfigurieren
|
||||
securityKeyNotSupported: Dein Browser unterstützt Hardware-Security-Keys nicht.
|
||||
chromePasskeyNotSupported: Chrome Passkeys werden momentan nicht unterstützt.
|
||||
renewTOTP: Konfiguriere deine Authenticator App neu
|
||||
renewTOTPCancel: Abbrechen
|
||||
tapSecurityKey: Bitte folge den Anweisungen deines Browsers, um einen Hardware-Security-Key
|
||||
oder einen Passkey zu registrieren
|
||||
removeKey: Entferne deinen Hardware-Security-Key
|
||||
removeKeyConfirm: Möchtest du wirklich deinen Key mit der Bezeichnung {name} löschen?
|
||||
renewTOTPConfirm: Das wird dazu führen, dass du Verifizierungscodes deiner vorherigen
|
||||
Authenticator App nicht mehr nutzen kannst
|
||||
whyTOTPOnlyRenew: Die Authentificator App kann nicht entfernt werden, solange ein
|
||||
Hardware-Security-Key registriert ist.
|
||||
step2Click: Ein Klick auf diesen QR-Code erlaubt es dir eine 2FA-Methode zu deinem
|
||||
Security Key oder deiner Authenticator App hinzuzufügen.
|
||||
registerTOTPBeforeKey: Bitte registriere eine Authentificator App, um einen Hardware-Security-Key
|
||||
oder einen Passkey zu nutzen.
|
||||
securityKeyName: Gib einen Namen für den Key ein
|
||||
_permissions:
|
||||
"read:account": "Deine Nutzerkontoinformationen lesen"
|
||||
"write:account": "Deine Nutzerkontoinformationen bearbeiten"
|
||||
|
@ -1451,7 +1472,7 @@ _widgets:
|
|||
trends: "Trends"
|
||||
clock: "Uhr"
|
||||
rss: "RSS-Reader"
|
||||
rssTicker: "RSS-Laufschrift (Ticker)"
|
||||
rssTicker: "RSS Ticker"
|
||||
activity: "Aktivität"
|
||||
photos: "Fotos"
|
||||
digitalClock: "Digitaluhr"
|
||||
|
@ -1962,8 +1983,8 @@ renoteMute: Boosts stummschalten
|
|||
renoteUnmute: Stummschaltung von Boosts aufheben
|
||||
noInstances: Keine Server gefunden
|
||||
privateModeInfo: Wenn diese Option aktiviert ist, können nur als vertrauenswürdig
|
||||
eingestufte Server mit diesem Server verknüpft werden. Alle Beiträge werden für
|
||||
die Öffentlichkeit verborgen.
|
||||
eingestufte Server mit diesem Server kommunizieren. Alle Beiträge werden für die
|
||||
Öffentlichkeit verborgen.
|
||||
allowedInstances: Vertrauenswürdige Server
|
||||
selectInstance: Wähle einen Server aus
|
||||
silencedInstancesDescription: Liste die Hostnamen der Server auf, die du stummschalten
|
||||
|
@ -1972,7 +1993,7 @@ silencedInstancesDescription: Liste die Hostnamen der Server auf, die du stummsc
|
|||
wenn sie nicht gefolgt werden. Dies wirkt sich nicht auf die blockierten Server
|
||||
aus.
|
||||
editNote: Beitrag bearbeiten
|
||||
edited: Bearbeitet
|
||||
edited: 'Bearbeitet um {date} {time}'
|
||||
silenceThisInstance: Diesen Server stummschalten
|
||||
silencedInstances: Stummgeschaltete Server
|
||||
silenced: Stummgeschaltet
|
||||
|
@ -2074,11 +2095,11 @@ jumpToPrevious: Zum Vorherigen springen
|
|||
silencedWarning: Diese Meldung wird angezeigt, weil diese Nutzer von Servern stammen,
|
||||
die Ihr Administrator abgeschaltet hat, so dass es sich möglicherweise um Spam handelt.
|
||||
_experiments:
|
||||
enablePostEditing: Beitragsbearbeitung ermöglichen
|
||||
title: Funktionstests
|
||||
postEditingCaption: Zeigt die Option für Nutzer an, ihre bestehenden Beiträge über
|
||||
das Menü "Beitragsoptionen" zu bearbeiten
|
||||
enablePostImports: Beitragsimporte aktivieren
|
||||
postImportsCaption: Erlaubt es Nutzer:innen ihre Posts von alten Calckey, Misskey,
|
||||
Mastodon, Akkoma und Pleroma Accounts zu importieren. Bei Engpässen in der Warteschlange
|
||||
kann es zu Verlangsamungen beim Laden während des Imports kommen.
|
||||
noGraze: Bitte deaktivieren Sie die Browsererweiterung "Graze for Mastodon", da sie
|
||||
die Funktion von Calckey stört.
|
||||
indexFrom: Indexieren ab Beitragskennung aufwärts
|
||||
|
@ -2109,6 +2130,23 @@ _filters:
|
|||
withFile: Mit Datei
|
||||
fromDomain: Von Domain
|
||||
notesBefore: Beiträge vor
|
||||
followingOnly: Nur Folgende
|
||||
isBot: Dieses Konto ist ein Bot
|
||||
isModerator: Moderator
|
||||
isAdmin: Administrator
|
||||
_dialog:
|
||||
charactersExceeded: 'Maximale Anzahl an Zeichen aufgebraucht! Limit: {current} /
|
||||
{max}'
|
||||
charactersBelow: Nicht genug Zeichen! Du hast aktuell {current} von {min} Zeichen
|
||||
searchPlaceholder: Calckey durchsuchen
|
||||
antennasDesc: "Antennen zeigen neue Posts an, die deinen definierten Kriterien entsprechen!\n
|
||||
Sie können von der Timeline-Seite aufgerufen werden."
|
||||
isPatron: Calckey Patron
|
||||
removeReaction: Entferne deine Reaktion
|
||||
listsDesc: Listen lassen dich Timelines mit bestimmten Nutzer:innen erstellen. Sie
|
||||
können von der Timeline-Seite erreicht werden.
|
||||
clipsDesc: Clips sind wie teilbare, kategorisierte Lesezeichen. Du kannst Clips vom
|
||||
Menü individueller Posts aus erstellen.
|
||||
channelFederationWarn: Kanäle föderieren noch nicht zu anderen Servern
|
||||
reactionPickerSkinTone: Bevorzugte Emoji-Hautfarbe
|
||||
swipeOnMobile: Wischen zwischen den Seiten erlauben
|
||||
|
|
|
@ -52,7 +52,7 @@ deleteAndEdit: "Delete and edit"
|
|||
deleteAndEditConfirm: "Are you sure you want to delete this post and edit it? You
|
||||
will lose all reactions, boosts and replies to it."
|
||||
editNote: "Edit note"
|
||||
edited: "Edited"
|
||||
edited: "Edited at {date} {time}"
|
||||
addToList: "Add to list"
|
||||
sendMessage: "Send a message"
|
||||
copyUsername: "Copy username"
|
||||
|
@ -123,6 +123,7 @@ clickToShow: "Click to show"
|
|||
sensitive: "NSFW"
|
||||
add: "Add"
|
||||
reaction: "Reactions"
|
||||
removeReaction: "Remove your reaction"
|
||||
enableEmojiReactions: "Enable emoji reactions"
|
||||
showEmojisInReactionNotifications: "Show emojis in reaction notifications"
|
||||
reactionSetting: "Reactions to show in the reaction picker"
|
||||
|
@ -829,7 +830,7 @@ useReactionPickerForContextMenu: "Open reaction picker on right-click"
|
|||
typingUsers: "{users} is typing"
|
||||
jumpToSpecifiedDate: "Jump to specific date"
|
||||
showingPastTimeline: "Currently displaying an old timeline"
|
||||
clear: "Return"
|
||||
clear: "Clear"
|
||||
markAllAsRead: "Mark all as read"
|
||||
goBack: "Back"
|
||||
unlikeConfirm: "Really remove your like?"
|
||||
|
@ -940,6 +941,7 @@ deleteAccountConfirm: "This will irreversibly delete your account. Proceed?"
|
|||
incorrectPassword: "Incorrect password."
|
||||
voteConfirm: "Confirm your vote for \"{choice}\"?"
|
||||
hide: "Hide"
|
||||
alt: "ALT"
|
||||
leaveGroup: "Leave group"
|
||||
leaveGroupConfirm: "Are you sure you want to leave \"{name}\"?"
|
||||
useDrawerReactionPickerForMobile: "Display reaction picker as drawer on mobile"
|
||||
|
@ -1051,6 +1053,7 @@ recommendedInstancesDescription: "Recommended servers separated by line breaks t
|
|||
caption: "Auto Caption"
|
||||
splash: "Splash Screen"
|
||||
updateAvailable: "There might be an update available!"
|
||||
swipeOnMobile: "Allow swiping between pages"
|
||||
swipeOnDesktop: "Allow mobile-style swiping on desktop"
|
||||
logoImageUrl: "Logo image URL"
|
||||
showAdminUpdates: "Indicate a new Calckey version is avaliable (admin only)"
|
||||
|
@ -1108,6 +1111,9 @@ isLocked: "This account has follow approvals"
|
|||
isModerator: "Moderator"
|
||||
isAdmin: "Administrator"
|
||||
isPatron: "Calckey Patron"
|
||||
reactionPickerSkinTone: "Preferred emoji skin tone"
|
||||
enableServerMachineStats: "Enable server hardware statistics"
|
||||
enableIdenticonGeneration: "Enable Identicon generation"
|
||||
|
||||
_sensitiveMediaDetection:
|
||||
description: "Reduces the effort of server moderation through automatically recognizing
|
||||
|
@ -1209,6 +1215,7 @@ _aboutMisskey:
|
|||
morePatrons: "We also appreciate the support of many other helpers not listed here.
|
||||
Thank you! 🥰"
|
||||
patrons: "Calckey patrons"
|
||||
patronsList: "Listed chronologically, not by donation size. Donate with the link above to get your name on here!"
|
||||
_nsfw:
|
||||
respect: "Hide NSFW media"
|
||||
ignore: "Don't hide NSFW media"
|
||||
|
@ -1509,6 +1516,7 @@ _2fa:
|
|||
renewTOTPConfirm: "This will cause verification codes from your previous app to stop working"
|
||||
renewTOTPOk: "Reconfigure"
|
||||
renewTOTPCancel: "Cancel"
|
||||
token: "2FA Token"
|
||||
_permissions:
|
||||
"read:account": "View your account information"
|
||||
"write:account": "Edit your account information"
|
||||
|
@ -1546,11 +1554,12 @@ _auth:
|
|||
shareAccess: "Would you like to authorize \"{name}\" to access this account?"
|
||||
shareAccessAsk: "Are you sure you want to authorize this application to access your
|
||||
account?"
|
||||
permissionAsk: "This application requests the following permissions"
|
||||
permissionAsk: "This application requests the following permissions:"
|
||||
pleaseGoBack: "Please go back to the application"
|
||||
callback: "Returning to the application"
|
||||
denied: "Access denied"
|
||||
copyAsk: "Please paste the following authorization code to the application"
|
||||
copyAsk: "Please paste the following authorization code to the application:"
|
||||
allPermissions: "Full account access"
|
||||
_antennaSources:
|
||||
all: "All posts"
|
||||
homeTimeline: "Posts from followed users"
|
||||
|
@ -1625,11 +1634,11 @@ _poll:
|
|||
remainingSeconds: "{s} second(s) remaining"
|
||||
_visibility:
|
||||
public: "Public"
|
||||
publicDescription: "Your post will be visible for all users"
|
||||
publicDescription: "Your post will be visible in all public timelines"
|
||||
home: "Unlisted"
|
||||
homeDescription: "Post to home timeline only"
|
||||
followers: "Followers"
|
||||
followersDescription: "Make visible to your followers only"
|
||||
followersDescription: "Make visible to your followers and mentioned users only"
|
||||
specified: "Direct"
|
||||
specifiedDescription: "Make visible for specified users only"
|
||||
localOnly: "Local only"
|
||||
|
@ -2063,14 +2072,17 @@ _deck:
|
|||
direct: "Direct messages"
|
||||
_experiments:
|
||||
title: "Experiments"
|
||||
enablePostEditing: "Enable post editing"
|
||||
postEditingCaption: "Shows the option for users to edit their existing posts via\
|
||||
\ the post options menu, and allows post edits from other instances to be recieved."
|
||||
enablePostImports: "Enable post imports"
|
||||
postImportsCaption: "Allows users to import their posts from past Calckey,\
|
||||
\ Misskey, Mastodon, Akkoma, and Pleroma accounts. It may cause slowdowns during\
|
||||
\ load if your queue is bottlenecked."
|
||||
|
||||
_dialog:
|
||||
charactersExceeded: "Max characters exceeded! Current: {current}/Limit: {max}"
|
||||
charactersBelow: "Not enough characters! Current: {current}/Limit: {min}"
|
||||
_skinTones:
|
||||
yellow: "Yellow"
|
||||
light: "Light"
|
||||
mediumLight: "Medium Light"
|
||||
medium: "Medium"
|
||||
mediumDark: "Medium Dark"
|
||||
dark: "Dark"
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
_lang_: "Español"
|
||||
headlineMisskey: "¡Un proyecto de código abierto y una plataforma de medios de comunicación\
|
||||
\ descentralizada que es gratis para siempre! \U0001F680"
|
||||
introMisskey: "¡Bienvenido! ¡Calckey es un proyecto de código abierto, plataforma\
|
||||
\ descentralizado medios de comunicación social que es gratis para siempre! \U0001F680"
|
||||
headlineMisskey: "¡Un proyecto de código abierto y una plataforma de medios de comunicación
|
||||
descentralizada que es gratis para siempre! 🚀"
|
||||
introMisskey: "¡Bienvenido! ¡Calckey es un proyecto de código abierto, plataforma
|
||||
descentralizado medios de comunicación social que es gratis para siempre! 🚀"
|
||||
monthAndDay: "{day}/{month}"
|
||||
search: "Buscar"
|
||||
notifications: "Notificaciones"
|
||||
|
@ -17,7 +17,7 @@ enterUsername: "Introduce el nombre de usuario"
|
|||
renotedBy: "Impulsado por {user}"
|
||||
noNotes: "No hay publicaciones"
|
||||
noNotifications: "No hay notificaciones"
|
||||
instance: "Instancia"
|
||||
instance: "Servidor"
|
||||
settings: "Configuración"
|
||||
basicSettings: "Configuración Básica"
|
||||
otherSettings: "Configuración avanzada"
|
||||
|
@ -45,8 +45,8 @@ copyContent: "Copiar contenido"
|
|||
copyLink: "Copiar enlace"
|
||||
delete: "Borrar"
|
||||
deleteAndEdit: "Borrar y editar"
|
||||
deleteAndEditConfirm: "¿Estás seguro de que quieres borrar esta publicación y editarla?\
|
||||
\ Perderás todas las reacciones, impulsos y respuestas."
|
||||
deleteAndEditConfirm: "¿Estás seguro de que quieres borrar esta publicación y editarla?
|
||||
Perderás todas las reacciones, impulsos y respuestas."
|
||||
addToList: "Agregar a lista"
|
||||
sendMessage: "Enviar un mensaje"
|
||||
copyUsername: "Copiar nombre de usuario"
|
||||
|
@ -66,11 +66,11 @@ import: "Importar"
|
|||
export: "Exportar"
|
||||
files: "Archivos"
|
||||
download: "Descargar"
|
||||
driveFileDeleteConfirm: "¿Desea borrar el archivo \"{name}\"? Las publicaciones que\
|
||||
\ tengan este archivo como adjunto serán eliminadas."
|
||||
driveFileDeleteConfirm: "¿Desea borrar el archivo \"{name}\"? Será removido de todas
|
||||
las publicaciones que tengan este archivo adjunto."
|
||||
unfollowConfirm: "¿Desea dejar de seguir a {name}?"
|
||||
exportRequested: "Se ha solicitado la exportación. Puede tomar un tiempo. Cuando termine\
|
||||
\ la exportación, se añadirá en el drive."
|
||||
exportRequested: "Se ha solicitado la exportación. Puede tomar un tiempo. Cuando termine
|
||||
la exportación, se añadirá en el drive."
|
||||
importRequested: "Se ha solicitado la importación. Puede tomar un tiempo."
|
||||
lists: "Listas"
|
||||
noLists: "No tiene listas"
|
||||
|
@ -85,11 +85,11 @@ error: "Error"
|
|||
somethingHappened: "Ocurrió un error"
|
||||
retry: "Reintentar"
|
||||
pageLoadError: "Error al cargar la página."
|
||||
pageLoadErrorDescription: "Normalmente es debido a la red o al caché del navegador.\
|
||||
\ Por favor limpie el caché o intente más tarde."
|
||||
pageLoadErrorDescription: "Normalmente es debido a la red o al caché del navegador.
|
||||
Por favor limpie el caché o intente más tarde."
|
||||
serverIsDead: "No hay respuesta del servidor. Espere un momento y vuelva a intentarlo."
|
||||
youShouldUpgradeClient: "Para ver esta página, por favor refrezca el navegador y utiliza\
|
||||
\ una versión más reciente del cliente."
|
||||
youShouldUpgradeClient: "Para ver esta página, por favor refrezca el navegador y utiliza
|
||||
una versión más reciente del cliente."
|
||||
enterListName: "Ingrese nombre de lista"
|
||||
privacy: "Privacidad"
|
||||
makeFollowManuallyApprove: "Aprobar manualmente las solicitudes de seguimiento"
|
||||
|
@ -114,8 +114,8 @@ sensitive: "Marcado como sensible"
|
|||
add: "Agregar"
|
||||
reaction: "Reacción"
|
||||
reactionSetting: "Reacciones para mostrar en el menú de reacciones"
|
||||
reactionSettingDescription2: "Arrastre para reordenar, click para borrar, apriete\
|
||||
\ la tecla + para añadir."
|
||||
reactionSettingDescription2: "Arrastre para reordenar, click para borrar, apriete
|
||||
la tecla + para añadir."
|
||||
rememberNoteVisibility: "Recordar la configuración de visibilidad de la publicación"
|
||||
attachCancel: "Quitar adjunto"
|
||||
markAsSensitive: "Marcar como sensible"
|
||||
|
@ -144,24 +144,24 @@ emojiUrl: "URL de la imágen del emoji"
|
|||
addEmoji: "Agregar emoji"
|
||||
settingGuide: "Configuración sugerida"
|
||||
cacheRemoteFiles: "Mantener en cache los archivos remotos"
|
||||
cacheRemoteFilesDescription: "Si desactiva esta configuración, Los archivos remotos\
|
||||
\ se cargarán desde el link directo sin usar la caché. Con eso se puede ahorrar\
|
||||
\ almacenamiento del servidor, pero eso aumentará el tráfico al no crear miniaturas."
|
||||
cacheRemoteFilesDescription: "Si desactiva esta configuración, los archivos remotos
|
||||
se cargarán desde el servidor remoto sin usar la caché. Con eso se puede ahorrar
|
||||
almacenamiento del servidor, pero eso aumentará el tráfico al no crear miniaturas."
|
||||
flagAsBot: "Esta cuenta es un bot"
|
||||
flagAsBotDescription: "En caso de que esta cuenta fuera usada por un programa, active\
|
||||
\ esta opción. Al hacerlo, esta opción servirá para otros desarrolladores para evitar\
|
||||
\ cadenas infinitas de reacciones, y ajustará los sistemas internos de Calckey para\
|
||||
\ que trate a esta cuenta como un bot."
|
||||
flagAsBotDescription: "En caso de que esta cuenta fuera usada por un programa, active
|
||||
esta opción. Al hacerlo, esta opción servirá para otros desarrolladores para evitar
|
||||
cadenas infinitas de reacciones, y ajustará los sistemas internos de Calckey para
|
||||
que trate a esta cuenta como un bot."
|
||||
flagAsCat: "Esta cuenta es un gato"
|
||||
flagAsCatDescription: "Vas a tener orejas de gato y hablar como un gato!"
|
||||
flagShowTimelineReplies: "Mostrar respuestas a las notas en la biografía"
|
||||
flagShowTimelineRepliesDescription: "Cuando se marca, la línea de tiempo muestra respuestas\
|
||||
\ a otras publicaciones además de las publicaciones del usuario."
|
||||
autoAcceptFollowed: "Aceptar automáticamente las solicitudes de seguimiento de los\
|
||||
\ usuarios que sigues"
|
||||
flagShowTimelineRepliesDescription: "Cuando se marca, la línea de tiempo muestra respuestas
|
||||
a otras publicaciones además de las publicaciones del usuario."
|
||||
autoAcceptFollowed: "Aceptar automáticamente las solicitudes de seguimiento de los
|
||||
usuarios que sigues"
|
||||
addAccount: "Agregar Cuenta"
|
||||
loginFailed: "Error al iniciar sesión"
|
||||
showOnRemote: "Ver en una instancia remota"
|
||||
showOnRemote: "Ver en servidor remoto"
|
||||
general: "General"
|
||||
wallpaper: "Fondo de pantalla"
|
||||
setWallpaper: "Establecer fondo de pantalla"
|
||||
|
@ -170,17 +170,17 @@ searchWith: "Buscar: {q}"
|
|||
youHaveNoLists: "No tienes listas"
|
||||
followConfirm: "¿Desea seguir a {name}?"
|
||||
proxyAccount: "Cuenta proxy"
|
||||
proxyAccountDescription: "Una cuenta proxy es una cuenta que actúa como un seguidor\
|
||||
\ remoto de un usuario bajo ciertas condiciones. Por ejemplo, cuando un usuario\
|
||||
\ añade un usuario remoto a una lista, si ningún usuario local sigue al usuario\
|
||||
\ agregado a la lista, la instancia no puede obtener su actividad. Así que la cuenta\
|
||||
\ proxy sigue al usuario añadido a la lista."
|
||||
proxyAccountDescription: "Una cuenta proxy es una cuenta que actúa como un seguidor
|
||||
remoto de un usuario bajo ciertas condiciones. Por ejemplo, cuando un usuario añade
|
||||
un usuario remoto a una lista, si ningún usuario local sigue al usuario agregado
|
||||
a la lista, el servidor no puede obtener su actividad. Así que la cuenta proxy sigue
|
||||
al usuario añadido a la lista."
|
||||
host: "Host"
|
||||
selectUser: "Elegir usuario"
|
||||
recipient: "Recipiente"
|
||||
annotation: "Anotación"
|
||||
federation: "Federación"
|
||||
instances: "Instancia"
|
||||
instances: "Servidores"
|
||||
registeredAt: "Registrado en"
|
||||
latestRequestSentAt: "Ultimo pedido enviado"
|
||||
latestRequestReceivedAt: "Ultimo pedido recibido"
|
||||
|
@ -190,7 +190,7 @@ charts: "Chat"
|
|||
perHour: "por hora"
|
||||
perDay: "por día"
|
||||
stopActivityDelivery: "Dejar de enviar actividades"
|
||||
blockThisInstance: "Bloquear instancia"
|
||||
blockThisInstance: "Bloquear este servidor"
|
||||
operations: "Operaciones"
|
||||
software: "Software"
|
||||
version: "Versión"
|
||||
|
@ -200,18 +200,17 @@ jobQueue: "Cola de trabajos"
|
|||
cpuAndMemory: "CPU y Memoria"
|
||||
network: "Red"
|
||||
disk: "Disco"
|
||||
instanceInfo: "información de la instancia"
|
||||
instanceInfo: "Información del servidor"
|
||||
statistics: "Estadísticas"
|
||||
clearQueue: "Limpiar cola"
|
||||
clearQueueConfirmTitle: "¿Desea limpiar la cola?"
|
||||
clearQueueConfirmText: "Las publicaciones aún no entregadas no se federarán. Normalmente\
|
||||
\ no se necesita ejecutar esta operación."
|
||||
clearQueueConfirmText: "Las publicaciones aún no entregadas no se federarán. Normalmente
|
||||
no se necesita ejecutar esta operación."
|
||||
clearCachedFiles: "Limpiar caché"
|
||||
clearCachedFilesConfirm: "¿Desea borrar todos los archivos remotos cacheados?"
|
||||
blockedInstances: "Instancias bloqueadas"
|
||||
blockedInstancesDescription: "Seleccione los hosts de las instancias que desea bloquear,\
|
||||
\ separadas por una linea nueva. Las instancias bloqueadas no podrán comunicarse\
|
||||
\ con esta instancia."
|
||||
blockedInstances: "Servidores bloqueados"
|
||||
blockedInstancesDescription: "Escriba los hosts de los servidores que desea bloquear.
|
||||
Los servidores bloqueados no podrán comunicarse con este servidor."
|
||||
muteAndBlock: "Silenciar y bloquear"
|
||||
mutedUsers: "Usuarios silenciados"
|
||||
blockedUsers: "Usuarios bloqueados"
|
||||
|
@ -234,9 +233,9 @@ all: "Todo"
|
|||
subscribing: "Suscribiendo"
|
||||
publishing: "Publicando"
|
||||
notResponding: "Sin respuestas"
|
||||
instanceFollowing: "Siguiendo instancias"
|
||||
instanceFollowers: "Seguidores de la instancia"
|
||||
instanceUsers: "Usuarios de la instancia"
|
||||
instanceFollowing: "Siguiendo en este servidor"
|
||||
instanceFollowers: "Seguidores del servidor"
|
||||
instanceUsers: "Usuarios de este servidor"
|
||||
changePassword: "Cambiar contraseña"
|
||||
security: "Seguridad"
|
||||
retypedNotMatch: "No hay coincidencia."
|
||||
|
@ -260,9 +259,9 @@ saved: "Guardado"
|
|||
messaging: "Chat"
|
||||
upload: "Subir"
|
||||
keepOriginalUploading: "Mantener la imagen original"
|
||||
keepOriginalUploadingDescription: "Mantener la versión original al cargar imágenes.\
|
||||
\ Si está desactivado, el navegador generará imágenes para la publicación web en\
|
||||
\ el momento de recargar la página."
|
||||
keepOriginalUploadingDescription: "Mantener la versión original al cargar imágenes.
|
||||
Si está desactivado, el navegador generará imágenes para la publicación web en el
|
||||
momento de recargar la página."
|
||||
fromDrive: "Desde el drive"
|
||||
fromUrl: "Desde la URL"
|
||||
uploadFromUrl: "Subir desde una URL"
|
||||
|
@ -311,8 +310,8 @@ unableToDelete: "No se puede borrar"
|
|||
inputNewFileName: "Ingrese un nuevo nombre de archivo"
|
||||
inputNewDescription: "Ingrese nueva descripción"
|
||||
inputNewFolderName: "Ingrese un nuevo nombre de la carpeta"
|
||||
circularReferenceFolder: "La carpeta de destino es una sub-carpeta de la carpeta que\
|
||||
\ quieres mover."
|
||||
circularReferenceFolder: "La carpeta de destino es una sub-carpeta de la carpeta que
|
||||
quieres mover."
|
||||
hasChildFilesOrFolders: "No se puede borrar esta carpeta. No está vacía."
|
||||
copyUrl: "Copiar URL"
|
||||
rename: "Renombrar"
|
||||
|
@ -329,8 +328,8 @@ unwatch: "Dejar de ver"
|
|||
accept: "Aceptar"
|
||||
reject: "Rechazar"
|
||||
normal: "Normal"
|
||||
instanceName: "Nombre de la instancia"
|
||||
instanceDescription: "Descripción de la instancia"
|
||||
instanceName: "Nombre del servidor"
|
||||
instanceDescription: "Descripción del servidor"
|
||||
maintainerName: "Nombre del administrador"
|
||||
maintainerEmail: "Correo del administrador"
|
||||
tosUrl: "URL de los términos de uso"
|
||||
|
@ -346,8 +345,8 @@ connectService: "Conectar"
|
|||
disconnectService: "Desconectar"
|
||||
enableLocalTimeline: "Habilitar linea de tiempo local"
|
||||
enableGlobalTimeline: "Habilitar linea de tiempo global"
|
||||
disablingTimelinesInfo: "Aunque se desactiven estas lineas de tiempo, por conveniencia\
|
||||
\ el administrador y los moderadores pueden seguir usándolos"
|
||||
disablingTimelinesInfo: "Aunque se desactiven estas lineas de tiempo, por conveniencia
|
||||
el administrador y los moderadores pueden seguir usándolos"
|
||||
registration: "Registro"
|
||||
enableRegistration: "Permitir nuevos registros"
|
||||
invite: "Invitar"
|
||||
|
@ -359,11 +358,11 @@ bannerUrl: "URL de la imagen del banner"
|
|||
backgroundImageUrl: "URL de la imagen de fondo"
|
||||
basicInfo: "Información básica"
|
||||
pinnedUsers: "Usuarios fijados"
|
||||
pinnedUsersDescription: "Describir los usuarios que quiere fijar en la página \"Descubrir\"\
|
||||
\ separados por una linea nueva"
|
||||
pinnedUsersDescription: "Describir los usuarios que quiere fijar en la pestaña \"\
|
||||
Explorar\" separados por líneas nuevas."
|
||||
pinnedPages: "Páginas fijadas"
|
||||
pinnedPagesDescription: "Describa las rutas de las páginas que desea fijar a la página\
|
||||
\ principal de la instancia, separadas por lineas nuevas"
|
||||
pinnedPagesDescription: "Describa las rutas de las páginas que desea fijar a la página
|
||||
principal del servidor, separadas por líneas nuevas."
|
||||
pinnedClipId: "Id del clip fijado"
|
||||
pinnedNotes: "Publicación fijada"
|
||||
hcaptcha: "hCaptcha"
|
||||
|
@ -374,17 +373,17 @@ recaptcha: "reCAPTCHA"
|
|||
enableRecaptcha: "activar reCAPTCHA"
|
||||
recaptchaSiteKey: "Clave del sitio"
|
||||
recaptchaSecretKey: "Clave secreta"
|
||||
avoidMultiCaptchaConfirm: "El uso de múltiples Captchas puede causar interferencia.\
|
||||
\ ¿Desea desactivar el otro Captcha? Puede dejar múltiples Captchas habilitadas\
|
||||
\ presionando cancelar."
|
||||
avoidMultiCaptchaConfirm: "El uso de múltiples Captchas puede causar interferencia.
|
||||
¿Desea desactivar el otro Captcha? Puede dejar múltiples Captchas habilitadas presionando
|
||||
cancelar."
|
||||
antennas: "Antenas"
|
||||
manageAntennas: "Administrar antenas"
|
||||
name: "Nombre"
|
||||
antennaSource: "Origen de la antena"
|
||||
antennaKeywords: "Palabras clave para recibir"
|
||||
antennaExcludeKeywords: "Palabras clave para excluir"
|
||||
antennaKeywordsDescription: "Separar con espacios es una declaración AND, separar\
|
||||
\ con una linea nueva es una declaración OR"
|
||||
antennaKeywordsDescription: "Separar con espacios es una declaración AND, separar
|
||||
con una linea nueva es una declaración OR"
|
||||
notifyAntenna: "Notificar nueva publicación"
|
||||
withFileAntenna: "Sólo publicaciones con archivos adjuntados"
|
||||
enableServiceworker: "Activar ServiceWorker"
|
||||
|
@ -472,8 +471,8 @@ strongPassword: "Muy buena contraseña"
|
|||
passwordMatched: "Correcto"
|
||||
passwordNotMatched: "Las contraseñas no son las mismas"
|
||||
signinWith: "Inicie sesión con {x}"
|
||||
signinFailed: "Autenticación fallida. Asegúrate de haber usado el nombre de usuario\
|
||||
\ y contraseña correctos."
|
||||
signinFailed: "Autenticación fallida. Asegúrate de haber usado el nombre de usuario
|
||||
y contraseña correctos."
|
||||
tapSecurityKey: "Toque la clave de seguridad"
|
||||
or: "O"
|
||||
language: "Idioma"
|
||||
|
@ -483,8 +482,8 @@ aboutX: "Acerca de {x}"
|
|||
useOsNativeEmojis: "Usa los emojis nativos de la plataforma"
|
||||
disableDrawer: "No mostrar los menús en cajones"
|
||||
youHaveNoGroups: "Sin grupos"
|
||||
joinOrCreateGroup: "Obtenga una invitación para unirse al grupos o puede crear su\
|
||||
\ propio grupo."
|
||||
joinOrCreateGroup: "Obtenga una invitación para unirse al grupos o puede crear su
|
||||
propio grupo."
|
||||
noHistory: "No hay datos en el historial"
|
||||
signinHistory: "Historial de ingresos"
|
||||
disableAnimatedMfm: "Deshabilitar MFM que tiene animaciones"
|
||||
|
@ -515,28 +514,28 @@ showFeaturedNotesInTimeline: "Mostrar publicaciones destacadas en la línea de t
|
|||
objectStorage: "Almacenamiento de objetos"
|
||||
useObjectStorage: "Usar almacenamiento de objetos"
|
||||
objectStorageBaseUrl: "Base URL"
|
||||
objectStorageBaseUrlDesc: "Prefijo de URL utilizado para construir URL para hacer\
|
||||
\ referencia a objetos (medios). Especifique su URL si está utilizando un CDN o\
|
||||
\ Proxy; de lo contrario, especifique la dirección a la que se puede acceder públicamente\
|
||||
\ de acuerdo con la guía de servicio que va a utilizar. i.g 'https://<bucket>.s3.amazonaws.com'\
|
||||
\ para AWS S3 y 'https://storage.googleapis.com/<bucket>' para GCS."
|
||||
objectStorageBaseUrlDesc: "Prefijo de URL utilizado para construir URL para hacer
|
||||
referencia a objetos (medios). Especifique su URL si está utilizando un CDN o Proxy;
|
||||
de lo contrario, especifique la dirección a la que se puede acceder públicamente
|
||||
de acuerdo con la guía de servicio que va a utilizar. i.g 'https://<bucket>.s3.amazonaws.com'
|
||||
para AWS S3 y 'https://storage.googleapis.com/<bucket>' para GCS."
|
||||
objectStorageBucket: "Bucket"
|
||||
objectStorageBucketDesc: "Especifique el nombre del depósito utilizado en el servicio\
|
||||
\ configurado."
|
||||
objectStorageBucketDesc: "Especifique el nombre del depósito utilizado en el servicio
|
||||
configurado."
|
||||
objectStoragePrefix: "Prefix"
|
||||
objectStoragePrefixDesc: "Los archivos se almacenarán en el directorio de este prefijo."
|
||||
objectStorageEndpoint: "Endpoint"
|
||||
objectStorageEndpointDesc: "Deje esto en blanco si está utilizando AWS S3; de lo contrario,\
|
||||
\ especifique el punto final como '<host>' o '<host>: <port>' de acuerdo con la\
|
||||
\ guía de servicio que va a utilizar."
|
||||
objectStorageEndpointDesc: "Deje esto en blanco si está utilizando AWS S3; de lo contrario,
|
||||
especifique el punto final como '<host>' o '<host>: <port>' de acuerdo con la guía
|
||||
de servicio que va a utilizar."
|
||||
objectStorageRegion: "Region"
|
||||
objectStorageRegionDesc: "Especifique una región como 'xx-east-1'. Si su servicio\
|
||||
\ no tiene distinción sobre regiones, déjelo en blanco o complete con 'us-east-1'."
|
||||
objectStorageRegionDesc: "Especifique una región como 'xx-east-1'. Si su servicio
|
||||
no tiene distinción sobre regiones, déjelo en blanco o complete con 'us-east-1'."
|
||||
objectStorageUseSSL: "Usar SSL"
|
||||
objectStorageUseSSLDesc: "Desactive esto si no va a usar HTTPS para la conexión API"
|
||||
objectStorageUseProxy: "Conectarse a través de Proxy"
|
||||
objectStorageUseProxyDesc: "Desactive esto si no va a usar Proxy para la conexión\
|
||||
\ de Almacenamiento de objetos"
|
||||
objectStorageUseProxyDesc: "Desactive esto si no va a usar Proxy para la conexión
|
||||
de Almacenamiento de objetos"
|
||||
objectStorageSetPublicRead: "Seleccionar \"public-read\" al subir "
|
||||
serverLogs: "Registros del servidor"
|
||||
deleteAll: "Eliminar todos"
|
||||
|
@ -564,8 +563,8 @@ sort: "Ordenar"
|
|||
ascendingOrder: "Ascendente"
|
||||
descendingOrder: "Descendente"
|
||||
scratchpad: "Scratch pad"
|
||||
scratchpadDescription: "Scratchpad proporciona un entorno experimental para AiScript.\
|
||||
\ Puede escribir, ejecutar y verificar los resultados que interactúan con Calckey."
|
||||
scratchpadDescription: "Scratchpad proporciona un entorno experimental para AiScript.
|
||||
Puede escribir, ejecutar y verificar los resultados que interactúan con Calckey."
|
||||
output: "Salida"
|
||||
script: "Script"
|
||||
disablePagesScript: "Deshabilitar AiScript en Páginas"
|
||||
|
@ -573,14 +572,14 @@ updateRemoteUser: "Actualizar información de usuario remoto"
|
|||
deleteAllFiles: "Borrar todos los archivos"
|
||||
deleteAllFilesConfirm: "¿Desea borrar todos los archivos?"
|
||||
removeAllFollowing: "Retener todos los siguientes"
|
||||
removeAllFollowingDescription: "Cancelar todos los siguientes del servidor {host}.\
|
||||
\ Ejecutar en caso de que esta instancia haya dejado de existir."
|
||||
removeAllFollowingDescription: "Cancelar todos los siguientes del servidor {host}.
|
||||
Ejecutar en caso de que esta instancia haya dejado de existir."
|
||||
userSuspended: "Este usuario ha sido suspendido."
|
||||
userSilenced: "Este usuario ha sido silenciado."
|
||||
yourAccountSuspendedTitle: "Esta cuenta ha sido suspendida"
|
||||
yourAccountSuspendedDescription: "Esta cuenta ha sido suspendida debido a violaciones\
|
||||
\ de los términos de servicio del servidor y otras razones. Para más información,\
|
||||
\ póngase en contacto con el administrador. Por favor, no cree una nueva cuenta."
|
||||
yourAccountSuspendedDescription: "Esta cuenta ha sido suspendida debido a violaciones
|
||||
de los términos de servicio del servidor y otras razones. Para más información,
|
||||
póngase en contacto con el administrador. Por favor, no cree una nueva cuenta."
|
||||
menu: "Menú"
|
||||
divider: "Divisor"
|
||||
addItem: "Agregar elemento"
|
||||
|
@ -634,15 +633,15 @@ smtpHost: "Host"
|
|||
smtpPort: "Puerto"
|
||||
smtpUser: "Nombre de usuario"
|
||||
smtpPass: "Contraseña"
|
||||
emptyToDisableSmtpAuth: "Deje el nombre del usuario y la contraseña en blanco para\
|
||||
\ deshabilitar la autenticación SMTP"
|
||||
emptyToDisableSmtpAuth: "Deje el nombre del usuario y la contraseña en blanco para
|
||||
deshabilitar la autenticación SMTP"
|
||||
smtpSecure: "Usar SSL/TLS implícito en la conexión SMTP"
|
||||
smtpSecureInfo: "Apagar cuando se use STARTTLS"
|
||||
testEmail: "Prueba de envío"
|
||||
wordMute: "Silenciar palabras"
|
||||
regexpError: "Error de la expresión regular"
|
||||
regexpErrorDescription: "Ocurrió un error en la expresión regular en la linea {line}\
|
||||
\ de las palabras muteadas {tab}"
|
||||
regexpErrorDescription: "Ocurrió un error en la expresión regular en la linea {line}
|
||||
de las palabras muteadas {tab}"
|
||||
instanceMute: "Instancias silenciadas"
|
||||
userSaysSomething: "{name} dijo algo"
|
||||
makeActive: "Activar"
|
||||
|
@ -658,13 +657,13 @@ create: "Crear"
|
|||
notificationSetting: "Ajustes de Notificaciones"
|
||||
notificationSettingDesc: "Por favor elija el tipo de notificación a mostrar"
|
||||
useGlobalSetting: "Usar ajustes globales"
|
||||
useGlobalSettingDesc: "Al activarse, se usará la configuración de notificaciones de\
|
||||
\ la cuenta, al desactivarse se pueden hacer configuraciones particulares."
|
||||
useGlobalSettingDesc: "Al activarse, se usará la configuración de notificaciones de
|
||||
la cuenta, al desactivarse se pueden hacer configuraciones particulares."
|
||||
other: "Otro"
|
||||
regenerateLoginToken: "Regenerar token de login"
|
||||
regenerateLoginTokenDescription: "Regenerar el token usado internamente durante el\
|
||||
\ login. No siempre es necesario hacerlo. Al hacerlo de nuevo, se deslogueará en\
|
||||
\ todos los dispositivos."
|
||||
regenerateLoginTokenDescription: "Regenerar el token usado internamente durante el
|
||||
login. No siempre es necesario hacerlo. Al hacerlo de nuevo, se deslogueará en todos
|
||||
los dispositivos."
|
||||
setMultipleBySeparatingWithSpace: "Puedes añadir mas de uno, separado por espacios."
|
||||
fileIdOrUrl: "Id del archivo o URL"
|
||||
behavior: "Comportamiento"
|
||||
|
@ -672,15 +671,15 @@ sample: "Muestra"
|
|||
abuseReports: "Reportes"
|
||||
reportAbuse: "Reportar"
|
||||
reportAbuseOf: "Reportar a {name}"
|
||||
fillAbuseReportDescription: "Ingrese los detalles del reporte. Si hay una nota en\
|
||||
\ particular, ingrese la URL de esta."
|
||||
fillAbuseReportDescription: "Ingrese los detalles del reporte. Si hay una nota en
|
||||
particular, ingrese la URL de esta."
|
||||
abuseReported: "Se ha enviado el reporte. Muchas gracias."
|
||||
reporter: "Reportador"
|
||||
reporteeOrigin: "Reportar a"
|
||||
reporterOrigin: "Origen del reporte"
|
||||
forwardReport: "Transferir un informe a una instancia remota"
|
||||
forwardReportIsAnonymous: "No puede ver su información de la instancia remota y aparecerá\
|
||||
\ como una cuenta anónima del sistema"
|
||||
forwardReportIsAnonymous: "No puede ver su información de la instancia remota y aparecerá
|
||||
como una cuenta anónima del sistema"
|
||||
send: "Enviar"
|
||||
abuseMarkAsResolved: "Marcar reporte como resuelto"
|
||||
openInNewTab: "Abrir en una Nueva Pestaña"
|
||||
|
@ -701,8 +700,8 @@ unclip: "Quitar clip"
|
|||
confirmToUnclipAlreadyClippedNote: "Esta nota ya está incluida en el clip \"{name}\"\
|
||||
. ¿Quiere quitar la nota del clip?"
|
||||
public: "Público"
|
||||
i18nInfo: "Calckey está siendo traducido a varios idiomas gracias a voluntarios. Se\
|
||||
\ puede colaborar traduciendo en {link}"
|
||||
i18nInfo: "Calckey está siendo traducido a varios idiomas gracias a voluntarios. Se
|
||||
puede colaborar traduciendo en {link}"
|
||||
manageAccessTokens: "Administrar tokens de acceso"
|
||||
accountInfo: "Información de la Cuenta"
|
||||
notesCount: "Cantidad de notas"
|
||||
|
@ -721,18 +720,18 @@ no: "No"
|
|||
driveFilesCount: "Cantidad de archivos en el drive"
|
||||
driveUsage: "Uso del drive"
|
||||
noCrawle: "Rechazar indexación del crawler"
|
||||
noCrawleDescription: "Pedir a los motores de búsqueda que no indexen tu perfil, notas,\
|
||||
\ páginas, etc."
|
||||
lockedAccountInfo: "A menos que configures la visibilidad de tus notas como \"Sólo\
|
||||
\ seguidores\", tus notas serán visibles para cualquiera, incluso si requieres que\
|
||||
\ los seguidores sean aprobados manualmente."
|
||||
alwaysMarkSensitive: "Marcar los medios de comunicación como contenido sensible por\
|
||||
\ defecto"
|
||||
noCrawleDescription: "Pedir a los motores de búsqueda que no indexen tu perfil, notas,
|
||||
páginas, etc."
|
||||
lockedAccountInfo: "A menos que configures la visibilidad de tus notas como \"Sólo
|
||||
seguidores\", tus notas serán visibles para cualquiera, incluso si requieres que
|
||||
los seguidores sean aprobados manualmente."
|
||||
alwaysMarkSensitive: "Marcar los medios de comunicación como contenido sensible por
|
||||
defecto"
|
||||
loadRawImages: "Cargar las imágenes originales en lugar de mostrar las miniaturas"
|
||||
disableShowingAnimatedImages: "No reproducir imágenes animadas"
|
||||
verificationEmailSent: "Se le ha enviado un correo electrónico de confirmación. Por\
|
||||
\ favor, acceda al enlace proporcionado en el correo electrónico para completar\
|
||||
\ la configuración."
|
||||
verificationEmailSent: "Se le ha enviado un correo electrónico de confirmación. Por
|
||||
favor, acceda al enlace proporcionado en el correo electrónico para completar la
|
||||
configuración."
|
||||
notSet: "Sin especificar"
|
||||
emailVerified: "Su dirección de correo electrónico ha sido verificada."
|
||||
noteFavoritesCount: "Número de notas favoritas"
|
||||
|
@ -744,16 +743,16 @@ clips: "Clip"
|
|||
experimentalFeatures: "Características experimentales"
|
||||
developer: "Desarrolladores"
|
||||
makeExplorable: "Hacer visible la cuenta en \"Explorar\""
|
||||
makeExplorableDescription: "Si desactiva esta opción, su cuenta no aparecerá en la\
|
||||
\ sección \"Explorar\"."
|
||||
makeExplorableDescription: "Si desactiva esta opción, su cuenta no aparecerá en la
|
||||
sección \"Explorar\"."
|
||||
showGapBetweenNotesInTimeline: "Mostrar un intervalo entre notas en la línea de tiempo"
|
||||
duplicate: "Duplicar"
|
||||
left: "Izquierda"
|
||||
center: "Centrar"
|
||||
wide: "Ancho"
|
||||
narrow: "Estrecho"
|
||||
reloadToApplySetting: "Esta configuración sólo se aplicará después de recargar la\
|
||||
\ página. ¿Recargar ahora?"
|
||||
reloadToApplySetting: "Esta configuración sólo se aplicará después de recargar la
|
||||
página. ¿Recargar ahora?"
|
||||
needReloadToApply: "Se requiere un reinicio para la aplicar los cambios"
|
||||
showTitlebar: "Mostrar la barra de título"
|
||||
clearCache: "Limpiar caché"
|
||||
|
@ -761,11 +760,11 @@ onlineUsersCount: "{n} usuarios en línea"
|
|||
nUsers: "{n} Usuarios"
|
||||
nNotes: "{n} Notas"
|
||||
sendErrorReports: "Envíar informe de errores"
|
||||
sendErrorReportsDescription: "Si habilita esta opción, los detalles de los errores\
|
||||
\ serán compartidos con Calckey cuando ocurra un problema, lo que ayudará a mejorar\
|
||||
\ la calidad de Calckey. \nEsto incluye información como la versión del sistema\
|
||||
\ operativo, el tipo de navegador que está utilizando y su historial en Calckey,\
|
||||
\ entre otros datos."
|
||||
sendErrorReportsDescription: "Si habilita esta opción, los detalles de los errores
|
||||
serán compartidos con Calckey cuando ocurra un problema, lo que ayudará a mejorar
|
||||
la calidad de Calckey. \nEsto incluye información como la versión del sistema operativo,
|
||||
el tipo de navegador que está utilizando y su historial en Calckey, entre otros
|
||||
datos."
|
||||
myTheme: "Mi Tema"
|
||||
backgroundColor: "Fondo"
|
||||
accentColor: "Acento"
|
||||
|
@ -793,8 +792,8 @@ receiveAnnouncementFromInstance: "Recibir notificaciones de la instancia"
|
|||
emailNotification: "Notificaciones por correo electrónico"
|
||||
publish: "Publicar"
|
||||
inChannelSearch: "Buscar en el canal"
|
||||
useReactionPickerForContextMenu: "Haga clic con el botón derecho para abrir el menu\
|
||||
\ de reacciones"
|
||||
useReactionPickerForContextMenu: "Haga clic con el botón derecho para abrir el menu
|
||||
de reacciones"
|
||||
typingUsers: "{users} está escribiendo"
|
||||
jumpToSpecifiedDate: "Saltar a una fecha específica"
|
||||
showingPastTimeline: "Mostrar líneas de tiempo antiguas"
|
||||
|
@ -805,16 +804,16 @@ unlikeConfirm: "¿Quitar como favorito?"
|
|||
fullView: "Vista completa"
|
||||
quitFullView: "quitar vista completa"
|
||||
addDescription: "Agregar descripción"
|
||||
userPagePinTip: "Puede mantener sus notas visibles aquí seleccionando Pin en el menú\
|
||||
\ de notas individuales"
|
||||
userPagePinTip: "Puede mantener sus notas visibles aquí seleccionando Pin en el menú
|
||||
de notas individuales"
|
||||
notSpecifiedMentionWarning: "Algunas menciones no están incluidas en el destino"
|
||||
info: "Información"
|
||||
userInfo: "Información del usuario"
|
||||
unknown: "Desconocido"
|
||||
onlineStatus: "En línea"
|
||||
hideOnlineStatus: "mostrarse como desconectado"
|
||||
hideOnlineStatusDescription: "Ocultar su estado en línea puede reducir la eficacia\
|
||||
\ de algunas funciones, como la búsqueda"
|
||||
hideOnlineStatusDescription: "Ocultar su estado en línea puede reducir la eficacia
|
||||
de algunas funciones, como la búsqueda"
|
||||
online: "En línea"
|
||||
active: "Activo"
|
||||
offline: "Sin conexión"
|
||||
|
@ -849,8 +848,8 @@ emailNotConfiguredWarning: "No se ha configurado una dirección de correo electr
|
|||
ratio: "Proporción"
|
||||
previewNoteText: "Mostrar vista preliminar"
|
||||
customCss: "CSS personalizado"
|
||||
customCssWarn: "Este ajuste sólo debe utilizarse si se sabe lo que hace. Introducir\
|
||||
\ valores inadecuados puede hacer que el cliente deje de funcionar con normalidad."
|
||||
customCssWarn: "Este ajuste sólo debe utilizarse si se sabe lo que hace. Introducir
|
||||
valores inadecuados puede hacer que el cliente deje de funcionar con normalidad."
|
||||
global: "Global"
|
||||
squareAvatars: "Mostrar iconos cuadrados"
|
||||
sent: "Enviar"
|
||||
|
@ -865,9 +864,9 @@ whatIsNew: "Mostrar cambios"
|
|||
translate: "Traducir"
|
||||
translatedFrom: "Traducido de {x}"
|
||||
accountDeletionInProgress: "La eliminación de la cuenta está en curso"
|
||||
usernameInfo: "Un nombre que identifique su cuenta de otras en este servidor. Puede\
|
||||
\ utilizar el alfabeto (a~z, A~Z), dígitos (0~9) o guiones bajos (_). Los nombres\
|
||||
\ de usuario no se pueden cambiar posteriormente."
|
||||
usernameInfo: "Un nombre que identifique su cuenta de otras en este servidor. Puede
|
||||
utilizar el alfabeto (a~z, A~Z), dígitos (0~9) o guiones bajos (_). Los nombres
|
||||
de usuario no se pueden cambiar posteriormente."
|
||||
aiChanMode: "Modo Ai"
|
||||
keepCw: "Mantener la advertencia de contenido"
|
||||
pubSub: "Cuentas Pub/Sub"
|
||||
|
@ -877,21 +876,21 @@ unresolved: "Sin resolver"
|
|||
breakFollow: "Dejar de seguir"
|
||||
itsOn: "¡Está encendido!"
|
||||
itsOff: "¡Está apagado!"
|
||||
emailRequiredForSignup: "Se requere una dirección de correo electrónico para el registro\
|
||||
\ de la cuenta"
|
||||
emailRequiredForSignup: "Se requere una dirección de correo electrónico para el registro
|
||||
de la cuenta"
|
||||
unread: "No leído"
|
||||
filter: "Filtro"
|
||||
controlPanel: "Panel de control"
|
||||
manageAccounts: "Administrar cuenta"
|
||||
makeReactionsPublic: "Hacer el historial de reacciones público"
|
||||
makeReactionsPublicDescription: "Todas las reacciones que hayas hecho serán públicamente\
|
||||
\ visibles."
|
||||
makeReactionsPublicDescription: "Todas las reacciones que hayas hecho serán públicamente
|
||||
visibles."
|
||||
classic: "Clásico"
|
||||
muteThread: "Ocultar hilo"
|
||||
unmuteThread: "Mostrar hilo"
|
||||
ffVisibility: "Visibilidad de seguidores y seguidos"
|
||||
ffVisibilityDescription: "Puedes configurar quien puede ver a quienes sigues y quienes\
|
||||
\ te siguen"
|
||||
ffVisibilityDescription: "Puedes configurar quien puede ver a quienes sigues y quienes
|
||||
te siguen"
|
||||
continueThread: "Ver la continuación del hilo"
|
||||
deleteAccountConfirm: "La cuenta será borrada. ¿Está seguro?"
|
||||
incorrectPassword: "La contraseña es incorrecta"
|
||||
|
@ -932,16 +931,16 @@ thereIsUnresolvedAbuseReportWarning: "Hay reportes sin resolver"
|
|||
recommended: "Recomendado"
|
||||
check: "Verificar"
|
||||
driveCapOverrideLabel: "Cambiar la capacidad de la unidad para este usuario"
|
||||
driveCapOverrideCaption: "Restablecer la capacidad a su predeterminado ingresando\
|
||||
\ un valor de 0 o menos"
|
||||
driveCapOverrideCaption: "Restablecer la capacidad a su predeterminado ingresando
|
||||
un valor de 0 o menos"
|
||||
requireAdminForView: "Necesitas iniciar sesión como administrador para ver esto."
|
||||
isSystemAccount: "Cuenta creada y operada automáticamente por el sistema"
|
||||
typeToConfirm: "Ingrese {x} para confirmar"
|
||||
deleteAccount: "Borrar cuenta"
|
||||
document: "Documento"
|
||||
numberOfPageCache: "Cantidad de páginas cacheadas"
|
||||
numberOfPageCacheDescription: "Al aumentar el número mejora la conveniencia pero tambien\
|
||||
\ puede aumentar la carga y la memoria a usarse"
|
||||
numberOfPageCacheDescription: "Al aumentar el número mejora la conveniencia pero tambien
|
||||
puede aumentar la carga y la memoria a usarse"
|
||||
logoutConfirm: "¿Cerrar sesión?"
|
||||
lastActiveDate: "Utilizado por última vez el"
|
||||
statusbar: "Barra de estado"
|
||||
|
@ -958,37 +957,36 @@ sensitiveMediaDetection: "Detección de contenido NSFW"
|
|||
localOnly: "Solo local"
|
||||
remoteOnly: "Sólo remoto"
|
||||
failedToUpload: "La subida falló"
|
||||
cannotUploadBecauseInappropriate: "Este archivo no se puede subir debido a que algunas\
|
||||
\ partes han sido detectadas comoNSFW."
|
||||
cannotUploadBecauseNoFreeSpace: "La subida falló debido a falta de espacio libre en\
|
||||
\ la unidad del usuario."
|
||||
cannotUploadBecauseInappropriate: "Este archivo no se puede subir debido a que algunas
|
||||
partes han sido detectadas comoNSFW."
|
||||
cannotUploadBecauseNoFreeSpace: "La subida falló debido a falta de espacio libre en
|
||||
la unidad del usuario."
|
||||
beta: "Beta"
|
||||
enableAutoSensitive: "Marcar automáticamente contenido NSFW"
|
||||
enableAutoSensitiveDescription: "Permite la detección y marcado automático de contenido\
|
||||
\ NSFW usando 'Machine Learning' cuando sea posible. Incluso si esta opción está\
|
||||
\ desactivada, puede ser activado para toda la instancia."
|
||||
activeEmailValidationDescription: "Habilita la validación estricta de direcciones\
|
||||
\ de correo electrónico, lo cual incluye la revisión de direcciones desechables\
|
||||
\ y si se puede comunicar con éstas. Cuando está deshabilitado, sólo el formato\
|
||||
\ de la dirección es validado."
|
||||
enableAutoSensitiveDescription: "Permite la detección y marcado automático de contenido
|
||||
NSFW usando 'Machine Learning' cuando sea posible. Incluso si esta opción está desactivada,
|
||||
puede ser activado para toda la instancia."
|
||||
activeEmailValidationDescription: "Habilita la validación estricta de direcciones
|
||||
de correo electrónico, lo cual incluye la revisión de direcciones desechables y
|
||||
si se puede comunicar con éstas. Cuando está deshabilitado, sólo el formato de la
|
||||
dirección es validado."
|
||||
navbar: "Barra de navegación"
|
||||
shuffle: "Aleatorio"
|
||||
account: "Cuentas"
|
||||
move: "Mover"
|
||||
_sensitiveMediaDetection:
|
||||
description: "Reduce el esfuerzo de la moderación el el servidor a través del reconocimiento\
|
||||
\ automático de contenido NSFW usando 'Machine Learning'. Esto puede incrementar\
|
||||
\ ligeramente la carga en el servidor."
|
||||
description: "Reduce el esfuerzo de la moderación el el servidor a través del reconocimiento
|
||||
automático de contenido NSFW usando 'Machine Learning'. Esto puede incrementar
|
||||
ligeramente la carga en el servidor."
|
||||
sensitivity: "Sensibilidad de detección"
|
||||
sensitivityDescription: "Reducir la sensibilidad puede acarrear a varios falsos\
|
||||
\ positivos, mientras que incrementarla puede reducir las detecciones (falsos\
|
||||
\ negativos)."
|
||||
sensitivityDescription: "Reducir la sensibilidad puede acarrear a varios falsos
|
||||
positivos, mientras que incrementarla puede reducir las detecciones (falsos negativos)."
|
||||
setSensitiveFlagAutomatically: "Marcar como NSFW"
|
||||
setSensitiveFlagAutomaticallyDescription: "Los resultados de la detección interna\
|
||||
\ pueden ser retenidos incluso si la opción está desactivada."
|
||||
setSensitiveFlagAutomaticallyDescription: "Los resultados de la detección interna
|
||||
pueden ser retenidos incluso si la opción está desactivada."
|
||||
analyzeVideos: "Habilitar el análisis de videos"
|
||||
analyzeVideosDescription: "Analizar videos en adición a las imágenes. Esto puede\
|
||||
\ incrementar ligeramente la carga del servidor."
|
||||
analyzeVideosDescription: "Analizar videos en adición a las imágenes. Esto puede
|
||||
incrementar ligeramente la carga del servidor."
|
||||
_emailUnavailable:
|
||||
used: "Ya fue usado"
|
||||
format: "El formato de este correo electrónico no es válido"
|
||||
|
@ -1002,15 +1000,15 @@ _ffVisibility:
|
|||
_signup:
|
||||
almostThere: "Ya falta poco"
|
||||
emailAddressInfo: "Ingrese el correo electrónico que usa. Este no se hará público."
|
||||
emailSent: "Se envió un correo de verificación a la dirección {email}. Acceda al\
|
||||
\ link enviado en el correo para completar el ingreso."
|
||||
emailSent: "Se envió un correo de verificación a la dirección {email}. Acceda al
|
||||
link enviado en el correo para completar el ingreso."
|
||||
_accountDelete:
|
||||
accountDelete: "Eliminar Cuenta"
|
||||
mayTakeTime: "La eliminación de la cuenta es un proceso que precisa de carga. Puede\
|
||||
\ pasar un tiempo hasta que se complete si es mucho el contenido creado y los\
|
||||
\ archivos subidos."
|
||||
sendEmail: "Cuando se termine de borrar la cuenta, se enviará un correo a la dirección\
|
||||
\ usada para el registro."
|
||||
mayTakeTime: "La eliminación de la cuenta es un proceso que precisa de carga. Puede
|
||||
pasar un tiempo hasta que se complete si es mucho el contenido creado y los archivos
|
||||
subidos."
|
||||
sendEmail: "Cuando se termine de borrar la cuenta, se enviará un correo a la dirección
|
||||
usada para el registro."
|
||||
requestAccountDelete: "Pedir la eliminación de la cuenta."
|
||||
started: "El proceso de eliminación ha comenzado."
|
||||
inProgress: "La eliminación está en proceso."
|
||||
|
@ -1018,12 +1016,11 @@ _ad:
|
|||
back: "Deseleccionar"
|
||||
reduceFrequencyOfThisAd: "Mostrar menos este anuncio."
|
||||
_forgotPassword:
|
||||
enterEmail: "Ingrese el correo usado para registrar la cuenta. Se enviará un link\
|
||||
\ para resetear la contraseña."
|
||||
enterEmail: "Ingrese el correo usado para registrar la cuenta. Se enviará un link
|
||||
para resetear la contraseña."
|
||||
ifNoEmail: "Si no utilizó un correo para crear la cuenta, contáctese con el administrador."
|
||||
contactAdmin: "Esta instancia no admite el uso de direcciones de correo electrónico,\
|
||||
\ póngase en contacto con el administrador de la instancia para restablecer su\
|
||||
\ contraseña"
|
||||
contactAdmin: "Esta instancia no admite el uso de direcciones de correo electrónico,
|
||||
póngase en contacto con el administrador de la instancia para restablecer su contraseña"
|
||||
_gallery:
|
||||
my: "Mi galería"
|
||||
liked: "Publicaciones que me gustan"
|
||||
|
@ -1046,15 +1043,15 @@ _preferencesBackups:
|
|||
save: "Guardar cambios"
|
||||
inputName: "Por favor, ingresa un nombre para este respaldo"
|
||||
cannotSave: "Fallo al guardar"
|
||||
nameAlreadyExists: "Un respaldo llamado \"{name}\" ya existe. Por favor ingresa\
|
||||
\ un nombre diferente"
|
||||
nameAlreadyExists: "Un respaldo llamado \"{name}\" ya existe. Por favor ingresa
|
||||
un nombre diferente"
|
||||
applyConfirm: "¿Realmente quieres aplicar los cambios desde el archivo \"{name}\"\
|
||||
\ a este dispositivo? Las configuraciones existentes serán sobreescritas. "
|
||||
saveConfirm: "¿Guardar respaldo como \"{name}\"?"
|
||||
deleteConfirm: "¿Borrar el respaldo \"{name}\"?"
|
||||
renameConfirm: "¿Renombrar este respaldo de \"{old}\" a \"{new}\"?"
|
||||
noBackups: "No existen respaldos. Deberás respaldar las configuraciones del cliente\
|
||||
\ en este servidor usando \"Crear nuevo respaldo\""
|
||||
noBackups: "No existen respaldos. Deberás respaldar las configuraciones del cliente
|
||||
en este servidor usando \"Crear nuevo respaldo\""
|
||||
createdAt: "Creado: {date} {time}"
|
||||
updatedAt: "Actualizado: {date} {time}"
|
||||
cannotLoad: "La carga falló"
|
||||
|
@ -1066,15 +1063,15 @@ _registry:
|
|||
domain: "Dominio"
|
||||
createKey: "Crear una llave"
|
||||
_aboutMisskey:
|
||||
about: "Calckey es una bifurcación de Misskey creada por ThatOneCalculator, que\
|
||||
\ ha estado en desarrollo desde el 2022."
|
||||
about: "Calckey es una bifurcación de Misskey creada por ThatOneCalculator, que
|
||||
ha estado en desarrollo desde el 2022."
|
||||
contributors: "Principales colaboradores"
|
||||
allContributors: "Todos los colaboradores"
|
||||
source: "Código fuente"
|
||||
translation: "Traducir Calckey"
|
||||
donate: "Donar a Calckey"
|
||||
morePatrons: "También apreciamos el apoyo de muchos más que no están enlistados\
|
||||
\ aquí. ¡Gracias! \U0001F970"
|
||||
morePatrons: "También apreciamos el apoyo de muchos más que no están enlistados
|
||||
aquí. ¡Gracias! 🥰"
|
||||
patrons: "Mecenas de Calckey"
|
||||
_nsfw:
|
||||
respect: "Ocultar medios NSFW"
|
||||
|
@ -1082,13 +1079,13 @@ _nsfw:
|
|||
force: "Ocultar todos los medios"
|
||||
_mfm:
|
||||
cheatSheet: "Hoja de referencia de MFM"
|
||||
intro: "MFM es un lenguaje de marcado dedicado que se puede usar en varios lugares\
|
||||
\ dentro de Misskey, Calckey, Akkoma, y mucho más. Aquí puede ver una lista de\
|
||||
\ sintaxis disponibles en MFM."
|
||||
intro: "MFM es un lenguaje de marcado dedicado que se puede usar en varios lugares
|
||||
dentro de Misskey, Calckey, Akkoma, y mucho más. Aquí puede ver una lista de sintaxis
|
||||
disponibles en MFM."
|
||||
dummy: "Calckey expande el mundo de la Fediverso"
|
||||
mention: "Menciones"
|
||||
mentionDescription: "El signo @ seguido de un nombre de usuario se puede utilizar\
|
||||
\ para notificar a un usuario en particular."
|
||||
mentionDescription: "El signo @ seguido de un nombre de usuario se puede utilizar
|
||||
para notificar a un usuario en particular."
|
||||
hashtag: "Hashtag"
|
||||
hashtagDescription: "Puede especificar un hashtag con un numeral y el texto."
|
||||
url: "URL"
|
||||
|
@ -1104,8 +1101,8 @@ _mfm:
|
|||
inlineCode: "Código (insertado)"
|
||||
inlineCodeDescription: "Muestra el código de un programa resaltando su sintaxis"
|
||||
blockCode: "Código (bloque)"
|
||||
blockCodeDescription: "Código de resaltado de sintaxis, como programas de varias\
|
||||
\ líneas con bloques."
|
||||
blockCodeDescription: "Código de resaltado de sintaxis, como programas de varias
|
||||
líneas con bloques."
|
||||
inlineMath: "Fórmula (insertado)"
|
||||
inlineMathDescription: "Muestra fórmulas (KaTeX) insertadas"
|
||||
blockMath: "Fórmula (bloque)"
|
||||
|
@ -1117,8 +1114,8 @@ _mfm:
|
|||
search: "Buscar"
|
||||
searchDescription: "Muestra una caja de búsqueda con texto pre-escrito"
|
||||
flip: "Echar de un capirotazo"
|
||||
flipDescription: "Voltea el contenido hacia arriba / abajo o hacia la izquierda\
|
||||
\ / derecha."
|
||||
flipDescription: "Voltea el contenido hacia arriba / abajo o hacia la izquierda
|
||||
/ derecha."
|
||||
jelly: "Animación (gelatina)"
|
||||
jellyDescription: "Aplica un efecto de animación tipo gelatina"
|
||||
tada: "Animación (tadá)"
|
||||
|
@ -1140,8 +1137,8 @@ _mfm:
|
|||
x4: "Totalmente grande"
|
||||
x4Description: "Muestra el contenido totalmente grande"
|
||||
blur: "Desenfoque"
|
||||
blurDescription: "Para desenfocar el contenido. Se muestra claramente al colocar\
|
||||
\ el puntero encima."
|
||||
blurDescription: "Para desenfocar el contenido. Se muestra claramente al colocar
|
||||
el puntero encima."
|
||||
font: "Fuente"
|
||||
fontDescription: "Elegir la fuente del contenido"
|
||||
rainbow: "Arcoíris"
|
||||
|
@ -1151,8 +1148,8 @@ _mfm:
|
|||
rotate: "Rotar"
|
||||
rotateDescription: "Rota el contenido a un ángulo especificado."
|
||||
plain: "Plano"
|
||||
plainDescription: "Desactiva los efectos de todo el contenido MFM con este efecto\
|
||||
\ MFM."
|
||||
plainDescription: "Desactiva los efectos de todo el contenido MFM con este efecto
|
||||
MFM."
|
||||
position: Posición
|
||||
_instanceTicker:
|
||||
none: "No mostrar"
|
||||
|
@ -1182,20 +1179,19 @@ _menuDisplay:
|
|||
hide: "Ocultar"
|
||||
_wordMute:
|
||||
muteWords: "Palabras que silenciar"
|
||||
muteWordsDescription: "Separar con espacios indica una declaracion And, separar\
|
||||
\ con lineas nuevas indica una declaracion Or。"
|
||||
muteWordsDescription2: "Encerrar las palabras clave entre numerales para usar expresiones\
|
||||
\ regulares"
|
||||
muteWordsDescription: "Separar con espacios indica una declaracion And, separar
|
||||
con lineas nuevas indica una declaracion Or。"
|
||||
muteWordsDescription2: "Encerrar las palabras clave entre numerales para usar expresiones
|
||||
regulares"
|
||||
softDescription: "Ocultar en la linea de tiempo las notas que cumplen las condiciones"
|
||||
hardDescription: "Evitar que se agreguen a la linea de tiempo las notas que cumplen\
|
||||
\ las condiciones. Las notas no agregadas seguirán quitadas aunque cambien las\
|
||||
\ condiciones."
|
||||
hardDescription: "Evitar que se agreguen a la linea de tiempo las notas que cumplen
|
||||
las condiciones. Las notas no agregadas seguirán quitadas aunque cambien las condiciones."
|
||||
soft: "Suave"
|
||||
hard: "Duro"
|
||||
mutedNotes: "Notas silenciadas"
|
||||
_instanceMute:
|
||||
instanceMuteDescription: "Silencia todas las notas y reposts de la instancias seleccionadas,\
|
||||
\ incluyendo respuestas a los usuarios de las mismas"
|
||||
instanceMuteDescription: "Silencia todas las notas y reposts de la instancias seleccionadas,
|
||||
incluyendo respuestas a los usuarios de las mismas"
|
||||
instanceMuteDescription2: "Separar por líneas"
|
||||
title: "Oculta las notas de las instancias listadas."
|
||||
heading: "Instancias a silenciar"
|
||||
|
@ -1301,47 +1297,47 @@ _tutorial:
|
|||
step1_1: "¡Bienvenido!"
|
||||
step1_2: "Vamos a configurarte. Estarás listo y funcionando en poco tiempo"
|
||||
step2_1: "En primer lugar, rellena tu perfil"
|
||||
step2_2: "Proporcionar algo de información sobre quién eres hará que sea más fácil\
|
||||
\ para los demás saber si quieren ver tus notas o seguirte."
|
||||
step2_2: "Proporcionar algo de información sobre quién eres hará que sea más fácil
|
||||
para los demás saber si quieren ver tus notas o seguirte."
|
||||
step3_1: "¡Ahora es el momento de seguir a algunas personas!"
|
||||
step3_2: "Tu página de inicio y tus líneas de tiempo sociales se basan en quién\
|
||||
\ sigues, así que intenta seguir un par de cuentas para empezar.\nHaz clic en\
|
||||
\ el círculo más en la parte superior derecha de un perfil para seguirlos."
|
||||
step3_2: "Tu página de inicio y tus líneas de tiempo sociales se basan en quién
|
||||
sigues, así que intenta seguir un par de cuentas para empezar.\nHaz clic en el
|
||||
círculo más en la parte superior derecha de un perfil para seguirlos."
|
||||
step4_1: "Vamos a salir a la calle"
|
||||
step4_2: "Para tu primer post, a algunas personas les gusta hacer un post de {introduction}\
|
||||
\ o un simple \"¡Hola mundo!\""
|
||||
step4_2: "Para tu primer post, a algunas personas les gusta hacer un post de {introduction}
|
||||
o un simple \"¡Hola mundo!\""
|
||||
step5_1: "¡Líneas de tiempo, líneas de tiempo por todas partes!"
|
||||
step5_2: "Su instancia tiene {timelines} diferentes líneas de tiempo habilitadas"
|
||||
step5_3: "La línea de tiempo Inicio {icon} es donde puedes ver las publicaciones\
|
||||
\ de tus seguidores."
|
||||
step5_4: "La línea de tiempo Local {icon} es donde puedes ver las publicaciones\
|
||||
\ de todos los demás en esta instancia."
|
||||
step5_5: "La línea de tiempo {icon} recomendada es donde puedes ver las publicaciones\
|
||||
\ de las instancias que los administradores recomiendan."
|
||||
step5_6: "La línea de tiempo Social {icon} es donde puedes ver las publicaciones\
|
||||
\ de los amigos de tus seguidores."
|
||||
step5_7: "La línea de tiempo Global {icon} es donde puedes ver las publicaciones\
|
||||
\ de todas las demás instancias conectadas."
|
||||
step5_3: "La línea de tiempo Inicio {icon} es donde puedes ver las publicaciones
|
||||
de tus seguidores."
|
||||
step5_4: "La línea de tiempo Local {icon} es donde puedes ver las publicaciones
|
||||
de todos los demás en esta instancia."
|
||||
step5_5: "La línea de tiempo {icon} recomendada es donde puedes ver las publicaciones
|
||||
de las instancias que los administradores recomiendan."
|
||||
step5_6: "La línea de tiempo Social {icon} es donde puedes ver las publicaciones
|
||||
de los amigos de tus seguidores."
|
||||
step5_7: "La línea de tiempo Global {icon} es donde puedes ver las publicaciones
|
||||
de todas las demás instancias conectadas."
|
||||
step6_1: "Entonces, ¿qué es este lugar?"
|
||||
step6_2: "Bueno, no sólo te has unido a Calckey. Te has unido a un portal del Fediverso,\
|
||||
\ una red interconectada de miles de servidores, llamada \"instancias\""
|
||||
step6_3: "Cada servidor funciona de forma diferente, y no todos los servidores ejecutan\
|
||||
\ Calckey. Sin embargo, ¡éste lo hace! Es un poco complicado, pero le cogerás\
|
||||
\ el tranquillo enseguida"
|
||||
step6_2: "Bueno, no sólo te has unido a Calckey. Te has unido a un portal del Fediverso,
|
||||
una red interconectada de miles de servidores, llamada \"instancias\""
|
||||
step6_3: "Cada servidor funciona de forma diferente, y no todos los servidores ejecutan
|
||||
Calckey. Sin embargo, ¡éste lo hace! Es un poco complicado, pero le cogerás el
|
||||
tranquillo enseguida"
|
||||
step6_4: "¡Ahora ve, explora y diviértete!"
|
||||
_2fa:
|
||||
alreadyRegistered: "Ya has completado la configuración."
|
||||
registerTOTP: "Registrar dispositivo"
|
||||
registerSecurityKey: "Registrar clave"
|
||||
step1: "Primero, instale en su dispositivo la aplicación de autenticación {a} o\
|
||||
\ {b} u otra."
|
||||
step1: "Primero, instale en su dispositivo la aplicación de autenticación {a} o
|
||||
{b} u otra."
|
||||
step2: "Luego, escanee con la aplicación el código QR mostrado en pantalla."
|
||||
step2Url: "En una aplicación de escritorio se puede ingresar la siguiente URL:"
|
||||
step3: "Para terminar, ingrese el token mostrado en la aplicación."
|
||||
step4: "Ahora cuando inicie sesión, ingrese el mismo token"
|
||||
securityKeyInfo: "Se puede configurar el inicio de sesión usando una clave de seguridad\
|
||||
\ de hardware que soporte FIDO2 o con un certificado de huella digital o con un\
|
||||
\ PIN"
|
||||
securityKeyInfo: "Se puede configurar el inicio de sesión usando una clave de seguridad
|
||||
de hardware que soporte FIDO2 o con un certificado de huella digital o con un
|
||||
PIN"
|
||||
_permissions:
|
||||
"read:account": "Ver información de la cuenta"
|
||||
"write:account": "Editar información de la cuenta"
|
||||
|
@ -1377,8 +1373,8 @@ _permissions:
|
|||
"write:gallery-likes": "Editar favoritos de la galería"
|
||||
_auth:
|
||||
shareAccess: "¿Desea permitir el acceso a la cuenta \"{name}\"?"
|
||||
shareAccessAsk: "¿Está seguro de que desea autorizar esta aplicación para acceder\
|
||||
\ a su cuenta?"
|
||||
shareAccessAsk: "¿Está seguro de que desea autorizar esta aplicación para acceder
|
||||
a su cuenta?"
|
||||
permissionAsk: "Esta aplicación requiere los siguientes permisos"
|
||||
pleaseGoBack: "Por favor, vuelve a la aplicación"
|
||||
callback: "Volviendo a la aplicación"
|
||||
|
@ -1772,8 +1768,8 @@ _pages:
|
|||
_seedRandomPick:
|
||||
arg1: "Semilla"
|
||||
arg2: "Listas"
|
||||
DRPWPM: "Elegir aleatoriamente de la lista ponderada (Diariamente para cada\
|
||||
\ usuario)"
|
||||
DRPWPM: "Elegir aleatoriamente de la lista ponderada (Diariamente para cada
|
||||
usuario)"
|
||||
_DRPWPM:
|
||||
arg1: "Lista de texto"
|
||||
pick: "Elegir de la lista"
|
||||
|
@ -1804,8 +1800,8 @@ _pages:
|
|||
_for:
|
||||
arg1: "Cantidad de repeticiones"
|
||||
arg2: "Acción"
|
||||
typeError: "El slot {slot} acepta el tipo {expect} pero fue ingresado el tipo\
|
||||
\ {actual}"
|
||||
typeError: "El slot {slot} acepta el tipo {expect} pero fue ingresado el tipo
|
||||
{actual}"
|
||||
thereIsEmptySlot: "El slot {slot} está vacío"
|
||||
types:
|
||||
string: "Texto"
|
||||
|
@ -1869,10 +1865,10 @@ _deck:
|
|||
newProfile: "Nuevo perfil"
|
||||
deleteProfile: "Eliminar perfil"
|
||||
introduction: "¡Crea la interfaz perfecta para tí organizando las columnas libremente!"
|
||||
introduction2: "Presiona en la + de la derecha de la pantalla para añadir nuevas\
|
||||
\ columnas donde quieras."
|
||||
widgetsIntroduction: "Por favor selecciona \"Editar Widgets\" en el menú columna\
|
||||
\ y agrega un widget."
|
||||
introduction2: "Presiona en la + de la derecha de la pantalla para añadir nuevas
|
||||
columnas donde quieras."
|
||||
widgetsIntroduction: "Por favor selecciona \"Editar Widgets\" en el menú columna
|
||||
y agrega un widget."
|
||||
_columns:
|
||||
main: "Principal"
|
||||
widgets: "Widgets"
|
||||
|
@ -1885,11 +1881,11 @@ _deck:
|
|||
manageGroups: Administrar grupos
|
||||
replayTutorial: Repetir Tutorial
|
||||
privateMode: Modo privado
|
||||
addInstance: Añadir una instancia
|
||||
addInstance: Añadir un servidor
|
||||
renoteMute: Silenciar impulsos
|
||||
renoteUnmute: Dejar de silenciar impulsos
|
||||
flagSpeakAsCat: Habla como un gato
|
||||
selectInstance: Selectiona una instancia
|
||||
selectInstance: Selecciona un servidor
|
||||
flagSpeakAsCatDescription: Tu publicación se "nyanified" cuando esté en modo gato
|
||||
allowedInstances: Instancias en la lista blanca
|
||||
breakFollowConfirm: ¿Estás seguro de que quieres eliminar el seguidor?
|
||||
|
@ -1905,7 +1901,7 @@ license: Licencia
|
|||
noThankYou: No gracias
|
||||
userSaysSomethingReason: '{name} dijo {reason}'
|
||||
hiddenTags: Etiquetas Ocultas
|
||||
noInstances: No hay instancias
|
||||
noInstances: No hay servidores
|
||||
accountMoved: 'Usuario ha movido a una cuenta nueva:'
|
||||
caption: Auto Subtítulos
|
||||
showAds: Mostrar Anuncios
|
||||
|
@ -1922,8 +1918,26 @@ apps: Aplicaciones
|
|||
migration: Migración
|
||||
silenced: Silenciado
|
||||
deleted: Eliminado
|
||||
edited: Editado
|
||||
edited: 'Editado a las {date} {time}'
|
||||
editNote: Editar nota
|
||||
silenceThisInstance: Silenciar esta instancia
|
||||
silenceThisInstance: Silenciar este servidor
|
||||
findOtherInstance: Buscar otro servidor
|
||||
userSaysSomethingReasonRenote: '{name} impulsó una publicación que contiene {reason]'
|
||||
enableRecommendedTimeline: Habilitar línea de tiempo "Recomendado"
|
||||
searchPlaceholder: Buscar en Calckey
|
||||
listsDesc: Las listas te permiten crear líneas de tiempo con usuarios específicos.
|
||||
Puedes acceder a ellas desde la pestaña "Línea de tiempo".
|
||||
removeReaction: Quitar tu reacción
|
||||
selectChannel: Seleccionar canal
|
||||
showEmojisInReactionNotifications: Mostrar emojis en notificaciones de reacciones
|
||||
silencedInstancesDescription: Escriba los hosts de los servidores que desea bloquear.
|
||||
Las cuentas en estos servidores serán tratadas como "silenciadas", solo podrán hacer
|
||||
solicitudes de seguimiento, y no podrán mencionar a usuarios de este servidor si
|
||||
no les siguen. Esto no afecta los servidores bloqueados.
|
||||
silencedInstances: Servidores silenciados
|
||||
hiddenTagsDescription: 'Escriba los hashtags (sin el #) que desea ocultar de las secciones
|
||||
de Tendencias y Explorar. Los hashtags ocultos seguirán siendo descubribles por
|
||||
otros métodos.'
|
||||
jumpToPrevious: Ver anterior
|
||||
enableEmojiReactions: Habilitar reacciones de emoji
|
||||
cw: Aviso de contenido
|
||||
|
|
|
@ -831,7 +831,7 @@ makeReactionsPublic: Aseta reaktiohistoria julkiseksi
|
|||
unread: Lukematon
|
||||
deleted: Poistettu
|
||||
editNote: Muokkaa viestiä
|
||||
edited: Muokattu
|
||||
edited: 'Muokattu klo {date} {time}'
|
||||
avoidMultiCaptchaConfirm: Useiden Captcha-järjestelmien käyttö voi aiheuttaa häiriöitä
|
||||
niiden välillä. Haluatko poistaa käytöstä muut tällä hetkellä käytössä olevat Captcha-järjestelmät?
|
||||
Jos haluat, että ne pysyvät käytössä, paina peruutusnäppäintä.
|
||||
|
|
|
@ -2022,13 +2022,13 @@ silencedInstances: Instances silencieuses
|
|||
silenced: Silencieux
|
||||
deleted: Effacé
|
||||
editNote: Modifier note
|
||||
edited: Modifié
|
||||
edited: 'Modifié à {date} {time}'
|
||||
flagShowTimelineRepliesDescription: Si activé, affiche dans le fil les réponses des
|
||||
personnes aux publications des autres.
|
||||
_experiments:
|
||||
alpha: Alpha
|
||||
beta: Beta
|
||||
enablePostEditing: Autoriser l'édition de note
|
||||
enablePostImports: Autoriser l'importation de messages
|
||||
title: Expérimentations
|
||||
findOtherInstance: Trouver un autre serveur
|
||||
userSaysSomethingReasonQuote: '{name} a cité une note contenant {reason}'
|
||||
|
|
113
locales/index.js
113
locales/index.js
|
@ -2,59 +2,90 @@
|
|||
* Languages Loader
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
let languages = []
|
||||
let languages_custom = []
|
||||
|
||||
const merge = (...args) => args.reduce((a, c) => ({
|
||||
...a,
|
||||
...c,
|
||||
...Object.entries(a)
|
||||
.filter(([k]) => c && typeof c[k] === 'object')
|
||||
.reduce((a, [k, v]) => (a[k] = merge(v, c[k]), a), {})
|
||||
}), {});
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const languages = [];
|
||||
const languages_custom = [];
|
||||
|
||||
const merge = (...args) =>
|
||||
args.reduce(
|
||||
(a, c) => ({
|
||||
...a,
|
||||
...c,
|
||||
...Object.entries(a)
|
||||
.filter(([k]) => c && typeof c[k] === "object")
|
||||
.reduce((a, [k, v]) => ((a[k] = merge(v, c[k])), a), {}),
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
fs.readdirSync(__dirname).forEach((file) => {
|
||||
if (file.includes('.yml')){
|
||||
file = file.slice(0, file.indexOf('.'))
|
||||
if (file.includes(".yml")) {
|
||||
file = file.slice(0, file.indexOf("."));
|
||||
languages.push(file);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
fs.readdirSync(__dirname + '/../custom/locales').forEach((file) => {
|
||||
if (file.includes('.yml')){
|
||||
file = file.slice(0, file.indexOf('.'))
|
||||
fs.readdirSync(__dirname + "/../custom/locales").forEach((file) => {
|
||||
if (file.includes(".yml")) {
|
||||
file = file.slice(0, file.indexOf("."));
|
||||
languages_custom.push(file);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const primaries = {
|
||||
'en': 'US',
|
||||
'ja': 'JP',
|
||||
'zh': 'CN',
|
||||
en: "US",
|
||||
ja: "JP",
|
||||
zh: "CN",
|
||||
};
|
||||
|
||||
// 何故か文字列にバックスペース文字が混入することがあり、YAMLが壊れるので取り除く
|
||||
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
|
||||
const clean = (text) =>
|
||||
text.replace(new RegExp(String.fromCodePoint(0x08), "g"), "");
|
||||
|
||||
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(`${__dirname}/${c}.yml`, 'utf-8'))) || {}, a), {});
|
||||
const locales_custom = languages_custom.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(`${__dirname}/../custom/locales/${c}.yml`, 'utf-8'))) || {}, a), {});
|
||||
Object.assign(locales, locales_custom)
|
||||
const locales = languages.reduce(
|
||||
(a, c) => (
|
||||
(a[c] =
|
||||
yaml.load(clean(fs.readFileSync(`${__dirname}/${c}.yml`, "utf-8"))) ||
|
||||
{}),
|
||||
a
|
||||
),
|
||||
{},
|
||||
);
|
||||
const locales_custom = languages_custom.reduce(
|
||||
(a, c) => (
|
||||
(a[c] =
|
||||
yaml.load(
|
||||
clean(
|
||||
fs.readFileSync(`${__dirname}/../custom/locales/${c}.yml`, "utf-8"),
|
||||
),
|
||||
) || {}),
|
||||
a
|
||||
),
|
||||
{},
|
||||
);
|
||||
Object.assign(locales, locales_custom);
|
||||
|
||||
module.exports = Object.entries(locales)
|
||||
.reduce((a, [k ,v]) => (a[k] = (() => {
|
||||
const [lang] = k.split('-');
|
||||
switch (k) {
|
||||
case 'ja-JP': return v;
|
||||
case 'ja-KS':
|
||||
case 'en-US': return merge(locales['ja-JP'], v);
|
||||
default: return merge(
|
||||
locales['ja-JP'],
|
||||
locales['en-US'],
|
||||
locales[`${lang}-${primaries[lang]}`] || {},
|
||||
v
|
||||
);
|
||||
}
|
||||
})(), a), {});
|
||||
module.exports = Object.entries(locales).reduce(
|
||||
(a, [k, v]) => (
|
||||
(a[k] = (() => {
|
||||
const [lang] = k.split("-");
|
||||
switch (k) {
|
||||
case "ja-JP":
|
||||
return v;
|
||||
case "ja-KS":
|
||||
case "en-US":
|
||||
return merge(locales["ja-JP"], v);
|
||||
default:
|
||||
return merge(
|
||||
locales["ja-JP"],
|
||||
locales["en-US"],
|
||||
locales[`${lang}-${primaries[lang]}`] || {},
|
||||
v,
|
||||
);
|
||||
}
|
||||
})()),
|
||||
a
|
||||
),
|
||||
{},
|
||||
);
|
||||
|
|
|
@ -33,9 +33,9 @@ logout: "Esci"
|
|||
signup: "Iscriviti"
|
||||
uploading: "Caricamento..."
|
||||
save: "Salva"
|
||||
users: "Utente"
|
||||
users: "Utenti"
|
||||
addUser: "Aggiungi utente"
|
||||
favorite: "Preferiti"
|
||||
favorite: "Aggiungi ai preferiti"
|
||||
favorites: "Preferiti"
|
||||
unfavorite: "Rimuovi nota dai preferiti"
|
||||
favorited: "Aggiunta ai tuoi preferiti."
|
||||
|
@ -58,7 +58,7 @@ loadMore: "Mostra di più"
|
|||
showMore: "Mostra di più"
|
||||
showLess: "Chiudi"
|
||||
youGotNewFollower: "Ha iniziato a seguirti"
|
||||
receiveFollowRequest: "Hai ricevuto una richiesta di follow."
|
||||
receiveFollowRequest: "Hai ricevuto una richiesta di follow"
|
||||
followRequestAccepted: "Richiesta di follow accettata"
|
||||
mention: "Menzioni"
|
||||
mentions: "Menzioni"
|
||||
|
@ -1559,3 +1559,5 @@ _deck:
|
|||
mentions: "Menzioni"
|
||||
direct: "Diretta"
|
||||
noThankYou: No grazie
|
||||
addInstance: Aggiungi un'istanza
|
||||
deleted: Eliminato
|
||||
|
|
|
@ -978,6 +978,8 @@ enableCustomKaTeXMacro: "カスタムKaTeXマクロを有効にする"
|
|||
preventAiLearning: "AIによる学習を防止"
|
||||
preventAiLearningDescription: "投稿したノート、添付した画像などのコンテンツを学習の対象にしないようAIに要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されます。"
|
||||
noGraze: "ブラウザの拡張機能「Graze for Mastodon」は、Calckeyの動作を妨げるため、無効にしてください。"
|
||||
enableServerMachineStats: "サーバーのマシン情報を公開する"
|
||||
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
|
||||
|
||||
_sensitiveMediaDetection:
|
||||
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。"
|
||||
|
@ -1336,6 +1338,7 @@ _2fa:
|
|||
renewTOTPConfirm: "今までの認証アプリの確認コードは使用できなくなります"
|
||||
renewTOTPOk: "再設定する"
|
||||
renewTOTPCancel: "やめておく"
|
||||
token: "多要素認証トークン"
|
||||
_permissions:
|
||||
"read:account": "アカウントの情報を見る"
|
||||
"write:account": "アカウントの情報を変更する"
|
||||
|
@ -1886,16 +1889,14 @@ hiddenTagsDescription: 'トレンドと「みつける」から除外したい
|
|||
hiddenTags: 非表示にするハッシュタグ
|
||||
apps: "アプリ"
|
||||
_experiments:
|
||||
enablePostEditing: 投稿の編集機能を有効にする
|
||||
title: 試験的な機能
|
||||
postEditingCaption: 投稿のメニューに既存の投稿を編集するボタンを表示し、他サーバーの編集も受信できるようにします。
|
||||
postImportsCaption:
|
||||
postImportsCaption:
|
||||
ユーザーが過去の投稿をCalckey・Misskey・Mastodon・Akkoma・Pleromaからインポートすることを許可します。キューが溜まっているときにインポートするとサーバーに負荷がかかる可能性があります。
|
||||
enablePostImports: 投稿のインポートを有効にする
|
||||
sendModMail: モデレーション通知を送る
|
||||
deleted: 削除済み
|
||||
editNote: 投稿を編集
|
||||
edited: 編集済み
|
||||
edited: '編集済み: {date} {time}'
|
||||
signupsDisabled:
|
||||
現在、このサーバーでは新規登録が一般開放されていません。招待コードをお持ちの場合には、以下の欄に入力してください。招待コードをお持ちでない場合にも、新規登録を開放している他のサーバーには入れますよ!
|
||||
findOtherInstance: 他のサーバーを探す
|
||||
|
|
|
@ -413,7 +413,7 @@ selectList: Selecteer een lijst
|
|||
selectAntenna: Selecteer een antenne
|
||||
deleted: Verwijderd
|
||||
editNote: Bewerk notitie
|
||||
edited: Bewerkt
|
||||
edited: 'Bewerkt om {date} {time}'
|
||||
emojis: Emojis
|
||||
emojiName: Emoji naam
|
||||
emojiUrl: Emoji URL
|
||||
|
|
|
@ -1998,7 +1998,7 @@ silenceThisInstance: Wycisz ten serwer
|
|||
silencedInstances: Wyciszone serwery
|
||||
deleted: Usunięte
|
||||
editNote: Edytuj wpis
|
||||
edited: Edytowany
|
||||
edited: 'Edytowano o {date} {time}'
|
||||
silenced: Wyciszony
|
||||
findOtherInstance: Znajdź inny serwer
|
||||
userSaysSomethingReasonReply: '{name} odpowiedział na wpis zawierający {reason}'
|
||||
|
@ -2029,3 +2029,4 @@ channelFederationWarn: Kanały nie są jeszcze federowane z innymi serwerami
|
|||
newer: nowsze
|
||||
older: starsze
|
||||
cw: Ostrzeżenie zawartości
|
||||
removeReaction: Usuń reakcję
|
||||
|
|
|
@ -12,3 +12,76 @@ notifications: Notificações
|
|||
password: Senha
|
||||
forgotPassword: Esqueci a senha
|
||||
cancel: Cancelar
|
||||
noThankYou: Não, obrigade
|
||||
save: Salvar
|
||||
enterUsername: Insira nome de usuário
|
||||
cw: Aviso de conteúdo
|
||||
driveFileDeleteConfirm: Tem a certeza de que pretende apagar o arquivo "{name}"? O
|
||||
arquivo será removido de todas as mensagens que o contenham como anexo.
|
||||
deleteAndEdit: Deletar e editar
|
||||
import: Importar
|
||||
exportRequested: Você pediu uma exportação. Isso pode demorar um pouco. Será adicionado
|
||||
ao seu Drive quando for completo.
|
||||
note: Postar
|
||||
notes: Postagens
|
||||
deleteAndEditConfirm: Você tem certeza que quer deletar esse post e edita-lo? Você
|
||||
vai perder todas as reações, impulsionamentos e respostas dele.
|
||||
showLess: Fechar
|
||||
importRequested: Você requisitou uma importação. Isso pode demorar um pouco.
|
||||
listsDesc: Listas deixam você criar linhas do tempo com usuários específicos. Elas
|
||||
podem ser acessadas pela página de linhas do tempo.
|
||||
edited: 'Editado às {date} {time}'
|
||||
sendMessage: Enviar uma mensagem
|
||||
older: antigo
|
||||
createList: Criar lista
|
||||
loadMore: Carregar mais
|
||||
mentions: Menções
|
||||
importAndExport: Importar/Exportar Dados
|
||||
files: Arquivos
|
||||
lists: Listas
|
||||
manageLists: Gerenciar listas
|
||||
error: Erro
|
||||
somethingHappened: Ocorreu um erro
|
||||
retry: Tentar novamente
|
||||
renotedBy: Impulsionado por {user}
|
||||
noNotes: Nenhum post
|
||||
noNotifications: Nenhuma notificação
|
||||
instance: Servidor
|
||||
settings: Configurações
|
||||
basicSettings: Configurações Básicas
|
||||
otherSettings: Outras Configurações
|
||||
openInWindow: Abrir em janela
|
||||
profile: Perfil
|
||||
noAccountDescription: Esse usuário ainda não escreveu sua bio.
|
||||
login: Entrar
|
||||
loggingIn: Entrando
|
||||
logout: Sair
|
||||
signup: Criar conta
|
||||
uploading: Enviando...
|
||||
users: Usuários
|
||||
addUser: Adicione um usuário
|
||||
addInstance: Adicionar um servidor
|
||||
cantFavorite: Não foi possível adicionar aos marcadores.
|
||||
pin: Fixar no perfil
|
||||
unpin: Desfixar do perfil
|
||||
copyContent: Copiar conteúdos
|
||||
copyLink: Copiar link
|
||||
delete: Deletar
|
||||
deleted: Deletado
|
||||
editNote: Editar anotação
|
||||
addToList: Adicionar a lista
|
||||
copyUsername: Copiar nome de usuário
|
||||
searchUser: Procurar por um usuário
|
||||
reply: Responder
|
||||
jumpToPrevious: Pular para o anterior
|
||||
showMore: Mostrar mais
|
||||
newer: novo
|
||||
youGotNewFollower: seguiu você
|
||||
mention: Mencionar
|
||||
directNotes: Mensagens diretas
|
||||
export: Exportar
|
||||
unfollowConfirm: Você tem certez que deseja para de seguir {name}?
|
||||
noLists: Você não possui nenhuma lista
|
||||
following: Seguindo
|
||||
followers: Seguidores
|
||||
followsYou: Segue você
|
||||
|
|
|
@ -1987,5 +1987,5 @@ apps: Приложения
|
|||
silenceThisInstance: Заглушить инстанс
|
||||
silencedInstances: Заглушенные инстансы
|
||||
editNote: Редактировать заметку
|
||||
edited: Редактировано
|
||||
edited: 'Редактировано в {date} {time}'
|
||||
deleted: Удалённое
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
_lang_: "简体中文"
|
||||
headlineMisskey: "通过帖子连接在一起的网络"
|
||||
introMisskey: "欢迎!Misskey是一个开源的、去中心化的“微博客”服务。\n通过编写「帖文」来和大家分享你的以及你周围的事情吧!📡\n通过「回应」功能,可以让你快速地对大家的帖文表达反馈👍\n来探索新的世界吧!🚀"
|
||||
headlineMisskey: "一个开源、去中心化的社交媒体平台,永远免费!🚀"
|
||||
introMisskey: "欢迎!Calckey 是一个开源的、去中心化的“微博客”服务。\n通过编写「帖文」来和大家分享你的以及你周围的事情吧!📡\n通过「回应」功能,可以让你快速地对大家的帖文表达反馈👍\n\
|
||||
来探索新的世界吧!🚀"
|
||||
monthAndDay: "{month}月 {day}日"
|
||||
search: "搜索"
|
||||
notifications: "通知"
|
||||
|
@ -10,13 +10,13 @@ password: "密码"
|
|||
forgotPassword: "忘记密码"
|
||||
fetchingAsApObject: "正在联邦宇宙查询中"
|
||||
ok: "OK"
|
||||
gotIt: "我明白了"
|
||||
gotIt: "知道了!"
|
||||
cancel: "取消"
|
||||
enterUsername: "输入用户名"
|
||||
renotedBy: "由 {user} 转贴"
|
||||
noNotes: "没有帖文"
|
||||
renotedBy: "转发自 {user}"
|
||||
noNotes: "没有帖子"
|
||||
noNotifications: "无通知"
|
||||
instance: "实例"
|
||||
instance: "服务器"
|
||||
settings: "设置"
|
||||
basicSettings: "基本设置"
|
||||
otherSettings: "其他设置"
|
||||
|
@ -44,7 +44,7 @@ copyContent: "复制内容"
|
|||
copyLink: "复制链接"
|
||||
delete: "删除"
|
||||
deleteAndEdit: "删除并编辑"
|
||||
deleteAndEditConfirm: "要删除此帖并再次编辑吗?对此帖的所有回应、转发和回复也将被删除。"
|
||||
deleteAndEditConfirm: "要删除此帖子并再次编辑吗?对此帖子的所有回应、转发和回复也将被删除。"
|
||||
addToList: "添加至列表"
|
||||
sendMessage: "发送"
|
||||
copyUsername: "复制用户名"
|
||||
|
@ -64,7 +64,7 @@ import: "导入"
|
|||
export: "导出"
|
||||
files: "文件"
|
||||
download: "下载"
|
||||
driveFileDeleteConfirm: "要删除「{name}」文件吗?附加此文件的帖子也会被删除。"
|
||||
driveFileDeleteConfirm: "要删除文件「{name}」吗?它将被所有作为附件包含它的帖子中删除。"
|
||||
unfollowConfirm: "要取消对{name}的关注吗?"
|
||||
exportRequested: "导出请求已提交,这可能需要花一些时间,导出的文件将保存到网盘中。"
|
||||
importRequested: "导入请求已提交,这可能需要花一点时间。"
|
||||
|
@ -78,7 +78,7 @@ followsYou: "正在关注你"
|
|||
createList: "创建列表"
|
||||
manageLists: "管理列表"
|
||||
error: "错误"
|
||||
somethingHappened: "出现了一些问题!"
|
||||
somethingHappened: "发生了一个错误"
|
||||
retry: "重试"
|
||||
pageLoadError: "页面加载失败。"
|
||||
pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。"
|
||||
|
@ -97,7 +97,7 @@ enterEmoji: "输入表情符号"
|
|||
renote: "转发"
|
||||
unrenote: "取消转发"
|
||||
renoted: "已转发。"
|
||||
cantRenote: "该帖无法转发。"
|
||||
cantRenote: "此帖子无法转发。"
|
||||
cantReRenote: "转发无法被再次转发。"
|
||||
quote: "引用"
|
||||
pinnedNote: "已置顶的帖子"
|
||||
|
@ -111,7 +111,7 @@ enableEmojiReaction: "启用表情符号回应"
|
|||
showEmojisInReactionNotifications: "在回应通知中显示表情符号"
|
||||
reactionSetting: "在选择器中显示的回应"
|
||||
reactionSettingDescription2: "拖动重新排序,单击删除,点击 + 添加。"
|
||||
rememberNoteVisibility: "保存上次设置的可见性"
|
||||
rememberNoteVisibility: "保存帖子可见性设置"
|
||||
attachCancel: "删除附件"
|
||||
markAsSensitive: "标记为敏感内容"
|
||||
unmarkAsSensitive: "取消标记为敏感内容"
|
||||
|
@ -141,7 +141,7 @@ emojiUrl: "表情符号地址"
|
|||
addEmoji: "添加表情符号"
|
||||
settingGuide: "推荐配置"
|
||||
cacheRemoteFiles: "远程文件缓存"
|
||||
cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程实例载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。"
|
||||
cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程服务器载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。"
|
||||
flagAsBot: "这是一个机器人账号"
|
||||
flagAsBotDescription: "如果此帐户由程序控制,请启用此项。启用后,此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为,并让Misskey的内部系统将此帐户识别为机器人。"
|
||||
flagAsCat: "将这个账户设定为一只猫"
|
||||
|
@ -151,7 +151,7 @@ flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的
|
|||
autoAcceptFollowed: "自动允许关注者的关注"
|
||||
addAccount: "添加账户"
|
||||
loginFailed: "登录失败"
|
||||
showOnRemote: "转到所在实例显示"
|
||||
showOnRemote: "转到所在服务器显示"
|
||||
general: "常规设置"
|
||||
wallpaper: "壁纸"
|
||||
setWallpaper: "设置壁纸"
|
||||
|
@ -160,13 +160,13 @@ searchWith: "搜索:{q}"
|
|||
youHaveNoLists: "列表为空"
|
||||
followConfirm: "你确定要关注{name}吗?"
|
||||
proxyAccount: "代理账户"
|
||||
proxyAccountDescription: "代理账户是在某些情况下充当用户的远程关注者的账户。 例如,当一个用户列出一个远程用户时,如果没有人跟随该列出的用户,则该活动将不会传递到该实例,因此将代之以代理账户。"
|
||||
proxyAccountDescription: "代理账户是在某些情况下充当用户的远程关注者的账户。 例如,当一个用户列出一个远程用户时,如果没有人跟随该列出的用户,则该活动将不会传递到该服务器,因此将代之以代理账户。"
|
||||
host: "主机名"
|
||||
selectUser: "选择用户"
|
||||
recipient: "收件人"
|
||||
annotation: "注解"
|
||||
federation: "联合"
|
||||
instances: "实例"
|
||||
instances: "服务器"
|
||||
registeredAt: "初次观测"
|
||||
latestRequestSentAt: "上次发送的请求"
|
||||
latestRequestReceivedAt: "上次收到的请求"
|
||||
|
@ -176,7 +176,7 @@ charts: "图表"
|
|||
perHour: "每小时"
|
||||
perDay: "每天"
|
||||
stopActivityDelivery: "停止发送活动"
|
||||
blockThisInstance: "阻止此实例向本实例推流"
|
||||
blockThisInstance: "屏蔽此服务器"
|
||||
operations: "操作"
|
||||
software: "软件"
|
||||
version: "版本"
|
||||
|
@ -186,23 +186,23 @@ jobQueue: "作业队列"
|
|||
cpuAndMemory: "CPU和内存"
|
||||
network: "网络"
|
||||
disk: "存储"
|
||||
instanceInfo: "实例信息"
|
||||
instanceInfo: "服务器信息"
|
||||
statistics: "统计"
|
||||
clearQueue: "清除队列"
|
||||
clearQueueConfirmTitle: "确定清除队列?"
|
||||
clearQueueConfirmText: "未送达的帖子将不会送达。 通常,您不需要这样做。"
|
||||
clearCachedFiles: "清除缓存"
|
||||
clearCachedFilesConfirm: "确定要清除缓存文件?"
|
||||
blockedInstances: "被阻拦的实例"
|
||||
blockedInstancesDescription: "设定要阻拦的实例,以换行来进行分割。被阻拦的实例将无法与本实例进行交换通讯。"
|
||||
blockedInstances: "已屏蔽的服务器"
|
||||
blockedInstancesDescription: "设定要屏蔽的服务器,以换行来进行分割。被屏蔽的服务器将无法与本服务器进行交换通讯。"
|
||||
muteAndBlock: "屏蔽/拉黑"
|
||||
mutedUsers: "已屏蔽用户"
|
||||
blockedUsers: "被拉黑的用户"
|
||||
noUsers: "无用户"
|
||||
editProfile: "编辑资料"
|
||||
noteDeleteConfirm: "要删除该帖子吗?"
|
||||
pinLimitExceeded: "无法置顶更多了"
|
||||
intro: "Misskey的部署结束啦!填写管理员账号吧!"
|
||||
pinLimitExceeded: "无法置顶更多帖子了"
|
||||
intro: "Calckey安装完成!请创建一个管理员用户。"
|
||||
done: "完成"
|
||||
processing: "正在处理"
|
||||
preview: "预览"
|
||||
|
@ -217,15 +217,15 @@ all: "全部"
|
|||
subscribing: "已订阅"
|
||||
publishing: "直播中"
|
||||
notResponding: "没有响应"
|
||||
instanceFollowing: "关注实例"
|
||||
instanceFollowers: "关注实例"
|
||||
instanceUsers: "实例用户"
|
||||
instanceFollowing: "关注服务器"
|
||||
instanceFollowers: "服务器的关注者"
|
||||
instanceUsers: "此服务器的用户"
|
||||
changePassword: "修改密码"
|
||||
security: "安全"
|
||||
retypedNotMatch: "两次输入不一致!"
|
||||
retypedNotMatch: "两次输入不匹配。"
|
||||
currentPassword: "现在的密码"
|
||||
newPassword: "新密码"
|
||||
newPasswordRetype: "重新输入密码:"
|
||||
newPasswordRetype: "重新输入新密码"
|
||||
attachFile: "插入附件"
|
||||
more: "更多!"
|
||||
featured: "热门"
|
||||
|
@ -309,8 +309,8 @@ unwatch: "取消关注"
|
|||
accept: "允许"
|
||||
reject: "拒绝"
|
||||
normal: "正常"
|
||||
instanceName: "实例名称"
|
||||
instanceDescription: "实例介绍"
|
||||
instanceName: "服务器名称"
|
||||
instanceDescription: "服务器简介"
|
||||
maintainerName: "管理员名称"
|
||||
maintainerEmail: "管理员电子邮箱"
|
||||
tosUrl: "服务条款URL"
|
||||
|
@ -321,7 +321,7 @@ dayX: "{day}日"
|
|||
monthX: "{month}月"
|
||||
yearX: "{year}年"
|
||||
pages: "页面"
|
||||
integration: "关联"
|
||||
integration: "整合"
|
||||
connectService: "连接"
|
||||
disconnectService: "断开连接"
|
||||
enableLocalTimeline: "启用本地时间线功能"
|
||||
|
@ -340,7 +340,7 @@ basicInfo: "基本信息"
|
|||
pinnedUsers: "置顶用户"
|
||||
pinnedUsersDescription: "在「发现」页面中使用换行标记想要置顶的用户。"
|
||||
pinnedPages: "固定页面"
|
||||
pinnedPagesDescription: "输入您要固定到实例首页的页面路径,以换行符分隔。"
|
||||
pinnedPagesDescription: "输入您要固定到服务器首页的页面路径,以换行符分隔。"
|
||||
pinnedClipId: "置顶的便签ID"
|
||||
pinnedNotes: "已置顶的帖子"
|
||||
hcaptcha: "hCaptcha"
|
||||
|
@ -359,7 +359,7 @@ antennaSource: "接收来源"
|
|||
antennaKeywords: "包含关键字"
|
||||
antennaExcludeKeywords: "排除关键字"
|
||||
antennaKeywordsDescription: "使用空格分隔会产生AND规范,并且使用换行符分隔会产生OR规范"
|
||||
notifyAntenna: "开启通知"
|
||||
notifyAntenna: "新帖子通知"
|
||||
withFileAntenna: "仅带有附件的帖子"
|
||||
enableServiceworker: "启用ServiceWorker"
|
||||
antennaUsersDescription: "指定用户名,用换行符分隔"
|
||||
|
@ -391,7 +391,7 @@ nUsersMentioned: "{n} 被提到"
|
|||
securityKey: "安全密钥"
|
||||
securityKeyName: "密钥名称"
|
||||
registerSecurityKey: "注册硬件安全密钥"
|
||||
lastUsed: "最后使用:"
|
||||
lastUsed: "上次使用"
|
||||
unregister: "删除账户"
|
||||
passwordLessLogin: "无密码登录"
|
||||
resetPassword: "重置密码"
|
||||
|
@ -424,7 +424,7 @@ text: "文本"
|
|||
enable: "启用"
|
||||
next: "下一个"
|
||||
retype: "重新输入"
|
||||
noteOf: "{user}的帖子"
|
||||
noteOf: "{user} 的帖子"
|
||||
inviteToGroup: "群组邀请"
|
||||
quoteAttached: "已引用"
|
||||
quoteQuestion: "是否引用此链接内容?"
|
||||
|
@ -535,7 +535,7 @@ updateRemoteUser: "更新远程用户信息"
|
|||
deleteAllFiles: "删除所有文件"
|
||||
deleteAllFilesConfirm: "要删除所有文件吗?"
|
||||
removeAllFollowing: "取消所有关注"
|
||||
removeAllFollowingDescription: "取消{host}的所有关注者。当实例不存在时执行。"
|
||||
removeAllFollowingDescription: "取消 {host} 的所有关注者。如果服务器已不存在,请执行它。"
|
||||
userSuspended: "该用户已被冻结。"
|
||||
userSilenced: "该用户已被禁言。"
|
||||
yourAccountSuspendedTitle: "账户已被冻结"
|
||||
|
@ -600,7 +600,7 @@ testEmail: "邮件发送测试"
|
|||
wordMute: "文字屏蔽"
|
||||
regexpError: "正则表达式错误"
|
||||
regexpErrorDescription: "{tab} 屏蔽文字的第 {line} 行的正则表达式有错误:"
|
||||
instanceMute: "实例的屏蔽"
|
||||
instanceMute: "服务器静音"
|
||||
userSaysSomething: "{name}说了什么"
|
||||
makeActive: "启用"
|
||||
display: "显示"
|
||||
|
@ -626,40 +626,40 @@ sample: "示例"
|
|||
abuseReports: "举报"
|
||||
reportAbuse: "举报"
|
||||
reportAbuseOf: "举报{name}"
|
||||
fillAbuseReportDescription: "请填写举报的详细原因。如果有对方发的帖子,请同时填写URL地址。"
|
||||
fillAbuseReportDescription: "请填写举报的详细原因。如果有对方发的帖子,请同时填写 URL 地址。"
|
||||
abuseReported: "内容已发送。感谢您提交信息。"
|
||||
reporter: "举报者"
|
||||
reporteeOrigin: "举报来源"
|
||||
reporterOrigin: "举报者来源"
|
||||
forwardReport: "将该举报信息转发给远程实例"
|
||||
forwardReportIsAnonymous: "勾选则在远程实例上显示的举报者是匿名的系统账号,而不是您的账号。"
|
||||
forwardReport: "将该举报信息转发给远程服务器"
|
||||
forwardReportIsAnonymous: "勾选则在远程服务器上显示的举报者是匿名的系统账号,而不是您的账号。"
|
||||
send: "发送"
|
||||
abuseMarkAsResolved: "处理完毕"
|
||||
openInNewTab: "在新标签页中打开"
|
||||
openInSideView: "在侧边栏中打开"
|
||||
defaultNavigationBehaviour: "默认导航"
|
||||
editTheseSettingsMayBreakAccount: "编辑这些设置可以会损坏您的账号"
|
||||
instanceTicker: "帖子的实例信息"
|
||||
instanceTicker: "帖子的服务器信息"
|
||||
waitingFor: "等待{x}"
|
||||
random: "随机"
|
||||
system: "系统"
|
||||
switchUi: "切换界面"
|
||||
switchUi: "界面"
|
||||
desktop: "桌面"
|
||||
clip: "便签"
|
||||
createNew: "新建"
|
||||
optional: "可选"
|
||||
createNewClip: "新建便签"
|
||||
unclip: "移除便签"
|
||||
confirmToUnclipAlreadyClippedNote: "本帖已包含在便签\"{name}\"里。您想要将本帖从该便签中移除吗?"
|
||||
confirmToUnclipAlreadyClippedNote: "本帖已包含在便签 \"{name}\" 里。您想要将本帖从该便签中移除吗?"
|
||||
public: "公开"
|
||||
i18nInfo: "Calckey已经被志愿者们翻译成了各种语言。如果你也有兴趣,可以通过{link}帮助翻译。"
|
||||
manageAccessTokens: "管理 Access Tokens"
|
||||
accountInfo: "账户信息"
|
||||
notesCount: "帖子数量"
|
||||
repliesCount: "回复数量"
|
||||
renotesCount: "转帖数量"
|
||||
renotesCount: "推贴数量"
|
||||
repliedCount: "回复数"
|
||||
renotedCount: "转发数"
|
||||
renotedCount: "收到的推贴数"
|
||||
followingCount: "正在关注数量"
|
||||
followersCount: "关注者数量"
|
||||
sentReactionsCount: "发送回应数"
|
||||
|
@ -701,9 +701,9 @@ showTitlebar: "显示标题栏"
|
|||
clearCache: "清除缓存"
|
||||
onlineUsersCount: "{n}人在线"
|
||||
nUsers: "{n}用户"
|
||||
nNotes: "{n}帖子"
|
||||
nNotes: "{n} 帖子"
|
||||
sendErrorReports: "发送错误报告"
|
||||
sendErrorReportsDescription: "启用后,如果出现问题,可以与Misskey共享详细的错误信息,从而帮助提高软件的质量。"
|
||||
sendErrorReportsDescription: "启用后,如果出现问题,可以与 Misskey 共享详细的错误信息,从而帮助提高软件的质量。"
|
||||
myTheme: "我的主题"
|
||||
backgroundColor: "背景"
|
||||
accentColor: "强调色"
|
||||
|
@ -727,7 +727,7 @@ capacity: "容量"
|
|||
inUse: "已使用"
|
||||
editCode: "编辑代码"
|
||||
apply: "应用"
|
||||
receiveAnnouncementFromInstance: "从实例接收通知"
|
||||
receiveAnnouncementFromInstance: "从服务器接收通知"
|
||||
emailNotification: "邮件通知"
|
||||
publish: "发布"
|
||||
inChannelSearch: "频道内搜索"
|
||||
|
@ -755,11 +755,11 @@ active: "活动"
|
|||
offline: "离线"
|
||||
notRecommended: "不推荐"
|
||||
botProtection: "Bot防御"
|
||||
instanceBlocking: "被阻拦的实例"
|
||||
instanceBlocking: "联邦管理"
|
||||
selectAccount: "选择账户"
|
||||
switchAccount: "切换账户"
|
||||
enabled: "已启用"
|
||||
disabled: "已禁用 "
|
||||
disabled: "已禁用"
|
||||
quickAction: "快捷操作"
|
||||
user: "用户"
|
||||
administration: "管理"
|
||||
|
@ -816,7 +816,7 @@ controlPanel: "控制面板"
|
|||
manageAccounts: "管理账户"
|
||||
makeReactionsPublic: "将回应设置为公开"
|
||||
makeReactionsPublicDescription: "将您发表过的回应设置成公开可见。"
|
||||
classic: "经典"
|
||||
classic: "居中"
|
||||
muteThread: "屏蔽帖子列表"
|
||||
unmuteThread: "取消屏蔽帖子列表"
|
||||
ffVisibility: "连接的可见范围"
|
||||
|
@ -835,12 +835,12 @@ overridedDeviceKind: "设备类型"
|
|||
smartphone: "智能手机"
|
||||
tablet: "平板"
|
||||
auto: "自动"
|
||||
themeColor: "主题颜色"
|
||||
themeColor: "服务器滚动条颜色"
|
||||
size: "大小"
|
||||
numberOfColumn: "列数"
|
||||
searchByGoogle: "Google"
|
||||
instanceDefaultLightTheme: "实例默认浅色主题"
|
||||
instanceDefaultDarkTheme: "实例默认深色主题"
|
||||
instanceDefaultLightTheme: "服务器默认浅色主题"
|
||||
instanceDefaultDarkTheme: "服务器默认深色主题"
|
||||
instanceDefaultThemeDescription: "以对象格式键入主题代码"
|
||||
mutePeriod: "屏蔽期限"
|
||||
indefinitely: "永久"
|
||||
|
@ -863,7 +863,7 @@ check: "检查"
|
|||
driveCapOverrideLabel: "變更此用戶的雲端硬碟容量上限"
|
||||
driveCapOverrideCaption: "设定为 0 以下则会解除此限制。"
|
||||
requireAdminForView: "需要使用管理员账户登录才能查看。"
|
||||
isSystemAccount: "该账号由系统自动创建和管理。"
|
||||
isSystemAccount: "该账号由系统自动创建和管理。请不要修改、编辑、删除或以其他方式篡改这个账户,否则可能会破坏你的服务器。"
|
||||
typeToConfirm: "输入 {x} 以确认操作。"
|
||||
deleteAccount: "删除账户"
|
||||
document: "文档"
|
||||
|
@ -875,7 +875,7 @@ statusbar: "状态栏"
|
|||
pleaseSelect: "请选择"
|
||||
reverse: "翻转"
|
||||
colored: "彩色"
|
||||
refreshInterval: "刷新间隔"
|
||||
refreshInterval: "更新间隔 "
|
||||
label: "标签"
|
||||
type: "类型"
|
||||
speed: "速度"
|
||||
|
@ -889,14 +889,17 @@ cannotUploadBecauseInappropriate: "因为可能含有不适宜的内容,无法
|
|||
cannotUploadBecauseNoFreeSpace: "因为已无可用空间,无法上传。"
|
||||
beta: "测试"
|
||||
enableAutoSensitive: "自动 NSFW 识别"
|
||||
enableAutoSensitiveDescription: "如果可用,请使用机器学习在媒体上自动设置 NSFW 标志。即使关闭此功能,也可能会根据实例自动设置。"
|
||||
enableAutoSensitiveDescription: "如果可用,请使用机器学习在媒体上自动设置 NSFW 标志。即使关闭此功能,也可能会根据服务器自动设置。"
|
||||
activeEmailValidationDescription: "积极地验证用户的电子邮件地址,判断它是一次性的电子邮件地址,还是可以实际通信的地址。关闭时,则只检查字符串是否正确。"
|
||||
navbar: "导航栏"
|
||||
shuffle: "随机"
|
||||
account: "账户"
|
||||
move: "移动"
|
||||
customKaTeXMacro: "自定义 KaTeX 宏"
|
||||
customKaTeXMacroDescription: "使用宏来轻松的输入数学表达式吧!宏的用法与 LaTeX 中的命令定义相同。你可以使用 \\newcommand{\\name}{content} 或 \\newcommand{\\name}[number of arguments]{content} 来输入数学表达式。举个例子,\\newcommand{\\add}[2]{#1 + #2} 会将 \\add{3}{foo} 展开为 3 + foo。此外,宏名称外的花括号 {} 可以被替换为圆括号 () 和方括号 [],这会影响用于参数的括号。每行只能够定义一个宏,无法在中间换行,且无效的行将被忽略。只支持简单字符串替换功能,不支持高级语法,如条件分支等。"
|
||||
customKaTeXMacroDescription: "使用宏来轻松的输入数学表达式吧!宏的用法与 LaTeX 中的命令定义相同。你可以使用 \\newcommand{\\
|
||||
name}{content} 或 \\newcommand{\\name}[number of arguments]{content} 来输入数学表达式。举个例子,\\
|
||||
newcommand{\\add}[2]{#1 + #2} 会将 \\add{3}{foo} 展开为 3 + foo。此外,宏名称外的花括号 {} 可以被替换为圆括号
|
||||
() 和方括号 [],这会影响用于参数的括号。每行只能够定义一个宏,无法在中间换行,且无效的行将被忽略。只支持简单字符串替换功能,不支持高级语法,如条件分支等。"
|
||||
enableCustomKaTeXMacro: "启用自定义 KaTeX 宏"
|
||||
_sensitiveMediaDetection:
|
||||
description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。"
|
||||
|
@ -932,8 +935,8 @@ _ad:
|
|||
reduceFrequencyOfThisAd: "减少此广告的频率"
|
||||
_forgotPassword:
|
||||
enterEmail: "请输入您验证账号时用的电子邮箱地址,密码重置链接将发送至该邮箱上。"
|
||||
ifNoEmail: "如果您没有使用电子邮件地址进行验证,请联系管理员。"
|
||||
contactAdmin: "该实例不支持发送电子邮件。如果您想重设密码,请联系管理员。"
|
||||
ifNoEmail: "如果您没有使用电子邮件地址进行验证,请联系服务器管理员。"
|
||||
contactAdmin: "该服务器不支持发送电子邮件。如果您想重设密码,请联系管理员。"
|
||||
_gallery:
|
||||
my: "我的图库"
|
||||
liked: "喜欢的图片"
|
||||
|
@ -981,6 +984,7 @@ _aboutMisskey:
|
|||
donate: "赞助Misskey"
|
||||
morePatrons: "还有很多其他的人也在支持我们,非常感谢🥰"
|
||||
patrons: "支持者"
|
||||
patronsList: 按时间顺序而不是捐赠金额排列。通过上面的链接捐款,让您的名字出现在这里!
|
||||
_nsfw:
|
||||
respect: "隐藏敏感内容"
|
||||
ignore: "不隐藏敏感内容"
|
||||
|
@ -1051,6 +1055,24 @@ _mfm:
|
|||
rotateDescription: "旋转指定的角度。"
|
||||
plain: "简洁"
|
||||
plainDescription: "禁用所有内部语法。"
|
||||
crop: 裁剪
|
||||
scale: 缩放
|
||||
position: 位置
|
||||
fade: 渐淡
|
||||
advanced: 高级 MFM
|
||||
background: 背景色
|
||||
fadeDescription: 内容淡入和淡出。
|
||||
warn: MFM 可能包含快速移动或华丽的动画
|
||||
advancedDescription: 如果禁用,则仅允许基本标记,除非正在播放动态 MFM
|
||||
foreground: 前景色
|
||||
backgroundDescription: 更改文本的背景色。
|
||||
play: 播放 MFM
|
||||
alwaysPlay: 始终自动播放所有动态的 MFM
|
||||
stop: 停止播放 MFM
|
||||
positionDescription: 将内容移动指定的量。
|
||||
cropDescription: 裁剪内容。
|
||||
scaleDescription: 按指定量缩放内容。
|
||||
foregroundDescription: 更改文本的前景色。
|
||||
_instanceTicker:
|
||||
none: "不显示"
|
||||
remote: "仅远程用户"
|
||||
|
@ -1059,6 +1081,7 @@ _serverDisconnectedBehavior:
|
|||
reload: "自动重载"
|
||||
dialog: "对话框警告"
|
||||
quiet: "安静警告"
|
||||
nothing: 什么也不做
|
||||
_channel:
|
||||
create: "创建频道"
|
||||
edit: "编辑频道"
|
||||
|
@ -1068,7 +1091,7 @@ _channel:
|
|||
owned: "管理中"
|
||||
following: "正在关注"
|
||||
usersCount: "有{n}人参与"
|
||||
notesCount: "有{n}个帖子"
|
||||
notesCount: "{n} 帖子"
|
||||
nameAndDescription: "名称与描述"
|
||||
nameOnly: "仅名称"
|
||||
_menuDisplay:
|
||||
|
@ -1084,12 +1107,12 @@ _wordMute:
|
|||
hardDescription: "防止将具有指定条件的帖子添加到时间线。 即使您更改条件,未添加的帖文也会被排除在外。"
|
||||
soft: "软屏蔽"
|
||||
hard: "硬屏蔽"
|
||||
mutedNotes: "被屏蔽的帖子"
|
||||
mutedNotes: "已静音的帖子"
|
||||
_instanceMute:
|
||||
instanceMuteDescription: "屏蔽配置实例中的所有帖子和转帖,包括实例的用户回复。"
|
||||
instanceMuteDescription: "屏蔽列出服务器中的所有帖子和转帖,包括服务器的用户回复。"
|
||||
instanceMuteDescription2: "设置时用换行符来分隔"
|
||||
title: "隐藏实例已设置的帖子。"
|
||||
heading: "屏蔽实例"
|
||||
title: "隐藏服务器已设置的帖子。"
|
||||
heading: "要静音的服务器列表"
|
||||
_theme:
|
||||
explore: "寻找主题"
|
||||
install: "安装主题"
|
||||
|
@ -1165,7 +1188,7 @@ _theme:
|
|||
accentLighten: "强调色(浅)"
|
||||
fgHighlighted: "高亮显示文本"
|
||||
_sfx:
|
||||
note: "帖子"
|
||||
note: "新的帖子"
|
||||
noteMy: "我的帖子"
|
||||
notification: "通知"
|
||||
chat: "聊天"
|
||||
|
@ -1178,7 +1201,7 @@ _ago:
|
|||
secondsAgo: "{n}秒前"
|
||||
minutesAgo: "{n}分前"
|
||||
hoursAgo: "{n}小时前"
|
||||
daysAgo: "{n}日前"
|
||||
daysAgo: "{n}天前"
|
||||
weeksAgo: "{n}周前"
|
||||
monthsAgo: "{n}月前"
|
||||
yearsAgo: "{n}年前"
|
||||
|
@ -1192,22 +1215,22 @@ _tutorial:
|
|||
step1_1: "欢迎!"
|
||||
step1_2: "让我们把你安排好。你很快就会启动并运行!"
|
||||
step2_1: "首先,请完成您的个人资料。"
|
||||
step2_2: "通过提供一些关于你自己的信息,其他人会更容易了解他们是否想看到你的帖子或关注你。"
|
||||
step3_1: "现在是时候跟随一些人了!"
|
||||
step2_2: "提供一些关于你的信息,让其他人更容易知道他们是否想看你的帖子或关注你。"
|
||||
step3_1: "现在是时候关注一些人了!"
|
||||
step3_2: "你的主页和社交馈送是基于你所关注的人,所以试着先关注几个账户。{n点击个人资料右上角的加号圈就可以关注它。"
|
||||
step4_1: "让我们出去找你。"
|
||||
step4_2: "对于他们的第一条信息,有些人喜欢做{introduction}或一个简单的 \"hello world!\""
|
||||
step5_1: "时间限制,到处是时间限制!"
|
||||
step5_2: "您的实例已启用各种时间线的{timelines}。"
|
||||
step5_3: "主{icon}时间线是你可以看到你的订阅者的帖子的时间线。"
|
||||
step5_4: "本地{icon}时间线是你可以看到实例中所有其他用户的信息的时间线。"
|
||||
step5_5: "推荐的{icon}时间线 - 是时间轴,你可以看到管理员推荐的实例的信息"
|
||||
step5_6: "社交{icon}时间线显示来自你的订阅者朋友的信息。"
|
||||
step5_7: "全球{icon}时间线是你可以看到来自所有其他连接的实例的消息。"
|
||||
step4_2: "对于第一条帖子,可以做一个 {introduction} 或一个简单的 \"hello world!\""
|
||||
step5_1: "时间线,无处不在的时间线!"
|
||||
step5_2: "您的服务器已启用{timelines}种不同的时间线。"
|
||||
step5_3: "主页{icon}时间线是你可以看到你关注账户的帖子的时间线。"
|
||||
step5_4: "本地{icon}时间线是你可以看到此服务器上其它用户的帖子的时间线。"
|
||||
step5_5: "社交{icon}时间线是主页和本地时间线的结合。"
|
||||
step5_6: "推荐{icon}时间线是你可以看到管理员推荐服务器的帖子的时间线。"
|
||||
step5_7: "全球{icon}时间线是你可以看到来自其它所有互联服务器的帖子的时间线。"
|
||||
step6_1: "那么,这里是什么地方?"
|
||||
step6_2: "好吧,你不只是加入卡尔基。你已经加入了Fediverse的一个门户,这是一个由成千上万台服务器组成的互联网络,被称为 \"实例\""
|
||||
step6_2: "好吧,你不只是加入Calckey。你已经加入了Fediverse的一个门户,这是一个由成千上万台服务器组成的互联网络。"
|
||||
step6_3: "每个服务器的工作方式不同,并不是所有的服务器都运行Calckey。但这个人确实如此! 这有点复杂,但你很快就会明白的。"
|
||||
step6_4: "现在去学习并享受乐趣!"
|
||||
step6_4: "现在,去吧,去探索,去享受乐趣吧!"
|
||||
_2fa:
|
||||
alreadyRegistered: "此设备已被注册"
|
||||
registerTOTP: "注册设备"
|
||||
|
@ -1218,6 +1241,21 @@ _2fa:
|
|||
step3: "输入您的应用提供的动态口令以完成设置。"
|
||||
step4: "从现在开始,任何登录操作都将要求您提供动态口令。"
|
||||
securityKeyInfo: "您可以设置使用支持FIDO2的硬件安全密钥、设备上的指纹或PIN来保护您的登录过程。"
|
||||
renewTOTPOk: 重新配置
|
||||
renewTOTPCancel: 取消
|
||||
token: 2FA 令牌
|
||||
renewTOTP: 重新配置身份验证器应用程序
|
||||
registerTOTPBeforeKey: 请设置一个认证器应用来注册一个安全或通行密钥。
|
||||
renewTOTPConfirm: 这将导致您之前的应用程序中的验证码停止工作
|
||||
step3Title: 输入验证码
|
||||
step2Click: 点击此二维码将允许您在安全密钥或手机验证器应用中注册 2FA。
|
||||
securityKeyNotSupported: 您的浏览器不支持安全密钥。
|
||||
securityKeyName: 输入密钥名称
|
||||
chromePasskeyNotSupported: 目前不支持 Chrome passkeys。
|
||||
tapSecurityKey: 请按照您的浏览器的指示注册安全或通行密钥
|
||||
removeKey: 移除安全密钥
|
||||
removeKeyConfirm: 真的要删除 {name} 密钥吗?
|
||||
whyTOTPOnlyRenew: 只要注册了安全密钥,就无法删除身份验证器应用程序。
|
||||
_permissions:
|
||||
"read:account": "查看账户信息"
|
||||
"write:account": "更改帐户信息"
|
||||
|
@ -1254,16 +1292,19 @@ _permissions:
|
|||
_auth:
|
||||
shareAccess: "您要授权允许“{name}”访问您的帐户吗?"
|
||||
shareAccessAsk: "您确定要授权此应用程序访问您的帐户吗?"
|
||||
permissionAsk: "这个应用程序需要以下权限"
|
||||
permissionAsk: "此应用程序请求以下权限:"
|
||||
pleaseGoBack: "请返回到应用程序"
|
||||
callback: "回到应用程序"
|
||||
denied: "拒绝访问"
|
||||
allPermissions: 完全的账户访问权限
|
||||
copyAsk: 请将以下授权码粘贴到应用程序中:
|
||||
_antennaSources:
|
||||
all: "所有帖子"
|
||||
homeTimeline: "已关注用户的帖子"
|
||||
users: "来自指定用户的帖子"
|
||||
userList: "来自指定列表中的帖子"
|
||||
userGroup: "来自指定群组中用户的帖子"
|
||||
instances: 服务器上所有用户的帖子
|
||||
_weekday:
|
||||
sunday: "星期日"
|
||||
monday: "星期一"
|
||||
|
@ -1280,21 +1321,28 @@ _widgets:
|
|||
trends: "趋势"
|
||||
clock: "时钟"
|
||||
rss: "RSS阅读器"
|
||||
rssTicker: "RSS Ticker"
|
||||
rssTicker: "RSS滚动条"
|
||||
activity: "活动"
|
||||
photos: "照片"
|
||||
digitalClock: "数字时钟"
|
||||
unixClock: "UNIX时钟"
|
||||
federation: "联邦宇宙"
|
||||
instanceCloud: "实例云"
|
||||
postForm: "投稿窗口"
|
||||
instanceCloud: "服务器云端"
|
||||
postForm: "发布窗口"
|
||||
slideshow: "幻灯片展示"
|
||||
button: "按钮"
|
||||
onlineUsers: "在线用户"
|
||||
jobQueue: "作业队列"
|
||||
serverMetric: "服务器监控"
|
||||
serverMetric: "服务器指标"
|
||||
aiscript: "AiScript控制台"
|
||||
aichan: "小蓝"
|
||||
userList: 用户列表
|
||||
meiliStatus: 服务器状态
|
||||
meiliIndexCount: 已索引的帖子
|
||||
meiliSize: 索引大小
|
||||
serverInfo: 服务器信息
|
||||
_userList:
|
||||
chooseList: 选择一个列表
|
||||
_cw:
|
||||
hide: "隐藏"
|
||||
show: "查看更多"
|
||||
|
@ -1324,11 +1372,11 @@ _poll:
|
|||
remainingSeconds: "{s}秒后截止"
|
||||
_visibility:
|
||||
public: "公开"
|
||||
publicDescription: "您的帖子将出现在全局时间线上"
|
||||
home: "首页"
|
||||
publicDescription: "您的帖子将出现在公共时间线上"
|
||||
home: "不公开"
|
||||
homeDescription: "仅发送至首页的时间线"
|
||||
followers: "仅关注者"
|
||||
followersDescription: "仅发送至关注者"
|
||||
followersDescription: "仅对你的关注者和提及的用户可见"
|
||||
specified: "指定用户"
|
||||
specifiedDescription: "仅发送至指定用户"
|
||||
localOnly: "仅限本地"
|
||||
|
@ -1356,6 +1404,7 @@ _profile:
|
|||
metadataContent: "内容"
|
||||
changeAvatar: "修改头像"
|
||||
changeBanner: "修改横幅"
|
||||
locationDescription: 如果你先输入你的城市,它将向其他用户显示您的当地时间。
|
||||
_exportOrImport:
|
||||
allNotes: "所有帖子"
|
||||
followingList: "关注中"
|
||||
|
@ -1384,7 +1433,7 @@ _instanceCharts:
|
|||
usersTotal: "用户总计"
|
||||
notes: "帖子:增加/减少"
|
||||
notesTotal: "帖子总计"
|
||||
ff: "关注/被关注:数量变化"
|
||||
ff: "被关注用户/关注者的数量差异 "
|
||||
ffTotal: "关注/被关注者总计"
|
||||
cacheSize: "缓存大小:增加/减少"
|
||||
cacheSizeTotal: "缓存大小总计"
|
||||
|
@ -1395,6 +1444,7 @@ _timelines:
|
|||
local: "本地"
|
||||
social: "社交"
|
||||
global: "全局"
|
||||
recommended: 推荐
|
||||
_pages:
|
||||
newPage: "创建页面"
|
||||
editPage: "编辑页面"
|
||||
|
@ -1472,7 +1522,7 @@ _pages:
|
|||
note: "嵌入的帖子"
|
||||
_note:
|
||||
id: "帖子ID"
|
||||
idDescription: "您也可以通过粘贴帖子的URL来进行设置。"
|
||||
idDescription: "你也可以将帖子 URL 粘贴到此处。"
|
||||
detailed: "显示详细信息"
|
||||
switch: "开关"
|
||||
_switch:
|
||||
|
@ -1721,6 +1771,9 @@ _notification:
|
|||
followBack: "回关"
|
||||
reply: "回复"
|
||||
renote: "转发"
|
||||
reacted: 对你的帖子做出了回应
|
||||
voted: 在你的投票中投了票
|
||||
renoted: 推荐了你的帖子
|
||||
_deck:
|
||||
alwaysShowMainColumn: "总是显示主列"
|
||||
columnAlign: "列对齐"
|
||||
|
@ -1733,9 +1786,9 @@ _deck:
|
|||
stackLeft: "向左折叠"
|
||||
popRight: "向右弹出"
|
||||
profile: "配置文件"
|
||||
newProfile: "新建配置文件"
|
||||
newProfile: "新建工作区"
|
||||
renameProfile: "重命名配置文件"
|
||||
deleteProfile: "删除配置文件"
|
||||
deleteProfile: "删除工作区"
|
||||
nameAlreadyExists: "该配置文件名已存在。"
|
||||
introduction: "将各列进行组合以创建您自己的界面!"
|
||||
introduction2: "您可以随时通过屏幕右侧的 + 来添加列"
|
||||
|
@ -1748,4 +1801,143 @@ _deck:
|
|||
antenna: "天线"
|
||||
list: "列表"
|
||||
mentions: "提及"
|
||||
direct: "指定用户"
|
||||
direct: "私信"
|
||||
channel: 频道
|
||||
apps: 应用
|
||||
_messaging:
|
||||
dms: 私信
|
||||
groups: 群组
|
||||
migration: 迁移
|
||||
_experiments:
|
||||
title: 实验性功能
|
||||
postImportsCaption: 允许用户从过去的 Calckey、Misskey、Mastodon、Akkoma 和 Pleroma 帐户导入帖子。如果您的队列出现拥堵,则可能会导致加载速度减慢。
|
||||
enablePostImports: 启用帖子导入
|
||||
license: 许可证
|
||||
flagSpeakAsCatDescription: 在猫模式下你的帖子会喵化
|
||||
allowedInstances: 白名单服务器
|
||||
listsDesc: 列表可以让你创建含有指定用户的时间线,它们可以从时间线页面访问。
|
||||
flagSpeakAsCat: 像猫一样说话
|
||||
removeReaction: 移除你的回应
|
||||
expandOnNoteClick: 点击打开帖子
|
||||
expandOnNoteClickDesc: 如果禁用,你仍然可以在右键菜单中或通过点击时间戳打开帖子。
|
||||
sendPushNotificationReadMessage: 删除已阅读的推送通知
|
||||
customMOTD: 自定义 MOTD(启动屏幕消息)
|
||||
sendPushNotificationReadMessageCaption: 短暂显示 "{emptyPushNotificationMessage}" 的通知,如果启用,可能会增加你的设备的耗电量。
|
||||
adminCustomCssWarn: 仅当你知道此设置的作用时才应使用它。输入不正确的值可能会导致每个人的客户端停止正常运行。请在用户设置中进行测试来确保您的 CSS
|
||||
正常工作。
|
||||
customMOTDDescription: 自定义 MOTD(启动屏幕)消息,一行一个,每次用户加载/刷新页面时都会随机显示。
|
||||
customSplashIconsDescription: 用换行符隔开的自定义闪屏图标的URL,在用户每次加载/重新加载页面时随机显示。请确保图片是在一个静态的
|
||||
URL 上,最好全部调整为 192x192 的大小。
|
||||
recommendedInstancesDescription: 推荐的服务器以换行符分隔,它们将出现在推荐的时间线中。不要添加 "https://",仅添加域名。
|
||||
splash: 启动画面
|
||||
showUpdates: Calckey 更新后显示弹出窗口
|
||||
selectInstance: 选择一个服务器
|
||||
silencedInstances: 静默的服务器
|
||||
antennaInstancesDescription: 每行列出一个服务器主机
|
||||
pushNotification: 推送通知
|
||||
subscribePushNotification: 启用推送通知
|
||||
showAdminUpdates: 提示新的 Calckey 版本可用(仅对于管理员)
|
||||
searchPlaceholder: 搜索 Calckey
|
||||
addInstance: 添加一个服务器
|
||||
jumpToPrevious: 跳转至上一个
|
||||
silenceThisInstance: 使此服务器静音
|
||||
manageGroups: 管理群组
|
||||
antennasDesc: "天线会显示符合您设置条件的新帖子!\n可以从时间线页面访问它们。"
|
||||
channelFederationWarn: 频道还没有与其他服务器联合
|
||||
seperateRenoteQuote: 单独的推荐和引用按钮
|
||||
customSplashIcons: 自定义闪屏图标(urls)
|
||||
alt: 替代文字
|
||||
pushNotificationNotSupported: 你的浏览器或者服务器不支持推送通知
|
||||
showAds: 显示广告
|
||||
enterSendsMessage: 按回车键发送信息(关闭则是 Ctrl + Retun)
|
||||
recommendedInstances: 推荐服务器
|
||||
updateAvailable: 可能有可用更新!
|
||||
swipeOnMobile: 允许在页面之间滑动
|
||||
swipeOnDesktop: 允许在桌面端以移动设备方式滑动
|
||||
logoImageUrl: Logo 图像 URL
|
||||
deleted: 已删除
|
||||
editNote: 编辑帖子
|
||||
edited: 于 {date} {time} 编辑
|
||||
selectChannel: 选择一个频道
|
||||
accountMoved: 用户已迁移至新账户:
|
||||
silencedInstancesDescription: 列出你想静默的服务器的主机名。列出的服务器中的账户被视为 "静默",只能发出跟随请求,如果不被跟随,就不能提及本地账户。这不会影响被封锁的服务器。
|
||||
hiddenTags: 隐藏的哈希标签
|
||||
userSaysSomethingReason: '{name} 说 {reason}'
|
||||
clipsDesc: 便签就像可共享的分类书签。您可以从各个帖子的菜单中创建便签。
|
||||
privateModeInfo: 当启用时,只有白名单上的服务器可以与你的服务器联合,所有的帖子都会对公共时间线隐藏。
|
||||
allowedInstancesDescription: 要列入联合白名单的服务器的主机名,一行一个(仅适用于私密模式)。
|
||||
breakFollowConfirm: 你确定要移除关注者吗?
|
||||
caption: 自动显示说明文字
|
||||
newer: 更新的
|
||||
older: 更老的
|
||||
noInstances: 没有服务器
|
||||
silenced: 静默的
|
||||
accessibility: 无障碍
|
||||
secureMode: 安全模式(仅允许授权的拉取)
|
||||
replayTutorial: 重播教程
|
||||
userSaysSomethingReasonReply: '{name} 回复了包含 {reason} 的帖子'
|
||||
userSaysSomethingReasonQuote: '{name} 引用了一篇包含 {reason} 的帖子'
|
||||
userSaysSomethingReasonRenote: '{name} 推荐了一个包含 {reason} 的帖子'
|
||||
noThankYou: 不,谢谢
|
||||
secureModeInfo: 当向其他服务器请求时,不要在没有验证的情况下发回。
|
||||
privateMode: 私密模式
|
||||
instanceSecurity: 服务器安全
|
||||
image: 图像
|
||||
video: 视频
|
||||
audio: 音频
|
||||
cannotUploadBecauseExceedsFileSizeLimit: 无法上传此文件,因为它超出了允许的最大大小。
|
||||
unsubscribePushNotification: 禁用推送通知
|
||||
pushNotificationAlreadySubscribed: 推送通知已启用
|
||||
enableEmojiReactions: 启用 emoji 回应
|
||||
cw: 内容警告
|
||||
hiddenTagsDescription: 列出你想隐藏的话题标签(不带#)以避免在趋势和探索中显示。隐藏的标签仍然可以通过其他方式被发现。
|
||||
enableRecommendedTimeline: 启用推荐时间线
|
||||
_skinTones:
|
||||
medium: 中等
|
||||
light: 浅色
|
||||
yellow: 黄色
|
||||
dark: 深色
|
||||
mediumLight: 中等偏淡
|
||||
mediumDark: 中等偏深
|
||||
isModerator: 协作者
|
||||
isAdmin: 管理员
|
||||
findOtherInstance: 寻找其它服务器
|
||||
moveFromDescription: 这将为您的旧帐户设置一个别名,以便您可以从该旧帐户转移到当前帐户。在从旧帐户转移之前执行此操作。请输入格式如@person@server.com
|
||||
的帐户标签
|
||||
indexPosts: 索引帖子
|
||||
signupsDisabled: 该服务器目前关闭注册,但您随时可以在另一台服务器上注册!如果您有该服务器的邀请码,请在下面输入。
|
||||
silencedWarning: 显示这个页面是因为这些用户来自你的管理员设置的静默服务器,所以他们有可能是垃圾信息。
|
||||
isBot: 这个账户是一个机器人
|
||||
moveAccountDescription: 这个过程是不可逆的。在移动之前,请确保您已在新帐户上为当前帐户设置了别名。请输入格式如 @person@server.com
|
||||
帐户标签
|
||||
moveFromLabel: 您要移出的旧帐户:
|
||||
preventAiLearning: 阻止 AI 机器人抓取
|
||||
preventAiLearningDescription: 请求第三方人工智能语言模型不要研究您上传的内容,例如帖子和图像。
|
||||
noGraze: 请禁用 "Graze for Mastodon" 浏览器扩展,因为它会干扰 Calckey。
|
||||
moveTo: 将当前帐户移至新帐户
|
||||
moveToLabel: 你要迁移到的目标帐户:
|
||||
moveAccount: 移动账户!
|
||||
migrationConfirm: "你确实确定要将帐户迁移到 {account} 吗?此操作无法撤消,并且你将无法再次正常使用旧账户。\n另外,请确保你已将此当前帐户设置为要移出的帐户。"
|
||||
indexFromDescription: 留空以索引每个帖子
|
||||
noteId: 帖子 ID
|
||||
moveFrom: 从旧帐户移至此帐户
|
||||
defaultReaction: 发出和收到的帖子的默认表情符号反应
|
||||
indexNotice: 现在开始索引。这可能需要一段时间,请至少一个小时内不要重新启动服务器。
|
||||
indexFrom: 从帖子 ID 开始的索引
|
||||
sendModMail: 发送审核通知
|
||||
isLocked: 该帐户设置了关注请求
|
||||
_filters:
|
||||
notesBefore: 在之前的帖子
|
||||
followingOnly: 仅关注中
|
||||
notesAfter: 在之后的帖子
|
||||
fromDomain: 来自域名
|
||||
withFile: 带有文件
|
||||
fromUser: 来自用户
|
||||
followersOnly: 仅关注者
|
||||
reactionPickerSkinTone: 首选的表情符号肤色
|
||||
isPatron: Calckey 赞助
|
||||
_dialog:
|
||||
charactersExceeded: 超出了最大字符数!当前:{current} / 限制:{max}
|
||||
charactersBelow: 没有足够的字符!当前:{current} / 限制:{min}
|
||||
enableIdenticonGeneration: 启用Identicon生成
|
||||
enableServerMachineStats: 启用服务器硬件统计
|
||||
|
|
|
@ -1816,7 +1816,6 @@ silenceThisInstance: 靜音此伺服器
|
|||
silencedInstances: 已靜音的伺服器
|
||||
silenced: 已靜音
|
||||
_experiments:
|
||||
enablePostEditing: 啟用帖子編輯
|
||||
title: 試驗功能
|
||||
findOtherInstance: 找找另一個伺服器
|
||||
noGraze: 瀏覽器擴展 "Graze for Mastodon" 會與Calckey發生衝突,請停用該擴展。
|
||||
|
@ -1829,7 +1828,7 @@ indexPosts: 索引帖子
|
|||
indexNotice: 現在開始索引。 這可能需要一段時間,請不要在一個小時內重啟你的伺服器。
|
||||
deleted: 已刪除
|
||||
editNote: 編輯筆記
|
||||
edited: 已修改
|
||||
edited: '於 {date} {time} 編輯'
|
||||
userSaysSomethingReason: '{name} 說了 {reason}'
|
||||
allowedInstancesDescription: 要加入聯邦白名單的服務器,每台伺服器用新行分隔(僅適用於私有模式)。
|
||||
defaultReaction: 默認的表情符號反應
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
{
|
||||
"name": "calckey",
|
||||
"version": "14.0.0-dev51",
|
||||
"version": "14.0.0-rc3",
|
||||
"codename": "aqua",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://codeberg.org/calckey/calckey.git"
|
||||
},
|
||||
"packageManager": "pnpm@8.6.2",
|
||||
"packageManager": "pnpm@8.6.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"rebuild": "pnpm run clean && pnpm -r run build && pnpm run gulp",
|
||||
"build": "pnpm -r run build && pnpm run gulp",
|
||||
"rebuild": "pnpm run clean && pnpm node ./scripts/build-greet.js && pnpm -r run build && pnpm run gulp",
|
||||
"build": "pnpm node ./scripts/build-greet.js && pnpm -r run build && pnpm run gulp",
|
||||
"start": "pnpm --filter backend run start",
|
||||
"start:test": "pnpm --filter backend run start:test",
|
||||
"init": "pnpm run migrate",
|
||||
|
@ -46,6 +46,7 @@
|
|||
"devDependencies": {
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"chalk": "4.1.2",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "10.11.0",
|
||||
"execa": "5.1.1",
|
||||
|
|
9
packages/README.md
Normal file
9
packages/README.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
# 📦 Packages
|
||||
|
||||
This directory contains all of the packages Calckey uses.
|
||||
|
||||
- `backend`: Main backend code written in TypeScript for NodeJS
|
||||
- `backend/native-utils`: Backend code written in Rust, bound to NodeJS by [NAPI-RS](https://napi.rs/)
|
||||
- `client`: Web interface written in Vue3 and TypeScript
|
||||
- `sw`: Web [Service Worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) written in TypeScript
|
||||
- `calckey-js`: TypeScript SDK for both backend and client, also published on [NPM](https://www.npmjs.com/package/calckey-js) for public use
|
|
@ -1,15 +1,15 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"dynamicImport": true,
|
||||
"decorators": true
|
||||
},
|
||||
"transform": {
|
||||
"legacyDecorator": true,
|
||||
"decoratorMetadata": true
|
||||
},
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"dynamicImport": true,
|
||||
"decorators": true
|
||||
},
|
||||
"transform": {
|
||||
"legacyDecorator": true,
|
||||
"decoratorMetadata": true
|
||||
},
|
||||
"experimental": {
|
||||
"keepImportAssertions": true
|
||||
},
|
||||
|
@ -20,6 +20,6 @@
|
|||
]
|
||||
},
|
||||
"target": "es2022"
|
||||
},
|
||||
"minify": false
|
||||
},
|
||||
"minify": false
|
||||
}
|
||||
|
|
BIN
packages/backend/assets/avatar.png
Normal file
BIN
packages/backend/assets/avatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
|
@ -1 +1 @@
|
|||
<svg viewBox="1.95 0.97 128 128" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg"><linearGradient id="a" gradientTransform="rotate(90)"><stop offset="5%" stop-color="#9ccfd8" style="--darkreader-inline-stopcolor:#265760"/><stop offset="95%" stop-color="#31748f" style="--darkreader-inline-stopcolor:#275d72"/></linearGradient><defs><linearGradient xlink:href="#a" id="f" gradientTransform="scale(1.27567 .7839)" x1="-43.77" y1="98.469" x2="-27.05" y2="137.466" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#a" id="d" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#a" id="e" gradientTransform="scale(1.27567 .7839)" x1="-43.77" y1="98.468" x2="-8.156" y2="98.468" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#a" id="c" gradientTransform="scale(1.27567 .7839)" x1="1.571" y1="1.27" x2="133.179" y2="1.27" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#a" id="b" gradientTransform="scale(1.27567 .7839)" x1="1.571" y1="1.27" x2="133.179" y2="1.27" gradientUnits="userSpaceOnUse"/></defs><g style="fill:url(#b)" transform="translate(.934 25.196) scale(.75646)"><g style="fill:url(#c)" fill="url(#a)" word-spacing="0" letter-spacing="0" font-family="'OTADESIGN Rounded'" font-weight="400"><g transform="translate(-55.341 -52.023) scale(.26953)" style="fill:url(#d)"/><g style="fill:url(#e)"><path style="fill:url(#f)" d="M-41.832 77.19c-3.868 0-7.177 1.358-9.93 4.074-2.716 2.752-4.074 6.063-4.074 9.931 0 3.869 1.358 7.04 4.074 9.793 2.753 2.716 6.064 4.073 9.932 4.073 3.831 0 7.122-1.357 9.875-4.073.855-.855 1.283-1.896 1.283-3.123 0-1.228-.428-2.271-1.283-3.127-.856-.855-1.897-1.281-3.123-1.281-1.229 0-2.27.426-3.125 1.281-1.004 1.042-2.213 1.563-3.627 1.563-3.035-.31-5.208-2.263-5.246-5.106.038-2.842 2.21-4.935 5.244-5.246 1.414 0 2.623.52 3.627 1.563.855.855 1.898 1.283 3.127 1.283 1.226 0 2.267-.428 3.123-1.283.855-.856 1.283-1.897 1.283-3.125 0-1.227-.428-2.268-1.283-3.123-2.753-2.716-6.046-4.075-9.877-4.075zm20.902 6.91c-2.88 0-5.353 1.02-7.422 3.06-.642.643-.964 1.426-.964 2.348 0 .923.322 1.706.964 2.35.644.642 1.427.962 2.348.962.924 0 1.707-.32 2.35-.963.754-.783 1.662-1.173 2.724-1.173 1.09 0 2.026.376 2.809 1.13a3.909 3.909 0 0 1 1.135 2.811c0 1.062-.393 1.97-1.176 2.725-.392.419-.868.7-1.426.84-.141.027-.252.012-.336-.044-.056-.084-.028-.168.084-.251l.84-.881c.643-.643.965-1.411.965-2.305 0-.922-.28-1.663-.838-2.223-.559-.559-1.343-.84-2.35-.84-.698 0-1.397.35-2.095 1.05l-4.866 4.822c-.643.643-.964 1.426-.964 2.347 0 .923.321 1.705.964 2.348 1.957 1.93 4.375 2.894 7.254 2.894 2.908 0 5.396-1.034 7.465-3.103 2.041-2.041 3.06-4.5 3.06-7.379 0-2.907-1.019-5.396-3.06-7.465-2.069-2.04-4.557-3.06-7.465-3.06z" transform="translate(208.34 -284.25) scale(3.6954)" clip-rule="evenodd" fill-rule="evenodd"/></g></g></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128" viewBox="1.95 0.97 128 128"><linearGradient id="a" gradientTransform="rotate(90)"><stop offset="5%" stop-color="#9ccfd8" style="--darkreader-inline-stopcolor:#265760"/><stop offset="95%" stop-color="#31748f" style="--darkreader-inline-stopcolor:#275d72"/></linearGradient><defs><linearGradient xlink:href="#a" id="f" x1="-43.77" x2="-27.05" y1="98.469" y2="137.466" gradientTransform="scale(1.27567 .7839)" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#a" id="d" x1="0" x2="1" y1="0" y2="0" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#a" id="e" x1="-43.77" x2="-8.156" y1="98.468" y2="98.468" gradientTransform="scale(1.27567 .7839)" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#a" id="c" x1="1.571" x2="133.179" y1="1.27" y2="1.27" gradientTransform="scale(1.27567 .7839)" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#a" id="b" x1="1.571" x2="133.179" y1="1.27" y2="1.27" gradientTransform="scale(1.27567 .7839)" gradientUnits="userSpaceOnUse"/></defs><g style="fill:url(#b)" transform="translate(.934 25.196) scale(.75646)"><g fill="url(#a)" font-family="'OTADESIGN Rounded'" font-weight="400" letter-spacing="0" style="fill:url(#c)" word-spacing="0"><g style="fill:url(#e)"><path fill-rule="evenodd" d="M-41.832 77.19c-3.868 0-7.177 1.358-9.93 4.074-2.716 2.752-4.074 6.063-4.074 9.931 0 3.869 1.358 7.04 4.074 9.793 2.753 2.716 6.064 4.073 9.932 4.073 3.831 0 7.122-1.357 9.875-4.073.855-.855 1.283-1.896 1.283-3.123 0-1.228-.428-2.271-1.283-3.127-.856-.855-1.897-1.281-3.123-1.281-1.229 0-2.27.426-3.125 1.281-1.004 1.042-2.213 1.563-3.627 1.563-3.035-.31-5.208-2.263-5.246-5.106.038-2.842 2.21-4.935 5.244-5.246 1.414 0 2.623.52 3.627 1.563.855.855 1.898 1.283 3.127 1.283 1.226 0 2.267-.428 3.123-1.283.855-.856 1.283-1.897 1.283-3.125 0-1.227-.428-2.268-1.283-3.123-2.753-2.716-6.046-4.075-9.877-4.075zm20.902 6.91c-2.88 0-5.353 1.02-7.422 3.06-.642.643-.964 1.426-.964 2.348 0 .923.322 1.706.964 2.35.644.642 1.427.962 2.348.962.924 0 1.707-.32 2.35-.963.754-.783 1.662-1.173 2.724-1.173 1.09 0 2.026.376 2.809 1.13a3.909 3.909 0 0 1 1.135 2.811c0 1.062-.393 1.97-1.176 2.725-.392.419-.868.7-1.426.84-.141.027-.252.012-.336-.044-.056-.084-.028-.168.084-.251l.84-.881c.643-.643.965-1.411.965-2.305 0-.922-.28-1.663-.838-2.223-.559-.559-1.343-.84-2.35-.84-.698 0-1.397.35-2.095 1.05l-4.866 4.822c-.643.643-.964 1.426-.964 2.347 0 .923.321 1.705.964 2.348 1.957 1.93 4.375 2.894 7.254 2.894 2.908 0 5.396-1.034 7.465-3.103 2.041-2.041 3.06-4.5 3.06-7.379 0-2.907-1.019-5.396-3.06-7.465-2.069-2.04-4.557-3.06-7.465-3.06z" clip-rule="evenodd" style="fill:url(#f)" transform="translate(208.34 -284.25) scale(3.6954)"/></g></g></g></svg>
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.7 KiB |
|
@ -1 +1 @@
|
|||
<svg xml:space="preserve" viewBox="0 0 512 512" height="512" width="512" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="a"><stop style="stop-color:#31748f;stop-opacity:1" offset="0"/><stop style="stop-color:#9ccfd8;stop-opacity:1" offset="1"/></linearGradient><linearGradient xlink:href="#a" id="b" x1="254.819" y1="411.542" x2="259.34" y2="-1.41" gradientUnits="userSpaceOnUse"/></defs><path style="fill:url(#b);fill-opacity:1;stroke-width:.996356" d="M0 0v512h512V0Z"/><g style="font-weight:400;font-family:"OTADESIGN Rounded";letter-spacing:0;word-spacing:0;fill:#fff"><g transform="translate(-38.55 37.929) scale(.5619)" style="fill:#fff"/><path style="fill:#fff" d="M-41.832 77.19c-3.868 0-7.177 1.358-9.93 4.074-2.716 2.752-4.074 6.063-4.074 9.931 0 3.869 1.358 7.04 4.074 9.793 2.753 2.716 6.064 4.073 9.932 4.073 3.831 0 7.122-1.357 9.875-4.073.855-.855 1.283-1.896 1.283-3.123 0-1.228-.428-2.271-1.283-3.127-.856-.855-1.897-1.281-3.123-1.281-1.229 0-2.27.426-3.125 1.281-1.004 1.042-2.213 1.563-3.627 1.563-3.035-.31-5.208-2.263-5.246-5.106.038-2.842 2.21-4.935 5.244-5.246 1.414 0 2.623.52 3.627 1.563.855.855 1.898 1.283 3.127 1.283 1.226 0 2.267-.428 3.123-1.283.855-.856 1.283-1.897 1.283-3.125 0-1.227-.428-2.268-1.283-3.123-2.753-2.716-6.046-4.075-9.877-4.075zm20.902 6.91c-2.88 0-5.353 1.02-7.422 3.06-.642.643-.964 1.426-.964 2.348 0 .923.322 1.706.964 2.35.644.642 1.427.962 2.348.962.924 0 1.707-.32 2.35-.963.754-.783 1.662-1.173 2.724-1.173 1.09 0 2.026.376 2.809 1.13a3.909 3.909 0 0 1 1.135 2.811c0 1.062-.393 1.97-1.176 2.725-.392.419-.868.7-1.426.84-.141.027-.252.012-.336-.044-.056-.084-.028-.168.084-.251l.84-.881c.643-.643.965-1.411.965-2.305 0-.922-.28-1.663-.838-2.223-.559-.559-1.343-.84-2.35-.84-.698 0-1.397.35-2.095 1.05l-4.866 4.822c-.643.643-.964 1.426-.964 2.347 0 .923.321 1.705.964 2.348 1.957 1.93 4.375 2.894 7.254 2.894 2.908 0 5.396-1.034 7.465-3.103 2.041-2.041 3.06-4.5 3.06-7.379 0-2.907-1.019-5.396-3.06-7.465-2.069-2.04-4.557-3.06-7.465-3.06z" transform="translate(511.15 -446.2) scale(7.70387)" clip-rule="evenodd" fill-rule="evenodd"/></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" width="512" height="512"><defs><linearGradient id="a"><stop offset="0" style="stop-color:#31748f;stop-opacity:1"/><stop offset="1" style="stop-color:#9ccfd8;stop-opacity:1"/></linearGradient><linearGradient xlink:href="#a" id="b" x1="254.819" x2="259.34" y1="411.542" y2="-1.41" gradientUnits="userSpaceOnUse"/></defs><path d="M0 0v512h512V0Z" style="fill:url(#b);fill-opacity:1;stroke-width:.996356"/><g style="font-weight:400;font-family:"OTADESIGN Rounded";letter-spacing:0;word-spacing:0;fill:#fff"><path fill-rule="evenodd" d="M-41.832 77.19c-3.868 0-7.177 1.358-9.93 4.074-2.716 2.752-4.074 6.063-4.074 9.931 0 3.869 1.358 7.04 4.074 9.793 2.753 2.716 6.064 4.073 9.932 4.073 3.831 0 7.122-1.357 9.875-4.073.855-.855 1.283-1.896 1.283-3.123 0-1.228-.428-2.271-1.283-3.127-.856-.855-1.897-1.281-3.123-1.281-1.229 0-2.27.426-3.125 1.281-1.004 1.042-2.213 1.563-3.627 1.563-3.035-.31-5.208-2.263-5.246-5.106.038-2.842 2.21-4.935 5.244-5.246 1.414 0 2.623.52 3.627 1.563.855.855 1.898 1.283 3.127 1.283 1.226 0 2.267-.428 3.123-1.283.855-.856 1.283-1.897 1.283-3.125 0-1.227-.428-2.268-1.283-3.123-2.753-2.716-6.046-4.075-9.877-4.075zm20.902 6.91c-2.88 0-5.353 1.02-7.422 3.06-.642.643-.964 1.426-.964 2.348 0 .923.322 1.706.964 2.35.644.642 1.427.962 2.348.962.924 0 1.707-.32 2.35-.963.754-.783 1.662-1.173 2.724-1.173 1.09 0 2.026.376 2.809 1.13a3.909 3.909 0 0 1 1.135 2.811c0 1.062-.393 1.97-1.176 2.725-.392.419-.868.7-1.426.84-.141.027-.252.012-.336-.044-.056-.084-.028-.168.084-.251l.84-.881c.643-.643.965-1.411.965-2.305 0-.922-.28-1.663-.838-2.223-.559-.559-1.343-.84-2.35-.84-.698 0-1.397.35-2.095 1.05l-4.866 4.822c-.643.643-.964 1.426-.964 2.347 0 .923.321 1.705.964 2.348 1.957 1.93 4.375 2.894 7.254 2.894 2.908 0 5.396-1.034 7.465-3.103 2.041-2.041 3.06-4.5 3.06-7.379 0-2.907-1.019-5.396-3.06-7.465-2.069-2.04-4.557-3.06-7.465-3.06z" clip-rule="evenodd" style="fill:#fff" transform="translate(511.15 -446.2) scale(7.70387)"/></g></svg>
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2 KiB |
|
@ -220,7 +220,7 @@ export class Init1000000000000 {
|
|||
`CREATE INDEX "IDX_3c601b70a1066d2c8b517094cb" ON "notification" ("notifieeId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "meta" ("id" character varying(32) NOT NULL, "name" character varying(128), "description" character varying(1024), "maintainerName" character varying(128), "maintainerEmail" character varying(128), "announcements" jsonb NOT NULL DEFAULT '[]', "disableRegistration" boolean NOT NULL DEFAULT false, "disableLocalTimeline" boolean NOT NULL DEFAULT false, "disableGlobalTimeline" boolean NOT NULL DEFAULT false, "enableEmojiReaction" boolean NOT NULL DEFAULT true, "useStarForReactionFallback" boolean NOT NULL DEFAULT false, "langs" character varying(64) array NOT NULL DEFAULT '{}'::varchar[], "hiddenTags" character varying(256) array NOT NULL DEFAULT '{}'::varchar[], "blockedHosts" character varying(256) array NOT NULL DEFAULT '{}'::varchar[], "mascotImageUrl" character varying(512) DEFAULT '/static-assets/badges/info.png', "bannerUrl" character varying(512), "errorImageUrl" character varying(512) DEFAULT '/static-assets/badges/error.png', "iconUrl" character varying(512), "cacheRemoteFiles" boolean NOT NULL DEFAULT true, "proxyAccount" character varying(128), "enableRecaptcha" boolean NOT NULL DEFAULT false, "recaptchaSiteKey" character varying(64), "recaptchaSecretKey" character varying(64), "localDriveCapacityMb" integer NOT NULL DEFAULT 1024, "remoteDriveCapacityMb" integer NOT NULL DEFAULT 32, "maxNoteTextLength" integer NOT NULL DEFAULT 500, "summalyProxy" character varying(128), "enableEmail" boolean NOT NULL DEFAULT false, "email" character varying(128), "smtpSecure" boolean NOT NULL DEFAULT false, "smtpHost" character varying(128), "smtpPort" integer, "smtpUser" character varying(128), "smtpPass" character varying(128), "enableServiceWorker" boolean NOT NULL DEFAULT false, "swPublicKey" character varying(128), "swPrivateKey" character varying(128), "enableTwitterIntegration" boolean NOT NULL DEFAULT false, "twitterConsumerKey" character varying(128), "twitterConsumerSecret" character varying(128), "enableGithubIntegration" boolean NOT NULL DEFAULT false, "githubClientId" character varying(128), "githubClientSecret" character varying(128), "enableDiscordIntegration" boolean NOT NULL DEFAULT false, "discordClientId" character varying(128), "discordClientSecret" character varying(128), CONSTRAINT "PK_c4c17a6c2bd7651338b60fc590b" PRIMARY KEY ("id"))`,
|
||||
`CREATE TABLE "meta" ("id" character varying(32) NOT NULL, "name" character varying(128), "description" character varying(1024), "maintainerName" character varying(128), "maintainerEmail" character varying(128), "announcements" jsonb NOT NULL DEFAULT '[]', "disableRegistration" boolean NOT NULL DEFAULT false, "disableLocalTimeline" boolean NOT NULL DEFAULT false, "disableGlobalTimeline" boolean NOT NULL DEFAULT false, "enableEmojiReaction" boolean NOT NULL DEFAULT true, "useStarForReactionFallback" boolean NOT NULL DEFAULT false, "langs" character varying(64) array NOT NULL DEFAULT '{}'::varchar[], "hiddenTags" character varying(256) array NOT NULL DEFAULT '{}'::varchar[], "blockedHosts" character varying(256) array NOT NULL DEFAULT '{}'::varchar[], "mascotImageUrl" character varying(512) DEFAULT '/static-assets/badges/info.png', "bannerUrl" character varying(512), "errorImageUrl" character varying(512) DEFAULT '/static-assets/badges/error.png', "iconUrl" character varying(512), "cacheRemoteFiles" boolean NOT NULL DEFAULT false, "proxyAccount" character varying(128), "enableRecaptcha" boolean NOT NULL DEFAULT false, "recaptchaSiteKey" character varying(64), "recaptchaSecretKey" character varying(64), "localDriveCapacityMb" integer NOT NULL DEFAULT 1024, "remoteDriveCapacityMb" integer NOT NULL DEFAULT 32, "maxNoteTextLength" integer NOT NULL DEFAULT 500, "summalyProxy" character varying(128), "enableEmail" boolean NOT NULL DEFAULT false, "email" character varying(128), "smtpSecure" boolean NOT NULL DEFAULT false, "smtpHost" character varying(128), "smtpPort" integer, "smtpUser" character varying(128), "smtpPass" character varying(128), "enableServiceWorker" boolean NOT NULL DEFAULT false, "swPublicKey" character varying(128), "swPrivateKey" character varying(128), "enableTwitterIntegration" boolean NOT NULL DEFAULT false, "twitterConsumerKey" character varying(128), "twitterConsumerSecret" character varying(128), "enableGithubIntegration" boolean NOT NULL DEFAULT false, "githubClientId" character varying(128), "githubClientSecret" character varying(128), "enableDiscordIntegration" boolean NOT NULL DEFAULT false, "discordClientId" character varying(128), "discordClientSecret" character varying(128), CONSTRAINT "PK_c4c17a6c2bd7651338b60fc590b" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "following" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "followeeId" character varying(32) NOT NULL, "followerId" character varying(32) NOT NULL, "followerHost" character varying(128), "followerInbox" character varying(512), "followerSharedInbox" character varying(512), "followeeHost" character varying(128), "followeeInbox" character varying(512), "followeeSharedInbox" character varying(512), CONSTRAINT "PK_c76c6e044bdf76ecf8bfb82a645" PRIMARY KEY ("id"))`,
|
||||
|
|
21
packages/backend/migration/1688280713783-add-meta-options.js
Normal file
21
packages/backend/migration/1688280713783-add-meta-options.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
export class AddMetaOptions1688280713783 {
|
||||
name = "AddMetaOptions1688280713783";
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "meta" ADD "enableServerMachineStats" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "meta" ADD "enableIdenticonGeneration" boolean NOT NULL DEFAULT true`,
|
||||
);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "meta" DROP COLUMN "enableIdenticonGeneration"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "meta" DROP COLUMN "enableServerMachineStats"`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -10,14 +10,14 @@ path = "src/lib.rs"
|
|||
|
||||
[features]
|
||||
default = []
|
||||
convert = ["dep:native-utils"]
|
||||
convert = ["dep:native-utils", "dep:indicatif", "dep:futures"]
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0.96"
|
||||
native-utils = { path = "../", optional = true }
|
||||
indicatif = { version = "0.17.4", features = ["tokio"] }
|
||||
indicatif = { version = "0.17.4", features = ["tokio"], optional = true }
|
||||
tokio = { version = "1.28.2", features = ["full"] }
|
||||
futures = "0.3.28"
|
||||
futures = { version = "0.3.28", optional = true }
|
||||
serde_yaml = "0.9.21"
|
||||
serde = { version = "1.0.163", features = ["derive"] }
|
||||
urlencoding = "2.1.2"
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
pub use sea_orm_migration::prelude::*;
|
||||
|
||||
mod m20230531_180824_drop_reversi;
|
||||
mod m20230627_185451_index_note_url;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||
vec![Box::new(m20230531_180824_drop_reversi::Migration)]
|
||||
vec![
|
||||
Box::new(m20230531_180824_drop_reversi::Migration),
|
||||
Box::new(m20230627_185451_index_note_url::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("IDX_note_url")
|
||||
.table(Note::Table)
|
||||
.col(Note::Url)
|
||||
.if_not_exists()
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.name("IDX_note_url")
|
||||
.table(Note::Table)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Learn more at https://docs.rs/sea-query#iden
|
||||
#[derive(Iden)]
|
||||
enum Note {
|
||||
Table,
|
||||
Url,
|
||||
}
|
|
@ -1,48 +1,50 @@
|
|||
{
|
||||
"name": "native-utils",
|
||||
"version": "0.0.0",
|
||||
"main": "built/index.js",
|
||||
"types": "built/index.d.ts",
|
||||
"napi": {
|
||||
"name": "native-utils",
|
||||
"triples": {
|
||||
"additional": [
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-linux-android",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"aarch64-unknown-linux-musl",
|
||||
"aarch64-pc-windows-msvc",
|
||||
"armv7-unknown-linux-gnueabihf",
|
||||
"x86_64-unknown-linux-musl",
|
||||
"x86_64-unknown-freebsd",
|
||||
"i686-pc-windows-msvc",
|
||||
"armv7-linux-androideabi",
|
||||
"universal-apple-darwin"
|
||||
]
|
||||
}
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "2.16.1",
|
||||
"ava": "5.1.1"
|
||||
},
|
||||
"ava": {
|
||||
"timeout": "3m"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"scripts": {
|
||||
"artifacts": "napi artifacts",
|
||||
"build": "napi build --features napi --platform --release ./built/",
|
||||
"build:debug": "napi build --platform",
|
||||
"prepublishOnly": "napi prepublish -t npm",
|
||||
"test": "pnpm run cargo:test && pnpm run build && ava",
|
||||
"universal": "napi universal",
|
||||
"version": "napi version",
|
||||
"name": "native-utils",
|
||||
"version": "0.0.0",
|
||||
"main": "built/index.js",
|
||||
"types": "built/index.d.ts",
|
||||
"napi": {
|
||||
"name": "native-utils",
|
||||
"triples": {
|
||||
"additional": [
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-linux-android",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"aarch64-unknown-linux-musl",
|
||||
"aarch64-pc-windows-msvc",
|
||||
"armv7-unknown-linux-gnueabihf",
|
||||
"x86_64-unknown-linux-musl",
|
||||
"x86_64-unknown-freebsd",
|
||||
"i686-pc-windows-msvc",
|
||||
"armv7-linux-androideabi",
|
||||
"universal-apple-darwin"
|
||||
]
|
||||
}
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "2.16.1",
|
||||
"ava": "5.1.1"
|
||||
},
|
||||
"ava": {
|
||||
"timeout": "3m"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"scripts": {
|
||||
"artifacts": "napi artifacts",
|
||||
"build": "pnpm run build:napi && pnpm run build:migration",
|
||||
"build:napi": "napi build --features napi --platform --release ./built/",
|
||||
"build:migration": "cargo build --locked --release --manifest-path ./migration/Cargo.toml && cp ./target/release/migration ./built/migration",
|
||||
"build:debug": "napi build --platform ./built/ && cargo build --manifest-path ./migration/Cargo.toml",
|
||||
"prepublishOnly": "napi prepublish -t npm",
|
||||
"test": "pnpm run cargo:test && pnpm run build:napi && ava",
|
||||
"universal": "napi universal",
|
||||
"version": "napi version",
|
||||
"format": "cargo fmt --all",
|
||||
"cargo:test": "pnpm run cargo:unit && pnpm run cargo:integration",
|
||||
"cargo:unit": "cargo test unit_test && cargo test -F napi unit_test",
|
||||
"cargo:integration": "cargo test -F noarray int_test -- --test-threads=1"
|
||||
}
|
||||
"cargo:unit": "cargo test unit_test && cargo test -F napi unit_test",
|
||||
"cargo:integration": "cargo test -F noarray int_test -- --test-threads=1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,18 +13,19 @@ pub enum IdConvertType {
|
|||
|
||||
#[napi]
|
||||
pub fn convert_id(in_id: String, id_convert_type: IdConvertType) -> napi::Result<String> {
|
||||
println!("converting id: {}", in_id);
|
||||
use IdConvertType::*;
|
||||
match id_convert_type {
|
||||
MastodonId => {
|
||||
let mut out: i64 = 0;
|
||||
let mut out: i128 = 0;
|
||||
for (i, c) in in_id.to_lowercase().chars().rev().enumerate() {
|
||||
out += num_from_char(c)? as i64 * 36_i64.pow(i as u32);
|
||||
out += num_from_char(c)? as i128 * 36_i128.pow(i as u32);
|
||||
}
|
||||
|
||||
Ok(out.to_string())
|
||||
}
|
||||
CalckeyId => {
|
||||
let mut input: i64 = match in_id.parse() {
|
||||
let mut input: i128 = match in_id.parse() {
|
||||
Ok(s) => s,
|
||||
Err(_) => {
|
||||
return Err(Error::new(
|
||||
|
|
|
@ -8,10 +8,10 @@
|
|||
"start:test": "NODE_ENV=test pnpm node ./built/index.js",
|
||||
"migrate": "pnpm run migrate:typeorm && pnpm run migrate:cargo",
|
||||
"migrate:typeorm": "typeorm migration:run -d ormconfig.js",
|
||||
"migrate:cargo": "cargo run --manifest-path ./native-utils/migration/Cargo.toml -- up",
|
||||
"migrate:cargo": "./native-utils/built/migration up",
|
||||
"revertmigration": "pnpm run revertmigration:cargo && pnpm run revertmigration:typeorm",
|
||||
"revertmigration:typeorm": "typeorm migration:revert -d ormconfig.js",
|
||||
"revertmigration:cargo": "cargo run --manifest-path ./native-utils/migration/Cargo.toml -- down",
|
||||
"revertmigration:cargo": "./native-utils/built/migration down",
|
||||
"check:connect": "node ./check_connect.js",
|
||||
"build": "pnpm swc src -d built -D",
|
||||
"watch": "pnpm swc src -d built -D -w",
|
||||
|
@ -20,9 +20,6 @@
|
|||
"test": "pnpm run mocha",
|
||||
"format": "pnpm rome format * --write"
|
||||
},
|
||||
"resolutions": {
|
||||
"chokidar": "^3.3.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-android-arm64": "1.3.11",
|
||||
"@tensorflow/tfjs-node": "3.21.1"
|
||||
|
@ -37,6 +34,7 @@
|
|||
"@koa/cors": "3.4.3",
|
||||
"@koa/multer": "3.0.2",
|
||||
"@koa/router": "9.0.1",
|
||||
"@msgpack/msgpack": "3.0.0-beta2",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@redocly/openapi-core": "1.0.0-beta.120",
|
||||
"@sinonjs/fake-timers": "9.1.2",
|
||||
|
@ -46,7 +44,6 @@
|
|||
"ajv": "8.12.0",
|
||||
"archiver": "5.3.1",
|
||||
"argon2": "^0.30.3",
|
||||
"async-mutex": "^0.4.0",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autolinker": "4.0.0",
|
||||
"autwh": "0.1.0",
|
||||
|
@ -115,6 +112,7 @@
|
|||
"ratelimiter": "3.4.1",
|
||||
"re2": "1.19.0",
|
||||
"redis-lock": "0.1.4",
|
||||
"redis-semaphore": "5.3.1",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rename": "1.0.4",
|
||||
"rndstr": "1.0.0",
|
||||
|
|
|
@ -20,9 +20,11 @@ export type Source = {
|
|||
host: string;
|
||||
port: number;
|
||||
family?: number;
|
||||
pass: string;
|
||||
pass?: string;
|
||||
db?: number;
|
||||
prefix?: string;
|
||||
user?: string;
|
||||
tls?: { [y: string]: string };
|
||||
};
|
||||
elasticsearch: {
|
||||
host: string;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import si from "systeminformation";
|
||||
import Xev from "xev";
|
||||
import * as osUtils from "os-utils";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
import meilisearch from "../db/meilisearch.js";
|
||||
|
||||
const ev = new Xev();
|
||||
|
@ -20,6 +21,9 @@ export default function () {
|
|||
ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length || 50));
|
||||
});
|
||||
|
||||
const meta = fetchMeta();
|
||||
if (!meta.enableServerMachineStats) return;
|
||||
|
||||
async function tick() {
|
||||
const cpu = await cpuUsage();
|
||||
const memStats = await mem();
|
||||
|
|
|
@ -207,9 +207,11 @@ export const db = new DataSource({
|
|||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
family: config.redis.family == null ? 0 : config.redis.family,
|
||||
username: config.redis.user ?? "default",
|
||||
password: config.redis.pass,
|
||||
keyPrefix: `${config.redis.prefix}:query:`,
|
||||
db: config.redis.db || 0,
|
||||
tls: config.redis.tls,
|
||||
},
|
||||
}
|
||||
: false,
|
||||
|
|
|
@ -5,10 +5,12 @@ export function createConnection() {
|
|||
return new Redis({
|
||||
port: config.redis.port,
|
||||
host: config.redis.host,
|
||||
family: config.redis.family == null ? 0 : config.redis.family,
|
||||
family: config.redis.family ?? 0,
|
||||
password: config.redis.pass,
|
||||
username: config.redis.user ?? "default",
|
||||
keyPrefix: `${config.redis.prefix}:`,
|
||||
db: config.redis.db || 0,
|
||||
tls: config.redis.tls,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,43 +1,85 @@
|
|||
import { redisClient } from "@/db/redis.js";
|
||||
import { encode, decode } from "@msgpack/msgpack";
|
||||
import { ChainableCommander } from "ioredis";
|
||||
|
||||
export class Cache<T> {
|
||||
public cache: Map<string | null, { date: number; value: T }>;
|
||||
private lifetime: number;
|
||||
private ttl: number;
|
||||
private prefix: string;
|
||||
|
||||
constructor(lifetime: Cache<never>["lifetime"]) {
|
||||
this.cache = new Map();
|
||||
this.lifetime = lifetime;
|
||||
constructor(name: string, ttlSeconds: number) {
|
||||
this.ttl = ttlSeconds;
|
||||
this.prefix = `cache:${name}`;
|
||||
}
|
||||
|
||||
public set(key: string | null, value: T): void {
|
||||
this.cache.set(key, {
|
||||
date: Date.now(),
|
||||
value,
|
||||
});
|
||||
private prefixedKey(key: string | null): string {
|
||||
return key ? `${this.prefix}:${key}` : this.prefix;
|
||||
}
|
||||
|
||||
public get(key: string | null): T | undefined {
|
||||
const cached = this.cache.get(key);
|
||||
if (cached == null) return undefined;
|
||||
if (Date.now() - cached.date > this.lifetime) {
|
||||
this.cache.delete(key);
|
||||
return undefined;
|
||||
public async set(
|
||||
key: string | null,
|
||||
value: T,
|
||||
transaction?: ChainableCommander,
|
||||
): Promise<void> {
|
||||
const _key = this.prefixedKey(key);
|
||||
const _value = Buffer.from(encode(value));
|
||||
const commander = transaction ?? redisClient;
|
||||
await commander.set(_key, _value, "EX", this.ttl);
|
||||
}
|
||||
|
||||
public async get(key: string | null, renew = false): Promise<T | undefined> {
|
||||
const _key = this.prefixedKey(key);
|
||||
const cached = await redisClient.getBuffer(_key);
|
||||
if (cached === null) return undefined;
|
||||
|
||||
if (renew) await redisClient.expire(_key, this.ttl);
|
||||
|
||||
return decode(cached) as T;
|
||||
}
|
||||
|
||||
public async getAll(renew = false): Promise<Map<string, T>> {
|
||||
const keys = await redisClient.keys(`${this.prefix}*`);
|
||||
const map = new Map<string, T>();
|
||||
if (keys.length === 0) {
|
||||
return map;
|
||||
}
|
||||
return cached.value;
|
||||
const values = await redisClient.mgetBuffer(keys);
|
||||
|
||||
for (const [i, key] of keys.entries()) {
|
||||
const val = values[i];
|
||||
if (val !== null) {
|
||||
map.set(key, decode(val) as T);
|
||||
}
|
||||
}
|
||||
|
||||
if (renew) {
|
||||
const trans = redisClient.multi();
|
||||
for (const key of map.keys()) {
|
||||
trans.expire(key, this.ttl);
|
||||
}
|
||||
await trans.exec();
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
public delete(key: string | null) {
|
||||
this.cache.delete(key);
|
||||
public async delete(...keys: (string | null)[]): Promise<void> {
|
||||
if (keys.length > 0) {
|
||||
const _keys = keys.map(this.prefixedKey);
|
||||
await redisClient.del(_keys);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
||||
* Returns if cached value exists. Otherwise, calls fetcher and caches.
|
||||
* Overwrites cached value if invalidated by the optional validator.
|
||||
*/
|
||||
public async fetch(
|
||||
key: string | null,
|
||||
fetcher: () => Promise<T>,
|
||||
renew = false,
|
||||
validator?: (cachedValue: T) => boolean,
|
||||
): Promise<T> {
|
||||
const cachedValue = this.get(key);
|
||||
const cachedValue = await this.get(key, renew);
|
||||
if (cachedValue !== undefined) {
|
||||
if (validator) {
|
||||
if (validator(cachedValue)) {
|
||||
|
@ -52,20 +94,21 @@ export class Cache<T> {
|
|||
|
||||
// Cache MISS
|
||||
const value = await fetcher();
|
||||
this.set(key, value);
|
||||
await this.set(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
||||
* Returns if cached value exists. Otherwise, calls fetcher and caches if the fetcher returns a value.
|
||||
* Overwrites cached value if invalidated by the optional validator.
|
||||
*/
|
||||
public async fetchMaybe(
|
||||
key: string | null,
|
||||
fetcher: () => Promise<T | undefined>,
|
||||
renew = false,
|
||||
validator?: (cachedValue: T) => boolean,
|
||||
): Promise<T | undefined> {
|
||||
const cachedValue = this.get(key);
|
||||
const cachedValue = await this.get(key, renew);
|
||||
if (cachedValue !== undefined) {
|
||||
if (validator) {
|
||||
if (validator(cachedValue)) {
|
||||
|
@ -81,7 +124,7 @@ export class Cache<T> {
|
|||
// Cache MISS
|
||||
const value = await fetcher();
|
||||
if (value !== undefined) {
|
||||
this.set(key, value);
|
||||
await this.set(key, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import * as Acct from "@/misc/acct.js";
|
|||
import type { Packed } from "./schema.js";
|
||||
import { Cache } from "./cache.js";
|
||||
|
||||
const blockingCache = new Cache<User["id"][]>(1000 * 60 * 5);
|
||||
const blockingCache = new Cache<User["id"][]>("blocking", 60 * 5);
|
||||
|
||||
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
|
||||
|
||||
|
|
17
packages/backend/src/misc/convert-milliseconds.ts
Normal file
17
packages/backend/src/misc/convert-milliseconds.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
export function convertMilliseconds(ms: number) {
|
||||
let seconds = Math.round(ms / 1000);
|
||||
let minutes = Math.round(seconds / 60);
|
||||
let hours = Math.round(minutes / 60);
|
||||
const days = Math.round(hours / 24);
|
||||
seconds %= 60;
|
||||
minutes %= 60;
|
||||
hours %= 24;
|
||||
|
||||
const result = [];
|
||||
if (days > 0) result.push(`${days} day(s)`);
|
||||
if (hours > 0) result.push(`${hours} hour(s)`);
|
||||
if (minutes > 0) result.push(`${minutes} minute(s)`);
|
||||
if (seconds > 0) result.push(`${seconds} second(s)`);
|
||||
|
||||
return result.join(", ");
|
||||
}
|
|
@ -1,33 +1,41 @@
|
|||
import probeImageSize from "probe-image-size";
|
||||
import { Mutex, withTimeout } from "async-mutex";
|
||||
import { Mutex } from "redis-semaphore";
|
||||
|
||||
import { FILE_TYPE_BROWSERSAFE } from "@/const.js";
|
||||
import Logger from "@/services/logger.js";
|
||||
import { Cache } from "./cache.js";
|
||||
import { redisClient } from "@/db/redis.js";
|
||||
|
||||
export type Size = {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
const cache = new Cache<boolean>(1000 * 60 * 10); // once every 10 minutes for the same url
|
||||
const mutex = withTimeout(new Mutex(), 1000);
|
||||
const cache = new Cache<boolean>("emojiMeta", 60 * 10); // once every 10 minutes for the same url
|
||||
const logger = new Logger("emoji");
|
||||
|
||||
export async function getEmojiSize(url: string): Promise<Size> {
|
||||
const logger = new Logger("emoji");
|
||||
let attempted = true;
|
||||
|
||||
await mutex.runExclusive(() => {
|
||||
const attempted = cache.get(url);
|
||||
if (!attempted) {
|
||||
cache.set(url, true);
|
||||
} else {
|
||||
logger.warn(`Attempt limit exceeded: ${url}`);
|
||||
throw new Error("Too many attempts");
|
||||
}
|
||||
});
|
||||
const lock = new Mutex(redisClient, "getEmojiSize");
|
||||
await lock.acquire();
|
||||
|
||||
try {
|
||||
logger.info(`Retrieving emoji size from ${url}`);
|
||||
attempted = (await cache.get(url)) === true;
|
||||
if (!attempted) {
|
||||
await cache.set(url, true);
|
||||
}
|
||||
} finally {
|
||||
await lock.release();
|
||||
}
|
||||
|
||||
if (attempted) {
|
||||
logger.warn(`Attempt limit exceeded: ${url}`);
|
||||
throw new Error("Too many attempts");
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug(`Retrieving emoji size from ${url}`);
|
||||
const { width, height, mime } = await probeImageSize(url, {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export function isDuplicateKeyValueError(e: unknown | Error): boolean {
|
||||
return (e as Error).message?.startsWith("duplicate key value");
|
||||
const nodeError = e as NodeJS.ErrnoException;
|
||||
return nodeError.code === "23505";
|
||||
}
|
||||
|
|
|
@ -3,10 +3,12 @@ import type { User } from "@/models/entities/user.js";
|
|||
import type { UserKeypair } from "@/models/entities/user-keypair.js";
|
||||
import { Cache } from "./cache.js";
|
||||
|
||||
const cache = new Cache<UserKeypair>(Infinity);
|
||||
const cache = new Cache<UserKeypair>("keypairStore", 60 * 30);
|
||||
|
||||
export async function getUserKeypair(userId: User["id"]): Promise<UserKeypair> {
|
||||
return await cache.fetch(userId, () =>
|
||||
UserKeypairs.findOneByOrFail({ userId: userId }),
|
||||
return await cache.fetch(
|
||||
userId,
|
||||
() => UserKeypairs.findOneByOrFail({ userId: userId }),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,8 +7,9 @@ import { isSelfHost, toPunyNullable } from "./convert-host.js";
|
|||
import { decodeReaction } from "./reaction-lib.js";
|
||||
import config from "@/config/index.js";
|
||||
import { query } from "@/prelude/url.js";
|
||||
import { redisClient } from "@/db/redis.js";
|
||||
|
||||
const cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12);
|
||||
const cache = new Cache<Emoji | null>("populateEmojis", 60 * 60 * 12);
|
||||
|
||||
/**
|
||||
* 添付用絵文字情報
|
||||
|
@ -75,7 +76,7 @@ export async function populateEmoji(
|
|||
|
||||
if (emoji && !(emoji.width && emoji.height)) {
|
||||
emoji = await queryOrNull();
|
||||
cache.set(cacheKey, emoji);
|
||||
await cache.set(cacheKey, emoji);
|
||||
}
|
||||
|
||||
if (emoji == null) return null;
|
||||
|
@ -150,7 +151,7 @@ export async function prefetchEmojis(
|
|||
emojis: { name: string; host: string | null }[],
|
||||
): Promise<void> {
|
||||
const notCachedEmojis = emojis.filter(
|
||||
(emoji) => cache.get(`${emoji.name} ${emoji.host}`) == null,
|
||||
async (emoji) => !(await cache.get(`${emoji.name} ${emoji.host}`)),
|
||||
);
|
||||
const emojisQuery: any[] = [];
|
||||
const hosts = new Set(notCachedEmojis.map((e) => e.host));
|
||||
|
@ -169,7 +170,9 @@ export async function prefetchEmojis(
|
|||
select: ["name", "host", "originalUrl", "publicUrl"],
|
||||
})
|
||||
: [];
|
||||
const trans = redisClient.multi();
|
||||
for (const emoji of _emojis) {
|
||||
cache.set(`${emoji.name} ${emoji.host}`, emoji);
|
||||
cache.set(`${emoji.name} ${emoji.host}`, emoji, trans);
|
||||
}
|
||||
await trans.exec();
|
||||
}
|
||||
|
|
|
@ -198,7 +198,7 @@ export class Meta {
|
|||
public iconUrl: string | null;
|
||||
|
||||
@Column("boolean", {
|
||||
default: true,
|
||||
default: false,
|
||||
})
|
||||
public cacheRemoteFiles: boolean;
|
||||
|
||||
|
@ -546,4 +546,14 @@ export class Meta {
|
|||
default: {},
|
||||
})
|
||||
public experimentalFeatures: Record<string, unknown>;
|
||||
|
||||
@Column("boolean", {
|
||||
default: false,
|
||||
})
|
||||
public enableServerMachineStats: boolean;
|
||||
|
||||
@Column("boolean", {
|
||||
default: true,
|
||||
})
|
||||
public enableIdenticonGeneration: boolean;
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ import {
|
|||
import { db } from "@/db/postgre.js";
|
||||
import { IdentifiableError } from "@/misc/identifiable-error.js";
|
||||
|
||||
async function populatePoll(note: Note, meId: User["id"] | null) {
|
||||
export async function populatePoll(note: Note, meId: User["id"] | null) {
|
||||
const poll = await Polls.findOneByOrFail({ noteId: note.id });
|
||||
const choices = poll.choices.map((c) => ({
|
||||
text: c,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { URL } from "url";
|
||||
import { In, Not } from "typeorm";
|
||||
import Ajv from "ajv";
|
||||
import type { ILocalUser, IRemoteUser } from "@/models/entities/user.js";
|
||||
|
@ -40,7 +39,10 @@ import {
|
|||
} from "../index.js";
|
||||
import type { Instance } from "../entities/instance.js";
|
||||
|
||||
const userInstanceCache = new Cache<Instance | null>(1000 * 60 * 60 * 3);
|
||||
const userInstanceCache = new Cache<Instance | null>(
|
||||
"userInstance",
|
||||
60 * 60 * 3,
|
||||
);
|
||||
|
||||
type IsUserDetailed<Detailed extends boolean> = Detailed extends true
|
||||
? Packed<"UserDetailed">
|
||||
|
|
|
@ -482,7 +482,8 @@ export function createCleanRemoteFilesJob() {
|
|||
export function createIndexAllNotesJob(data = {}) {
|
||||
return backgroundQueue.add("indexAllNotes", data, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
removeOnFail: false,
|
||||
timeout: 1000 * 60 * 60 * 24,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -7,8 +7,10 @@ export function initialize<T>(name: string, limitPerSec = -1) {
|
|||
port: config.redis.port,
|
||||
host: config.redis.host,
|
||||
family: config.redis.family == null ? 0 : config.redis.family,
|
||||
username: config.redis.user ?? "default",
|
||||
password: config.redis.pass,
|
||||
db: config.redis.db || 0,
|
||||
tls: config.redis.tls,
|
||||
},
|
||||
prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : "queue",
|
||||
limiter:
|
||||
|
|
|
@ -20,7 +20,7 @@ export default async function indexAllNotes(
|
|||
let total: number = (job.data.total as number) ?? 0;
|
||||
|
||||
let running = true;
|
||||
const take = 50000;
|
||||
const take = 100000;
|
||||
const batch = 100;
|
||||
while (running) {
|
||||
logger.info(
|
||||
|
|
|
@ -17,9 +17,7 @@ export async function deleteAccount(
|
|||
logger.info(`Deleting account of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await Users.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
if (!user) return;
|
||||
|
||||
{
|
||||
// Delete notes
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import type Bull from "bull";
|
||||
import { In } from "typeorm";
|
||||
import { Notes, Polls, PollVotes } from "@/models/index.js";
|
||||
import { Notes, PollVotes } from "@/models/index.js";
|
||||
import { queueLogger } from "../logger.js";
|
||||
import type { EndedPollNotificationJobData } from "@/queue/types.js";
|
||||
import { createNotification } from "@/services/create-notification.js";
|
||||
import { deliverQuestionUpdate } from "@/services/note/polls/update.js";
|
||||
|
||||
const logger = queueLogger.createSubLogger("ended-poll-notification");
|
||||
|
||||
|
@ -32,5 +32,8 @@ export async function endedPollNotification(
|
|||
});
|
||||
}
|
||||
|
||||
// Broadcast the poll result once it ends
|
||||
await deliverQuestionUpdate(note.id);
|
||||
|
||||
done();
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ import DbResolver from "@/remote/activitypub/db-resolver.js";
|
|||
import { getApId } from "@/remote/activitypub/type.js";
|
||||
import { shouldBlockInstance } from "@/misc/should-block-instance.js";
|
||||
import type { IncomingMessage } from "http";
|
||||
import type { CacheableRemoteUser } from "@/models/entities/user.js";
|
||||
import type { UserPublickey } from "@/models/entities/user-publickey.js";
|
||||
|
||||
export async function hasSignature(req: IncomingMessage): Promise<string> {
|
||||
const meta = await fetchMeta();
|
||||
|
@ -95,3 +97,22 @@ export async function checkFetch(req: IncomingMessage): Promise<number> {
|
|||
}
|
||||
return 200;
|
||||
}
|
||||
|
||||
export async function getSignatureUser(req: IncomingMessage): Promise<{
|
||||
user: CacheableRemoteUser;
|
||||
key: UserPublickey | null;
|
||||
} | null> {
|
||||
const signature = httpSignature.parseRequest(req, { headers: [] });
|
||||
const keyId = new URL(signature.keyId);
|
||||
const dbResolver = new DbResolver();
|
||||
|
||||
// Retrieve from DB by HTTP-Signature keyId
|
||||
const authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId);
|
||||
if (authUser) {
|
||||
return authUser;
|
||||
}
|
||||
|
||||
// Resolve if failed to retrieve by keyId
|
||||
keyId.hash = "";
|
||||
return await dbResolver.getAuthUserFromApId(getApId(keyId.toString()));
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import type {
|
|||
CacheableRemoteUser,
|
||||
CacheableUser,
|
||||
} from "@/models/entities/user.js";
|
||||
import { User, IRemoteUser } from "@/models/entities/user.js";
|
||||
import type { UserPublickey } from "@/models/entities/user-publickey.js";
|
||||
import type { MessagingMessage } from "@/models/entities/messaging-message.js";
|
||||
import {
|
||||
|
@ -20,8 +19,11 @@ import type { IObject } from "./type.js";
|
|||
import { getApId } from "./type.js";
|
||||
import { resolvePerson } from "./models/person.js";
|
||||
|
||||
const publicKeyCache = new Cache<UserPublickey | null>(Infinity);
|
||||
const publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity);
|
||||
const publicKeyCache = new Cache<UserPublickey | null>("publicKey", 60 * 30);
|
||||
const publicKeyByUserIdCache = new Cache<UserPublickey | null>(
|
||||
"publicKeyByUserId",
|
||||
60 * 30,
|
||||
);
|
||||
|
||||
export type UriParseResult =
|
||||
| {
|
||||
|
@ -80,8 +82,15 @@ export default class DbResolver {
|
|||
id: parsed.id,
|
||||
});
|
||||
} else {
|
||||
return await Notes.findOneBy({
|
||||
uri: parsed.uri,
|
||||
return await Notes.findOne({
|
||||
where: [
|
||||
{
|
||||
uri: parsed.uri,
|
||||
},
|
||||
{
|
||||
url: parsed.uri,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -116,17 +125,23 @@ export default class DbResolver {
|
|||
if (parsed.type !== "users") return null;
|
||||
|
||||
return (
|
||||
(await userByIdCache.fetchMaybe(parsed.id, () =>
|
||||
Users.findOneBy({
|
||||
id: parsed.id,
|
||||
}).then((x) => x ?? undefined),
|
||||
(await userByIdCache.fetchMaybe(
|
||||
parsed.id,
|
||||
() =>
|
||||
Users.findOneBy({
|
||||
id: parsed.id,
|
||||
}).then((x) => x ?? undefined),
|
||||
true,
|
||||
)) ?? null
|
||||
);
|
||||
} else {
|
||||
return await uriPersonCache.fetch(parsed.uri, () =>
|
||||
Users.findOneBy({
|
||||
uri: parsed.uri,
|
||||
}),
|
||||
return await uriPersonCache.fetch(
|
||||
parsed.uri,
|
||||
() =>
|
||||
Users.findOneBy({
|
||||
uri: parsed.uri,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -149,14 +164,17 @@ export default class DbResolver {
|
|||
|
||||
return key;
|
||||
},
|
||||
true,
|
||||
(key) => key != null,
|
||||
);
|
||||
|
||||
if (key == null) return null;
|
||||
|
||||
return {
|
||||
user: (await userByIdCache.fetch(key.userId, () =>
|
||||
Users.findOneByOrFail({ id: key.userId }),
|
||||
user: (await userByIdCache.fetch(
|
||||
key.userId,
|
||||
() => Users.findOneByOrFail({ id: key.userId }),
|
||||
true,
|
||||
)) as CacheableRemoteUser,
|
||||
key,
|
||||
};
|
||||
|
@ -176,6 +194,7 @@ export default class DbResolver {
|
|||
const key = await publicKeyByUserIdCache.fetch(
|
||||
user.id,
|
||||
() => UserPublickeys.findOneBy({ userId: user.id }),
|
||||
true,
|
||||
(v) => v != null,
|
||||
);
|
||||
|
||||
|
|
|
@ -15,9 +15,11 @@ export async function deleteActor(
|
|||
return `skip: delete actor ${actor.uri} !== ${uri}`;
|
||||
}
|
||||
|
||||
const user = await Users.findOneByOrFail({ id: actor.id });
|
||||
if (user.isDeleted) {
|
||||
logger.info("skip: already deleted");
|
||||
const user = await Users.findOneBy({ id: actor.id });
|
||||
if (!user) {
|
||||
return `skip: actor ${actor.id} not found in the local database`;
|
||||
} else if (user.isDeleted) {
|
||||
return `skip: user ${user.id} already deleted`;
|
||||
}
|
||||
|
||||
const job = await createDeleteAccountJob(actor);
|
||||
|
|
|
@ -26,11 +26,11 @@ export async function createImage(
|
|||
const image = (await new Resolver().resolve(value)) as any;
|
||||
|
||||
if (image.url == null) {
|
||||
throw new Error("invalid image: url not privided");
|
||||
throw new Error("Invalid image, URL not provided");
|
||||
}
|
||||
|
||||
if (!image.url.startsWith("https://") && !image.url.startsWith("http://")) {
|
||||
throw new Error("invalid image: unexpected shcema of url: " + image.url);
|
||||
throw new Error(`Invalid image, unexpected schema: ${image.url}`);
|
||||
}
|
||||
|
||||
logger.info(`Creating the Image: ${image.url}`);
|
||||
|
|
|
@ -13,11 +13,10 @@ import type {
|
|||
import { htmlToMfm } from "../misc/html-to-mfm.js";
|
||||
import { extractApHashtags } from "./tag.js";
|
||||
import { unique, toArray, toSingle } from "@/prelude/array.js";
|
||||
import { extractPollFromQuestion, updateQuestion } from "./question.js";
|
||||
import { extractPollFromQuestion } from "./question.js";
|
||||
import vote from "@/services/note/polls/vote.js";
|
||||
import { apLogger } from "../logger.js";
|
||||
import { DriveFile } from "@/models/entities/drive-file.js";
|
||||
import { deliverQuestionUpdate } from "@/services/note/polls/update.js";
|
||||
import { extractDbHost, toPuny } from "@/misc/convert-host.js";
|
||||
import {
|
||||
Emojis,
|
||||
|
@ -334,9 +333,6 @@ export async function createNote(
|
|||
`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`,
|
||||
);
|
||||
await vote(actor, reply, index);
|
||||
|
||||
// リモートフォロワーにUpdate配信
|
||||
deliverQuestionUpdate(reply.id);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
@ -545,10 +541,6 @@ function notEmpty(partial: Partial<any>) {
|
|||
export async function updateNote(value: string | IObject, resolver?: Resolver) {
|
||||
const uri = typeof value === "string" ? value : value.id;
|
||||
if (!uri) throw new Error("Missing note uri");
|
||||
const instanceMeta = await fetchMeta();
|
||||
if (instanceMeta.experimentalFeatures?.postEdits === false) {
|
||||
throw new Error("Post edits disabled.");
|
||||
}
|
||||
|
||||
// Skip if URI points to this server
|
||||
if (uri.startsWith(`${config.url}/`)) throw new Error("uri points local");
|
||||
|
|
|
@ -135,14 +135,14 @@ export async function fetchPerson(
|
|||
): Promise<CacheableUser | null> {
|
||||
if (typeof uri !== "string") throw new Error("uri is not string");
|
||||
|
||||
const cached = uriPersonCache.get(uri);
|
||||
const cached = await uriPersonCache.get(uri, true);
|
||||
if (cached) return cached;
|
||||
|
||||
// Fetch from the database if the URI points to this server
|
||||
if (uri.startsWith(`${config.url}/`)) {
|
||||
const id = uri.split("/").pop();
|
||||
const u = await Users.findOneBy({ id });
|
||||
if (u) uriPersonCache.set(uri, u);
|
||||
if (u) await uriPersonCache.set(uri, u);
|
||||
return u;
|
||||
}
|
||||
|
||||
|
@ -150,7 +150,7 @@ export async function fetchPerson(
|
|||
const exist = await Users.findOneBy({ uri });
|
||||
|
||||
if (exist) {
|
||||
uriPersonCache.set(uri, exist);
|
||||
await uriPersonCache.set(uri, exist);
|
||||
return exist;
|
||||
}
|
||||
//#endregion
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import config from "@/config/index.js";
|
||||
import Resolver from "../resolver.js";
|
||||
import type { IObject, IQuestion } from "../type.js";
|
||||
import { isQuestion } from "../type.js";
|
||||
import { getApId, isQuestion } from "../type.js";
|
||||
import { apLogger } from "../logger.js";
|
||||
import { Notes, Polls } from "@/models/index.js";
|
||||
import type { IPoll } from "@/models/entities/poll.js";
|
||||
|
@ -47,11 +47,14 @@ export async function extractPollFromQuestion(
|
|||
|
||||
/**
|
||||
* Update votes of Question
|
||||
* @param uri URI of AP Question object
|
||||
* @param value URI of AP Question object or object itself
|
||||
* @returns true if updated
|
||||
*/
|
||||
export async function updateQuestion(value: any, resolver?: Resolver) {
|
||||
const uri = typeof value === "string" ? value : value.id;
|
||||
export async function updateQuestion(
|
||||
value: string | IQuestion,
|
||||
resolver?: Resolver,
|
||||
): Promise<boolean> {
|
||||
const uri = typeof value === "string" ? value : getApId(value);
|
||||
|
||||
// Skip if URI points to this server
|
||||
if (uri.startsWith(`${config.url}/`)) throw new Error("uri points local");
|
||||
|
@ -65,22 +68,23 @@ export async function updateQuestion(value: any, resolver?: Resolver) {
|
|||
//#endregion
|
||||
|
||||
// resolve new Question object
|
||||
if (resolver == null) resolver = new Resolver();
|
||||
const question = (await resolver.resolve(value)) as IQuestion;
|
||||
const _resolver = resolver ?? new Resolver();
|
||||
const question = (await _resolver.resolve(value)) as IQuestion;
|
||||
apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
|
||||
|
||||
if (question.type !== "Question") throw new Error("object is not a Question");
|
||||
|
||||
const apChoices = question.oneOf || question.anyOf;
|
||||
if (!apChoices) return false;
|
||||
|
||||
let changed = false;
|
||||
|
||||
for (const choice of poll.choices) {
|
||||
const oldCount = poll.votes[poll.choices.indexOf(choice)];
|
||||
const newCount = apChoices!.filter((ap) => ap.name === choice)[0].replies!
|
||||
.totalItems;
|
||||
const newCount = apChoices.filter((ap) => ap.name === choice)[0].replies
|
||||
?.totalItems;
|
||||
|
||||
if (oldCount !== newCount) {
|
||||
if (newCount !== undefined && oldCount !== newCount) {
|
||||
changed = true;
|
||||
poll.votes[poll.choices.indexOf(choice)] = newCount;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { getUserKeypair } from "@/misc/keypair-store.js";
|
|||
import type { User } from "@/models/entities/user.js";
|
||||
import { getResponse } from "../../misc/fetch.js";
|
||||
import { createSignedPost, createSignedGet } from "./ap-request.js";
|
||||
import { apLogger } from "@/remote/activitypub/logger.js";
|
||||
|
||||
export default async (user: { id: User["id"] }, url: string, object: any) => {
|
||||
const body = JSON.stringify(object);
|
||||
|
@ -35,6 +36,7 @@ export default async (user: { id: User["id"] }, url: string, object: any) => {
|
|||
* @param url URL to fetch
|
||||
*/
|
||||
export async function signedGet(url: string, user: { id: User["id"] }) {
|
||||
apLogger.debug(`Running signedGet on url: ${url}`);
|
||||
const keypair = await getUserKeypair(user.id);
|
||||
|
||||
const req = createSignedGet({
|
||||
|
|
|
@ -23,6 +23,7 @@ import renderCreate from "@/remote/activitypub/renderer/create.js";
|
|||
import { renderActivity } from "@/remote/activitypub/renderer/index.js";
|
||||
import renderFollow from "@/remote/activitypub/renderer/follow.js";
|
||||
import { shouldBlockInstance } from "@/misc/should-block-instance.js";
|
||||
import { apLogger } from "@/remote/activitypub/logger.js";
|
||||
|
||||
export default class Resolver {
|
||||
private history: Set<string>;
|
||||
|
@ -34,6 +35,15 @@ export default class Resolver {
|
|||
this.recursionLimit = recursionLimit;
|
||||
}
|
||||
|
||||
public setUser(user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public reset(): Resolver {
|
||||
this.history = new Set();
|
||||
return this;
|
||||
}
|
||||
|
||||
public getHistory(): string[] {
|
||||
return Array.from(this.history);
|
||||
}
|
||||
|
@ -56,15 +66,20 @@ export default class Resolver {
|
|||
}
|
||||
|
||||
if (typeof value !== "string") {
|
||||
apLogger.debug("Object to resolve is not a string");
|
||||
if (typeof value.id !== "undefined") {
|
||||
const host = extractDbHost(getApId(value));
|
||||
if (await shouldBlockInstance(host)) {
|
||||
throw new Error("instance is blocked");
|
||||
}
|
||||
}
|
||||
apLogger.debug("Returning existing object:");
|
||||
apLogger.debug(JSON.stringify(value, null, 2));
|
||||
return value;
|
||||
}
|
||||
|
||||
apLogger.debug(`Resolving: ${value}`);
|
||||
|
||||
if (value.includes("#")) {
|
||||
// URLs with fragment parts cannot be resolved correctly because
|
||||
// the fragment part does not get transmitted over HTTP(S).
|
||||
|
@ -102,6 +117,9 @@ export default class Resolver {
|
|||
this.user = await getInstanceActor();
|
||||
}
|
||||
|
||||
apLogger.debug("Getting object from remote, authenticated as user:");
|
||||
apLogger.debug(JSON.stringify(this.user, null, 2));
|
||||
|
||||
const object = (
|
||||
this.user
|
||||
? await signedGet(value, this.user)
|
||||
|
|
|
@ -20,7 +20,11 @@ import {
|
|||
import type { ILocalUser, User } from "@/models/entities/user.js";
|
||||
import { renderLike } from "@/remote/activitypub/renderer/like.js";
|
||||
import { getUserKeypair } from "@/misc/keypair-store.js";
|
||||
import { checkFetch, hasSignature } from "@/remote/activitypub/check-fetch.js";
|
||||
import {
|
||||
checkFetch,
|
||||
hasSignature,
|
||||
getSignatureUser,
|
||||
} from "@/remote/activitypub/check-fetch.js";
|
||||
import { getInstanceActor } from "@/services/instance-actor.js";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
import renderFollow from "@/remote/activitypub/renderer/follow.js";
|
||||
|
@ -28,6 +32,7 @@ import Featured from "./activitypub/featured.js";
|
|||
import Following from "./activitypub/following.js";
|
||||
import Followers from "./activitypub/followers.js";
|
||||
import Outbox, { packActivity } from "./activitypub/outbox.js";
|
||||
import { serverLogger } from "./index.js";
|
||||
|
||||
// Init router
|
||||
const router = new Router();
|
||||
|
@ -84,7 +89,7 @@ router.get("/notes/:note", async (ctx, next) => {
|
|||
|
||||
const note = await Notes.findOneBy({
|
||||
id: ctx.params.note,
|
||||
visibility: In(["public" as const, "home" as const]),
|
||||
visibility: In(["public" as const, "home" as const, "followers" as const]),
|
||||
localOnly: false,
|
||||
});
|
||||
|
||||
|
@ -103,6 +108,37 @@ router.get("/notes/:note", async (ctx, next) => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (note.visibility === "followers") {
|
||||
serverLogger.debug(
|
||||
"Responding to request for follower-only note, validating access...",
|
||||
);
|
||||
const remoteUser = await getSignatureUser(ctx.req);
|
||||
serverLogger.debug("Local note author user:");
|
||||
serverLogger.debug(JSON.stringify(note, null, 2));
|
||||
serverLogger.debug("Authenticated remote user:");
|
||||
serverLogger.debug(JSON.stringify(remoteUser, null, 2));
|
||||
|
||||
if (remoteUser == null) {
|
||||
serverLogger.debug("Rejecting: no user");
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const relation = await Users.getRelation(remoteUser.user.id, note.userId);
|
||||
serverLogger.debug("Relation:");
|
||||
serverLogger.debug(JSON.stringify(relation, null, 2));
|
||||
|
||||
if (!relation.isFollowing || relation.isBlocked) {
|
||||
serverLogger.debug(
|
||||
"Rejecting: authenticated user is not following us or was blocked by us",
|
||||
);
|
||||
ctx.status = 403;
|
||||
return;
|
||||
}
|
||||
|
||||
serverLogger.debug("Accepting: access criteria met");
|
||||
}
|
||||
|
||||
ctx.body = renderActivity(await renderNote(note, false));
|
||||
|
||||
const meta = await fetchMeta();
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
localUserByNativeTokenCache,
|
||||
} from "@/services/user-cache.js";
|
||||
|
||||
const appCache = new Cache<App>(Infinity);
|
||||
const appCache = new Cache<App>("app", 60 * 30);
|
||||
|
||||
export class AuthenticationError extends Error {
|
||||
constructor(message: string) {
|
||||
|
@ -49,6 +49,7 @@ export default async (
|
|||
const user = await localUserByNativeTokenCache.fetch(
|
||||
token,
|
||||
() => Users.findOneBy({ token }) as Promise<ILocalUser | null>,
|
||||
true,
|
||||
);
|
||||
|
||||
if (user == null) {
|
||||
|
@ -82,11 +83,14 @@ export default async (
|
|||
Users.findOneBy({
|
||||
id: accessToken.userId,
|
||||
}) as Promise<ILocalUser>,
|
||||
true,
|
||||
);
|
||||
|
||||
if (accessToken.appId) {
|
||||
const app = await appCache.fetch(accessToken.appId, () =>
|
||||
Apps.findOneByOrFail({ id: accessToken.appId! }),
|
||||
const app = await appCache.fetch(
|
||||
accessToken.appId,
|
||||
() => Apps.findOneByOrFail({ id: accessToken.appId! }),
|
||||
true,
|
||||
);
|
||||
|
||||
return [
|
||||
|
|
|
@ -66,8 +66,11 @@ export default async (
|
|||
limit as IEndpointMeta["limit"] & { key: NonNullable<string> },
|
||||
limitActor,
|
||||
).catch((e) => {
|
||||
const remainingTime = e.remainingTime
|
||||
? `Please try again in ${e.remainingTime}.`
|
||||
: "Please try again later.";
|
||||
throw new ApiError({
|
||||
message: "Rate limit exceeded. Please try again later.",
|
||||
message: `Rate limit exceeded. ${remainingTime}`,
|
||||
code: "RATE_LIMIT_EXCEEDED",
|
||||
id: "d5826d14-3982-4d2e-8011-b9e9f02499ef",
|
||||
httpStatusCode: 429,
|
||||
|
@ -94,7 +97,7 @@ export default async (
|
|||
}
|
||||
|
||||
if (ep.meta.requireAdmin && !user!.isAdmin) {
|
||||
throw new ApiError(accessDenied, { reason: "You are not the admin." });
|
||||
throw new ApiError(accessDenied, { reason: "You are not an admin." });
|
||||
}
|
||||
|
||||
if (ep.meta.requireModerator && !isModerator) {
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import type { IEndpoint } from "./endpoints";
|
||||
|
||||
import * as cp___instanceInfo from "./endpoints/compatibility/instance-info.js";
|
||||
import * as cp___customEmojis from "./endpoints/compatibility/custom-emojis.js";
|
||||
import * as cp___instance_info from "./endpoints/compatibility/instance-info.js";
|
||||
import * as cp___custom_emojis from "./endpoints/compatibility/custom-emojis.js";
|
||||
import * as ep___instance_peers from "./endpoints/compatibility/peers.js";
|
||||
|
||||
const cps = [
|
||||
["v1/instance", cp___instanceInfo],
|
||||
["v1/custom_emojis", cp___customEmojis],
|
||||
["v1/instance", cp___instance_info],
|
||||
["v1/custom_emojis", cp___custom_emojis],
|
||||
["v1/instance/peers", ep___instance_peers],
|
||||
];
|
||||
|
||||
const compatibility: IEndpoint[] = cps.map(([name, cp]) => {
|
||||
|
|
|
@ -6,7 +6,7 @@ import { ApiError } from "../../../error.js";
|
|||
import rndstr from "rndstr";
|
||||
import { publishBroadcastStream } from "@/services/stream.js";
|
||||
import { db } from "@/db/postgre.js";
|
||||
import { type Size, getEmojiSize } from "@/misc/emoji-meta.js";
|
||||
import { getEmojiSize } from "@/misc/emoji-meta.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["admin"],
|
||||
|
@ -40,12 +40,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
? file.name.split(".")[0]
|
||||
: `_${rndstr("a-z0-9", 8)}_`;
|
||||
|
||||
let size: Size = { width: 0, height: 0 };
|
||||
try {
|
||||
size = await getEmojiSize(file.url);
|
||||
} catch {
|
||||
/* skip if any error happens */
|
||||
}
|
||||
const size = await getEmojiSize(file.url);
|
||||
|
||||
const emoji = await Emojis.insert({
|
||||
id: genId(),
|
||||
|
|
|
@ -6,7 +6,7 @@ import type { DriveFile } from "@/models/entities/drive-file.js";
|
|||
import { uploadFromUrl } from "@/services/drive/upload-from-url.js";
|
||||
import { publishBroadcastStream } from "@/services/stream.js";
|
||||
import { db } from "@/db/postgre.js";
|
||||
import { type Size, getEmojiSize } from "@/misc/emoji-meta.js";
|
||||
import { getEmojiSize } from "@/misc/emoji-meta.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["admin"],
|
||||
|
@ -65,12 +65,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
throw new ApiError();
|
||||
}
|
||||
|
||||
let size: Size = { width: 0, height: 0 };
|
||||
try {
|
||||
size = await getEmojiSize(driveFile.url);
|
||||
} catch {
|
||||
/* skip if any error happens */
|
||||
}
|
||||
const size = await getEmojiSize(driveFile.url);
|
||||
|
||||
const copied = await Emojis.insert({
|
||||
id: genId(),
|
||||
|
|
|
@ -476,14 +476,21 @@ export const meta = {
|
|||
optional: true,
|
||||
nullable: true,
|
||||
properties: {
|
||||
postEditing: {
|
||||
type: "boolean",
|
||||
},
|
||||
postImports: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
},
|
||||
enableServerMachineStats: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
enableIdenticonGeneration: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
@ -595,5 +602,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
enableIpLogging: instance.enableIpLogging,
|
||||
enableActiveEmailValidation: instance.enableActiveEmailValidation,
|
||||
experimentalFeatures: instance.experimentalFeatures,
|
||||
enableServerMachineStats: instance.enableServerMachineStats,
|
||||
enableIdenticonGeneration: instance.enableIdenticonGeneration,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -42,6 +42,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
isModerator: user.isModerator,
|
||||
isSilenced: user.isSilenced,
|
||||
isSuspended: user.isSuspended,
|
||||
moderationNote: profile.moderationNote,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -174,7 +174,6 @@ export const paramDef = {
|
|||
type: "object",
|
||||
nullable: true,
|
||||
properties: {
|
||||
postEditing: { type: "boolean" },
|
||||
postImports: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import define from "../../define.js";
|
||||
import config from "@/config/index.js";
|
||||
import { createPerson } from "@/remote/activitypub/models/person.js";
|
||||
import { createNote } from "@/remote/activitypub/models/note.js";
|
||||
import DbResolver from "@/remote/activitypub/db-resolver.js";
|
||||
|
@ -9,11 +8,13 @@ import { extractDbHost } from "@/misc/convert-host.js";
|
|||
import { Users, Notes } from "@/models/index.js";
|
||||
import type { Note } from "@/models/entities/note.js";
|
||||
import type { CacheableLocalUser, User } from "@/models/entities/user.js";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
import { isActor, isPost, getApId } from "@/remote/activitypub/type.js";
|
||||
import type { SchemaType } from "@/misc/schema.js";
|
||||
import { HOUR } from "@/const.js";
|
||||
import { shouldBlockInstance } from "@/misc/should-block-instance.js";
|
||||
import { updateQuestion } from "@/remote/activitypub/models/question.js";
|
||||
import { populatePoll } from "@/models/repositories/note.js";
|
||||
import { redisClient } from "@/db/redis.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["federation"],
|
||||
|
@ -104,18 +105,30 @@ async function fetchAny(
|
|||
|
||||
const dbResolver = new DbResolver();
|
||||
|
||||
let local = await mergePack(
|
||||
me,
|
||||
...(await Promise.all([
|
||||
dbResolver.getUserFromApId(uri),
|
||||
dbResolver.getNoteFromApId(uri),
|
||||
])),
|
||||
);
|
||||
if (local != null) return local;
|
||||
const [user, note] = await Promise.all([
|
||||
dbResolver.getUserFromApId(uri),
|
||||
dbResolver.getNoteFromApId(uri),
|
||||
]);
|
||||
let local = await mergePack(me, user, note);
|
||||
if (local) {
|
||||
if (local.type === "Note" && note?.uri && note.hasPoll) {
|
||||
// Update questions if the stored (remote) note contains the poll
|
||||
const key = `pollFetched:${note.uri}`;
|
||||
if ((await redisClient.exists(key)) === 0) {
|
||||
if (await updateQuestion(note.uri)) {
|
||||
local.object.poll = await populatePoll(note, me?.id ?? null);
|
||||
}
|
||||
// Allow fetching the poll again after 1 minute
|
||||
await redisClient.set(key, 1, "EX", 60);
|
||||
}
|
||||
}
|
||||
return local;
|
||||
}
|
||||
|
||||
// fetching Object once from remote
|
||||
const resolver = new Resolver();
|
||||
const object = (await resolver.resolve(uri)) as any;
|
||||
resolver.setUser(me);
|
||||
const object = await resolver.resolve(uri);
|
||||
|
||||
// /@user If a URI other than the id is specified,
|
||||
// the URI is determined here
|
||||
|
@ -123,8 +136,8 @@ async function fetchAny(
|
|||
local = await mergePack(
|
||||
me,
|
||||
...(await Promise.all([
|
||||
dbResolver.getUserFromApId(object.id),
|
||||
dbResolver.getNoteFromApId(object.id),
|
||||
dbResolver.getUserFromApId(getApId(object)),
|
||||
dbResolver.getNoteFromApId(getApId(object)),
|
||||
])),
|
||||
);
|
||||
if (local != null) return local;
|
||||
|
@ -132,8 +145,12 @@ async function fetchAny(
|
|||
|
||||
return await mergePack(
|
||||
me,
|
||||
isActor(object) ? await createPerson(getApId(object)) : null,
|
||||
isPost(object) ? await createNote(getApId(object), undefined, true) : null,
|
||||
isActor(object)
|
||||
? await createPerson(getApId(object), resolver.reset())
|
||||
: null,
|
||||
isPost(object)
|
||||
? await createNote(getApId(object), resolver.reset(), true)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { Instances } from "@/models/index.js";
|
||||
import define from "../../define.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["meta"],
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
allowGet: true,
|
||||
cacheSec: 60,
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async (ps) => {
|
||||
const instances = await Instances.find({
|
||||
select: ["host"],
|
||||
where: {
|
||||
isSuspended: false,
|
||||
},
|
||||
});
|
||||
|
||||
return instances.map((instance) => instance.host);
|
||||
});
|
|
@ -529,7 +529,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
github: instance.enableGithubIntegration,
|
||||
discord: instance.enableDiscordIntegration,
|
||||
serviceWorker: instance.enableServiceWorker,
|
||||
postEditing: instance.experimentalFeatures?.postEditing || false,
|
||||
postEditing: true,
|
||||
postImports: instance.experimentalFeatures?.postImports || false,
|
||||
miauth: true,
|
||||
};
|
||||
|
|
|
@ -33,6 +33,7 @@ import { renderActivity } from "@/remote/activitypub/renderer/index.js";
|
|||
import renderNote from "@/remote/activitypub/renderer/note.js";
|
||||
import renderUpdate from "@/remote/activitypub/renderer/update.js";
|
||||
import { deliverToRelays } from "@/services/relay.js";
|
||||
// import { deliverQuestionUpdate } from "@/services/note/polls/update.js";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
|
||||
export const meta = {
|
||||
|
@ -139,12 +140,6 @@ export const meta = {
|
|||
code: "NOT_LOCAL_USER",
|
||||
id: "b907f407-2aa0-4283-800b-a2c56290b822",
|
||||
},
|
||||
|
||||
editsDisabled: {
|
||||
message: "Post edits are disabled.",
|
||||
code: "EDITS_DISABLED",
|
||||
id: "99306f00-fb81-11ed-be56-0242ac120002",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
@ -243,11 +238,6 @@ export const paramDef = {
|
|||
export default define(meta, paramDef, async (ps, user) => {
|
||||
if (user.movedToUri != null) throw new ApiError(meta.errors.accountLocked);
|
||||
|
||||
const instanceMeta = await fetchMeta();
|
||||
if (instanceMeta.experimentalFeatures?.postEdits === false) {
|
||||
throw new ApiError(meta.errors.editsDisabled);
|
||||
}
|
||||
|
||||
if (!Users.isLocalUser(user)) {
|
||||
throw new ApiError(meta.errors.notLocalUser);
|
||||
}
|
||||
|
@ -476,14 +466,20 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
if (poll.noteVisibility !== ps.visibility) {
|
||||
pollUpdate.noteVisibility = ps.visibility;
|
||||
}
|
||||
// We can't do an unordered equal check because the order of choices
|
||||
// is important and if it changes, we need to reset the votes.
|
||||
if (JSON.stringify(poll.choices) !== JSON.stringify(pp.choices)) {
|
||||
pollUpdate.choices = pp.choices;
|
||||
pollUpdate.votes = new Array(pp.choices.length).fill(0);
|
||||
// Keep votes for unmodified choices, reset votes if choice is modified or new
|
||||
const oldVoteCounts = new Map<string, number>();
|
||||
for (let i = 0; i < poll.choices.length; i++) {
|
||||
oldVoteCounts.set(poll.choices[i], poll.votes[i]);
|
||||
}
|
||||
const newVotes = pp.choices.map(
|
||||
(choice) => oldVoteCounts.get(choice) || 0,
|
||||
);
|
||||
pollUpdate.choices = pp.choices;
|
||||
pollUpdate.votes = newVotes;
|
||||
if (notEmpty(pollUpdate)) {
|
||||
await Polls.update(note.id, pollUpdate);
|
||||
// Seemingly already handled by by the rendered update activity
|
||||
// await deliverQuestionUpdate(note.id);
|
||||
}
|
||||
publishing = true;
|
||||
}
|
||||
|
@ -520,9 +516,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
if (ps.text !== note.text) {
|
||||
update.text = ps.text;
|
||||
}
|
||||
if (ps.cw !== note.cw) {
|
||||
if (ps.cw !== note.cw || (ps.cw && !note.cw)) {
|
||||
update.cw = ps.cw;
|
||||
}
|
||||
if (!ps.cw && note.cw) {
|
||||
update.cw = null;
|
||||
}
|
||||
if (ps.visibility !== note.visibility) {
|
||||
update.visibility = ps.visibility;
|
||||
}
|
||||
|
@ -593,7 +592,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
}
|
||||
|
||||
if (publishing) {
|
||||
index(note);
|
||||
index(note, true);
|
||||
|
||||
// Publish update event for the updated note details
|
||||
publishNoteStream(note.id, "updated", {
|
||||
|
|
|
@ -4,7 +4,6 @@ import { createNotification } from "@/services/create-notification.js";
|
|||
import { deliver } from "@/queue/index.js";
|
||||
import { renderActivity } from "@/remote/activitypub/renderer/index.js";
|
||||
import renderVote from "@/remote/activitypub/renderer/vote.js";
|
||||
import { deliverQuestionUpdate } from "@/services/note/polls/update.js";
|
||||
import {
|
||||
PollVotes,
|
||||
NoteWatchings,
|
||||
|
@ -178,7 +177,4 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
pollOwner.inbox,
|
||||
);
|
||||
}
|
||||
|
||||
// リモートフォロワーにUpdate配信
|
||||
deliverQuestionUpdate(note.id);
|
||||
});
|
||||
|
|
|
@ -11,19 +11,32 @@ export const meta = {
|
|||
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
properties: {
|
||||
forceUpdate: { type: "boolean", default: false },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async () => {
|
||||
export default define(meta, paramDef, async (ps) => {
|
||||
let patrons;
|
||||
const cachedPatrons = await redisClient.get("patrons");
|
||||
if (cachedPatrons) {
|
||||
if (!ps.forceUpdate && cachedPatrons) {
|
||||
patrons = JSON.parse(cachedPatrons);
|
||||
} else {
|
||||
AbortSignal.timeout ??= function timeout(ms) {
|
||||
const ctrl = new AbortController();
|
||||
setTimeout(() => ctrl.abort(), ms);
|
||||
return ctrl.signal;
|
||||
};
|
||||
|
||||
patrons = await fetch(
|
||||
"https://codeberg.org/calckey/calckey/raw/branch/develop/patrons.json",
|
||||
).then((response) => response.json());
|
||||
{ signal: AbortSignal.timeout(2000) },
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.catch(() => {
|
||||
patrons = cachedPatrons ? JSON.parse(cachedPatrons) : [];
|
||||
});
|
||||
await redisClient.set("patrons", JSON.stringify(patrons), "EX", 3600);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import * as os from "node:os";
|
||||
import si from "systeminformation";
|
||||
import define from "../define.js";
|
||||
import meilisearch from "../../../db/meilisearch.js";
|
||||
import meilisearch from "@/db/meilisearch.js";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
|
||||
export const meta = {
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
allowGet: true,
|
||||
cacheSec: 30,
|
||||
tags: ["meta"],
|
||||
} as const;
|
||||
|
||||
|
@ -19,8 +21,33 @@ export const paramDef = {
|
|||
export default define(meta, paramDef, async () => {
|
||||
const memStats = await si.mem();
|
||||
const fsStats = await si.fsSize();
|
||||
const meilisearchStats = await meilisearchStatus();
|
||||
|
||||
let fsIndex = 0;
|
||||
// Get the first index of fs sizes that are actualy used.
|
||||
for (const [i, stat] of fsStats.entries()) {
|
||||
if (stat.rw === true && stat.used > 0) {
|
||||
fsIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const instanceMeta = await fetchMeta();
|
||||
if (!instanceMeta.enableServerMachineStats) {
|
||||
return {
|
||||
machine: "Not specified",
|
||||
cpu: {
|
||||
model: "Not specified",
|
||||
cores: 0,
|
||||
},
|
||||
mem: {
|
||||
total: 0,
|
||||
},
|
||||
fs: {
|
||||
total: 0,
|
||||
used: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
machine: os.hostname(),
|
||||
cpu: {
|
||||
|
@ -31,8 +58,8 @@ export default define(meta, paramDef, async () => {
|
|||
total: memStats.total,
|
||||
},
|
||||
fs: {
|
||||
total: fsStats[0].size,
|
||||
used: fsStats[0].used,
|
||||
total: fsStats[fsIndex].size,
|
||||
used: fsStats[fsIndex].used,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
@ -32,6 +32,12 @@ export const paramDef = {
|
|||
username: { type: "string", nullable: true },
|
||||
host: { type: "string", nullable: true },
|
||||
limit: { type: "integer", minimum: 1, maximum: 100, default: 10 },
|
||||
maxDaysSinceLastActive: {
|
||||
type: "integer",
|
||||
minimum: 1,
|
||||
maximum: 1000,
|
||||
default: 30,
|
||||
},
|
||||
detail: { type: "boolean", default: true },
|
||||
},
|
||||
anyOf: [{ required: ["username"] }, { required: ["host"] }],
|
||||
|
@ -40,7 +46,9 @@ export const paramDef = {
|
|||
// TODO: avatar,bannerをJOINしたいけどエラーになる
|
||||
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
const activeThreshold = new Date(Date.now() - 1000 * 60 * 60 * 24 * 30); // 30日
|
||||
const activeThreshold = ps.maxDaysSinceLastActive
|
||||
? new Date(Date.now() - 1000 * 60 * 60 * 24 * ps.maxDaysSinceLastActive)
|
||||
: null;
|
||||
|
||||
if (ps.host) {
|
||||
const q = Users.createQueryBuilder("user")
|
||||
|
@ -75,8 +83,10 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
.andWhere("user.isSuspended = FALSE")
|
||||
.andWhere("user.usernameLower LIKE :username", {
|
||||
username: `${sqlLikeEscape(ps.username.toLowerCase())}%`,
|
||||
})
|
||||
.andWhere(
|
||||
});
|
||||
|
||||
if (activeThreshold) {
|
||||
query.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where("user.updatedAt IS NULL").orWhere(
|
||||
"user.updatedAt > :activeThreshold",
|
||||
|
@ -84,6 +94,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
query.setParameters(followingQuery.getParameters());
|
||||
|
||||
|
|
|
@ -87,7 +87,7 @@ mastoFileRouter.post("/v1/media", upload.single("file"), async (ctx) => {
|
|||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
let multipartData = await ctx.file;
|
||||
const multipartData = await ctx.file;
|
||||
if (!multipartData) {
|
||||
ctx.body = { error: "No image" };
|
||||
ctx.status = 401;
|
||||
|
@ -106,7 +106,7 @@ mastoFileRouter.post("/v2/media", upload.single("file"), async (ctx) => {
|
|||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
let multipartData = await ctx.file;
|
||||
const multipartData = await ctx.file;
|
||||
if (!multipartData) {
|
||||
ctx.body = { error: "No image" };
|
||||
ctx.status = 401;
|
||||
|
@ -185,17 +185,6 @@ router.use(discord.routes());
|
|||
router.use(github.routes());
|
||||
router.use(twitter.routes());
|
||||
|
||||
router.get("/v1/instance/peers", async (ctx) => {
|
||||
const instances = await Instances.find({
|
||||
select: ["host"],
|
||||
where: {
|
||||
isSuspended: false,
|
||||
},
|
||||
});
|
||||
|
||||
ctx.body = instances.map((instance) => instance.host);
|
||||
});
|
||||
|
||||
router.post("/miauth/:session/check", async (ctx) => {
|
||||
const token = await AccessTokens.findOneBy({
|
||||
session: ctx.params.session,
|
||||
|
|
|
@ -3,6 +3,7 @@ import { CacheableLocalUser, User } from "@/models/entities/user.js";
|
|||
import Logger from "@/services/logger.js";
|
||||
import { redisClient } from "../../db/redis.js";
|
||||
import type { IEndpointMeta } from "./endpoints.js";
|
||||
import { convertMilliseconds } from "@/misc/convert-milliseconds.js";
|
||||
|
||||
const logger = new Logger("limiter");
|
||||
|
||||
|
@ -76,7 +77,10 @@ export const limiter = (
|
|||
);
|
||||
|
||||
if (info.remaining === 0) {
|
||||
reject("RATE_LIMIT_EXCEEDED");
|
||||
reject({
|
||||
message: "RATE_LIMIT_EXCEEDED",
|
||||
remainingTime: convertMilliseconds(info.resetMs - Date.now()),
|
||||
});
|
||||
} else {
|
||||
ok();
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ export async function getInstance(response: Entity.Instance) {
|
|||
uri: response.uri,
|
||||
title: response.title || "Calckey",
|
||||
short_description:
|
||||
response.description.substring(0, 50) || "See real server website",
|
||||
response.description?.substring(0, 50) || "See real server website",
|
||||
description:
|
||||
response.description ||
|
||||
"This is a vanilla Calckey Instance. It doesnt seem to have a description. BTW you are using the Mastodon api to access this server :)",
|
||||
|
|
|
@ -3,7 +3,7 @@ export const errors = {
|
|||
INVALID_PARAM: {
|
||||
value: {
|
||||
error: {
|
||||
message: "Invalid param.",
|
||||
message: "Invalid parameter.",
|
||||
code: "INVALID_PARAM",
|
||||
id: "3d81ceae-475f-4600-b2a8-2bc116157532",
|
||||
},
|
||||
|
@ -25,8 +25,7 @@ export const errors = {
|
|||
AUTHENTICATION_FAILED: {
|
||||
value: {
|
||||
error: {
|
||||
message:
|
||||
"Authentication failed. Please ensure your token is correct.",
|
||||
message: "Authentication failed.",
|
||||
code: "AUTHENTICATION_FAILED",
|
||||
id: "b0a7f5f8-dc2f-4171-b91f-de88ad238e14",
|
||||
},
|
||||
|
@ -38,7 +37,7 @@ export const errors = {
|
|||
value: {
|
||||
error: {
|
||||
message:
|
||||
"You sent a request to Calc, Calckey's resident stoner furry, instead of the server.",
|
||||
"You sent a request to Calc instead of the server. How did this happen?",
|
||||
code: "I_AM_CALC",
|
||||
id: "60c46cd1-f23a-46b1-bebe-5d2b73951a84",
|
||||
},
|
||||
|
|
|
@ -182,7 +182,7 @@ export function genOpenapiSpec() {
|
|||
...(endpoint.meta.limit
|
||||
? {
|
||||
"429": {
|
||||
description: "To many requests",
|
||||
description: "Too many requests",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
|
|
|
@ -16,6 +16,7 @@ import { IsNull } from "typeorm";
|
|||
import config from "@/config/index.js";
|
||||
import Logger from "@/services/logger.js";
|
||||
import { UserProfiles, Users } from "@/models/index.js";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
import { genIdenticon } from "@/misc/gen-identicon.js";
|
||||
import { createTemp } from "@/misc/create-temp.js";
|
||||
import { publishMainStream } from "@/services/stream.js";
|
||||
|
@ -125,10 +126,15 @@ router.get("/avatar/@:acct", async (ctx) => {
|
|||
});
|
||||
|
||||
router.get("/identicon/:x", async (ctx) => {
|
||||
const [temp, cleanup] = await createTemp();
|
||||
await genIdenticon(ctx.params.x, fs.createWriteStream(temp));
|
||||
ctx.set("Content-Type", "image/png");
|
||||
ctx.body = fs.createReadStream(temp).on("close", () => cleanup());
|
||||
const meta = await fetchMeta();
|
||||
if (meta.enableIdenticonGeneration) {
|
||||
const [temp, cleanup] = await createTemp();
|
||||
await genIdenticon(ctx.params.x, fs.createWriteStream(temp));
|
||||
ctx.set("Content-Type", "image/png");
|
||||
ctx.body = fs.createReadStream(temp).on("close", () => cleanup());
|
||||
} else {
|
||||
ctx.redirect("/static-assets/avatar.png");
|
||||
}
|
||||
});
|
||||
|
||||
mastoRouter.get("/oauth/authorize", async (ctx) => {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue