Merge branch 'develop' of https://codeberg.org/calckey/calckey into logged-out
This commit is contained in:
commit
926d9087ec
301 changed files with 26358 additions and 22520 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"
|
||||
|
@ -349,6 +349,7 @@ _2fa:
|
|||
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"
|
||||
|
@ -385,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
|
||||
|
@ -1599,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.
|
||||
|
@ -1668,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
|
||||
|
@ -1786,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.
|
||||
|
@ -2031,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
|
||||
|
@ -2060,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ó
|
||||
|
@ -2079,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
|
||||
|
@ -2130,3 +2131,16 @@ _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
|
||||
enableIdenticonGeneration: Habilitar la generació d'Identicon
|
||||
enableServerMachineStats: Habilitar les estadístiques del maquinari del servidor
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -54,7 +54,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"
|
||||
|
@ -834,7 +834,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?"
|
||||
|
@ -945,6 +945,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"
|
||||
|
@ -1056,6 +1057,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)"
|
||||
|
@ -1113,6 +1115,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
|
||||
|
@ -1214,6 +1219,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"
|
||||
|
@ -1514,6 +1520,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"
|
||||
|
@ -1551,11 +1558,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"
|
||||
|
@ -1630,11 +1638,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"
|
||||
|
@ -2068,14 +2076,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}'
|
||||
|
|
|
@ -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ę
|
||||
|
|
|
@ -30,7 +30,7 @@ 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
|
||||
edited: 'Editado às {date} {time}'
|
||||
sendMessage: Enviar uma mensagem
|
||||
older: antigo
|
||||
createList: Criar lista
|
||||
|
|
|
@ -1987,5 +1987,5 @@ apps: Приложения
|
|||
silenceThisInstance: Заглушить инстанс
|
||||
silencedInstances: Заглушенные инстансы
|
||||
editNote: Редактировать заметку
|
||||
edited: Редактировано
|
||||
edited: 'Редактировано в {date} {time}'
|
||||
deleted: Удалённое
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -35,8 +35,11 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
|||
info["@context"] = undefined;
|
||||
logger.debug(JSON.stringify(info, null, 2));
|
||||
|
||||
if (!signature?.keyId) return `Invalid signature: ${signature}`;
|
||||
|
||||
if (!signature?.keyId) {
|
||||
const err = `Invalid signature: ${signature}`;
|
||||
job.moveToFailed({message: err});
|
||||
return err;
|
||||
}
|
||||
//#endregion
|
||||
const host = toPuny(new URL(signature.keyId).hostname);
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -83,7 +83,7 @@ const nodeinfo2 = async () => {
|
|||
disableGlobalTimeline: meta.disableGlobalTimeline,
|
||||
emailRequiredForSignup: meta.emailRequiredForSignup,
|
||||
searchFilters: config.meilisearch ? true : false,
|
||||
postEditing: meta.experimentalFeatures?.postEditing || false,
|
||||
postEditing: true,
|
||||
postImports: meta.experimentalFeatures?.postImports || false,
|
||||
enableHcaptcha: meta.enableHcaptcha,
|
||||
enableRecaptcha: meta.enableRecaptcha,
|
||||
|
@ -100,7 +100,10 @@ const nodeinfo2 = async () => {
|
|||
};
|
||||
};
|
||||
|
||||
const cache = new Cache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
|
||||
const cache = new Cache<Awaited<ReturnType<typeof nodeinfo2>>>(
|
||||
"nodeinfo",
|
||||
60 * 10,
|
||||
);
|
||||
|
||||
router.get(nodeinfo2_1path, async (ctx) => {
|
||||
const base = await cache.fetch(null, () => nodeinfo2());
|
||||
|
|
|
@ -4,22 +4,39 @@ import config from "@/config/index.js";
|
|||
import type { User } from "@/models/entities/user.js";
|
||||
import { Notes, DriveFiles, UserProfiles, Users } from "@/models/index.js";
|
||||
|
||||
export default async function (user: User) {
|
||||
export default async function (
|
||||
user: User,
|
||||
threadDepth = 5,
|
||||
history = 20,
|
||||
noteintitle = false,
|
||||
renotes = true,
|
||||
replies = true,
|
||||
) {
|
||||
const author = {
|
||||
link: `${config.url}/@${user.username}`,
|
||||
email: `${user.username}@${config.host}`,
|
||||
name: user.name || user.username,
|
||||
};
|
||||
|
||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||
|
||||
const searchCriteria = {
|
||||
userId: user.id,
|
||||
visibility: In(["public", "home"]),
|
||||
};
|
||||
|
||||
if (!renotes) {
|
||||
searchCriteria.renoteId = IsNull();
|
||||
}
|
||||
|
||||
if (!replies) {
|
||||
searchCriteria.replyId = IsNull();
|
||||
}
|
||||
|
||||
const notes = await Notes.find({
|
||||
where: {
|
||||
userId: user.id,
|
||||
renoteId: IsNull(),
|
||||
visibility: In(["public", "home"]),
|
||||
},
|
||||
where: searchCriteria,
|
||||
order: { createdAt: -1 },
|
||||
take: 20,
|
||||
take: history,
|
||||
});
|
||||
|
||||
const feed = new Feed({
|
||||
|
@ -43,22 +60,105 @@ export default async function (user: User) {
|
|||
});
|
||||
|
||||
for (const note of notes) {
|
||||
let contentStr = await noteToString(note, true);
|
||||
let next = note.renoteId ? note.renoteId : note.replyId;
|
||||
let depth = threadDepth;
|
||||
while (depth > 0 && next) {
|
||||
const finding = await findById(next);
|
||||
contentStr += finding.text;
|
||||
next = finding.next;
|
||||
depth -= 1;
|
||||
}
|
||||
|
||||
let title = `${author.name} `;
|
||||
if (note.renoteId) {
|
||||
title += "renotes";
|
||||
} else if (note.replyId) {
|
||||
title += "replies";
|
||||
} else {
|
||||
title += "says";
|
||||
}
|
||||
if (noteintitle) {
|
||||
const content = note.cw ?? note.text;
|
||||
if (content) {
|
||||
title += `: ${content}`;
|
||||
} else {
|
||||
title += "something";
|
||||
}
|
||||
}
|
||||
|
||||
feed.addItem({
|
||||
title: title
|
||||
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
|
||||
.substring(0, 100),
|
||||
link: `${config.url}/notes/${note.id}`,
|
||||
date: note.createdAt,
|
||||
description: note.cw
|
||||
? note.cw.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
|
||||
: undefined,
|
||||
content: contentStr.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ""),
|
||||
});
|
||||
}
|
||||
|
||||
async function noteToString(note, isTheNote = false) {
|
||||
const author = isTheNote
|
||||
? null
|
||||
: await Users.findOneBy({ id: note.userId });
|
||||
let outstr = author
|
||||
? `${author.name}(@${author.username}@${
|
||||
author.host ? author.host : config.host
|
||||
}) ${
|
||||
note.renoteId ? "renotes" : note.replyId ? "replies" : "says"
|
||||
}: <br>`
|
||||
: "";
|
||||
const files =
|
||||
note.fileIds.length > 0
|
||||
? await DriveFiles.findBy({
|
||||
id: In(note.fileIds),
|
||||
})
|
||||
: [];
|
||||
const file = files.find((file) => file.type.startsWith("image/"));
|
||||
let fileEle = "";
|
||||
for (const file of files) {
|
||||
if (file.type.startsWith("image/")) {
|
||||
fileEle += ` <br><img src="${DriveFiles.getPublicUrl(file)}">`;
|
||||
} else if (file.type.startsWith("audio/")) {
|
||||
fileEle += ` <br><audio controls src="${DriveFiles.getPublicUrl(
|
||||
file,
|
||||
)}" type="${file.type}">`;
|
||||
} else if (file.type.startsWith("video/")) {
|
||||
fileEle += ` <br><video controls src="${DriveFiles.getPublicUrl(
|
||||
file,
|
||||
)}" type="${file.type}">`;
|
||||
} else {
|
||||
fileEle += ` <br><a href="${DriveFiles.getPublicUrl(file)}" download="${
|
||||
file.name
|
||||
}">${file.name}</a>`;
|
||||
}
|
||||
}
|
||||
outstr += `${note.cw ? note.cw + "<br>" : ""}${note.text || ""}${fileEle}`;
|
||||
if (isTheNote) {
|
||||
outstr += ` <span class="${
|
||||
note.renoteId ? "renote_note" : note.replyId ? "reply_note" : "new_note"
|
||||
} ${
|
||||
fileEle.indexOf("img src") !== -1 ? "with_img" : "without_img"
|
||||
}"></span>`;
|
||||
}
|
||||
return outstr;
|
||||
}
|
||||
|
||||
feed.addItem({
|
||||
title: `New note by ${author.name}`,
|
||||
link: `${config.url}/notes/${note.id}`,
|
||||
date: note.createdAt,
|
||||
description: note.cw || undefined,
|
||||
content: note.text || undefined,
|
||||
image: file ? DriveFiles.getPublicUrl(file) || undefined : undefined,
|
||||
async function findById(id) {
|
||||
let text = "";
|
||||
let next = null;
|
||||
const findings = await Notes.findOneBy({
|
||||
id: id,
|
||||
visibility: In(["public", "home"]),
|
||||
});
|
||||
if (findings) {
|
||||
text += `<hr>`;
|
||||
text += await noteToString(findings);
|
||||
next = findings.renoteId ? findings.renoteId : findings.replyId;
|
||||
}
|
||||
return { text, next };
|
||||
}
|
||||
|
||||
return feed;
|
||||
|
|
|
@ -247,7 +247,14 @@ router.get("/api.json", async (ctx) => {
|
|||
ctx.body = genOpenapiSpec();
|
||||
});
|
||||
|
||||
const getFeed = async (acct: string) => {
|
||||
const getFeed = async (
|
||||
acct: string,
|
||||
threadDepth: string,
|
||||
historyCount: string,
|
||||
noteInTitle: string,
|
||||
noRenotes: string,
|
||||
noReplies: string,
|
||||
) => {
|
||||
const meta = await fetchMeta();
|
||||
if (meta.privateMode) {
|
||||
return;
|
||||
|
@ -257,14 +264,36 @@ const getFeed = async (acct: string) => {
|
|||
usernameLower: username.toLowerCase(),
|
||||
host: host ?? IsNull(),
|
||||
isSuspended: false,
|
||||
isLocked: false,
|
||||
});
|
||||
|
||||
return user && (await packFeed(user));
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
let thread = parseInt(threadDepth, 10);
|
||||
if (isNaN(thread) || thread < 0 || thread > 30) {
|
||||
thread = 3;
|
||||
}
|
||||
let history = parseInt(historyCount, 10);
|
||||
//cant be 0 here or it will get all posts
|
||||
if (isNaN(history) || history <= 0 || history > 30) {
|
||||
history = 20;
|
||||
}
|
||||
return (
|
||||
user &&
|
||||
(await packFeed(
|
||||
user,
|
||||
thread,
|
||||
history,
|
||||
!isNaN(noteInTitle),
|
||||
isNaN(noRenotes),
|
||||
isNaN(noReplies),
|
||||
))
|
||||
);
|
||||
};
|
||||
|
||||
// As the /@user[.json|.rss|.atom]/sub endpoint is complicated, we will use a regex to switch between them.
|
||||
const reUser = new RegExp(
|
||||
"^/@(?<user>[^/]+?)(?:.(?<feed>json|rss|atom))?(?:/(?<sub>[^/]+))?$",
|
||||
"^/@(?<user>[^/]+?)(?:.(?<feed>json|rss|atom)(?:\\?[^/]*)?)?(?:/(?<sub>[^/]+))?$",
|
||||
);
|
||||
router.get(reUser, async (ctx, next) => {
|
||||
const groups = reUser.exec(ctx.originalUrl)?.groups;
|
||||
|
@ -275,7 +304,7 @@ router.get(reUser, async (ctx, next) => {
|
|||
|
||||
ctx.params = groups;
|
||||
|
||||
console.log(ctx, ctx.params);
|
||||
//console.log(ctx, ctx.params, ctx.query);
|
||||
if (groups.feed) {
|
||||
if (groups.sub) {
|
||||
await next();
|
||||
|
@ -301,7 +330,14 @@ router.get(reUser, async (ctx, next) => {
|
|||
|
||||
// Atom
|
||||
const atomFeed: Router.Middleware = async (ctx) => {
|
||||
const feed = await getFeed(ctx.params.user);
|
||||
const feed = await getFeed(
|
||||
ctx.params.user,
|
||||
ctx.query.thread,
|
||||
ctx.query.history,
|
||||
ctx.query.noteintitle,
|
||||
ctx.query.norenotes,
|
||||
ctx.query.noreplies,
|
||||
);
|
||||
|
||||
if (feed) {
|
||||
ctx.set("Content-Type", "application/atom+xml; charset=utf-8");
|
||||
|
@ -313,7 +349,14 @@ const atomFeed: Router.Middleware = async (ctx) => {
|
|||
|
||||
// RSS
|
||||
const rssFeed: Router.Middleware = async (ctx) => {
|
||||
const feed = await getFeed(ctx.params.user);
|
||||
const feed = await getFeed(
|
||||
ctx.params.user,
|
||||
ctx.query.thread,
|
||||
ctx.query.history,
|
||||
ctx.query.noteintitle,
|
||||
ctx.query.norenotes,
|
||||
ctx.query.noreplies,
|
||||
);
|
||||
|
||||
if (feed) {
|
||||
ctx.set("Content-Type", "application/rss+xml; charset=utf-8");
|
||||
|
@ -325,7 +368,14 @@ const rssFeed: Router.Middleware = async (ctx) => {
|
|||
|
||||
// JSON
|
||||
const jsonFeed: Router.Middleware = async (ctx) => {
|
||||
const feed = await getFeed(ctx.params.user);
|
||||
const feed = await getFeed(
|
||||
ctx.params.user,
|
||||
ctx.query.thread,
|
||||
ctx.query.history,
|
||||
ctx.query.noteintitle,
|
||||
ctx.query.norenotes,
|
||||
ctx.query.noreplies,
|
||||
);
|
||||
|
||||
if (feed) {
|
||||
ctx.set("Content-Type", "application/json; charset=utf-8");
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue