Merge branch 'develop' of https://codeberg.org/calckey/calckey into logged-out

This commit is contained in:
freeplay 2023-07-03 15:22:11 -04:00
commit 926d9087ec
301 changed files with 26358 additions and 22520 deletions

View file

@ -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.

View file

@ -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

View file

@ -1,4 +0,0 @@
{
"eslint.packageManager": "pnpm",
"workspace.workspaceFolderCheckCwd": false
}

View file

@ -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)

File diff suppressed because it is too large Load diff

View file

@ -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" ]

View file

@ -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

View file

@ -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 └─────────────────────────────────────

View file

@ -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",
},
});

View file

@ -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: 投稿フォームの公開範囲指定のテスト

View file

@ -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");
});

View file

@ -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
};

View file

@ -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");
});

View file

@ -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;
}
});

View file

@ -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
View 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).

View file

@ -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:

View file

@ -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

View file

@ -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: Laplicació dautenticació 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

View file

@ -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é

View file

@ -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.

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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ä.

View file

@ -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}'

View file

@ -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

View file

@ -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: 他のサーバーを探す

View file

@ -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

View file

@ -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ę

View file

@ -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

View file

@ -1987,5 +1987,5 @@ apps: Приложения
silenceThisInstance: Заглушить инстанс
silencedInstances: Заглушенные инстансы
editNote: Редактировать заметку
edited: Редактировано
edited: 'Редактировано в {date} {time}'
deleted: Удалённое

File diff suppressed because it is too large Load diff

View file

@ -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: 默認的表情符號反應

View file

@ -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
View 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

View file

@ -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
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -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

View file

@ -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:&quot;OTADESIGN Rounded&quot;;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:&quot;OTADESIGN Rounded&quot;;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

View file

@ -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"))`,

View 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"`,
);
}
}

View file

@ -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"

View file

@ -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),
]
}
}

View file

@ -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,
}

View file

@ -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"
}
}

View file

@ -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(

View file

@ -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",

View file

@ -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;

View file

@ -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();

View file

@ -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,

View file

@ -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,
});
}

View file

@ -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;
}

View file

@ -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: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている

View 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(", ");
}

View file

@ -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,
});

View file

@ -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";
}

View file

@ -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,
);
}

View file

@ -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();
}

View file

@ -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;
}

View file

@ -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,

View file

@ -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">

View file

@ -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,
});
}

View file

@ -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:

View file

@ -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(

View file

@ -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();
}

View file

@ -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);

View file

@ -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()));
}

View file

@ -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,
);

View file

@ -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}`);

View file

@ -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");

View file

@ -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

View file

@ -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;
}

View file

@ -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({

View file

@ -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)

View file

@ -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();

View file

@ -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 [

View file

@ -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) {

View file

@ -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]) => {

View file

@ -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(),

View file

@ -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(),

View file

@ -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,
};
});

View file

@ -42,6 +42,7 @@ export default define(meta, paramDef, async (ps, me) => {
isModerator: user.isModerator,
isSilenced: user.isSilenced,
isSuspended: user.isSuspended,
moderationNote: profile.moderationNote,
};
}

View file

@ -174,7 +174,6 @@ export const paramDef = {
type: "object",
nullable: true,
properties: {
postEditing: { type: "boolean" },
postImports: { type: "boolean" },
},
},

View file

@ -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,
);
}

View file

@ -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);
});

View file

@ -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,
};

View file

@ -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", {

View file

@ -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);
});

View file

@ -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);
}

View file

@ -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,
},
};
});

View file

@ -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());

View file

@ -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,

View file

@ -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();
}

View file

@ -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",
},

View file

@ -182,7 +182,7 @@ export function genOpenapiSpec() {
...(endpoint.meta.limit
? {
"429": {
description: "To many requests",
description: "Too many requests",
content: {
"application/json": {
schema: {

View file

@ -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) => {

View file

@ -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());

View file

@ -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;

View file

@ -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