Compare commits
No commits in common. "main" and "main" have entirely different histories.
252 changed files with 11279 additions and 1660 deletions
4
.config/docker_example.env
Normal file
4
.config/docker_example.env
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# db settings
|
||||||
|
POSTGRES_PASSWORD=example-foundkey-pass
|
||||||
|
POSTGRES_USER=example-foundkey-user
|
||||||
|
POSTGRES_DB=foundkey
|
|
@ -68,6 +68,17 @@ redis:
|
||||||
#prefix: example-prefix
|
#prefix: example-prefix
|
||||||
#db: 1
|
#db: 1
|
||||||
|
|
||||||
|
# ┌─────────────────────────────┐
|
||||||
|
#───┘ Elasticsearch configuration └─────────────────────────────
|
||||||
|
|
||||||
|
# Elasticsearch is optional.
|
||||||
|
#elasticsearch:
|
||||||
|
# host: localhost
|
||||||
|
# port: 9200
|
||||||
|
# ssl: false
|
||||||
|
# user:
|
||||||
|
# pass:
|
||||||
|
|
||||||
# ┌─────────────────────┐
|
# ┌─────────────────────┐
|
||||||
#───┘ Other configuration └─────────────────────────────────────
|
#───┘ Other configuration └─────────────────────────────────────
|
||||||
|
|
||||||
|
|
12
.dockerignore
Normal file
12
.dockerignore
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
.autogen
|
||||||
|
.config
|
||||||
|
.woodpecker
|
||||||
|
Dockerfile
|
||||||
|
build/
|
||||||
|
built/
|
||||||
|
db/
|
||||||
|
docker-compose.yml
|
||||||
|
elasticsearch/
|
||||||
|
node_modules/
|
||||||
|
redis/
|
||||||
|
files/
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -18,9 +18,14 @@
|
||||||
node_modules
|
node_modules
|
||||||
report.*.json
|
report.*.json
|
||||||
|
|
||||||
|
# Cypress
|
||||||
|
cypress/screenshots
|
||||||
|
cypress/videos
|
||||||
|
|
||||||
# config
|
# config
|
||||||
/.config/*
|
/.config/*
|
||||||
!/.config/example.yml
|
!/.config/example.yml
|
||||||
|
!/.config/docker_example.env
|
||||||
|
|
||||||
# misskey
|
# misskey
|
||||||
/build
|
/build
|
||||||
|
@ -28,6 +33,7 @@ built
|
||||||
/data
|
/data
|
||||||
/.cache-loader
|
/.cache-loader
|
||||||
/db
|
/db
|
||||||
|
/elasticsearch
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
*.pem
|
*.pem
|
||||||
run.bat
|
run.bat
|
||||||
|
|
13
.woodpecker/misskey/test.yml
Normal file
13
.woodpecker/misskey/test.yml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
url: 'http://misskey.local'
|
||||||
|
|
||||||
|
port: 80
|
||||||
|
|
||||||
|
db:
|
||||||
|
host: postgres
|
||||||
|
port: 5432
|
||||||
|
db: test-misskey
|
||||||
|
user: postgres
|
||||||
|
pass: ''
|
||||||
|
redis:
|
||||||
|
host: redis
|
||||||
|
port: 6379
|
|
@ -17,4 +17,32 @@ pipeline:
|
||||||
commands:
|
commands:
|
||||||
- yarn install
|
- yarn install
|
||||||
- git diff --exit-code yarn.lock
|
- git diff --exit-code yarn.lock
|
||||||
|
- cp .woodpecker/misskey/test.yml .config
|
||||||
- yarn build
|
- yarn build
|
||||||
|
mocha:
|
||||||
|
when:
|
||||||
|
branch: main
|
||||||
|
event: push
|
||||||
|
image: node:18.6.0
|
||||||
|
commands:
|
||||||
|
- yarn mocha
|
||||||
|
e2e:
|
||||||
|
when:
|
||||||
|
branch: main
|
||||||
|
event: push
|
||||||
|
image: cypress/included:10.3.0
|
||||||
|
commands:
|
||||||
|
- npm run start:test &
|
||||||
|
- sleep 30 # wait for server to start
|
||||||
|
- cypress run --browser chrome
|
||||||
|
# TODO: upload screenshots and video artifacts?
|
||||||
|
# would need some kind of storage though
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:13
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=test-misskey
|
||||||
|
- POSTGRES_HOST_AUTH_METHOD=trust
|
||||||
|
redis:
|
||||||
|
image: redis:6
|
||||||
|
|
|
@ -62,8 +62,6 @@ representative at an online or offline event.
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
reported to the community leaders responsible for enforcement via email at
|
reported to the community leaders responsible for enforcement via email at
|
||||||
johann<EFBFBD>qwertqwefsday.eu and/or toast<73>bunkerlabs.net .
|
johann<EFBFBD>qwertqwefsday.eu and/or toast<73>bunkerlabs.net .
|
||||||
(The at sign has been replaced so that spammers do not find these email addresses easily.
|
|
||||||
If you are a human you hopefully know what to do.)
|
|
||||||
All complaints will be reviewed and investigated promptly and fairly.
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and security of the
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
|
|
@ -11,9 +11,13 @@ Please understand that in such cases we might edit your issue to translate it, t
|
||||||
## Development platform
|
## Development platform
|
||||||
FoundKey generally assumes that it is running on a Unix-like platform (e.g. Linux or macOS). If you are using Windows for development, we highly suggest using the Windows Subsystem for Linux (WSL) as the development environment.
|
FoundKey generally assumes that it is running on a Unix-like platform (e.g. Linux or macOS). If you are using Windows for development, we highly suggest using the Windows Subsystem for Linux (WSL) as the development environment.
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
See [ROADMAP.md](./ROADMAP.md)
|
||||||
|
|
||||||
## Issues
|
## Issues
|
||||||
Issues are intended for feature requests and bug tracking.
|
Issues are intended for feature requests and bug tracking.
|
||||||
Please note that in general, we are not looking for completely new features to add, but quality of life improvements will be considered.
|
|
||||||
|
For technical support or if you are not sure if what you are experiencing is a bug you can talk to people on the [IRC server](https://irc.akkoma.dev) in the `#foundkey` channel first.
|
||||||
|
|
||||||
Please do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged.
|
Please do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged.
|
||||||
|
|
||||||
|
@ -21,6 +25,7 @@ Please do not close issues that are about to be resolved. It should remain open
|
||||||
branch|what it's for
|
branch|what it's for
|
||||||
---|---
|
---|---
|
||||||
main|development branch
|
main|development branch
|
||||||
|
translate|managed by weblate, see [section about translation](#Translation)
|
||||||
|
|
||||||
For a production environment you might not want to follow the `main` branch directly but instead check out one of the git tags.
|
For a production environment you might not want to follow the `main` branch directly but instead check out one of the git tags.
|
||||||
|
|
||||||
|
@ -149,6 +154,8 @@ Here is the step by step checklist:
|
||||||
|
|
||||||
<small>a.k.a. Localization (l10n) or Internationalization (i18n)</small>
|
<small>a.k.a. Localization (l10n) or Internationalization (i18n)</small>
|
||||||
|
|
||||||
|
To translate text used in Foundkey, we use weblate at <https://translate.akkoma.dev/projects/foundkey/>.
|
||||||
|
|
||||||
Localization files are found in `/locales/` and are YAML files using the `yml` file extension.
|
Localization files are found in `/locales/` and are YAML files using the `yml` file extension.
|
||||||
The file name consists of the [IETF BCP 47](https://www.rfc-editor.org/info/bcp47) language code.
|
The file name consists of the [IETF BCP 47](https://www.rfc-editor.org/info/bcp47) language code.
|
||||||
|
|
||||||
|
@ -157,6 +164,39 @@ During development, it is useful to use the `npm run dev` command.
|
||||||
This command monitors the server-side and client-side source files and automatically builds them if they are modified.
|
This command monitors the server-side and client-side source files and automatically builds them if they are modified.
|
||||||
In addition, it will also automatically start the Misskey server process.
|
In addition, it will also automatically start the Misskey server process.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
- Test codes are located in [`/test`](/test).
|
||||||
|
|
||||||
|
### Run test
|
||||||
|
Create a config file.
|
||||||
|
```
|
||||||
|
cp test/test.yml .config/
|
||||||
|
```
|
||||||
|
Prepare DB/Redis for testing.
|
||||||
|
```
|
||||||
|
docker-compose -f test/docker-compose.yml up
|
||||||
|
```
|
||||||
|
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
|
||||||
|
|
||||||
|
Run all test.
|
||||||
|
```
|
||||||
|
npm run test
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Run specify test
|
||||||
|
```
|
||||||
|
npx cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT="./test/tsconfig.json" npx mocha test/foo.ts --require ts-node/register
|
||||||
|
```
|
||||||
|
|
||||||
|
### e2e tests
|
||||||
|
TODO
|
||||||
|
|
||||||
|
## Continuous integration (CI)
|
||||||
|
|
||||||
|
Foundkey uses Woodpecker for executing automated tests and lints.
|
||||||
|
CI runs can be found at [ci.akkoma.dev](https://ci.akkoma.dev/FoundKeyGang/FoundKey)
|
||||||
|
Configuration files are located in `/.woodpecker/`.
|
||||||
|
|
||||||
## Vue
|
## Vue
|
||||||
Misskey uses Vue(v3) as its front-end framework.
|
Misskey uses Vue(v3) as its front-end framework.
|
||||||
- Use TypeScript functionality.
|
- Use TypeScript functionality.
|
||||||
|
|
36
Dockerfile
Normal file
36
Dockerfile
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
FROM node:18.12.1-alpine3.16 AS base
|
||||||
|
|
||||||
|
ARG NODE_ENV=production
|
||||||
|
|
||||||
|
WORKDIR /foundkey
|
||||||
|
|
||||||
|
ENV BUILD_DEPS autoconf automake file g++ gcc libc-dev libtool make nasm pkgconfig python3 zlib-dev git
|
||||||
|
|
||||||
|
FROM base AS builder
|
||||||
|
|
||||||
|
COPY . ./
|
||||||
|
|
||||||
|
RUN apk add --no-cache $BUILD_DEPS && \
|
||||||
|
git submodule update --init && \
|
||||||
|
yarn install && \
|
||||||
|
yarn build && \
|
||||||
|
rm -rf .git
|
||||||
|
|
||||||
|
FROM base AS runner
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
ffmpeg \
|
||||||
|
tini
|
||||||
|
|
||||||
|
ENTRYPOINT ["/sbin/tini", "--"]
|
||||||
|
|
||||||
|
COPY --from=builder /foundkey/node_modules ./node_modules
|
||||||
|
COPY --from=builder /foundkey/built ./built
|
||||||
|
COPY --from=builder /foundkey/packages/backend/node_modules ./packages/backend/node_modules
|
||||||
|
COPY --from=builder /foundkey/packages/backend/built ./packages/backend/built
|
||||||
|
COPY --from=builder /foundkey/packages/foundkey-js/built ./packages/foundkey-js/built
|
||||||
|
COPY . ./
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
CMD ["npm", "run", "migrateandstart"]
|
||||||
|
|
|
@ -7,10 +7,6 @@ Look further up in the section to find the "base path" it is relative to.
|
||||||
|
|
||||||
All the backend code is in `/packages/backend/src`.
|
All the backend code is in `/packages/backend/src`.
|
||||||
|
|
||||||
The backend is started via `index.ts` which in turn starts `boot/index.ts`.
|
|
||||||
In the "boot" code is where the process is forked from the main process into additional and separate worker and frontend processes.
|
|
||||||
If you look into your operating system's process overview or similar, you might be able to see that the processes rename themselves accordingly.
|
|
||||||
|
|
||||||
### Database
|
### Database
|
||||||
|
|
||||||
For connecting to the database an ORM (object–relational mapping) is used.
|
For connecting to the database an ORM (object–relational mapping) is used.
|
||||||
|
|
|
@ -1,17 +1,14 @@
|
||||||
<div align="center"><img src="./logo.svg" height="200" alt="Foundkey logo, an owl holding a key"/></div>
|
<div align="center"><img src="./logo.svg" height="200" alt="Foundkey logo, an owl holding a key"/></div>
|
||||||
|
|
||||||
# FoundKey
|
# FoundKey
|
||||||
FoundKey is a free and open source microblogging server compatible with ActivityPub.
|
FoundKey is a free and open source microblogging server compatible with ActivityPub. Forked from Misskey, FoundKey improves on maintainability and behaviour, while also bringing in useful features.
|
||||||
It is currently under **LIMITED MAINTENANCE** and is not well suited for large instances.
|
|
||||||
No more than 20 users per instance are recommended.
|
|
||||||
|
|
||||||
Forked from Misskey, FoundKey improves on maintainability and behaviour, while also bringing in useful features.
|
|
||||||
|
|
||||||
See the [changelog](./CHANGELOG.md) and [roadmap](./ROADMAP.md) for more on what's changed and future plans.
|
See the [changelog](./CHANGELOG.md) and [roadmap](./ROADMAP.md) for more on what's changed and future plans.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
FoundKey's documentation is a work in progress, which can be found in the `docs/` folder.
|
FoundKey's documentation is a work in progress, which can be found in the `docs/` folder.
|
||||||
Feel free to contribute some documentation.
|
|
||||||
|
In the meantime, much of the documentation on the [Misskey Hub](https://misskey-hub.net/) will also apply to FoundKey.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
If you're interested in helping out with the project, please read the [contributing guide](./CONTRIBUTING.md).
|
If you're interested in helping out with the project, please read the [contributing guide](./CONTRIBUTING.md).
|
||||||
|
|
12
cypress.config.ts
Normal file
12
cypress.config.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
})
|
149
cypress/e2e/basic.cy.js
Normal file
149
cypress/e2e/basic.cy.js
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
describe('Before setup instance', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.resetState();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
|
||||||
|
// waitを入れることでそれを防止できる
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('successfully loads', () => {
|
||||||
|
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.wait('@signup').should('have.property', 'response.statusCode');
|
||||||
|
cy.wait('@signup');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('After setup instance', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.resetState();
|
||||||
|
|
||||||
|
// インスタンス初期セットアップ
|
||||||
|
cy.registerUser('admin', 'pass', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
|
||||||
|
// waitを入れることでそれを防止できる
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('successfully loads', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('signup', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
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.wait('@signup');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('After user signup', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.resetState();
|
||||||
|
|
||||||
|
// インスタンス初期セットアップ
|
||||||
|
cy.registerUser('admin', 'pass', true);
|
||||||
|
|
||||||
|
// ユーザー作成
|
||||||
|
cy.registerUser('alice', 'alice1234');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
|
||||||
|
// waitを入れることでそれを防止できる
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('successfully loads', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('signin', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
cy.intercept('POST', '/api/signin').as('signin');
|
||||||
|
|
||||||
|
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.wait('@signin');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('suspend', function() {
|
||||||
|
cy.request('POST', '/api/admin/suspend-user', {
|
||||||
|
i: this.admin.token,
|
||||||
|
userId: this.alice.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
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}');
|
||||||
|
|
||||||
|
// TODO: cypressにブラウザの言語指定できる機能が実装され次第英語のみテストするようにする
|
||||||
|
cy.contains(/アカウントが凍結されています|This account has been suspended due to/gi);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('After user singed in', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.resetState();
|
||||||
|
|
||||||
|
// インスタンス初期セットアップ
|
||||||
|
cy.registerUser('admin', 'pass', true);
|
||||||
|
|
||||||
|
// ユーザー作成
|
||||||
|
cy.registerUser('alice', 'alice1234');
|
||||||
|
|
||||||
|
cy.login('alice', 'alice1234');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
|
||||||
|
// waitを入れることでそれを防止できる
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
cy.contains('Hello, Misskey!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: 投稿フォームの公開範囲指定のテスト
|
||||||
|
// TODO: 投稿フォームのファイル添付のテスト
|
||||||
|
// TODO: 投稿フォームのハッシュタグ保持フィールドのテスト
|
65
cypress/e2e/widgets.cy.js
Normal file
65
cypress/e2e/widgets.cy.js
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
describe('After user signed in', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.resetState();
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
|
||||||
|
// インスタンス初期セットアップ
|
||||||
|
cy.registerUser('admin', 'pass', true);
|
||||||
|
|
||||||
|
// ユーザー作成
|
||||||
|
cy.registerUser('alice', 'alice1234');
|
||||||
|
|
||||||
|
cy.login('alice', 'alice1234');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
|
||||||
|
// waitを入れることでそれを防止できる
|
||||||
|
cy.wait(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
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('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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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('aichan');
|
||||||
|
});
|
5
cypress/fixtures/example.json
Normal file
5
cypress/fixtures/example.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"name": "Using fixtures to represent data",
|
||||||
|
"email": "hello@cypress.io",
|
||||||
|
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||||
|
}
|
22
cypress/plugins/index.js
Normal file
22
cypress/plugins/index.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
// ***********************************************************
|
||||||
|
// This example plugins/index.js can be used to load plugins
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off loading
|
||||||
|
// the plugins file with the 'pluginsFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/plugins-guide
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// This function is called when a project is opened or re-opened (e.g. due to
|
||||||
|
// the project's config changing)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Cypress.PluginConfig}
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
module.exports = (on, config) => {
|
||||||
|
// `on` is used to hook into various events Cypress emits
|
||||||
|
// `config` is the resolved Cypress config
|
||||||
|
}
|
55
cypress/support/commands.js
Normal file
55
cypress/support/commands.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// ***********************************************
|
||||||
|
// This example commands.js shows you how to
|
||||||
|
// create various custom commands and overwrite
|
||||||
|
// existing commands.
|
||||||
|
//
|
||||||
|
// For more comprehensive examples of custom
|
||||||
|
// commands please read more here:
|
||||||
|
// https://on.cypress.io/custom-commands
|
||||||
|
// ***********************************************
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a parent command --
|
||||||
|
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a child command --
|
||||||
|
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a dual command --
|
||||||
|
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- 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');
|
||||||
|
});
|
||||||
|
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';
|
||||||
|
|
||||||
|
cy.request('POST', route, {
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
}).its('body').as(username);
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('login', (username, password) => {
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
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.wait('@signin').as('signedIn');
|
||||||
|
});
|
32
cypress/support/e2e.js
Normal file
32
cypress/support/e2e.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// ***********************************************************
|
||||||
|
// This example support/index.js is processed and
|
||||||
|
// loaded automatically before your test files.
|
||||||
|
//
|
||||||
|
// This is a great place to put global configuration and
|
||||||
|
// behavior that modifies Cypress.
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off
|
||||||
|
// automatically serving support files with the
|
||||||
|
// 'supportFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/configuration
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// Import commands.js using ES2015 syntax:
|
||||||
|
import './commands'
|
||||||
|
|
||||||
|
// Alternatively you can use CommonJS syntax:
|
||||||
|
// require('./commands')
|
||||||
|
|
||||||
|
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))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
52
docker-compose.yml
Normal file
52
docker-compose.yml
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
restart: always
|
||||||
|
links:
|
||||||
|
- db
|
||||||
|
- redis
|
||||||
|
# - es
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:3000:3000"
|
||||||
|
networks:
|
||||||
|
- internal_network
|
||||||
|
- external_network
|
||||||
|
volumes:
|
||||||
|
- ./files:/foundkey/files
|
||||||
|
- ./.config:/foundkey/.config:ro
|
||||||
|
|
||||||
|
redis:
|
||||||
|
restart: always
|
||||||
|
image: redis:7.0-alpine
|
||||||
|
networks:
|
||||||
|
- internal_network
|
||||||
|
volumes:
|
||||||
|
- ./redis:/data
|
||||||
|
|
||||||
|
db:
|
||||||
|
restart: always
|
||||||
|
image: postgres:14.5-alpine
|
||||||
|
networks:
|
||||||
|
- internal_network
|
||||||
|
env_file:
|
||||||
|
- .config/docker.env
|
||||||
|
volumes:
|
||||||
|
- ./db:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
# es:
|
||||||
|
# restart: always
|
||||||
|
# image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.4.2
|
||||||
|
# environment:
|
||||||
|
# - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
|
||||||
|
# - "TAKE_FILE_OWNERSHIP=111"
|
||||||
|
# networks:
|
||||||
|
# - internal_network
|
||||||
|
# volumes:
|
||||||
|
# - ./elasticsearch:/usr/share/elasticsearch/data
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal_network:
|
||||||
|
internal: true
|
||||||
|
external_network:
|
|
@ -74,9 +74,6 @@ The fields of `Meta` are currently not used or checked when importing emoji, exc
|
||||||
For each `Emoji`:
|
For each `Emoji`:
|
||||||
- `downloaded`: should always be true. If the field is missing or not truthy, the emoji will not be imported.
|
- `downloaded`: should always be true. If the field is missing or not truthy, the emoji will not be imported.
|
||||||
- `fileName`: name of the image file inside the packed file.
|
- `fileName`: name of the image file inside the packed file.
|
||||||
The filename has to match the following ECMAScript RegExp: `/^[a-zA-Z0-9_]+?([a-zA-Z0-9\.]+)?$/`
|
|
||||||
(i.e. composed of latin letters, digits, underscores or dots, not starting with a dot and not ending with an underscore)
|
|
||||||
If the file does not match this RegExp, the respective emoji will not be imported!
|
|
||||||
- `emoji`: data associated with the emoji as it was stored in the database. Currently most of these fields are
|
- `emoji`: data associated with the emoji as it was stored in the database. Currently most of these fields are
|
||||||
not even checked for existence. The following are currently used:
|
not even checked for existence. The following are currently used:
|
||||||
- `name`: name of the emoji for the user, e.g. `blobfox` if a user should type in `:blobfox:` to get the emoji.
|
- `name`: name of the emoji for the user, e.g. `blobfox` if a user should type in `:blobfox:` to get the emoji.
|
||||||
|
|
85
docs/install-docker.md
Normal file
85
docs/install-docker.md
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
# Create FoundKey instance with Docker Compose
|
||||||
|
|
||||||
|
This guide describes how to install and setup FoundKey with Docker Compose.
|
||||||
|
|
||||||
|
**WARNING:**
|
||||||
|
Never change the domain name (hostname) of an instance once you start using it!
|
||||||
|
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
- Docker or Podman
|
||||||
|
- Docker Compose plugin (or podman-compose)
|
||||||
|
|
||||||
|
If using Podman, replace `docker` with `podman`. Commands using `docker compose` should be replaced with `podman-compose`.
|
||||||
|
|
||||||
|
You may need to prefix `docker` commands with `sudo` unless your user is in the `docker` group or you are running Docker in rootless mode.
|
||||||
|
|
||||||
|
## Get the repository
|
||||||
|
```sh
|
||||||
|
git clone https://akkoma.dev/FoundKeyGang/FoundKey.git
|
||||||
|
cd FoundKey
|
||||||
|
```
|
||||||
|
|
||||||
|
To make it easier to perform your own changes on top, we suggest making a branch based on the latest tag.
|
||||||
|
In this example, we'll use `v13.0.0-preview1` as the tag to check out and `my-branch` as the branch name.
|
||||||
|
```sh
|
||||||
|
git checkout tags/v13.0.0-preview1 -b my-branch
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configure
|
||||||
|
|
||||||
|
Copy example configuration files with following:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp .config/docker_example.yml .config/default.yml
|
||||||
|
cp .config/docker_example.env .config/docker.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `default.yml` and `docker.env` according to the instructions in the files.
|
||||||
|
You will need to set the database host to `db` and Redis host to `redis` in order to use the internal container network for these services.
|
||||||
|
|
||||||
|
Edit `docker-compose.yml` if necessary. (e.g. if you want to change the port).
|
||||||
|
If you are using SELinux (eg. you're on Fedora or a RHEL derivative), you'll want to add the `Z` mount flag to the volume mounts to allow the containers to access the contents of those volumes.
|
||||||
|
|
||||||
|
Also check out the [Configure Foundkey](./install.md#configure-foundkey) section in the ordinary installation instructions.
|
||||||
|
|
||||||
|
## Build and initialize
|
||||||
|
The following command will build FoundKey and initialize the database.
|
||||||
|
This will take some time.
|
||||||
|
|
||||||
|
``` shell
|
||||||
|
docker compose build
|
||||||
|
docker compose run --rm web yarn run init
|
||||||
|
```
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
You can start FoundKey with the following command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
In case you are encountering issues, you can run `docker compose logs -f` to get the log output of the running containers.
|
||||||
|
|
||||||
|
## How to update your FoundKey server
|
||||||
|
When updating, be sure to check the [release notes](https://akkoma.dev/FoundKeyGang/FoundKey/src/branch/main/CHANGELOG.md) to know in advance the changes and whether or not additional work is required (in most cases, it is not).
|
||||||
|
|
||||||
|
To update your branch to the latest tag (in this example `v13.0.0-preview2`), you can do the following:
|
||||||
|
```sh
|
||||||
|
git fetch -t
|
||||||
|
|
||||||
|
# Use --squash if you want to merge all of the changes in the tag into a single commit.
|
||||||
|
# Useful if you have made additional changes.
|
||||||
|
git merge tags/v13.0.0-preview2
|
||||||
|
|
||||||
|
# Rebuild and restart the docker container.
|
||||||
|
docker compose build
|
||||||
|
docker compose down && docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
It may take some time depending on the contents of the update and the size of the database.
|
||||||
|
|
||||||
|
## How to execute CLI commands
|
||||||
|
```sh
|
||||||
|
docker compose run --rm web node packages/backend/built/tools/foo bar
|
||||||
|
```
|
|
@ -21,12 +21,10 @@ LINE_NUM="$(npx typeorm migration:show -d ormconfig.js | grep -n nsfwDetection16
|
||||||
NUM_MIGRATIONS="$(npx typeorm migration:show -d ormconfig.js | tail -n+"$LINE_NUM" | grep '\[X\]' | nl)"
|
NUM_MIGRATIONS="$(npx typeorm migration:show -d ormconfig.js | tail -n+"$LINE_NUM" | grep '\[X\]' | nl)"
|
||||||
|
|
||||||
for i in $(seq 1 $NUM_MIGRATIONS); do
|
for i in $(seq 1 $NUM_MIGRATIONS); do
|
||||||
npx typeorm migration:revert -d ormconfig.js || continue
|
npx typeorm migration:revert -d ormconfig.js
|
||||||
done
|
done
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note:** TypeORM might hang when reverting a migration. If it says that the migration was reverted successfully, you can force close TypeORM using Ctrl-C. The script will continue until all of the migrations have been reverted.
|
|
||||||
|
|
||||||
## Switching repositories
|
## Switching repositories
|
||||||
To switch to the FoundKey repository, do the following in your Misskey install location:
|
To switch to the FoundKey repository, do the following in your Misskey install location:
|
||||||
```sh
|
```sh
|
||||||
|
|
|
@ -220,6 +220,7 @@ uploadFromUrl: "ارفع عبر رابط"
|
||||||
uploadFromUrlDescription: "رابط الملف المراد رفعه"
|
uploadFromUrlDescription: "رابط الملف المراد رفعه"
|
||||||
uploadFromUrlRequested: "الرفع مطلوب"
|
uploadFromUrlRequested: "الرفع مطلوب"
|
||||||
uploadFromUrlMayTakeTime: "سيستغرق بعض الوقت لاتمام الرفع"
|
uploadFromUrlMayTakeTime: "سيستغرق بعض الوقت لاتمام الرفع"
|
||||||
|
explore: "استكشاف"
|
||||||
messageRead: "مقروءة"
|
messageRead: "مقروءة"
|
||||||
noMoreHistory: "لا يوجد المزيد من التاريخ"
|
noMoreHistory: "لا يوجد المزيد من التاريخ"
|
||||||
startMessaging: "ابدأ محادثة"
|
startMessaging: "ابدأ محادثة"
|
||||||
|
@ -299,6 +300,9 @@ inMb: "بالميغابايت"
|
||||||
iconUrl: "رابط الأيقونة"
|
iconUrl: "رابط الأيقونة"
|
||||||
bannerUrl: "رابط صورة اللافتة"
|
bannerUrl: "رابط صورة اللافتة"
|
||||||
backgroundImageUrl: "رابط صورة الخلفية"
|
backgroundImageUrl: "رابط صورة الخلفية"
|
||||||
|
pinnedUsers: "المستخدمون المدبسون"
|
||||||
|
pinnedUsersDescription: "قائمة المستخدمين المدبسين في لسان \"استكشف\" ، اجعل كل اسم\
|
||||||
|
\ مستخدم في سطر لوحده."
|
||||||
hcaptchaSiteKey: "مفتاح الموقع"
|
hcaptchaSiteKey: "مفتاح الموقع"
|
||||||
hcaptchaSecretKey: "المفتاح السري"
|
hcaptchaSecretKey: "المفتاح السري"
|
||||||
recaptchaSiteKey: "مفتاح الموقع"
|
recaptchaSiteKey: "مفتاح الموقع"
|
||||||
|
@ -323,6 +327,11 @@ silence: "اكتم"
|
||||||
silenceConfirm: "أمتأكد من كتم هذا المستخدم؟"
|
silenceConfirm: "أمتأكد من كتم هذا المستخدم؟"
|
||||||
unsilence: "إلغاء الكتم"
|
unsilence: "إلغاء الكتم"
|
||||||
unsilenceConfirm: "أمتأكد من إلغاء كتم هذا المستخدم؟"
|
unsilenceConfirm: "أمتأكد من إلغاء كتم هذا المستخدم؟"
|
||||||
|
popularUsers: "المستخدمون الرائدون"
|
||||||
|
recentlyUpdatedUsers: "أصحاب النشاطات الأخيرة"
|
||||||
|
recentlyRegisteredUsers: "المستخدمون المنضمون حديثًا"
|
||||||
|
recentlyDiscoveredUsers: "المستخدمون المكتشفون حديثًا"
|
||||||
|
popularTags: "الوسوم الرائجة"
|
||||||
userList: "القوائم"
|
userList: "القوائم"
|
||||||
aboutMisskey: "عن FoundKey"
|
aboutMisskey: "عن FoundKey"
|
||||||
administrator: "المدير"
|
administrator: "المدير"
|
||||||
|
@ -363,6 +372,7 @@ messagingWithGroup: "محادثة جماعية"
|
||||||
title: "العنوان"
|
title: "العنوان"
|
||||||
text: "النص"
|
text: "النص"
|
||||||
enable: "تشغيل"
|
enable: "تشغيل"
|
||||||
|
next: "التالية"
|
||||||
retype: "أعد الكتابة"
|
retype: "أعد الكتابة"
|
||||||
noteOf: "ملاحظات {user}"
|
noteOf: "ملاحظات {user}"
|
||||||
inviteToGroup: "دعوة إلى فريق"
|
inviteToGroup: "دعوة إلى فريق"
|
||||||
|
@ -523,6 +533,7 @@ abuseReports: "البلاغات"
|
||||||
reportAbuse: "أبلغ"
|
reportAbuse: "أبلغ"
|
||||||
reportAbuseOf: "أبلغ عن {name}"
|
reportAbuseOf: "أبلغ عن {name}"
|
||||||
fillAbuseReportDescription: "أكتب بالتفصيل سبب البلاغ"
|
fillAbuseReportDescription: "أكتب بالتفصيل سبب البلاغ"
|
||||||
|
abuseReported: "أُرسل البلاغ، شكرًا لك"
|
||||||
reporter: "المُبلّغ"
|
reporter: "المُبلّغ"
|
||||||
reporteeOrigin: "أصل البلاغ"
|
reporteeOrigin: "أصل البلاغ"
|
||||||
reporterOrigin: "أصل المُبلّغ"
|
reporterOrigin: "أصل المُبلّغ"
|
||||||
|
@ -870,6 +881,35 @@ _time:
|
||||||
minute: "د"
|
minute: "د"
|
||||||
hour: "سا"
|
hour: "سا"
|
||||||
day: "ي"
|
day: "ي"
|
||||||
|
_tutorial:
|
||||||
|
title: "كيف تستخدم FoundKey"
|
||||||
|
step1_1: "مرحبًا!"
|
||||||
|
step1_2: "تدعى هذه الصفحة 'الخيط الزمني' وهي تحوي ملاحظات الأشخاص الذي تتابعهم مرتبة\
|
||||||
|
\ حسب تاريخ نشرها."
|
||||||
|
step1_3: "خيطك الزمني فارغ حاليًا بما أنك لا تتابع أي شخص ولم تنشر أي ملاحظة."
|
||||||
|
step2_1: "لننهي إعداد ملفك الشخصي قبل كتابة ملاحظة أو متابعة أشخاص."
|
||||||
|
step2_2: "أعطاء معلومات عن شخصيتك يمنح من له نفس إهتماماتك فرصة متابعتك والتفاعل\
|
||||||
|
\ مع ملاحظاتك."
|
||||||
|
step3_1: "هل أنهيت إعداد حسابك؟"
|
||||||
|
step3_2: "إذا تاليًا لتنشر ملاحظة. أنقر على أيقونة القلم في أعلى الشاشة"
|
||||||
|
step3_3: "املأ النموذج وانقر الزرّ الموجود في أعلى اليمين للإرسال."
|
||||||
|
step3_4: "ليس لديك ما تقوله؟ إذا اكتب \"بدأتُ استخدم ميسكي\"."
|
||||||
|
step4_1: "هل نشرت ملاحظتك الأولى؟"
|
||||||
|
step4_2: "مرحى! يمكنك الآن رؤية ملاحظتك في الخيط الزمني."
|
||||||
|
step5_1: "والآن، لنجعل الخيط الزمني أكثر حيوية وذلك بمتابعة بعض المستخدمين."
|
||||||
|
step5_2: "تعرض صفحة {features} الملاحظات المتداولة في هذا المثيل ويتيح لك {Explore}\
|
||||||
|
\ العثور على المستخدمين الرائدين. اعثر على الأشخاص الذين يثيرون إهتمامك وتابعهم!"
|
||||||
|
step5_3: "لمتابعة مستخدمين ادخل ملفهم الشخصي بالنقر على صورتهم الشخصية ثم اضغط زر\
|
||||||
|
\ 'تابع'."
|
||||||
|
step5_4: "إذا كان لدى المستخدم رمز قفل بجوار اسمه ، وجب عليك انتظاره ليقبل طلب المتابعة\
|
||||||
|
\ يدويًا."
|
||||||
|
step6_1: "الآن ستتمكن من رؤية ملاحظات المستخدمين المتابَعين في الخيط الزمني."
|
||||||
|
step6_2: "يمكنك التفاعل بسرعة مع الملاحظات عن طريق إضافة \"تفاعل\"."
|
||||||
|
step6_3: "لإضافة تفاعل لملاحظة ، انقر فوق علامة \"+\" أسفل للملاحظة واختر الإيموجي\
|
||||||
|
\ المطلوب."
|
||||||
|
step7_1: "مبارك ! أنهيت الدورة التعليمية الأساسية لاستخدام ميسكي."
|
||||||
|
step7_2: "إذا أردت معرفة المزيد عن ميسكي زر {help}."
|
||||||
|
step7_3: "حظًا سعيدًا واستمتع بوقتك مع ميسكي! \U0001F680"
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "سجلت سلفًا جهازًا للاستيثاق بعاملين."
|
alreadyRegistered: "سجلت سلفًا جهازًا للاستيثاق بعاملين."
|
||||||
registerDevice: "سجّل جهازًا جديدًا"
|
registerDevice: "سجّل جهازًا جديدًا"
|
||||||
|
|
|
@ -234,6 +234,7 @@ uploadFromUrl: "URL হতে আপলোড"
|
||||||
uploadFromUrlDescription: "যে ফাইলটি আপলোড করতে চান, সেটির URL"
|
uploadFromUrlDescription: "যে ফাইলটি আপলোড করতে চান, সেটির URL"
|
||||||
uploadFromUrlRequested: "আপলোড অনুরোধ করা হয়েছে"
|
uploadFromUrlRequested: "আপলোড অনুরোধ করা হয়েছে"
|
||||||
uploadFromUrlMayTakeTime: "URL হতে আপলোড হতে কিছু সময় লাগতে পারে।"
|
uploadFromUrlMayTakeTime: "URL হতে আপলোড হতে কিছু সময় লাগতে পারে।"
|
||||||
|
explore: "ঘুরে দেখুন"
|
||||||
messageRead: "পড়া"
|
messageRead: "পড়া"
|
||||||
noMoreHistory: "আর কোন ইতিহাস নেই"
|
noMoreHistory: "আর কোন ইতিহাস নেই"
|
||||||
startMessaging: "চ্যাট শুরু করুন"
|
startMessaging: "চ্যাট শুরু করুন"
|
||||||
|
@ -314,6 +315,9 @@ inMb: "মেগাবাইটে লিখুন"
|
||||||
iconUrl: "আইকনের URL (ফ্যাভিকন, ইত্যাদি)"
|
iconUrl: "আইকনের URL (ফ্যাভিকন, ইত্যাদি)"
|
||||||
bannerUrl: "ব্যানার ছবির URL"
|
bannerUrl: "ব্যানার ছবির URL"
|
||||||
backgroundImageUrl: "পটভূমির চিত্রের URL"
|
backgroundImageUrl: "পটভূমির চিত্রের URL"
|
||||||
|
pinnedUsers: "পিন করা ব্যাবহারকারীগণ"
|
||||||
|
pinnedUsersDescription: "আপনি যেসব ব্যবহারকারীদের \"ঘুরে দেখুন\" পৃষ্ঠায় পিন করতে\
|
||||||
|
\ চান তাদের বর্ণনা করুন, প্রত্যেকের বর্ণনা আলাদা লাইনে লিখুন"
|
||||||
hcaptchaSiteKey: "সাইট কী"
|
hcaptchaSiteKey: "সাইট কী"
|
||||||
hcaptchaSecretKey: "সিক্রেট কী"
|
hcaptchaSecretKey: "সিক্রেট কী"
|
||||||
recaptchaSiteKey: "সাইট কী"
|
recaptchaSiteKey: "সাইট কী"
|
||||||
|
@ -338,6 +342,11 @@ silence: "নীরব"
|
||||||
silenceConfirm: "আপনি কি এই ব্যাবহারকারীকের নীরব করতে চান?"
|
silenceConfirm: "আপনি কি এই ব্যাবহারকারীকের নীরব করতে চান?"
|
||||||
unsilence: "সরব"
|
unsilence: "সরব"
|
||||||
unsilenceConfirm: "আপনি কি এই ব্যাবহারকারীকের সরব করতে চান?"
|
unsilenceConfirm: "আপনি কি এই ব্যাবহারকারীকের সরব করতে চান?"
|
||||||
|
popularUsers: "জনপ্রিয় ব্যবহারকারীগন"
|
||||||
|
recentlyUpdatedUsers: "সম্প্রতি পোস্ট করা ব্যবহারকারীগন"
|
||||||
|
recentlyRegisteredUsers: "নতুন যোগ দেওয়া ব্যবহারকারীগন"
|
||||||
|
recentlyDiscoveredUsers: "নতুন খুঁজে পাওয়া ব্যবহারকারীগন"
|
||||||
|
popularTags: "জনপ্রিয় ট্যাগগুলি"
|
||||||
userList: "লিস্ট"
|
userList: "লিস্ট"
|
||||||
aboutMisskey: "FoundKey সম্পর্কে"
|
aboutMisskey: "FoundKey সম্পর্কে"
|
||||||
administrator: "প্রশাসক"
|
administrator: "প্রশাসক"
|
||||||
|
@ -378,6 +387,7 @@ messagingWithGroup: "গ্রুপ চ্যাট"
|
||||||
title: "শিরোনাম"
|
title: "শিরোনাম"
|
||||||
text: "পাঠ্য"
|
text: "পাঠ্য"
|
||||||
enable: "সক্রিয়"
|
enable: "সক্রিয়"
|
||||||
|
next: "পরবর্তী"
|
||||||
retype: "পুনঃ প্রবেশ"
|
retype: "পুনঃ প্রবেশ"
|
||||||
noteOf: "{user} এর নোট"
|
noteOf: "{user} এর নোট"
|
||||||
inviteToGroup: "গ্রুপে আমন্ত্রণ জানান"
|
inviteToGroup: "গ্রুপে আমন্ত্রণ জানান"
|
||||||
|
@ -569,6 +579,7 @@ abuseReports: "অভিযোগ"
|
||||||
reportAbuse: "অভিযোগ"
|
reportAbuse: "অভিযোগ"
|
||||||
reportAbuseOf: "{name} এ অভিযোগ করুন"
|
reportAbuseOf: "{name} এ অভিযোগ করুন"
|
||||||
fillAbuseReportDescription: "রিপোর্টের কারণ বর্ণনা করুন."
|
fillAbuseReportDescription: "রিপোর্টের কারণ বর্ণনা করুন."
|
||||||
|
abuseReported: "আপনার অভিযোগটি দাখিল করা হয়েছে। আপনাকে ধন্যবাদ।"
|
||||||
reporter: "অভিযোগকারী"
|
reporter: "অভিযোগকারী"
|
||||||
reporteeOrigin: "অভিযোগটির উৎস"
|
reporteeOrigin: "অভিযোগটির উৎস"
|
||||||
reporterOrigin: "অভিযোগকারীর উৎস"
|
reporterOrigin: "অভিযোগকারীর উৎস"
|
||||||
|
@ -964,6 +975,41 @@ _time:
|
||||||
minute: "মিনিট"
|
minute: "মিনিট"
|
||||||
hour: "ঘণ্টা"
|
hour: "ঘণ্টা"
|
||||||
day: "দিন"
|
day: "দিন"
|
||||||
|
_tutorial:
|
||||||
|
title: "FoundKey কিভাবে ব্যাবহার করবেন"
|
||||||
|
step1_1: "স্বাগতম!"
|
||||||
|
step1_2: "এই স্ক্রীনটিকে \"টাইমলাইন\" বলা হয় এবং কালানুক্রমিক ক্রমে আপনার এবং আপনি\
|
||||||
|
\ যাদের \"অনুসরণ করেন\" তাদের \"নোটগুলি\" দেখায়৷"
|
||||||
|
step1_3: "আপনি আপনার টাইমলাইনে কিছু দেখতে পাবেন না কারণ আপনি এখনও কোনো নোট পোস্ট\
|
||||||
|
\ করেননি এবং আপনি কাউকে অনুসরণ করছেন না৷"
|
||||||
|
step2_1: "নোট তৈরি করার আগে বা কাউকে অনুসরণ করার আগে প্রথমে আপনার প্রোফাইলটি সম্পূর্ণ\
|
||||||
|
\ করুন।"
|
||||||
|
step2_2: "আপনি কে তা জানা অনেক লোকের জন্য আপনার নোটগুলি দেখা এবং অনুসরণ করাকে সহজ\
|
||||||
|
\ করে তোলে৷"
|
||||||
|
step3_1: "আপনি কি সফলভাবে আপনার প্রোফাইল সেট আপ করেছেন?"
|
||||||
|
step3_2: "এখন, কিছু নোট পোস্ট করার চেষ্টা করুন। পোস্ট ফর্ম খুলতে পেন্সিল চিহ্নযুক্ত\
|
||||||
|
\ বাটনে ক্লিক করুন।"
|
||||||
|
step3_3: "বিষয়বস্তু লেখার পরে, আপনি ফর্মের উপরের ডানদিকের বাটনে ক্লিক করে পোস্ট\
|
||||||
|
\ করতে পারেন।"
|
||||||
|
step3_4: "পোস্ট করার মত কিছু মনে পরছে না? \"আমি মিসকি সেট আপ করছি\" বললে কেমন হয়?"
|
||||||
|
step4_1: "পোস্ট করেছেন?"
|
||||||
|
step4_2: "সাবাশ! এখন আপনার নোট টাইমলাইনে দেখা যাবে।"
|
||||||
|
step5_1: "এখন অন্যদেরকে অনুসরণ করে আপনার টাইমলাইনকে প্রাণবন্ত করে তুলুন।"
|
||||||
|
step5_2: "আপনি {featured}-এ জনপ্রিয় নোটগুলি দেখতে পারেন, যাতে আপনি যে ব্যক্তিকে\
|
||||||
|
\ পছন্দ করেন তাকে বেছে নিতে এবং অনুসরণ করতে পারেন, অথবা {explore}-এ জনপ্রিয় ব্যবহারকারীদের\
|
||||||
|
\ দেখতে পারেন৷"
|
||||||
|
step5_3: "একজন ব্যবহারকারীকে অনুসরণ করতে, ব্যবহারকারীর আইকনে ক্লিক করুন এবং ব্যবহারকারীর\
|
||||||
|
\ পৃষ্ঠাতে \"অনুসরণ করুন\" বাটনে ক্লিক করুন।"
|
||||||
|
step5_4: "যদি ব্যবহারকারীর নামের পাশে একটি লক আইকন থাকে তাহলে আপনার অনুসরণের অনুরোধ\
|
||||||
|
\ গ্রহণ করার জন্য তারা কিছু সময় নিতে পারে।"
|
||||||
|
step6_1: "সবকিছু ঠিক থাকলে আপনি টাইমলাইনে অন্য ব্যবহারকারীদের নোট দেখতে পাবেন।"
|
||||||
|
step6_2: "আপনি সহজেই আপনার প্রতিক্রিয়া জানাতে অন্য ব্যক্তির নোটে \"রিঅ্যাকশন\"\
|
||||||
|
\ যোগ করতে পারেন।"
|
||||||
|
step6_3: "একটি রিঅ্যাকশন যোগ করতে, নোটে \"+\" চিহ্নে ক্লিক করুন এবং আপনার পছন্দের\
|
||||||
|
\ রিঅ্যাকশন নির্বাচন করুন।"
|
||||||
|
step7_1: "অভিনন্দন! আপনি এখন FoundKey-র প্রাথমিক টিউটোরিয়ালটি শেষ করেছেন।"
|
||||||
|
step7_2: "আপনি যদি FoundKey সম্পর্কে আরও জানতে চান, তাহলে {help} এ দেখুন।"
|
||||||
|
step7_3: "এখন FoundKey উপভোগ করুন \U0001F680"
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "আপনি ইতিমধ্যে একটি 2-ফ্যাক্টর অথেনটিকেশন ডিভাইস নিবন্ধন করেছেন৷"
|
alreadyRegistered: "আপনি ইতিমধ্যে একটি 2-ফ্যাক্টর অথেনটিকেশন ডিভাইস নিবন্ধন করেছেন৷"
|
||||||
registerDevice: "নতুন ডিভাইস নিবন্ধন করুন"
|
registerDevice: "নতুন ডিভাইস নিবন্ধন করুন"
|
||||||
|
|
|
@ -210,6 +210,7 @@ fromUrl: "Z URL"
|
||||||
uploadFromUrl: "Nahrát z URL adresy"
|
uploadFromUrl: "Nahrát z URL adresy"
|
||||||
uploadFromUrlDescription: "URL adresa souboru, který chcete nahrát"
|
uploadFromUrlDescription: "URL adresa souboru, který chcete nahrát"
|
||||||
uploadFromUrlMayTakeTime: "Může trvat nějakou dobu, dokud nebude dokončeno nahrávání."
|
uploadFromUrlMayTakeTime: "Může trvat nějakou dobu, dokud nebude dokončeno nahrávání."
|
||||||
|
explore: "Objevovat"
|
||||||
messageRead: "Přečtené"
|
messageRead: "Přečtené"
|
||||||
noMoreHistory: "To je vše"
|
noMoreHistory: "To je vše"
|
||||||
startMessaging: "Zahájit chat"
|
startMessaging: "Zahájit chat"
|
||||||
|
@ -280,6 +281,7 @@ inMb: "V megabajtech"
|
||||||
iconUrl: "Favicon URL"
|
iconUrl: "Favicon URL"
|
||||||
bannerUrl: "Baner URL"
|
bannerUrl: "Baner URL"
|
||||||
backgroundImageUrl: "Adresa URL obrázku pozadí"
|
backgroundImageUrl: "Adresa URL obrázku pozadí"
|
||||||
|
pinnedUsers: "Připnutí uživatelé"
|
||||||
hcaptchaSecretKey: "Tajný Klíč (Secret Key)"
|
hcaptchaSecretKey: "Tajný Klíč (Secret Key)"
|
||||||
recaptchaSecretKey: "Tajný Klíč (Secret Key)"
|
recaptchaSecretKey: "Tajný Klíč (Secret Key)"
|
||||||
antennas: "Antény"
|
antennas: "Antény"
|
||||||
|
@ -288,6 +290,7 @@ name: "Jméno"
|
||||||
antennaSource: "Zdroj Antény"
|
antennaSource: "Zdroj Antény"
|
||||||
caseSensitive: "Rozlišuje malá a velká písmena"
|
caseSensitive: "Rozlišuje malá a velká písmena"
|
||||||
connectedTo: "Následující účty jsou připojeny"
|
connectedTo: "Následující účty jsou připojeny"
|
||||||
|
popularTags: "Populární tagy"
|
||||||
userList: "Seznamy"
|
userList: "Seznamy"
|
||||||
aboutMisskey: "O FoundKey"
|
aboutMisskey: "O FoundKey"
|
||||||
administrator: "Administrátor"
|
administrator: "Administrátor"
|
||||||
|
@ -325,6 +328,7 @@ transfer: "Převod"
|
||||||
title: "Titulek"
|
title: "Titulek"
|
||||||
text: "Text"
|
text: "Text"
|
||||||
enable: "Povolit"
|
enable: "Povolit"
|
||||||
|
next: "Další"
|
||||||
retype: "Zadejte znovu"
|
retype: "Zadejte znovu"
|
||||||
noteOf: "{user} poznámky"
|
noteOf: "{user} poznámky"
|
||||||
inviteToGroup: "Pozvat do skupiny"
|
inviteToGroup: "Pozvat do skupiny"
|
||||||
|
|
|
@ -245,6 +245,7 @@ uploadFromUrlDescription: "URL der hochzuladenden Datei"
|
||||||
uploadFromUrlRequested: "Upload angefordert"
|
uploadFromUrlRequested: "Upload angefordert"
|
||||||
uploadFromUrlMayTakeTime: "Es kann eine Weile dauern, bis das Hochladen abgeschlossen\
|
uploadFromUrlMayTakeTime: "Es kann eine Weile dauern, bis das Hochladen abgeschlossen\
|
||||||
\ ist."
|
\ ist."
|
||||||
|
explore: "Erkunden"
|
||||||
messageRead: "Gelesen"
|
messageRead: "Gelesen"
|
||||||
noMoreHistory: "Kein weiterer Verlauf vorhanden"
|
noMoreHistory: "Kein weiterer Verlauf vorhanden"
|
||||||
startMessaging: "Neuen Chat erstellen"
|
startMessaging: "Neuen Chat erstellen"
|
||||||
|
@ -327,6 +328,9 @@ inMb: "In Megabytes"
|
||||||
iconUrl: "Icon-URL (favicon etc)"
|
iconUrl: "Icon-URL (favicon etc)"
|
||||||
bannerUrl: "Banner-URL"
|
bannerUrl: "Banner-URL"
|
||||||
backgroundImageUrl: "Hintergrundbild-URL"
|
backgroundImageUrl: "Hintergrundbild-URL"
|
||||||
|
pinnedUsers: "Angeheftete Benutzer"
|
||||||
|
pinnedUsersDescription: "Gib durch Leerzeichen getrennte Benutzer an, die an die \"\
|
||||||
|
Erkunden\"-Seite angeheftet werden sollen."
|
||||||
hcaptchaSiteKey: "Site key"
|
hcaptchaSiteKey: "Site key"
|
||||||
hcaptchaSecretKey: "Geheimer Schlüssel"
|
hcaptchaSecretKey: "Geheimer Schlüssel"
|
||||||
recaptchaSiteKey: "Site-Schlüssel"
|
recaptchaSiteKey: "Site-Schlüssel"
|
||||||
|
@ -353,6 +357,11 @@ silenceConfirm: "Möchtest du diesen Benutzer wirklich instanzweit stummschalten
|
||||||
unsilence: "Instanzweite Stummschaltung aufheben"
|
unsilence: "Instanzweite Stummschaltung aufheben"
|
||||||
unsilenceConfirm: "Möchtest du die instanzweite Stummschaltung dieses Benutzers wirklich\
|
unsilenceConfirm: "Möchtest du die instanzweite Stummschaltung dieses Benutzers wirklich\
|
||||||
\ aufheben?"
|
\ aufheben?"
|
||||||
|
popularUsers: "Beliebte Benutzer"
|
||||||
|
recentlyUpdatedUsers: "Vor kurzem aktive Benutzer"
|
||||||
|
recentlyRegisteredUsers: "Vor kurzem registrierte Benutzer"
|
||||||
|
recentlyDiscoveredUsers: "Vor kurzem gefundene Benutzer"
|
||||||
|
popularTags: "Beliebte Schlagwörter"
|
||||||
userList: "Liste"
|
userList: "Liste"
|
||||||
aboutMisskey: "Über FoundKey"
|
aboutMisskey: "Über FoundKey"
|
||||||
administrator: "Administrator"
|
administrator: "Administrator"
|
||||||
|
@ -393,6 +402,7 @@ messagingWithGroup: "Gruppenchat"
|
||||||
title: "Titel"
|
title: "Titel"
|
||||||
text: "Text"
|
text: "Text"
|
||||||
enable: "Aktivieren"
|
enable: "Aktivieren"
|
||||||
|
next: "Weiter"
|
||||||
retype: "Erneut eingeben"
|
retype: "Erneut eingeben"
|
||||||
noteOf: "Notiz von {user}"
|
noteOf: "Notiz von {user}"
|
||||||
inviteToGroup: "Zu Gruppe einladen"
|
inviteToGroup: "Zu Gruppe einladen"
|
||||||
|
@ -590,6 +600,7 @@ reportAbuse: "Melden"
|
||||||
reportAbuseOf: "{name} melden"
|
reportAbuseOf: "{name} melden"
|
||||||
fillAbuseReportDescription: "Bitte gib zusätzliche Informationen zu dieser Meldung\
|
fillAbuseReportDescription: "Bitte gib zusätzliche Informationen zu dieser Meldung\
|
||||||
\ an."
|
\ an."
|
||||||
|
abuseReported: "Deine Meldung wurde versendet. Vielen Dank."
|
||||||
reporter: "Melder"
|
reporter: "Melder"
|
||||||
reporteeOrigin: "Herkunft des Gemeldeten"
|
reporteeOrigin: "Herkunft des Gemeldeten"
|
||||||
reporterOrigin: "Herkunft des Meldenden"
|
reporterOrigin: "Herkunft des Meldenden"
|
||||||
|
@ -1003,6 +1014,44 @@ _time:
|
||||||
minute: "Minute(n)"
|
minute: "Minute(n)"
|
||||||
hour: "Stunde(n)"
|
hour: "Stunde(n)"
|
||||||
day: "Tag(en)"
|
day: "Tag(en)"
|
||||||
|
_tutorial:
|
||||||
|
title: "Wie du FoundKey verwendest"
|
||||||
|
step1_1: "Willkommen!"
|
||||||
|
step1_2: "Diese Seite ist die „Chronik“. Sie zeigt dir deine geschrieben „Notizen“\
|
||||||
|
\ sowie die aller Benutzer, denen du „folgst“, in chronologischer Reihenfolge."
|
||||||
|
step1_3: "Deine Chronik sollte momentan leer sein, da du bis jetzt noch keine Notizen\
|
||||||
|
\ geschrieben hast und auch noch keinen Benutzern folgst."
|
||||||
|
step2_1: "Lass uns zuerst dein Profil vervollständigen, bevor du Notizen schreibst\
|
||||||
|
\ oder jemandem folgst."
|
||||||
|
step2_2: "Informationen darüber, was für eine Person du bist, macht es anderen leichter\
|
||||||
|
\ zu wissen, ob sie deine Notizen sehen wollen und ob sie dir folgen möchten."
|
||||||
|
step3_1: "Mit dem Einrichten deines Profils fertig?"
|
||||||
|
step3_2: "Dann lass uns als nächstes versuchen, eine Notiz zu schreiben. Dies kannst\
|
||||||
|
\ du tun, indem du auf den Knopf mit dem Stift-Icon auf dem Bildschirm drückst."
|
||||||
|
step3_3: "Fülle das Fenster aus und drücke auf den Knopf oben rechts zum Senden."
|
||||||
|
step3_4: "Fällt dir nichts ein, das du schreiben möchtest? Versuch's mit \"Hallo\
|
||||||
|
\ FoundKey!\""
|
||||||
|
step4_1: "Fertig mit dem Senden deiner ersten Notiz?"
|
||||||
|
step4_2: "Falls deine Notiz nun in deiner Chronik auftaucht, hast du alles richtig\
|
||||||
|
\ gemacht."
|
||||||
|
step5_1: "Lass uns nun deiner Chronik etwas mehr Leben einhauchen, indem du einigen\
|
||||||
|
\ anderen Benutzern folgst."
|
||||||
|
step5_2: "{featured} zeigt dir beliebte Notizen dieser Instanz. In {explore} kannst\
|
||||||
|
\ du beliebte Benutzer finden. Schau dort, ob du Benutzer findest, die dich interessieren!"
|
||||||
|
step5_3: "Klicke zum Anzeigen des Profils eines Benutzers auf dessen Profilbild\
|
||||||
|
\ und dann auf den \"Folgen\"-Knopf, um diesem zu folgen."
|
||||||
|
step5_4: "Je nach Benutzer kann es etwas Zeit in Anspruch nehmen, bis dieser deine\
|
||||||
|
\ Follow-Anfrage bestätigt."
|
||||||
|
step6_1: "Wenn du nun auch die Notizen anderer Benutzer in deiner Chronik siehst,\
|
||||||
|
\ hast du auch diesmal alles richtig gemacht."
|
||||||
|
step6_2: "Du kannst ebenso „Reaktionen“ verwenden, um schnell auf Notizen anderer\
|
||||||
|
\ Benutzer zu reagieren."
|
||||||
|
step6_3: "Um eine Reaktion anzufügen, klicke auf das „+“-Symbol in der Notiz und\
|
||||||
|
\ wähle ein Emoji aus, mit dem du reagieren möchtest."
|
||||||
|
step7_1: "Glückwunsch! Du hast die Einführung in die Verwendung von FoundKey abgeschlossen."
|
||||||
|
step7_2: "Wenn du mehr über FoundKey lernen möchtest, schau dich im {help}-Bereich\
|
||||||
|
\ um."
|
||||||
|
step7_3: "Und nun, viel Spaß mit FoundKey! \U0001F680"
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "Du hast bereits ein Gerät für Zwei-Faktor-Authentifizierung\
|
alreadyRegistered: "Du hast bereits ein Gerät für Zwei-Faktor-Authentifizierung\
|
||||||
\ registriert."
|
\ registriert."
|
||||||
|
@ -1260,7 +1309,7 @@ _notification:
|
||||||
groupInvited: "Erhaltene Gruppeneinladungen"
|
groupInvited: "Erhaltene Gruppeneinladungen"
|
||||||
app: "Benachrichtigungen von Apps"
|
app: "Benachrichtigungen von Apps"
|
||||||
move: Account-Umzüge
|
move: Account-Umzüge
|
||||||
update: Beobachtete Notiz wurde bearbeitet
|
updated: Beobachtete Notiz wurde bearbeitet
|
||||||
_actions:
|
_actions:
|
||||||
followBack: "folgt dir nun auch"
|
followBack: "folgt dir nun auch"
|
||||||
reply: "Antworten"
|
reply: "Antworten"
|
||||||
|
|
|
@ -242,6 +242,7 @@ uploadFromUrl: "Upload from a URL"
|
||||||
uploadFromUrlDescription: "URL of the file you want to upload"
|
uploadFromUrlDescription: "URL of the file you want to upload"
|
||||||
uploadFromUrlRequested: "Upload requested"
|
uploadFromUrlRequested: "Upload requested"
|
||||||
uploadFromUrlMayTakeTime: "It may take some time until the upload is complete."
|
uploadFromUrlMayTakeTime: "It may take some time until the upload is complete."
|
||||||
|
explore: "Explore"
|
||||||
messageRead: "Read"
|
messageRead: "Read"
|
||||||
noMoreHistory: "There is no further history"
|
noMoreHistory: "There is no further history"
|
||||||
startMessaging: "Start a new chat"
|
startMessaging: "Start a new chat"
|
||||||
|
@ -323,6 +324,9 @@ inMb: "In megabytes"
|
||||||
iconUrl: "Icon URL"
|
iconUrl: "Icon URL"
|
||||||
bannerUrl: "Banner image URL"
|
bannerUrl: "Banner image URL"
|
||||||
backgroundImageUrl: "Background image URL"
|
backgroundImageUrl: "Background image URL"
|
||||||
|
pinnedUsers: "Pinned users"
|
||||||
|
pinnedUsersDescription: "List usernames separated by line breaks to be pinned in the\
|
||||||
|
\ \"Explore\" tab."
|
||||||
hcaptchaSiteKey: "Site key"
|
hcaptchaSiteKey: "Site key"
|
||||||
hcaptchaSecretKey: "Secret key"
|
hcaptchaSecretKey: "Secret key"
|
||||||
recaptchaSiteKey: "Site key"
|
recaptchaSiteKey: "Site key"
|
||||||
|
@ -349,6 +353,11 @@ silence: "Silence"
|
||||||
silenceConfirm: "Are you sure that you want to silence this user?"
|
silenceConfirm: "Are you sure that you want to silence this user?"
|
||||||
unsilence: "Undo silencing"
|
unsilence: "Undo silencing"
|
||||||
unsilenceConfirm: "Are you sure that you want to undo the silencing of this user?"
|
unsilenceConfirm: "Are you sure that you want to undo the silencing of this user?"
|
||||||
|
popularUsers: "Popular users"
|
||||||
|
recentlyUpdatedUsers: "Recently active users"
|
||||||
|
recentlyRegisteredUsers: "Newly joined users"
|
||||||
|
recentlyDiscoveredUsers: "Newly discovered users"
|
||||||
|
popularTags: "Popular tags"
|
||||||
userList: "Lists"
|
userList: "Lists"
|
||||||
aboutMisskey: "About FoundKey"
|
aboutMisskey: "About FoundKey"
|
||||||
administrator: "Administrator"
|
administrator: "Administrator"
|
||||||
|
@ -389,6 +398,7 @@ messagingWithGroup: "Group chat"
|
||||||
title: "Title"
|
title: "Title"
|
||||||
text: "Text"
|
text: "Text"
|
||||||
enable: "Enable"
|
enable: "Enable"
|
||||||
|
next: "Next"
|
||||||
retype: "Enter again"
|
retype: "Enter again"
|
||||||
noteOf: "Note by {user}"
|
noteOf: "Note by {user}"
|
||||||
inviteToGroup: "Invite to group"
|
inviteToGroup: "Invite to group"
|
||||||
|
@ -494,8 +504,6 @@ output: "Output"
|
||||||
updateRemoteUser: "Update remote user information"
|
updateRemoteUser: "Update remote user information"
|
||||||
deleteAllFiles: "Delete all files"
|
deleteAllFiles: "Delete all files"
|
||||||
deleteAllFilesConfirm: "Are you sure that you want to delete all files?"
|
deleteAllFilesConfirm: "Are you sure that you want to delete all files?"
|
||||||
deleteAllNotes: "Delete all notes"
|
|
||||||
deleteAllNotesConfirm: "Are you sure that you want to delete all visible notes of this clip?"
|
|
||||||
removeAllFollowing: "Unfollow all followed users"
|
removeAllFollowing: "Unfollow all followed users"
|
||||||
removeAllFollowingDescription: "Executing this unfollows all accounts from {host}.\
|
removeAllFollowingDescription: "Executing this unfollows all accounts from {host}.\
|
||||||
\ Please run this if the instance e.g. no longer exists."
|
\ Please run this if the instance e.g. no longer exists."
|
||||||
|
@ -586,6 +594,7 @@ abuseReports: "Reports"
|
||||||
reportAbuse: "Report"
|
reportAbuse: "Report"
|
||||||
reportAbuseOf: "Report {name}"
|
reportAbuseOf: "Report {name}"
|
||||||
fillAbuseReportDescription: "Please fill in details regarding this report."
|
fillAbuseReportDescription: "Please fill in details regarding this report."
|
||||||
|
abuseReported: "Your report has been sent. Thank you very much."
|
||||||
reporter: "Reporter"
|
reporter: "Reporter"
|
||||||
reporteeOrigin: "Reportee Origin"
|
reporteeOrigin: "Reportee Origin"
|
||||||
reporterOrigin: "Reporter Origin"
|
reporterOrigin: "Reporter Origin"
|
||||||
|
@ -1020,6 +1029,39 @@ _time:
|
||||||
minute: "Minute(s)"
|
minute: "Minute(s)"
|
||||||
hour: "Hour(s)"
|
hour: "Hour(s)"
|
||||||
day: "Day(s)"
|
day: "Day(s)"
|
||||||
|
_tutorial:
|
||||||
|
title: "How to use FoundKey"
|
||||||
|
step1_1: "Welcome!"
|
||||||
|
step1_2: "This page is called the \"timeline\". It shows chronologically ordered\
|
||||||
|
\ \"notes\" of people who you \"follow\"."
|
||||||
|
step1_3: "Your timeline is currently empty, since you have not posted any notes\
|
||||||
|
\ or followed anyone yet."
|
||||||
|
step2_1: "Let's finish setting up your profile before writing a note or following\
|
||||||
|
\ anyone."
|
||||||
|
step2_2: "Providing some information about who you are will make it easier for others\
|
||||||
|
\ to tell if they want to see your notes or follow you."
|
||||||
|
step3_1: "Finished setting up your profile?"
|
||||||
|
step3_2: "Then let's try posting a note next. You can do so by pressing the button\
|
||||||
|
\ with a pencil icon on the screen."
|
||||||
|
step3_3: "Fill in the modal and press the button on the top right to post."
|
||||||
|
step3_4: "Have nothing to say? Try \"just setting up my msky\"!"
|
||||||
|
step4_1: "Finished posting your first note?"
|
||||||
|
step4_2: "Hurray! Now your first note should be displayed on your timeline."
|
||||||
|
step5_1: "Now, let's try making your timeline more lively by following other people."
|
||||||
|
step5_2: "{featured} will show you popular notes in this instance. {explore} will\
|
||||||
|
\ let you find popular users. Try finding people you'd like to follow there!"
|
||||||
|
step5_3: "To follow other users, click on their icon and press the \"Follow\" button\
|
||||||
|
\ on their profile."
|
||||||
|
step5_4: "If the other user has a lock icon next to their name, it may take some\
|
||||||
|
\ time for that user to manually approve your follow request."
|
||||||
|
step6_1: "You should be able to see other users' notes on your timeline now."
|
||||||
|
step6_2: "You can also put \"reactions\" on other people's notes to quickly respond\
|
||||||
|
\ to them."
|
||||||
|
step6_3: "To attach a \"reaction\", press the \"+\" mark on another user's note\
|
||||||
|
\ and choose an emoji you'd like to react with."
|
||||||
|
step7_1: "Congratulations! You have now finished FoundKey's basic tutorial."
|
||||||
|
step7_2: "If you would like to learn more about FoundKey, try the {help} section."
|
||||||
|
step7_3: "Now then, good luck and have fun with FoundKey! \U0001F680"
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "You have already registered a 2-factor authentication device."
|
alreadyRegistered: "You have already registered a 2-factor authentication device."
|
||||||
registerDevice: "Register a new device"
|
registerDevice: "Register a new device"
|
||||||
|
@ -1205,7 +1247,6 @@ _timelines:
|
||||||
local: "Local"
|
local: "Local"
|
||||||
social: "Social"
|
social: "Social"
|
||||||
global: "Global"
|
global: "Global"
|
||||||
shuffled: "Shuffled"
|
|
||||||
_pages:
|
_pages:
|
||||||
newPage: "Create a new Page"
|
newPage: "Create a new Page"
|
||||||
editPage: "Edit this Page"
|
editPage: "Edit this Page"
|
||||||
|
@ -1263,7 +1304,7 @@ _notification:
|
||||||
reaction: "Reactions"
|
reaction: "Reactions"
|
||||||
pollVote: "Votes on polls"
|
pollVote: "Votes on polls"
|
||||||
pollEnded: "Polls ending"
|
pollEnded: "Polls ending"
|
||||||
update: "Watched Note was updated"
|
updated: "Watched Note was updated"
|
||||||
receiveFollowRequest: "Received follow requests"
|
receiveFollowRequest: "Received follow requests"
|
||||||
followRequestAccepted: "Accepted follow requests"
|
followRequestAccepted: "Accepted follow requests"
|
||||||
groupInvited: "Group invitations"
|
groupInvited: "Group invitations"
|
||||||
|
|
|
@ -238,6 +238,7 @@ uploadFromUrl: "Subir desde una URL"
|
||||||
uploadFromUrlDescription: "URL del fichero que quieres subir"
|
uploadFromUrlDescription: "URL del fichero que quieres subir"
|
||||||
uploadFromUrlRequested: "Subida solicitada"
|
uploadFromUrlRequested: "Subida solicitada"
|
||||||
uploadFromUrlMayTakeTime: "Subir el fichero puede tardar un tiempo."
|
uploadFromUrlMayTakeTime: "Subir el fichero puede tardar un tiempo."
|
||||||
|
explore: "Explorar"
|
||||||
messageRead: "Ya leído"
|
messageRead: "Ya leído"
|
||||||
noMoreHistory: "El historial se ha acabado"
|
noMoreHistory: "El historial se ha acabado"
|
||||||
startMessaging: "Iniciar chat"
|
startMessaging: "Iniciar chat"
|
||||||
|
@ -318,6 +319,9 @@ inMb: "En megabytes"
|
||||||
iconUrl: "URL de la imagen del avatar"
|
iconUrl: "URL de la imagen del avatar"
|
||||||
bannerUrl: "URL de la imagen del banner"
|
bannerUrl: "URL de la imagen del banner"
|
||||||
backgroundImageUrl: "URL de la imagen de fondo"
|
backgroundImageUrl: "URL de la imagen de fondo"
|
||||||
|
pinnedUsers: "Usuarios fijados"
|
||||||
|
pinnedUsersDescription: "Describir los usuarios que quiere fijar en la página \"Descubrir\"\
|
||||||
|
\ separados por una linea nueva"
|
||||||
hcaptchaSiteKey: "Clave del sitio"
|
hcaptchaSiteKey: "Clave del sitio"
|
||||||
hcaptchaSecretKey: "Clave secreta"
|
hcaptchaSecretKey: "Clave secreta"
|
||||||
recaptchaSiteKey: "Clave del sitio"
|
recaptchaSiteKey: "Clave del sitio"
|
||||||
|
@ -342,6 +346,11 @@ silence: "Silenciar"
|
||||||
silenceConfirm: "¿Desea silenciar al usuario?"
|
silenceConfirm: "¿Desea silenciar al usuario?"
|
||||||
unsilence: "Dejar de silenciar"
|
unsilence: "Dejar de silenciar"
|
||||||
unsilenceConfirm: "¿Desea dejar de silenciar al usuario?"
|
unsilenceConfirm: "¿Desea dejar de silenciar al usuario?"
|
||||||
|
popularUsers: "Usuarios populares"
|
||||||
|
recentlyUpdatedUsers: "Usuarios activos recientemente"
|
||||||
|
recentlyRegisteredUsers: "Usuarios registrados recientemente"
|
||||||
|
recentlyDiscoveredUsers: "Usuarios descubiertos recientemente"
|
||||||
|
popularTags: "Etiquetas populares"
|
||||||
userList: "Lista"
|
userList: "Lista"
|
||||||
aboutMisskey: "Sobre FoundKey"
|
aboutMisskey: "Sobre FoundKey"
|
||||||
administrator: "Administrador"
|
administrator: "Administrador"
|
||||||
|
@ -382,6 +391,7 @@ messagingWithGroup: "Chatear en grupo"
|
||||||
title: "Título"
|
title: "Título"
|
||||||
text: "Texto"
|
text: "Texto"
|
||||||
enable: "Activar"
|
enable: "Activar"
|
||||||
|
next: "Siguiente"
|
||||||
retype: "Intentar de nuevo"
|
retype: "Intentar de nuevo"
|
||||||
noteOf: "Notas de {user}"
|
noteOf: "Notas de {user}"
|
||||||
inviteToGroup: "Invitar al grupo"
|
inviteToGroup: "Invitar al grupo"
|
||||||
|
@ -573,6 +583,7 @@ abuseReports: "Reportes"
|
||||||
reportAbuse: "Reportar"
|
reportAbuse: "Reportar"
|
||||||
reportAbuseOf: "Reportar a {name}"
|
reportAbuseOf: "Reportar a {name}"
|
||||||
fillAbuseReportDescription: "Ingrese los detalles del reporte."
|
fillAbuseReportDescription: "Ingrese los detalles del reporte."
|
||||||
|
abuseReported: "Se ha enviado el reporte. Muchas gracias."
|
||||||
reporteeOrigin: "Informar a"
|
reporteeOrigin: "Informar a"
|
||||||
reporterOrigin: "Origen del informe"
|
reporterOrigin: "Origen del informe"
|
||||||
forwardReport: "Transferir un informe a una instancia remota"
|
forwardReport: "Transferir un informe a una instancia remota"
|
||||||
|
@ -925,6 +936,41 @@ _time:
|
||||||
minute: "Minutos"
|
minute: "Minutos"
|
||||||
hour: "Horas"
|
hour: "Horas"
|
||||||
day: "Días"
|
day: "Días"
|
||||||
|
_tutorial:
|
||||||
|
title: "Cómo usar FoundKey"
|
||||||
|
step1_1: "Bienvenido"
|
||||||
|
step1_2: "Esta imagen se llama \"Linea de tiempo\" y muestra en orden cronológico\
|
||||||
|
\ las \"notas\" tuyas y de la gente que \"sigues\""
|
||||||
|
step1_3: "Si no estás escribiendo ninguna nota y no estás siguiendo a nadie, es\
|
||||||
|
\ esperable que no se muestre nada en la linea de tiempo"
|
||||||
|
step2_1: "Antes de crear notas y seguir a alguien, primero vamos a crear tu perfil"
|
||||||
|
step2_2: "Si provees información sobre quien eres, será más fácil para que otros\
|
||||||
|
\ usuarios te sigan"
|
||||||
|
step3_1: "¿Has podido crear tu perfil sin problemas?"
|
||||||
|
step3_2: "Con esto, prueba hacer una nota. Aprieta el botón con forma de lápiz que\
|
||||||
|
\ está arriba de la imagen y abre el formulario."
|
||||||
|
step3_3: "Si has escrito el contenido, aprieta el botón que está arriba a la derecha\
|
||||||
|
\ del formulario para postear."
|
||||||
|
step3_4: "¿No se te ocurre un contenido? Prueba con decir \"Empecé a usar FoundKey\""
|
||||||
|
step4_1: "¿Has posteado?"
|
||||||
|
step4_2: "Si tu nota puede verse en la linea de tiempo, fue todo un éxito."
|
||||||
|
step5_1: "Luego, ponte a seguir a otra gente y haz que tu linea de tiempo esté más\
|
||||||
|
\ animada."
|
||||||
|
step5_2: "Puedes ver las notas destacadas en {featured} y desde allí seguir a usuarios\
|
||||||
|
\ que te importan. También puedes buscar usuario destacados en {explore}."
|
||||||
|
step5_3: "Para seguir a un usuario, haz click en su avatar para ver su página de\
|
||||||
|
\ usuario y allí apretar el botón \"seguir\""
|
||||||
|
step5_4: "De esa manera, puede pasar un tiempo hasta que el usuario apruebe al seguidor."
|
||||||
|
step6_1: "Si puedes ver en la linea de tiempo las notas de otros usuarios, fue todo\
|
||||||
|
\ un éxito."
|
||||||
|
step6_2: "En las notas de otros usuarios puedes añadir una \"reacción\", para poder\
|
||||||
|
\ responder rápidamente."
|
||||||
|
step6_3: "Para añadir una reacción, haz click en el botón \"+\" de la nota y elige\
|
||||||
|
\ la reacción que prefieras."
|
||||||
|
step7_1: "Así terminó la explicación del funcionamiento básico de FoundKey. Eso\
|
||||||
|
\ fue todo."
|
||||||
|
step7_2: "Si quieres conocer más sobre FoundKey, prueba con la sección {help}."
|
||||||
|
step7_3: "Así, disfruta de FoundKey \U0001F680"
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "Ya has completado la configuración."
|
alreadyRegistered: "Ya has completado la configuración."
|
||||||
registerDevice: "Registrar dispositivo"
|
registerDevice: "Registrar dispositivo"
|
||||||
|
|
|
@ -238,6 +238,7 @@ uploadFromUrlDescription: "URL du fichier que vous souhaitez téléverser"
|
||||||
uploadFromUrlRequested: "Téléversement demandé"
|
uploadFromUrlRequested: "Téléversement demandé"
|
||||||
uploadFromUrlMayTakeTime: "Le téléversement de votre fichier peut prendre un certain\
|
uploadFromUrlMayTakeTime: "Le téléversement de votre fichier peut prendre un certain\
|
||||||
\ temps."
|
\ temps."
|
||||||
|
explore: "Découvrir"
|
||||||
messageRead: "Lu"
|
messageRead: "Lu"
|
||||||
noMoreHistory: "Il n’y a plus d’historique"
|
noMoreHistory: "Il n’y a plus d’historique"
|
||||||
startMessaging: "Commencer à discuter"
|
startMessaging: "Commencer à discuter"
|
||||||
|
@ -319,6 +320,9 @@ inMb: "en mégaoctets"
|
||||||
iconUrl: "URL de l'icône"
|
iconUrl: "URL de l'icône"
|
||||||
bannerUrl: "URL de l’image de la bannière"
|
bannerUrl: "URL de l’image de la bannière"
|
||||||
backgroundImageUrl: "URL de l'image d'arrière-plan"
|
backgroundImageUrl: "URL de l'image d'arrière-plan"
|
||||||
|
pinnedUsers: "Utilisateur·rice épinglé·e"
|
||||||
|
pinnedUsersDescription: "Listez les utilisateur·rice·s que vous souhaitez voir épinglé·e·s\
|
||||||
|
\ sur la page \"Découvrir\", un·e par ligne."
|
||||||
hcaptchaSiteKey: "Clé du site"
|
hcaptchaSiteKey: "Clé du site"
|
||||||
hcaptchaSecretKey: "Clé secrète"
|
hcaptchaSecretKey: "Clé secrète"
|
||||||
recaptchaSiteKey: "Clé du site"
|
recaptchaSiteKey: "Clé du site"
|
||||||
|
@ -345,6 +349,11 @@ silenceConfirm: "Êtes-vous sûr·e de vouloir mettre l’utilisateur·rice en s
|
||||||
unsilence: "Annuler la sourdine"
|
unsilence: "Annuler la sourdine"
|
||||||
unsilenceConfirm: "Êtes-vous sûr·e de vouloir annuler la mise en sourdine de cet·te\
|
unsilenceConfirm: "Êtes-vous sûr·e de vouloir annuler la mise en sourdine de cet·te\
|
||||||
\ utilisateur·rice ?"
|
\ utilisateur·rice ?"
|
||||||
|
popularUsers: "Utilisateur·rice·s populaires"
|
||||||
|
recentlyUpdatedUsers: "Utilisateur·rice·s actif·ve·s récemment"
|
||||||
|
recentlyRegisteredUsers: "Utilisateur·rice·s récemment inscrit·e·s"
|
||||||
|
recentlyDiscoveredUsers: "Utilisateur·rice·s récemment découvert·e·s"
|
||||||
|
popularTags: "Mots-clés populaires"
|
||||||
userList: "Listes"
|
userList: "Listes"
|
||||||
aboutMisskey: "À propos de FoundKey"
|
aboutMisskey: "À propos de FoundKey"
|
||||||
administrator: "Administrateur"
|
administrator: "Administrateur"
|
||||||
|
@ -385,6 +394,7 @@ messagingWithGroup: "Discuter avec un groupe"
|
||||||
title: "Titre"
|
title: "Titre"
|
||||||
text: "Texte"
|
text: "Texte"
|
||||||
enable: "Activer"
|
enable: "Activer"
|
||||||
|
next: "Suivant"
|
||||||
retype: "Confirmation"
|
retype: "Confirmation"
|
||||||
noteOf: "Notes de {user}"
|
noteOf: "Notes de {user}"
|
||||||
inviteToGroup: "Inviter dans un groupe"
|
inviteToGroup: "Inviter dans un groupe"
|
||||||
|
@ -585,6 +595,7 @@ abuseReports: "Signalements"
|
||||||
reportAbuse: "Signaler"
|
reportAbuse: "Signaler"
|
||||||
reportAbuseOf: "Signaler {name}"
|
reportAbuseOf: "Signaler {name}"
|
||||||
fillAbuseReportDescription: "Veuillez expliquer les raisons du signalement."
|
fillAbuseReportDescription: "Veuillez expliquer les raisons du signalement."
|
||||||
|
abuseReported: "Le rapport est envoyé. Merci."
|
||||||
reporter: "Signalé par"
|
reporter: "Signalé par"
|
||||||
reporteeOrigin: "Origine du signalement"
|
reporteeOrigin: "Origine du signalement"
|
||||||
reporterOrigin: "Signalé par"
|
reporterOrigin: "Signalé par"
|
||||||
|
@ -982,6 +993,46 @@ _time:
|
||||||
minute: "min"
|
minute: "min"
|
||||||
hour: "h"
|
hour: "h"
|
||||||
day: "j"
|
day: "j"
|
||||||
|
_tutorial:
|
||||||
|
title: "Comment utiliser FoundKey"
|
||||||
|
step1_1: "Bienvenue !"
|
||||||
|
step1_2: "Cette page est appelée « un fil ». Elle affiche les « notes » des personnes\
|
||||||
|
\ auxquelles vous êtes abonné dans un ordre chronologique."
|
||||||
|
step1_3: "Votre fil est actuellement vide vu que vous ne suivez aucun compte et\
|
||||||
|
\ que vous n’avez publié aucune note, pour l’instant."
|
||||||
|
step2_1: "Procédons d’abord à la préparation de votre profil avant d’écrire une\
|
||||||
|
\ note et/ou de vous abonner à un compte."
|
||||||
|
step2_2: "En fournissant quelques informations sur vous, il sera plus facile pour\
|
||||||
|
\ les autres de s’abonner à votre compte."
|
||||||
|
step3_1: "Vous avez fini de créer votre profil ?"
|
||||||
|
step3_2: "L’étape suivante consiste à créer une note. Vous pouvez commencer en cliquant\
|
||||||
|
\ sur l’icône crayon sur l’écran."
|
||||||
|
step3_3: "Remplissez le cadran et cliquez sur le bouton en haut à droite pour envoyer."
|
||||||
|
step3_4: "Vous n’avez rien à dire ? Essayez d’écrire « J’ai commencé à utiliser\
|
||||||
|
\ FoundKey » !"
|
||||||
|
step4_1: "Avez-vous publié votre première note ?"
|
||||||
|
step4_2: "Youpi ! Celle-ci est maintenant affichée sur votre fil d’actualité."
|
||||||
|
step5_1: "Maintenant, essayons de nous abonner à d’autres personnes afin de rendre\
|
||||||
|
\ votre fil plus vivant."
|
||||||
|
step5_2: "La page {featured} affiche les notes en tendance sur la présente instance\
|
||||||
|
\ et {explore} vous permet de trouver des utilisateur·rice·s en tendance. Essayez\
|
||||||
|
\ de vous abonner aux gens que vous aimez !"
|
||||||
|
step5_3: "Pour pouvoir suivre d’autres utilisateur·rice, cliquez sur leur avatar\
|
||||||
|
\ afin d’afficher la page du profil utilisateur ensuite appuyez sur le bouton\
|
||||||
|
\ « S’abonner »."
|
||||||
|
step5_4: "Si l’autre utilisateur possède une icône sous forme d’un cadenas à côté\
|
||||||
|
\ de son nom, il devra accepter votre demande d’abonnement manuellement."
|
||||||
|
step6_1: "Maintenant, vous êtes en mesure de voir s’afficher les notes des autres\
|
||||||
|
\ utilisateur·rice·s sur votre propre fil."
|
||||||
|
step6_2: "Vous avez également la possibilité d’intéragir rapidement avec les notes\
|
||||||
|
\ des autres utilisateur·rice·s en ajoutant des « réactions »."
|
||||||
|
step6_3: "Pour ajouter une réaction à une note, cliquez sur le signe « + » de celle-ci\
|
||||||
|
\ et sélectionnez l’émoji souhaité."
|
||||||
|
step7_1: "Félicitations ! Vous avez atteint la fin du tutoriel de base pour l’utilisation\
|
||||||
|
\ de FoundKey."
|
||||||
|
step7_2: "Si vous désirez en savoir plus sur FoundKey, jetez un œil sur la section\
|
||||||
|
\ {help}."
|
||||||
|
step7_3: "Bon courage et amusez-vous bien sur FoundKey ! \U0001F680"
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "Configuration déjà achevée."
|
alreadyRegistered: "Configuration déjà achevée."
|
||||||
registerDevice: "Ajouter un nouvel appareil"
|
registerDevice: "Ajouter un nouvel appareil"
|
||||||
|
|
|
@ -235,6 +235,7 @@ uploadFromUrl: "Unggah dari URL"
|
||||||
uploadFromUrlDescription: "URL berkas yang ingin kamu unggah"
|
uploadFromUrlDescription: "URL berkas yang ingin kamu unggah"
|
||||||
uploadFromUrlRequested: "Pengunggahan telah diminta"
|
uploadFromUrlRequested: "Pengunggahan telah diminta"
|
||||||
uploadFromUrlMayTakeTime: "Mungkin diperlukan waktu hingga unggahan selesai."
|
uploadFromUrlMayTakeTime: "Mungkin diperlukan waktu hingga unggahan selesai."
|
||||||
|
explore: "Jelajahi"
|
||||||
messageRead: "Telah dibaca"
|
messageRead: "Telah dibaca"
|
||||||
noMoreHistory: "Tidak ada sejarah lagi"
|
noMoreHistory: "Tidak ada sejarah lagi"
|
||||||
startMessaging: "Mulai mengirim pesan"
|
startMessaging: "Mulai mengirim pesan"
|
||||||
|
@ -316,6 +317,9 @@ inMb: "dalam Megabytes"
|
||||||
iconUrl: "URL Gambar ikon"
|
iconUrl: "URL Gambar ikon"
|
||||||
bannerUrl: "URL Banner"
|
bannerUrl: "URL Banner"
|
||||||
backgroundImageUrl: "URL Gambar latar"
|
backgroundImageUrl: "URL Gambar latar"
|
||||||
|
pinnedUsers: "Pengguna yang disematkan"
|
||||||
|
pinnedUsersDescription: "Tuliskan satu nama pengguna dalam satu baris. Pengguna yang\
|
||||||
|
\ dituliskan disini akan disematkan dalam bilah \"Jelajahi\"."
|
||||||
hcaptchaSiteKey: "Site Key"
|
hcaptchaSiteKey: "Site Key"
|
||||||
hcaptchaSecretKey: "Secret Key"
|
hcaptchaSecretKey: "Secret Key"
|
||||||
recaptchaSiteKey: "Site key"
|
recaptchaSiteKey: "Site key"
|
||||||
|
@ -340,6 +344,11 @@ silence: "Bungkam"
|
||||||
silenceConfirm: "Apakah kamu yakin ingin membungkam pengguna ini?"
|
silenceConfirm: "Apakah kamu yakin ingin membungkam pengguna ini?"
|
||||||
unsilence: "Hapus bungkam"
|
unsilence: "Hapus bungkam"
|
||||||
unsilenceConfirm: "Apakah kamu ingin untuk batal membungkam pengguna ini?"
|
unsilenceConfirm: "Apakah kamu ingin untuk batal membungkam pengguna ini?"
|
||||||
|
popularUsers: "Pengguna populer"
|
||||||
|
recentlyUpdatedUsers: "Pengguna dengan aktivitas terkini"
|
||||||
|
recentlyRegisteredUsers: "Pengguna baru saja bergabung"
|
||||||
|
recentlyDiscoveredUsers: "Pengguna baru saja dilihat"
|
||||||
|
popularTags: "Tag populer"
|
||||||
userList: "Daftar"
|
userList: "Daftar"
|
||||||
aboutMisskey: "Tentang FoundKey"
|
aboutMisskey: "Tentang FoundKey"
|
||||||
administrator: "Admin"
|
administrator: "Admin"
|
||||||
|
@ -380,6 +389,7 @@ messagingWithGroup: "Obrolan di dalam grup"
|
||||||
title: "Judul"
|
title: "Judul"
|
||||||
text: "Teks"
|
text: "Teks"
|
||||||
enable: "Aktifkan"
|
enable: "Aktifkan"
|
||||||
|
next: "Selanjutnya"
|
||||||
retype: "Masukkan ulang"
|
retype: "Masukkan ulang"
|
||||||
noteOf: "Catatan milik {user}"
|
noteOf: "Catatan milik {user}"
|
||||||
inviteToGroup: "Undang ke grup"
|
inviteToGroup: "Undang ke grup"
|
||||||
|
@ -575,6 +585,7 @@ abuseReports: "Laporkan"
|
||||||
reportAbuse: "Laporkan"
|
reportAbuse: "Laporkan"
|
||||||
reportAbuseOf: "Laporkan {name}"
|
reportAbuseOf: "Laporkan {name}"
|
||||||
fillAbuseReportDescription: "Mohon isi rincian laporan."
|
fillAbuseReportDescription: "Mohon isi rincian laporan."
|
||||||
|
abuseReported: "Laporan kamu telah dikirimkan. Terima kasih."
|
||||||
reporter: "Pelapor"
|
reporter: "Pelapor"
|
||||||
reporteeOrigin: "Yang dilaporkan"
|
reporteeOrigin: "Yang dilaporkan"
|
||||||
reporterOrigin: "Pelapor"
|
reporterOrigin: "Pelapor"
|
||||||
|
@ -978,6 +989,44 @@ _time:
|
||||||
minute: "menit"
|
minute: "menit"
|
||||||
hour: "jam"
|
hour: "jam"
|
||||||
day: "hari"
|
day: "hari"
|
||||||
|
_tutorial:
|
||||||
|
title: "Cara menggunakan FoundKey"
|
||||||
|
step1_1: "Selamat datang!"
|
||||||
|
step1_2: "Halaman ini disebut \"linimasa\". Halaman ini menampilkan \"catatan\"\
|
||||||
|
\ yang diurutkan secara kronologis dari orang-orang yang kamu \"ikuti\"."
|
||||||
|
step1_3: "Linimasa kamu kosong, karena kamu belum mencatat catatan apapun atau mengikuti\
|
||||||
|
\ siapapun."
|
||||||
|
step2_1: "Selesaikan menyetel profilmu sebelum menulis sebuah catatan atau mengikuti\
|
||||||
|
\ seseorang."
|
||||||
|
step2_2: "Menyediakan beberapa informasi tentang siapa kamu akan membuat orang lain\
|
||||||
|
\ mudah untuk mengikutimu kembali."
|
||||||
|
step3_1: "Selesai menyetel profil kamu?"
|
||||||
|
step3_2: "Langkah selanjutnya adalah membuat catatan. Kamu bisa lakukan ini dengan\
|
||||||
|
\ mengklik ikon pensil pada layar kamu."
|
||||||
|
step3_3: "Isilah di dalam modal dan tekan tombol pada atas kanan untuk memcatat\
|
||||||
|
\ catatan kamu."
|
||||||
|
step3_4: "Bingung tidak berpikiran untuk mengatakan sesuatu? Coba saja \"baru aja\
|
||||||
|
\ ikutan bikin akun misskey punyaku\"!"
|
||||||
|
step4_1: "Selesai mencatat catatan pertamamu?"
|
||||||
|
step4_2: "Horee! Sekarang catatan pertamamu sudah ditampilkan di linimasa milikmu."
|
||||||
|
step5_1: "Sekarang, mari mencoba untuk membuat linimasamu lebih hidup dengan mengikuti\
|
||||||
|
\ orang lain."
|
||||||
|
step5_2: "{featured} akan memperlihatkan catatan yang sedang tren saat ini untuk\
|
||||||
|
\ kamu. {explore} akan membantumu untuk mencari pengguna yang sedang tren juga\
|
||||||
|
\ saat ini. Coba ikuti seseorang yang kamu suka!"
|
||||||
|
step5_3: "Untuk mengikuti pengguna lain, klik pada ikon mereka dan tekan tombol\
|
||||||
|
\ follow pada profil mereka."
|
||||||
|
step5_4: "Jika pengguna lain memiliki ikon gembok di sebelah nama mereka, maka pengguna\
|
||||||
|
\ rersebut harus menyetujui permintaan mengikuti dari kamu secara manual."
|
||||||
|
step6_1: "Sekarang kamu dapat melihat catatan pengguna lain pada linimasamu."
|
||||||
|
step6_2: "Kamu juga bisa memberikan \"reaksi\" ke catatan orang lain untuk merespon\
|
||||||
|
\ dengan cepat."
|
||||||
|
step6_3: "Untuk memberikan \"reaksi\", tekan tanda \"+\" pada catatan pengguna lain\
|
||||||
|
\ dan pilih emoji yang kamu suka untuk memberikan reaksimu kepada mereka."
|
||||||
|
step7_1: "Yay, Selamat! Kamu sudah menyelesaikan tutorial dasar FoundKey."
|
||||||
|
step7_2: "Jika kamu ingin mempelajari lebih lanjut tentang FoundKey, cobalah berkunjung\
|
||||||
|
\ ke bagian {help}."
|
||||||
|
step7_3: "Semoga berhasil dan bersenang-senanglah! \U0001F680"
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "Kamu telah mendaftarkan perangkat otentikasi dua faktor."
|
alreadyRegistered: "Kamu telah mendaftarkan perangkat otentikasi dua faktor."
|
||||||
registerDevice: "Daftarkan perangkat baru"
|
registerDevice: "Daftarkan perangkat baru"
|
||||||
|
|
|
@ -27,6 +27,7 @@ const languages = [
|
||||||
'id-ID',
|
'id-ID',
|
||||||
'it-IT',
|
'it-IT',
|
||||||
'ja-JP',
|
'ja-JP',
|
||||||
|
'ja-KS',
|
||||||
'kab-KAB',
|
'kab-KAB',
|
||||||
'kn-IN',
|
'kn-IN',
|
||||||
'ko-KR',
|
'ko-KR',
|
||||||
|
|
|
@ -229,6 +229,7 @@ uploadFromUrl: "Incolla URL immagine"
|
||||||
uploadFromUrlDescription: "URL del file che vuoi caricare"
|
uploadFromUrlDescription: "URL del file che vuoi caricare"
|
||||||
uploadFromUrlRequested: "Caricamento richiesto"
|
uploadFromUrlRequested: "Caricamento richiesto"
|
||||||
uploadFromUrlMayTakeTime: "Il caricamento del file può richiedere tempo."
|
uploadFromUrlMayTakeTime: "Il caricamento del file può richiedere tempo."
|
||||||
|
explore: "Esplora"
|
||||||
messageRead: "Visualizzato"
|
messageRead: "Visualizzato"
|
||||||
noMoreHistory: "Non c'è più cronologia da visualizzare"
|
noMoreHistory: "Non c'è più cronologia da visualizzare"
|
||||||
startMessaging: "Nuovo messaggio"
|
startMessaging: "Nuovo messaggio"
|
||||||
|
@ -310,6 +311,9 @@ inMb: "in Megabytes"
|
||||||
iconUrl: "URL di icona (favicon, ecc.)"
|
iconUrl: "URL di icona (favicon, ecc.)"
|
||||||
bannerUrl: "URL dell'immagine d'intestazione"
|
bannerUrl: "URL dell'immagine d'intestazione"
|
||||||
backgroundImageUrl: "URL dello sfondo"
|
backgroundImageUrl: "URL dello sfondo"
|
||||||
|
pinnedUsers: "Utenti in evidenza"
|
||||||
|
pinnedUsersDescription: "Elenca gli/le utenti che vuoi fissare in cima alla pagina\
|
||||||
|
\ \"Esplora\", un@ per riga."
|
||||||
hcaptchaSiteKey: "Chiave del sito"
|
hcaptchaSiteKey: "Chiave del sito"
|
||||||
hcaptchaSecretKey: "Chiave segreta"
|
hcaptchaSecretKey: "Chiave segreta"
|
||||||
recaptchaSiteKey: "Chiave del sito"
|
recaptchaSiteKey: "Chiave del sito"
|
||||||
|
@ -334,6 +338,11 @@ silence: "Silenzia"
|
||||||
silenceConfirm: "Vuoi davvero silenziare l'utente?"
|
silenceConfirm: "Vuoi davvero silenziare l'utente?"
|
||||||
unsilence: "Riattiva"
|
unsilence: "Riattiva"
|
||||||
unsilenceConfirm: "Vuoi davvero riattivare l'utente?"
|
unsilenceConfirm: "Vuoi davvero riattivare l'utente?"
|
||||||
|
popularUsers: "Utenti popolari"
|
||||||
|
recentlyUpdatedUsers: "Utenti attivi di recente"
|
||||||
|
recentlyRegisteredUsers: "Utenti registrati di recente"
|
||||||
|
recentlyDiscoveredUsers: "Utenti scoperti di recente"
|
||||||
|
popularTags: "Tag di tendenza"
|
||||||
userList: "Liste"
|
userList: "Liste"
|
||||||
aboutMisskey: "Informazioni di FoundKey"
|
aboutMisskey: "Informazioni di FoundKey"
|
||||||
administrator: "Amministratore"
|
administrator: "Amministratore"
|
||||||
|
@ -374,6 +383,7 @@ messagingWithGroup: "Chattare in gruppo"
|
||||||
title: "Titolo"
|
title: "Titolo"
|
||||||
text: "Testo"
|
text: "Testo"
|
||||||
enable: "Abilita"
|
enable: "Abilita"
|
||||||
|
next: "Avanti"
|
||||||
retype: "Conferma"
|
retype: "Conferma"
|
||||||
noteOf: "Note di {user}"
|
noteOf: "Note di {user}"
|
||||||
inviteToGroup: "Invitare al gruppo"
|
inviteToGroup: "Invitare al gruppo"
|
||||||
|
@ -564,6 +574,7 @@ abuseReports: "Segnalazioni"
|
||||||
reportAbuse: "Segnalazioni"
|
reportAbuse: "Segnalazioni"
|
||||||
reportAbuseOf: "Segnala {name}"
|
reportAbuseOf: "Segnala {name}"
|
||||||
fillAbuseReportDescription: "Si prega di spiegare il motivo della segnalazione."
|
fillAbuseReportDescription: "Si prega di spiegare il motivo della segnalazione."
|
||||||
|
abuseReported: "La segnalazione è stata inviata. Grazie."
|
||||||
reporter: "il corrispondente"
|
reporter: "il corrispondente"
|
||||||
reporteeOrigin: "Origine del segnalato"
|
reporteeOrigin: "Origine del segnalato"
|
||||||
reporterOrigin: "Origine del segnalatore"
|
reporterOrigin: "Origine del segnalatore"
|
||||||
|
@ -890,6 +901,45 @@ _time:
|
||||||
minute: "min"
|
minute: "min"
|
||||||
hour: "ore"
|
hour: "ore"
|
||||||
day: "giorni"
|
day: "giorni"
|
||||||
|
_tutorial:
|
||||||
|
title: "Come usare FoundKey"
|
||||||
|
step1_1: "Benvenuto/a!"
|
||||||
|
step1_2: "Questa pagina si chiama una \" Timeline \". Mostra in ordine cronologico\
|
||||||
|
\ le \" note \" delle persone che segui."
|
||||||
|
step1_3: "Attualmente la tua Timeline è vuota perché non segui alcun account e non\
|
||||||
|
\ hai pubblicato alcuna nota ancora."
|
||||||
|
step2_1: "Prima di scrivere una nota o di seguire un account, imposta il tuo profilo!"
|
||||||
|
step2_2: "Aggiungere qualche informazione su di te aumenterà le tue possibilità\
|
||||||
|
\ di essere seguit@ da altre persone."
|
||||||
|
step3_1: "Hai finito di impostare il tuo profilo?"
|
||||||
|
step3_2: "Ora, puoi pubblicare una nota. Facciamo una prova! Premi il pulsante a\
|
||||||
|
\ forma di penna in cima allo schermo per aprire una finestra di dialogo."
|
||||||
|
step3_3: "Scritto il testo della nota, puoi pubblicarla premendo il pulsante nella\
|
||||||
|
\ parte superiore destra della finestra di dialogo."
|
||||||
|
step3_4: "Non ti viene niente in mente? Perché non scrivi semplicemente \"Ho appena\
|
||||||
|
\ cominciato a usare FoundKey\"?"
|
||||||
|
step4_1: "Hai pubblicato qualcosa?"
|
||||||
|
step4_2: "Se puoi visualizzare la tua nota sulla timeline, ce l'hai fatta!"
|
||||||
|
step5_1: "Adesso, cerca di seguire altre persone per vivacizzare la tua timeline."
|
||||||
|
step5_2: "La pagina {featured} mostra le note di tendenza su questa istanza, e magari\
|
||||||
|
\ ti aiuterà a trovare account che ti piacciono e che vorrai seguire. Oppure,\
|
||||||
|
\ potrai trovare utenti popolari usando {explore}."
|
||||||
|
step5_3: "Per seguire altrə utenti, clicca sul loro avatar per aprire la pagina\
|
||||||
|
\ di profilo dove puoi premere il pulsante \"Seguire\"."
|
||||||
|
step5_4: "Alcunə utenti scelgono di confermare manualmente le richieste di follow\
|
||||||
|
\ che ricevono, quindi a seconda delle persone potrebbe volerci un pò prima che\
|
||||||
|
\ la tua richiesta sia accolta."
|
||||||
|
step6_1: "Ora, se puoi visualizzare le note di altrə utenti sulla tua timeline,\
|
||||||
|
\ ce l'hai fatta!"
|
||||||
|
step6_2: "Puoi inviare una risposta rapida alle note di altrə utenti mandando loro\
|
||||||
|
\ \"reazioni\"."
|
||||||
|
step6_3: "Per inviare una reazione, premi l'icona + della nota e scegli l'emoji\
|
||||||
|
\ che vuoi mandare."
|
||||||
|
step7_1: "Complimenti! Sei arrivat@ alla fine dell'esercitazione di base su come\
|
||||||
|
\ usare FoundKey."
|
||||||
|
step7_2: "Se vuoi saperne di più su FoundKey, puoi dare un'occhiata alla sezione\
|
||||||
|
\ {help}."
|
||||||
|
step7_3: "Da ultimo, buon divertimento su FoundKey! \U0001F680"
|
||||||
_2fa:
|
_2fa:
|
||||||
registerDevice: "Aggiungi dispositivo"
|
registerDevice: "Aggiungi dispositivo"
|
||||||
_permissions:
|
_permissions:
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
_lang_: "日本語"
|
_lang_: "日本語"
|
||||||
|
|
||||||
headlineMisskey: "ノートでつながるネットワーク"
|
headlineMisskey: "ノートでつながるネットワーク"
|
||||||
introMisskey: "ようこそ!FoundKeyは、オープンソースの分散型マイクロブログサービスです。\n「ノート」を作成して、いま起こっていることを共有したり、あなたについて皆に発信しよう📡\n\
|
introMisskey: "ようこそ!FoundKeyは、オープンソースの分散型マイクロブログサービスです。\n「ノート」を作成して、いま起こっていることを共有したり、あなたについて皆に発信しよう\U0001F4E1\
|
||||||
「リアクション」機能で、皆のノートに素早く反応を追加することもできます👍\n新しい世界を探検しよう🚀"
|
\n「リアクション」機能で、皆のノートに素早く反応を追加することもできます\U0001F44D\n新しい世界を探検しよう\U0001F680"
|
||||||
monthAndDay: "{month}月 {day}日"
|
monthAndDay: "{month}月 {day}日"
|
||||||
search: "検索"
|
search: "検索"
|
||||||
notifications: "通知"
|
notifications: "通知"
|
||||||
|
@ -213,6 +213,7 @@ uploadFromUrl: "URLアップロード"
|
||||||
uploadFromUrlDescription: "アップロードしたいファイルのURL"
|
uploadFromUrlDescription: "アップロードしたいファイルのURL"
|
||||||
uploadFromUrlRequested: "アップロードをリクエストしました"
|
uploadFromUrlRequested: "アップロードをリクエストしました"
|
||||||
uploadFromUrlMayTakeTime: "アップロードが完了するまで時間がかかる場合があります。"
|
uploadFromUrlMayTakeTime: "アップロードが完了するまで時間がかかる場合があります。"
|
||||||
|
explore: "みつける"
|
||||||
messageRead: "既読"
|
messageRead: "既読"
|
||||||
noMoreHistory: "これより過去の履歴はありません"
|
noMoreHistory: "これより過去の履歴はありません"
|
||||||
startMessaging: "チャットを開始"
|
startMessaging: "チャットを開始"
|
||||||
|
@ -291,6 +292,8 @@ inMb: "メガバイト単位"
|
||||||
iconUrl: "アイコン画像のURL (faviconなど)"
|
iconUrl: "アイコン画像のURL (faviconなど)"
|
||||||
bannerUrl: "バナー画像のURL"
|
bannerUrl: "バナー画像のURL"
|
||||||
backgroundImageUrl: "背景画像のURL"
|
backgroundImageUrl: "背景画像のURL"
|
||||||
|
pinnedUsers: "ピン留めユーザー"
|
||||||
|
pinnedUsersDescription: "「みつける」ページなどにピン留めしたいユーザーを改行で区切って記述します。"
|
||||||
hcaptchaSiteKey: "サイトキー"
|
hcaptchaSiteKey: "サイトキー"
|
||||||
hcaptchaSecretKey: "シークレットキー"
|
hcaptchaSecretKey: "シークレットキー"
|
||||||
recaptchaSiteKey: "サイトキー"
|
recaptchaSiteKey: "サイトキー"
|
||||||
|
@ -314,6 +317,11 @@ silence: "サイレンス"
|
||||||
silenceConfirm: "サイレンスしますか?"
|
silenceConfirm: "サイレンスしますか?"
|
||||||
unsilence: "サイレンス解除"
|
unsilence: "サイレンス解除"
|
||||||
unsilenceConfirm: "サイレンス解除しますか?"
|
unsilenceConfirm: "サイレンス解除しますか?"
|
||||||
|
popularUsers: "人気のユーザー"
|
||||||
|
recentlyUpdatedUsers: "最近投稿したユーザー"
|
||||||
|
recentlyRegisteredUsers: "最近登録したユーザー"
|
||||||
|
recentlyDiscoveredUsers: "最近発見されたユーザー"
|
||||||
|
popularTags: "人気のタグ"
|
||||||
userList: "リスト"
|
userList: "リスト"
|
||||||
aboutMisskey: "FoundKeyについて"
|
aboutMisskey: "FoundKeyについて"
|
||||||
administrator: "管理者"
|
administrator: "管理者"
|
||||||
|
@ -354,6 +362,7 @@ messagingWithGroup: "グループでチャット"
|
||||||
title: "タイトル"
|
title: "タイトル"
|
||||||
text: "テキスト"
|
text: "テキスト"
|
||||||
enable: "有効にする"
|
enable: "有効にする"
|
||||||
|
next: "次"
|
||||||
retype: "再入力"
|
retype: "再入力"
|
||||||
noteOf: "{user}のノート"
|
noteOf: "{user}のノート"
|
||||||
inviteToGroup: "グループに招待"
|
inviteToGroup: "グループに招待"
|
||||||
|
@ -405,8 +414,8 @@ showFeaturedNotesInTimeline: "タイムラインにおすすめのノートを
|
||||||
objectStorage: "オブジェクトストレージ"
|
objectStorage: "オブジェクトストレージ"
|
||||||
useObjectStorage: "オブジェクトストレージを使用"
|
useObjectStorage: "オブジェクトストレージを使用"
|
||||||
objectStorageBaseUrl: "Base URL"
|
objectStorageBaseUrl: "Base URL"
|
||||||
objectStorageBaseUrlDesc: "参照に使用するURL。CDNやProxyを使用している場合はそのURL。\nS3: 'https://<bucket>.s3.amazonaws.com'、GCS等:
|
objectStorageBaseUrlDesc: "参照に使用するURL。CDNやProxyを使用している場合はそのURL。\nS3: 'https://<bucket>.s3.amazonaws.com'、GCS等:\
|
||||||
'https://storage.googleapis.com/<bucket>'。"
|
\ 'https://storage.googleapis.com/<bucket>'。"
|
||||||
objectStorageBucket: "Bucket"
|
objectStorageBucket: "Bucket"
|
||||||
objectStorageBucketDesc: "使用サービスのbucket名を指定してください。"
|
objectStorageBucketDesc: "使用サービスのbucket名を指定してください。"
|
||||||
objectStoragePrefix: "Prefix"
|
objectStoragePrefix: "Prefix"
|
||||||
|
@ -528,6 +537,7 @@ abuseReports: "通報"
|
||||||
reportAbuse: "通報"
|
reportAbuse: "通報"
|
||||||
reportAbuseOf: "{name}を通報する"
|
reportAbuseOf: "{name}を通報する"
|
||||||
fillAbuseReportDescription: "通報理由の詳細を記入してください。"
|
fillAbuseReportDescription: "通報理由の詳細を記入してください。"
|
||||||
|
abuseReported: "内容が送信されました。ご報告ありがとうございました。"
|
||||||
reporter: "通報者"
|
reporter: "通報者"
|
||||||
reporteeOrigin: "通報先"
|
reporteeOrigin: "通報先"
|
||||||
reporterOrigin: "通報元"
|
reporterOrigin: "通報元"
|
||||||
|
@ -928,6 +938,30 @@ _time:
|
||||||
hour: "時間"
|
hour: "時間"
|
||||||
day: "日"
|
day: "日"
|
||||||
|
|
||||||
|
_tutorial:
|
||||||
|
title: "FoundKeyの使い方"
|
||||||
|
step1_1: "ようこそ!"
|
||||||
|
step1_2: "この画面は「タイムライン」と呼ばれ、あなたや、あなたが「フォロー」する人の「ノート」が時系列で表示されます。"
|
||||||
|
step1_3: "あなたはまだ何もノートを投稿しておらず、誰もフォローしていないので、タイムラインには何も表示されていないはずです。"
|
||||||
|
step2_1: "ノートを作成したり誰かをフォローしたりする前に、まずあなたのプロフィールを完成させましょう。"
|
||||||
|
step2_2: "あなたがどんな人かわかると、多くの人にノートを見てもらえたり、フォローしてもらいやすくなります。"
|
||||||
|
step3_1: "プロフィール設定はうまくできましたか?"
|
||||||
|
step3_2: "では試しに、何かノートを投稿してみてください。画面上にある鉛筆マークのボタンを押すとフォームが開きます。"
|
||||||
|
step3_3: "内容を書いたら、フォーム右上のボタンを押すと投稿できます。"
|
||||||
|
step3_4: "内容が思いつかない?「FoundKey始めました」というのはいかがでしょう!"
|
||||||
|
step4_1: "投稿できましたか?"
|
||||||
|
step4_2: "あなたのノートがタイムラインに表示されていれば成功です。"
|
||||||
|
step5_1: "次は、他の人をフォローしてタイムラインを賑やかにしたいところです。"
|
||||||
|
step5_2: "{featured}で人気のノートが見れるので、その中から気になった人を選んでフォローしたり、{explore}で人気のユーザーを探すこともできます!"
|
||||||
|
step5_3: "ユーザーをフォローするには、ユーザーのアイコンをクリックしてユーザーページを表示し、「フォロー」ボタンを押します。"
|
||||||
|
step5_4: "ユーザーによっては、フォローが承認されるまで時間がかかる場合があります。"
|
||||||
|
step6_1: "タイムラインに他のユーザーのノートが表示されていれば成功です。"
|
||||||
|
step6_2: "他の人のノートには、「リアクション」を付けることができ、簡単にあなたの反応を伝えられます。"
|
||||||
|
step6_3: "リアクションを付けるには、ノートの「+」マークをクリックして、好きなリアクションを選択します。"
|
||||||
|
step7_1: "これで、FoundKeyの基本的な使い方の説明は終わりました。お疲れ様でした。"
|
||||||
|
step7_2: "もっとFoundKeyについて知りたいときは、{help}を見てみてください。"
|
||||||
|
step7_3: "では、FoundKeyをお楽しみください\U0001F680"
|
||||||
|
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "既に設定は完了しています。"
|
alreadyRegistered: "既に設定は完了しています。"
|
||||||
registerDevice: "デバイスを登録"
|
registerDevice: "デバイスを登録"
|
||||||
|
@ -1123,7 +1157,6 @@ _timelines:
|
||||||
social: "ソーシャル"
|
social: "ソーシャル"
|
||||||
global: "グローバル"
|
global: "グローバル"
|
||||||
|
|
||||||
shuffled: シャッフル
|
|
||||||
_pages:
|
_pages:
|
||||||
newPage: "ページの作成"
|
newPage: "ページの作成"
|
||||||
editPage: "ページの編集"
|
editPage: "ページの編集"
|
||||||
|
@ -1190,7 +1223,6 @@ _notification:
|
||||||
app: "連携アプリからの通知"
|
app: "連携アプリからの通知"
|
||||||
|
|
||||||
move: 自分以外のアカウントの引っ越し
|
move: 自分以外のアカウントの引っ越し
|
||||||
update: ウォッチ中のノートが更新された
|
|
||||||
_actions:
|
_actions:
|
||||||
followBack: "フォローバック"
|
followBack: "フォローバック"
|
||||||
reply: "返信"
|
reply: "返信"
|
||||||
|
|
801
locales/ja-KS.yml
Normal file
801
locales/ja-KS.yml
Normal file
|
@ -0,0 +1,801 @@
|
||||||
|
_lang_: "日本語 (関西弁)"
|
||||||
|
headlineMisskey: "ノートでつながるネットワーク"
|
||||||
|
introMisskey: "ようお越し!FoundKeyは、オープンソースの分散型マイクロブログサービスやねん。\n「ノート」を作って、いま起こっとることを共有したり、あんたについて皆に発信しよう\U0001F4E1\
|
||||||
|
\n「リアクション」機能で、皆のノートに素早く反応を追加したりもできるで✌\nほな新しい世界を探検しよか\U0001F680"
|
||||||
|
monthAndDay: "{month}月 {day}日"
|
||||||
|
search: "探す"
|
||||||
|
notifications: "通知"
|
||||||
|
username: "ユーザー名"
|
||||||
|
password: "パスワード"
|
||||||
|
forgotPassword: "パスワード忘れてん"
|
||||||
|
fetchingAsApObject: "今ちと連合に照会しとるで"
|
||||||
|
ok: "OKや"
|
||||||
|
gotIt: "ほい"
|
||||||
|
cancel: "やめとく"
|
||||||
|
renotedBy: "{user}がRenote"
|
||||||
|
noNotes: "ノートはあらへん"
|
||||||
|
noNotifications: "通知はあらへん"
|
||||||
|
instance: "インスタンス"
|
||||||
|
settings: "設定"
|
||||||
|
basicSettings: "基本設定"
|
||||||
|
otherSettings: "その他の設定"
|
||||||
|
openInWindow: "ウィンドウで開くで"
|
||||||
|
profile: "プロフィール"
|
||||||
|
timeline: "タイムライン"
|
||||||
|
noAccountDescription: "自己紹介食ってもた"
|
||||||
|
login: "ログイン"
|
||||||
|
loggingIn: "ログインしよるで"
|
||||||
|
logout: "ログアウト"
|
||||||
|
signup: "新規登録"
|
||||||
|
save: "保存"
|
||||||
|
users: "ユーザー"
|
||||||
|
addUser: "ユーザーを追加や"
|
||||||
|
pin: "ピン留めしとく"
|
||||||
|
unpin: "やっぱピン留めせん"
|
||||||
|
copyContent: "内容をコピー"
|
||||||
|
copyLink: "リンクをコピー"
|
||||||
|
delete: "ほかす"
|
||||||
|
deleteAndEdit: "ほかして直す"
|
||||||
|
deleteAndEditConfirm: "このノートをほかして書き直すんか?このノートへのリアクション、Renote、返信も全部消えてまうで。"
|
||||||
|
addToList: "リストに入れたる"
|
||||||
|
sendMessage: "メッセージを送る"
|
||||||
|
copyUsername: "ユーザー名をコピー"
|
||||||
|
reply: "返事"
|
||||||
|
loadMore: "まだまだあるで!"
|
||||||
|
showMore: "まだまだあるで!"
|
||||||
|
youGotNewFollower: "フォローされたで"
|
||||||
|
receiveFollowRequest: "フォローリクエストされたで"
|
||||||
|
followRequestAccepted: "フォローが承認されたで"
|
||||||
|
mention: "メンション"
|
||||||
|
mentions: "うち宛て"
|
||||||
|
directNotes: "ダイレクト投稿"
|
||||||
|
importAndExport: "インポートとエクスポート"
|
||||||
|
import: "インポート"
|
||||||
|
export: "エクスポート"
|
||||||
|
files: "ファイル"
|
||||||
|
download: "ダウンロード"
|
||||||
|
driveFileDeleteConfirm: "ファイル「{name}」を消してしもうてええか?このファイルを添付したノートも消えてまうで。"
|
||||||
|
unfollowConfirm: "{name}のフォローを解除してもええんか?"
|
||||||
|
exportRequested: "エクスポートしてな、ってリクエストしたけど、これ多分めっちゃ時間かかるで。エクスポート終わったら「ドライブ」に突っ込んどくで。"
|
||||||
|
importRequested: "インポートしてな、ってリクエストしたけど、これ多分めっちゃ時間かかるで。"
|
||||||
|
lists: "リスト"
|
||||||
|
note: "ノート"
|
||||||
|
notes: "ノート"
|
||||||
|
following: "フォロー"
|
||||||
|
followers: "フォロワー"
|
||||||
|
followsYou: "フォローされとるで"
|
||||||
|
createList: "リスト作る"
|
||||||
|
manageLists: "リストの管理"
|
||||||
|
error: "エラー"
|
||||||
|
somethingHappened: "なんかアカンことが起こったで"
|
||||||
|
retry: "もっぺんやる?"
|
||||||
|
pageLoadError: "ページの読み込みに失敗してしもうたで…"
|
||||||
|
pageLoadErrorDescription: "これは普通、ネットワークかブラウザキャッシュが原因やからね。キャッシュをクリアするか、もうちっとだけ待ってくれへんか?"
|
||||||
|
enterListName: "リスト名を入れてや"
|
||||||
|
privacy: "プライバシー"
|
||||||
|
makeFollowManuallyApprove: "自分が認めた人だけがこのアカウントをフォローできるようにする"
|
||||||
|
defaultNoteVisibility: "もとからの公開範囲"
|
||||||
|
follow: "フォロー"
|
||||||
|
followRequest: "フォローを頼む"
|
||||||
|
followRequests: "フォロー申請"
|
||||||
|
unfollow: "フォローやめる"
|
||||||
|
followRequestPending: "フォロー許してくれるん待っとる"
|
||||||
|
renote: "Renote"
|
||||||
|
unrenote: "Renoteやめる"
|
||||||
|
quote: "引用"
|
||||||
|
pinnedNote: "ピン留めされとるノート"
|
||||||
|
you: "あんた"
|
||||||
|
clickToShow: "押したら見えるで"
|
||||||
|
sensitive: "ちょっとアカンやつやで"
|
||||||
|
add: "増やす"
|
||||||
|
reaction: "リアクション"
|
||||||
|
reactionSettingDescription2: "ドラッグで並び替え、クリックで削除、+を押して追加やで。"
|
||||||
|
attachCancel: "のっけるのやめる"
|
||||||
|
markAsSensitive: "ちょっとこれはアカン"
|
||||||
|
unmarkAsSensitive: "そこまでアカンことないやろ"
|
||||||
|
enterFileName: "ファイル名を入れてや"
|
||||||
|
mute: "ミュート"
|
||||||
|
unmute: "ミュートやめたる"
|
||||||
|
block: "ブロック"
|
||||||
|
unblock: "ブロックやめたる"
|
||||||
|
suspend: "凍結"
|
||||||
|
unsuspend: "溶かす"
|
||||||
|
blockConfirm: "ブロックしてもええんか?"
|
||||||
|
unblockConfirm: "ブロックやめたるってほんまか?"
|
||||||
|
suspendConfirm: "凍結してしもうてええか?"
|
||||||
|
unsuspendConfirm: "解凍するけどええか?"
|
||||||
|
selectList: "リストを選ぶ"
|
||||||
|
selectAntenna: "アンテナを選ぶ"
|
||||||
|
selectWidget: "ウィジェットを選ぶ"
|
||||||
|
editWidgets: "ウィジェットをいじる"
|
||||||
|
editWidgetsExit: "編集終ったで"
|
||||||
|
customEmojis: "カスタム絵文字"
|
||||||
|
emoji: "絵文字"
|
||||||
|
emojis: "絵文字"
|
||||||
|
addEmoji: "絵文字を追加"
|
||||||
|
cacheRemoteFiles: "リモートのファイルをキャッシュする"
|
||||||
|
cacheRemoteFilesDescription: "この設定を切っとくと、リモートファイルをキャッシュせず直リンクするようになるで。サーバーの容量は節約できるけど、サムネイルが作られんくなるから通信量が増えるで。"
|
||||||
|
flagAsBot: "Botやで"
|
||||||
|
flagAsBotDescription: "もしこのアカウントがプログラムによって運用されるんやったら、このフラグをオンにしてたのむで。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、FoundKeyのシステム上での扱いがBotに合ったもんになるんやで。"
|
||||||
|
flagAsCat: "Catやで"
|
||||||
|
flagAsCatDescription: "ワレ、猫ちゃんならこのフラグをつけてみ?"
|
||||||
|
autoAcceptFollowed: "フォローしとるユーザーからのフォローリクエストを勝手に許可しとく"
|
||||||
|
addAccount: "アカウントを追加"
|
||||||
|
loginFailed: "ログインに失敗してしもうた…"
|
||||||
|
showOnRemote: "リモートで見る"
|
||||||
|
general: "全般"
|
||||||
|
setWallpaper: "壁紙を設定"
|
||||||
|
removeWallpaper: "壁紙を削除"
|
||||||
|
youHaveNoLists: "リストがあらへんで?"
|
||||||
|
followConfirm: "{name}をフォローしてええか?"
|
||||||
|
proxyAccount: "プロキシアカウント"
|
||||||
|
proxyAccountDescription: "プロキシアカウントは、代わりにフォローしてくれるアカウントや。例えば、551に豚まんが無いときやったり、ユーザーがリモートユーザーをアカウントに入れたとき、リストに入れられたユーザーが誰からもフォローされてないと寂しいやん。寂しいし、アクティビティも配達されへんから、プロキシアカウントがフォローしてくれるで。ええやつやん…"
|
||||||
|
host: "ホスト"
|
||||||
|
selectUser: "ユーザーを選ぶ"
|
||||||
|
recipient: "宛先"
|
||||||
|
annotation: "注釈"
|
||||||
|
federation: "連合"
|
||||||
|
registeredAt: "初観測"
|
||||||
|
latestRequestSentAt: "ちょっと前のリクエスト送信"
|
||||||
|
latestRequestReceivedAt: "ちょっと前のリクエスト受信"
|
||||||
|
latestStatus: "ちょっと前のステータス"
|
||||||
|
charts: "チャート"
|
||||||
|
perHour: "1時間ごと"
|
||||||
|
perDay: "1日ごと"
|
||||||
|
stopActivityDelivery: "アクティビティの配送をやめる"
|
||||||
|
blockThisInstance: "このインスタンスをブロック"
|
||||||
|
software: "ソフトウェア"
|
||||||
|
version: "バージョン"
|
||||||
|
withNFiles: "{n}個のファイル"
|
||||||
|
jobQueue: "ジョブキュー"
|
||||||
|
instanceInfo: "インスタンス情報"
|
||||||
|
statistics: "統計"
|
||||||
|
clearQueue: "キューにさいなら"
|
||||||
|
clearQueueConfirmTitle: "キューをクリアしまっか?"
|
||||||
|
clearQueueConfirmText: "未配達の投稿は配送されなくなるで。通常この操作を行う必要はあらへんや。"
|
||||||
|
clearCachedFiles: "キャッシュにさいなら"
|
||||||
|
clearCachedFilesConfirm: "キャッシュされとるリモートファイルをみんなほかしてええか?"
|
||||||
|
blockedInstances: "インスタンスブロック"
|
||||||
|
blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定してな。ブロックされてもうたインスタンスとはもう金輪際やり取りできひんくなるで。"
|
||||||
|
muteAndBlock: "ミュートとブロック"
|
||||||
|
mutedUsers: "ミュートしたユーザー"
|
||||||
|
blockedUsers: "ブロックしたユーザー"
|
||||||
|
noUsers: "ユーザーはおらへん"
|
||||||
|
editProfile: "プロフィールをいじる"
|
||||||
|
noteDeleteConfirm: "このノートを削除しまっか?"
|
||||||
|
pinLimitExceeded: "これ以上ピン留めできひん"
|
||||||
|
intro: "FoundKeyのインストールが完了してん!管理者アカウントを作ってや。"
|
||||||
|
done: "でけた"
|
||||||
|
processing: "処理しとる"
|
||||||
|
preview: "プレビュー"
|
||||||
|
default: "デフォルト"
|
||||||
|
noCustomEmojis: "絵文字はあらへん"
|
||||||
|
noJobs: "ジョブはあらへん"
|
||||||
|
federating: "連合しとる"
|
||||||
|
blocked: "ブロックしとる"
|
||||||
|
suspended: "配信せぇへん"
|
||||||
|
all: "みんな"
|
||||||
|
subscribing: "購読しとる"
|
||||||
|
publishing: "配信しとる"
|
||||||
|
notResponding: "応答してへんで"
|
||||||
|
changePassword: "パスワード変える"
|
||||||
|
security: "セキュリティ"
|
||||||
|
retypedNotMatch: "そやないねん。"
|
||||||
|
currentPassword: "今のパスワード"
|
||||||
|
newPassword: "今度のパスワード"
|
||||||
|
newPasswordRetype: "今度のパスワード(もっぺん入れて)"
|
||||||
|
attachFile: "ファイルのっける"
|
||||||
|
more: "他のやつ!"
|
||||||
|
featured: "ハイライト"
|
||||||
|
usernameOrUserId: "ユーザー名かユーザーID"
|
||||||
|
noSuchUser: "ユーザーが見つからへんで"
|
||||||
|
lookup: "見てきて"
|
||||||
|
announcements: "お知らせ"
|
||||||
|
imageUrl: "画像URL"
|
||||||
|
remove: "ほかす"
|
||||||
|
removeAreYouSure: "「{x}」はほかしてええか?"
|
||||||
|
deleteAreYouSure: "「{x}」はほかしてええか?"
|
||||||
|
resetAreYouSure: "リセットしてええん?"
|
||||||
|
saved: "保存したで!"
|
||||||
|
messaging: "チャット"
|
||||||
|
upload: "アップロード"
|
||||||
|
fromDrive: "ドライブから"
|
||||||
|
fromUrl: "URLから"
|
||||||
|
uploadFromUrl: "URLアップロード"
|
||||||
|
uploadFromUrlDescription: "このURLのファイルをアップロードしたいねん"
|
||||||
|
uploadFromUrlRequested: "アップロードしたい言うといたで"
|
||||||
|
uploadFromUrlMayTakeTime: "アップロード終わるんにちょい時間かかるかもしれへんわ。"
|
||||||
|
explore: "みつける"
|
||||||
|
messageRead: "もう読んだ"
|
||||||
|
noMoreHistory: "これより過去の履歴はあらへんで"
|
||||||
|
startMessaging: "チャットやるで"
|
||||||
|
nUsersRead: "{n}人が読んでもうた"
|
||||||
|
agreeTo: "{0}に同意したで"
|
||||||
|
tos: "利用規約"
|
||||||
|
start: "始める"
|
||||||
|
home: "ホーム"
|
||||||
|
remoteUserCaution: "リモートユーザーやから、足りひん情報あるかもしれへん。"
|
||||||
|
activity: "アクティビティ"
|
||||||
|
images: "画像"
|
||||||
|
birthday: "生まれた日"
|
||||||
|
yearsOld: "{age}歳"
|
||||||
|
registeredDate: "始めた日"
|
||||||
|
location: "場所"
|
||||||
|
theme: "テーマ"
|
||||||
|
themeForLightMode: "ライトモードではこのテーマつこて"
|
||||||
|
themeForDarkMode: "ダークモードではこのテーマつこて"
|
||||||
|
light: "ライト"
|
||||||
|
dark: "ダーク"
|
||||||
|
lightThemes: "デイゲーム"
|
||||||
|
darkThemes: "ナイトゲーム"
|
||||||
|
syncDeviceDarkMode: "デバイスのダークモードと一緒にする"
|
||||||
|
drive: "ドライブ"
|
||||||
|
selectFile: "ファイル選んでや"
|
||||||
|
selectFiles: "ファイル選んでや"
|
||||||
|
selectFolder: "フォルダ選んでや"
|
||||||
|
selectFolders: "フォルダ選んでや"
|
||||||
|
renameFile: "ファイル名をいらう"
|
||||||
|
folderName: "フォルダー名"
|
||||||
|
createFolder: "フォルダー作る"
|
||||||
|
renameFolder: "フォルダー名を変える"
|
||||||
|
deleteFolder: "フォルダーを消してまう"
|
||||||
|
addFile: "ファイルを追加"
|
||||||
|
unableToDelete: "消そうおもってんけどな、あかんかったわ"
|
||||||
|
inputNewFileName: "今度のファイル名は何にするん?"
|
||||||
|
inputNewDescription: "新しいキャプションを入力しましょ"
|
||||||
|
inputNewFolderName: "今度のフォルダ名は何にするん?"
|
||||||
|
circularReferenceFolder: "移動先のフォルダーは、移動するフォルダーのサブフォルダーや。"
|
||||||
|
hasChildFilesOrFolders: "このフォルダ、まだなんか入っとるから消されへん"
|
||||||
|
copyUrl: "URLをコピー"
|
||||||
|
rename: "名前を変えるで"
|
||||||
|
avatar: "アイコン"
|
||||||
|
banner: "バナー"
|
||||||
|
nsfw: "閲覧注意"
|
||||||
|
whenServerDisconnected: "サーバーとの接続が切れたとき"
|
||||||
|
disconnectedFromServer: "サーバーとの通信が切れたで"
|
||||||
|
reload: "リロード"
|
||||||
|
doNothing: "何もせんとく"
|
||||||
|
reloadConfirm: "リロードしてええか?"
|
||||||
|
watch: "ウォッチ"
|
||||||
|
unwatch: "ウォッチやめる"
|
||||||
|
accept: "ええで"
|
||||||
|
reject: "あかん"
|
||||||
|
normal: "ええ感じ"
|
||||||
|
instanceName: "インスタンス名"
|
||||||
|
instanceDescription: "インスタンスの紹介"
|
||||||
|
maintainerName: "管理者の名前"
|
||||||
|
maintainerEmail: "管理者のメールアドレス"
|
||||||
|
tosUrl: "利用規約のURL"
|
||||||
|
thisYear: "今年"
|
||||||
|
thisMonth: "今月"
|
||||||
|
today: "今日"
|
||||||
|
dayX: "{day}日"
|
||||||
|
monthX: "{month}月"
|
||||||
|
yearX: "{year}年"
|
||||||
|
pages: "ページ"
|
||||||
|
enableLocalTimeline: "ローカルタイムラインを使えるようにする"
|
||||||
|
enableGlobalTimeline: "グローバルタイムラインを使えるようにする"
|
||||||
|
disablingTimelinesInfo: "ここらへんのタイムラインを使えんようにしてしもても、管理者とモデレーターは使えるままになってるで、そうやなかったら不便やからな。"
|
||||||
|
enableRegistration: "一見さんでも誰でもいらっしゃ~い"
|
||||||
|
invite: "来てや"
|
||||||
|
driveCapacityPerLocalAccount: "ローカルユーザーひとりあたりのドライブ容量"
|
||||||
|
driveCapacityPerRemoteAccount: "リモートユーザーひとりあたりのドライブ容量"
|
||||||
|
inMb: "メガバイト単位"
|
||||||
|
iconUrl: "アイコン画像のURL"
|
||||||
|
bannerUrl: "バナー画像のURL"
|
||||||
|
pinnedUsers: "ピン留めしたユーザー"
|
||||||
|
pinnedUsersDescription: "「みつける」ページとかにピン留めしたいユーザーをここに書けばええんやで。他ん人との名前は改行で区切ればええんやで。"
|
||||||
|
hcaptchaSiteKey: "サイトキー"
|
||||||
|
hcaptchaSecretKey: "シークレットキー"
|
||||||
|
recaptchaSiteKey: "サイトキー"
|
||||||
|
recaptchaSecretKey: "シークレットキー"
|
||||||
|
antennas: "アンテナ"
|
||||||
|
manageAntennas: "アンテナいじる"
|
||||||
|
name: "名前"
|
||||||
|
antennaSource: "受信ソース(このソースは食われへん)"
|
||||||
|
antennaKeywords: "受信キーワード"
|
||||||
|
antennaExcludeKeywords: "除外キーワード"
|
||||||
|
antennaKeywordsDescription: "スペースで区切ったるとAND指定で、改行で区切ったるとOR指定や"
|
||||||
|
notifyAntenna: "新しいノートを通知すんで"
|
||||||
|
withFileAntenna: "なんか添付されたノートだけ"
|
||||||
|
antennaUsersDescription: "ユーザー名を改行で区切ったってな"
|
||||||
|
caseSensitive: "大文字と小文字は別もんや"
|
||||||
|
withReplies: "返信も入れたって"
|
||||||
|
connectedTo: "次のアカウントに繋がっとるで"
|
||||||
|
notesAndReplies: "投稿と返信"
|
||||||
|
withFiles: "ファイル付いとる"
|
||||||
|
silence: "サイレンス"
|
||||||
|
silenceConfirm: "サイレンスしよか?"
|
||||||
|
unsilence: "サイレンスやめるで"
|
||||||
|
unsilenceConfirm: "サイレンスやめよか?"
|
||||||
|
popularUsers: "人気のユーザー"
|
||||||
|
recentlyUpdatedUsers: "ちょっと前に投稿したばっかりのユーザー"
|
||||||
|
recentlyRegisteredUsers: "ちょっと前に始めたばっかりのユーザー"
|
||||||
|
recentlyDiscoveredUsers: "最近見っけたユーザー"
|
||||||
|
popularTags: "人気のタグ"
|
||||||
|
userList: "リスト"
|
||||||
|
aboutMisskey: "FoundKeyってなんや?"
|
||||||
|
administrator: "管理者"
|
||||||
|
token: "トークン"
|
||||||
|
twoStepAuthentication: "二段階認証"
|
||||||
|
moderator: "モデレーター"
|
||||||
|
nUsersMentioned: "{n}人が投稿"
|
||||||
|
securityKey: "セキュリティキー"
|
||||||
|
securityKeyName: "キーの名前"
|
||||||
|
registerSecurityKey: "セキュリティキーを登録するで"
|
||||||
|
lastUsed: "最後につこうた日"
|
||||||
|
unregister: "登録やめる"
|
||||||
|
passwordLessLogin: "パスワード無くてもログインできるようにする"
|
||||||
|
resetPassword: "パスワードをリセット"
|
||||||
|
newPasswordIs: "今度のパスワードは「{password}」や"
|
||||||
|
reduceUiAnimation: "UIの動きやアニメーションを減らす"
|
||||||
|
share: "わけわけ"
|
||||||
|
notFound: "見つからへんね"
|
||||||
|
notFoundDescription: "指定されたURLに該当するページはあらへんやった。"
|
||||||
|
uploadFolder: "とりあえずアップロードしたやつ置いとく所"
|
||||||
|
markAsReadAllNotifications: "通知はもう全て読んだわっ"
|
||||||
|
markAsReadAllUnreadNotes: "投稿は全て読んだわっ"
|
||||||
|
markAsReadAllTalkMessages: "チャットはもうぜんぶ読んだわっ"
|
||||||
|
help: "ヘルプ"
|
||||||
|
inputMessageHere: "ここにメッセージ書いてや"
|
||||||
|
close: "閉じる"
|
||||||
|
group: "グループ"
|
||||||
|
groups: "グループ"
|
||||||
|
createGroup: "グループを作るで"
|
||||||
|
ownedGroups: "所有しとるグループ"
|
||||||
|
joinedGroups: "参加しとるグループ"
|
||||||
|
invites: "来てや"
|
||||||
|
groupName: "グループ名"
|
||||||
|
members: "メンバー"
|
||||||
|
transfer: "譲渡"
|
||||||
|
messagingWithUser: "ユーザーとチャット"
|
||||||
|
messagingWithGroup: "グループでチャット"
|
||||||
|
title: "タイトル"
|
||||||
|
text: "テキスト"
|
||||||
|
enable: "有効にするで"
|
||||||
|
next: "次"
|
||||||
|
retype: "もっかい入力"
|
||||||
|
noteOf: "{user}のノート"
|
||||||
|
inviteToGroup: "グループに招く"
|
||||||
|
quoteAttached: "引用付いとるで"
|
||||||
|
quoteQuestion: "引用として添付してもええか?"
|
||||||
|
noMessagesYet: "まだチャットはあらへんで"
|
||||||
|
newMessageExists: "新しいメッセージがきたで"
|
||||||
|
onlyOneFileCanBeAttached: "すまん、メッセージに添付できるファイルはひとつだけなんや。"
|
||||||
|
signinRequired: "ログインしてくれへん?"
|
||||||
|
invitationCode: "招待コード"
|
||||||
|
checking: "確認しとるで"
|
||||||
|
available: "利用できる"
|
||||||
|
unavailable: "利用できん"
|
||||||
|
usernameInvalidFormat: "a~z、A~Z、0~9、_が使えるで"
|
||||||
|
tooShort: "短すぎやろ!"
|
||||||
|
tooLong: "長すぎやろ!"
|
||||||
|
weakPassword: "へぼいパスワード"
|
||||||
|
normalPassword: "普通のパスワード"
|
||||||
|
strongPassword: "ええ感じのパスワード"
|
||||||
|
passwordMatched: "よし!一致や!"
|
||||||
|
passwordNotMatched: "一致しとらんで?"
|
||||||
|
or: "それか"
|
||||||
|
language: "言語"
|
||||||
|
uiLanguage: "UIの表示言語"
|
||||||
|
groupInvited: "グループに招待されとるで"
|
||||||
|
useOsNativeEmojis: "OSネイティブの絵文字を使う"
|
||||||
|
youHaveNoGroups: "グループがあらへんねぇ。"
|
||||||
|
noHistory: "履歴はあらへんねぇ。"
|
||||||
|
signinHistory: "ログイン履歴"
|
||||||
|
disableAnimatedMfm: "動きがやかましいMFMを止める"
|
||||||
|
category: "カテゴリ"
|
||||||
|
tags: "タグ"
|
||||||
|
createAccount: "アカウントを作成"
|
||||||
|
fontSize: "フォントサイズ"
|
||||||
|
noFollowRequests: "フォロー申請はあらへんで"
|
||||||
|
openImageInNewTab: "画像を新しいタブで開く"
|
||||||
|
dashboard: "ダッシュボード"
|
||||||
|
local: "ローカル"
|
||||||
|
remote: "リモート"
|
||||||
|
dayOverDayChanges: "前日比"
|
||||||
|
appearance: "見た目"
|
||||||
|
clientSettings: "クライアントの設定"
|
||||||
|
showFeaturedNotesInTimeline: "タイムラインにおすすめのノートを表示してや"
|
||||||
|
objectStorage: "オブジェクトストレージ"
|
||||||
|
useObjectStorage: "オブジェクトストレージを使う"
|
||||||
|
objectStorageBaseUrl: "Base URL"
|
||||||
|
objectStorageBaseUrlDesc: "参照に使うにURLやで。CDNやProxyを使用してるんならそのURL、S3: 'https://<bucket>.s3.amazonaws.com'、GCSとかなら:\
|
||||||
|
\ 'https://storage.googleapis.com/<bucket>'。"
|
||||||
|
objectStorageBucket: "Bucket"
|
||||||
|
objectStoragePrefix: "Prefix"
|
||||||
|
objectStorageEndpoint: "Endpoint"
|
||||||
|
objectStorageRegion: "Region"
|
||||||
|
objectStorageUseSSL: "SSLを使う"
|
||||||
|
objectStorageUseProxy: "Proxyを使う"
|
||||||
|
objectStorageUseProxyDesc: "API接続にproxy使わんのやったら切ってくれへん?"
|
||||||
|
objectStorageSetPublicRead: "アップロードした時に'public-read'を設定してや"
|
||||||
|
showFixedPostForm: "タイムラインの上の方で投稿できるようにやってくれへん?"
|
||||||
|
newNoteRecived: "新しいノートがあるで"
|
||||||
|
sounds: "サウンド"
|
||||||
|
listen: "聴く"
|
||||||
|
none: "なし"
|
||||||
|
showInPage: "ページで表示"
|
||||||
|
popout: "ポップアウト"
|
||||||
|
volume: "音量"
|
||||||
|
masterVolume: "全体の音量"
|
||||||
|
details: "もっと"
|
||||||
|
unableToProcess: "なんか作業が止まってしまったようやね"
|
||||||
|
recentUsed: "最近使ったやつ"
|
||||||
|
install: "インストール"
|
||||||
|
uninstall: "アンインストール"
|
||||||
|
installedApps: "インストールされとるアプリ"
|
||||||
|
nothing: "あらへん"
|
||||||
|
installedDate: "インストールした日時"
|
||||||
|
lastUsedDate: "最後に使った日時"
|
||||||
|
state: "状態"
|
||||||
|
sort: "仕分ける"
|
||||||
|
ascendingOrder: "小さい順"
|
||||||
|
descendingOrder: "大きい順"
|
||||||
|
scratchpad: "スクラッチパッド"
|
||||||
|
scratchpadDescription: "スクラッチパッドではAiScriptを色々試すことができるんや。FoundKeyに対して色々できるコードを書いて動かしてみたり、結果を見たりできるで。"
|
||||||
|
output: "出力"
|
||||||
|
updateRemoteUser: "リモートユーザー情報の更新してくれん?"
|
||||||
|
deleteAllFilesConfirm: "ホンマにすべてのファイルを削除するん?消したもんはもう戻ってこんのやで?"
|
||||||
|
removeAllFollowing: "フォローを全解除"
|
||||||
|
removeAllFollowingDescription: "{host}からのフォローをすべて解除するで。そのインスタンスが消えて無くなった時とかには便利な機能やで。"
|
||||||
|
userSuspended: "このユーザーは...凍結されとる。"
|
||||||
|
userSilenced: "このユーザーは...サイレンスされとる。"
|
||||||
|
divider: "分割線"
|
||||||
|
relays: "リレー"
|
||||||
|
addRelay: "リレーの追加"
|
||||||
|
inboxUrl: "inboxのURL"
|
||||||
|
poll: "アンケート"
|
||||||
|
enablePlayer: "プレイヤーを開く"
|
||||||
|
disablePlayer: "プレイヤーを閉じる"
|
||||||
|
themeEditor: "テーマエディター"
|
||||||
|
description: "説明"
|
||||||
|
author: "作者"
|
||||||
|
leaveConfirm: "未保存の変更があるで!ほかしてええか?"
|
||||||
|
manage: "管理"
|
||||||
|
plugins: "プラグイン"
|
||||||
|
deck: "デッキ"
|
||||||
|
width: "幅"
|
||||||
|
height: "高さ"
|
||||||
|
large: "大"
|
||||||
|
medium: "中"
|
||||||
|
small: "小"
|
||||||
|
edit: "編集"
|
||||||
|
enableEmail: "メール配信を受け取る"
|
||||||
|
emailConfigInfo: "メールアドレスの確認とかパスワードリセットの時に使うで"
|
||||||
|
email: "メール"
|
||||||
|
emailAddress: "メールアドレス"
|
||||||
|
smtpConfig: "SMTP サーバーの設定"
|
||||||
|
smtpHost: "ホスト"
|
||||||
|
smtpPort: "ポート"
|
||||||
|
smtpUser: "ユーザー名"
|
||||||
|
smtpPass: "パスワード"
|
||||||
|
emptyToDisableSmtpAuth: "ユーザー名とパスワードになんも入れんかったら、SMTP認証を無効化するで"
|
||||||
|
smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
|
||||||
|
testEmail: "配信テスト"
|
||||||
|
wordMute: "ワードミュート"
|
||||||
|
userSaysSomething: "{name}が何か言ったようやで"
|
||||||
|
makeActive: "使うで"
|
||||||
|
display: "表示"
|
||||||
|
copy: "コピー"
|
||||||
|
overview: "概要"
|
||||||
|
database: "データベース"
|
||||||
|
channel: "チャンネル"
|
||||||
|
create: "作成"
|
||||||
|
notificationSetting: "通知設定"
|
||||||
|
notificationSettingDesc: "表示する通知の種類えらんでや。"
|
||||||
|
useGlobalSetting: "グローバル設定を使ってや"
|
||||||
|
other: "その他"
|
||||||
|
regenerateLoginToken: "ログイントークンを再生成"
|
||||||
|
behavior: "動作"
|
||||||
|
abuseReports: "通報"
|
||||||
|
reportAbuse: "通報"
|
||||||
|
reportAbuseOf: "{name}を通報する"
|
||||||
|
send: "送信"
|
||||||
|
abuseMarkAsResolved: "対応したで"
|
||||||
|
openInNewTab: "新しいタブで開く"
|
||||||
|
defaultNavigationBehaviour: "デフォルトのナビゲーション"
|
||||||
|
instanceTicker: "ノートのインスタンス情報"
|
||||||
|
system: "システム"
|
||||||
|
switchUi: "UI切り替え"
|
||||||
|
desktop: "デスクトップ"
|
||||||
|
clip: "クリップ"
|
||||||
|
receivedReactionsCount: "リアクションされた数"
|
||||||
|
pollVotesCount: "アンケートに投票した数"
|
||||||
|
pollVotedCount: "アンケートに投票された数"
|
||||||
|
yes: "はい"
|
||||||
|
no: "いいえ"
|
||||||
|
driveFilesCount: "ドライブのファイル数"
|
||||||
|
emailVerified: "メールアドレスは確認されたで"
|
||||||
|
pageLikesCount: "Pageにええやんと思った数"
|
||||||
|
pageLikedCount: "Pageにええやんと思ってくれた数"
|
||||||
|
clips: "クリップ"
|
||||||
|
duplicate: "複製"
|
||||||
|
left: "左"
|
||||||
|
center: "中央"
|
||||||
|
wide: "広い"
|
||||||
|
narrow: "狭い"
|
||||||
|
reloadToApplySetting: "設定はページリロード後に反映されるで。今リロードしとくか?"
|
||||||
|
clearCache: "キャッシュをほかす"
|
||||||
|
onlineUsersCount: "{n}人が起きとるで"
|
||||||
|
backgroundColor: "背景"
|
||||||
|
accentColor: "アクセント"
|
||||||
|
textColor: "文字"
|
||||||
|
saveAs: "名前を付けて保存"
|
||||||
|
createdAt: "作成した日"
|
||||||
|
updatedAt: "更新日時"
|
||||||
|
deleteConfirm: "ホンマに削除するで?"
|
||||||
|
closeAccount: "アカウントを閉鎖する"
|
||||||
|
newVersionOfClientAvailable: "新しいバージョンのクライアントが使えるで。"
|
||||||
|
usageAmount: "使用量"
|
||||||
|
capacity: "容量"
|
||||||
|
inUse: "使用中"
|
||||||
|
editCode: "コードを編集"
|
||||||
|
apply: "適用"
|
||||||
|
receiveAnnouncementFromInstance: "インスタンスからのお知らせを受け取る"
|
||||||
|
emailNotification: "メール通知"
|
||||||
|
useReactionPickerForContextMenu: "右クリックでリアクションピッカーを開くようにする"
|
||||||
|
typingUsers: "{users}が今書きよるで"
|
||||||
|
jumpToSpecifiedDate: "特定の日付にジャンプ"
|
||||||
|
clear: "クリア"
|
||||||
|
markAllAsRead: "もうみな読んでもうたわ"
|
||||||
|
goBack: "戻る"
|
||||||
|
info: "情報"
|
||||||
|
user: "ユーザー"
|
||||||
|
administration: "管理"
|
||||||
|
hashtags: "ハッシュタグ"
|
||||||
|
hide: "隠す"
|
||||||
|
indefinitely: "無期限"
|
||||||
|
_email:
|
||||||
|
_follow:
|
||||||
|
title: "フォローされたで"
|
||||||
|
_receiveFollowRequest:
|
||||||
|
title: "フォローリクエストを受け取ったで"
|
||||||
|
_plugin:
|
||||||
|
install: "プラグインのインストール"
|
||||||
|
installWarn: "信頼できへんプラグインはインストールせんとってな"
|
||||||
|
_registry:
|
||||||
|
scope: "スコープ"
|
||||||
|
key: "キー"
|
||||||
|
keys: "キー"
|
||||||
|
domain: "ドメイン"
|
||||||
|
createKey: "キーを作る"
|
||||||
|
_aboutMisskey:
|
||||||
|
about: "FoundKeyはsyuiloが2014年からずっと作ってはる、オープンソースなソフトウェアや。"
|
||||||
|
allContributors: "全ての貢献者"
|
||||||
|
source: "ソースコード"
|
||||||
|
_mfm:
|
||||||
|
cheatSheet: "MFMチートシート"
|
||||||
|
mention: "メンション"
|
||||||
|
hashtag: "ハッシュタグ"
|
||||||
|
url: "URL"
|
||||||
|
link: "リンク"
|
||||||
|
bold: "太字"
|
||||||
|
center: "中央寄せ"
|
||||||
|
inlineCode: "コード(インライン)"
|
||||||
|
blockCode: "コード(ブロック)"
|
||||||
|
inlineMath: "数式(インライン)"
|
||||||
|
quote: "引用"
|
||||||
|
emoji: "カスタム絵文字"
|
||||||
|
search: "探す"
|
||||||
|
shake: "アニメーション(ぶるぶる)"
|
||||||
|
twitch: "アニメーション(ブレ)"
|
||||||
|
spin: "アニメーション(回転)"
|
||||||
|
blur: "ぼかし"
|
||||||
|
font: "フォント"
|
||||||
|
rotate: "回転"
|
||||||
|
_instanceTicker:
|
||||||
|
none: "表示せん"
|
||||||
|
remote: "リモートユーザーに表示"
|
||||||
|
always: "常に表示"
|
||||||
|
_serverDisconnectedBehavior:
|
||||||
|
reload: "自動でリロード"
|
||||||
|
dialog: "ダイアログで警告"
|
||||||
|
_channel:
|
||||||
|
create: "チャンネルを作る"
|
||||||
|
edit: "チャンネルを編集"
|
||||||
|
setBanner: "バナーを設定"
|
||||||
|
removeBanner: "バナーを削除"
|
||||||
|
featured: "トレンド"
|
||||||
|
notesCount: "{n}こ投稿があるで"
|
||||||
|
_menuDisplay:
|
||||||
|
hide: "隠す"
|
||||||
|
_wordMute:
|
||||||
|
soft: "ソフト"
|
||||||
|
hard: "ハード"
|
||||||
|
_theme:
|
||||||
|
explore: "テーマを探す"
|
||||||
|
install: "テーマのインストール"
|
||||||
|
manage: "テーマの管理"
|
||||||
|
code: "テーマコード"
|
||||||
|
description: "説明"
|
||||||
|
installed: "{name}をインストールしたで。"
|
||||||
|
installedThemes: "インストールされとるテーマ"
|
||||||
|
builtinThemes: "標準のテーマ"
|
||||||
|
alreadyInstalled: "そのテーマはもうインストールされとるで?"
|
||||||
|
make: "テーマを作る"
|
||||||
|
_sfx:
|
||||||
|
note: "ノート"
|
||||||
|
noteMy: "ノート(自分)"
|
||||||
|
notification: "通知"
|
||||||
|
chat: "チャット"
|
||||||
|
_ago:
|
||||||
|
future: "未来"
|
||||||
|
justNow: "たった今"
|
||||||
|
secondsAgo: "{n}秒前"
|
||||||
|
minutesAgo: "{n}分前"
|
||||||
|
hoursAgo: "{n}時間前"
|
||||||
|
daysAgo: "{n}日前"
|
||||||
|
weeksAgo: "{n}週間前"
|
||||||
|
monthsAgo: "{n}ヶ月前"
|
||||||
|
yearsAgo: "{n}年前"
|
||||||
|
_time:
|
||||||
|
second: "秒"
|
||||||
|
minute: "分"
|
||||||
|
hour: "時間"
|
||||||
|
day: "日"
|
||||||
|
_tutorial:
|
||||||
|
step3_1: "プロフィール設定はええ感じにできたか?"
|
||||||
|
_2fa:
|
||||||
|
alreadyRegistered: "もう設定終わっとるわ。"
|
||||||
|
_permissions:
|
||||||
|
"write:votes": "投票する"
|
||||||
|
"read:pages": "ページを見る"
|
||||||
|
"read:page-likes": "ページのええやんを見る"
|
||||||
|
"write:page-likes": "ページのええやんを操作する"
|
||||||
|
"read:user-groups": "ユーザーグループを見る"
|
||||||
|
"read:channels": "チャンネルを見る"
|
||||||
|
_auth:
|
||||||
|
permissionAsk: "このアプリは次の権限を要求しとるで"
|
||||||
|
_antennaSources:
|
||||||
|
all: "みんなのノート"
|
||||||
|
homeTimeline: "フォローしとるユーザーのノート"
|
||||||
|
_weekday:
|
||||||
|
sunday: "日曜日"
|
||||||
|
monday: "月曜日"
|
||||||
|
tuesday: "火曜日"
|
||||||
|
wednesday: "水曜日"
|
||||||
|
thursday: "木曜日"
|
||||||
|
friday: "金曜日"
|
||||||
|
saturday: "土曜日"
|
||||||
|
_widgets:
|
||||||
|
memo: "付箋"
|
||||||
|
notifications: "通知"
|
||||||
|
timeline: "タイムライン"
|
||||||
|
calendar: "カレンダー"
|
||||||
|
trends: "トレンド"
|
||||||
|
clock: "時計"
|
||||||
|
rss: "RSSリーダー"
|
||||||
|
activity: "アクティビティ"
|
||||||
|
photos: "フォト"
|
||||||
|
digitalClock: "デジタル時計"
|
||||||
|
federation: "連合"
|
||||||
|
postForm: "投稿フォーム"
|
||||||
|
slideshow: "スライドショー"
|
||||||
|
button: "ボタン"
|
||||||
|
onlineUsers: "オンラインユーザー"
|
||||||
|
jobQueue: "ジョブキュー"
|
||||||
|
serverMetric: "サーバーメトリクス"
|
||||||
|
aiscript: "AiScriptコンソール"
|
||||||
|
_cw:
|
||||||
|
hide: "隠す"
|
||||||
|
show: "続き見して"
|
||||||
|
chars: "{count}文字"
|
||||||
|
files: "{count}ファイル"
|
||||||
|
_poll:
|
||||||
|
choiceN: "選択肢{n}"
|
||||||
|
noMore: "これ以上追加でけへん"
|
||||||
|
canMultipleVote: "複数回答可"
|
||||||
|
expiration: "期限"
|
||||||
|
infinite: "無期限"
|
||||||
|
at: "日時指定"
|
||||||
|
after: "経過指定"
|
||||||
|
deadlineDate: "期日"
|
||||||
|
deadlineTime: "時間"
|
||||||
|
duration: "期間"
|
||||||
|
votesCount: "{n}票"
|
||||||
|
vote: "投票する"
|
||||||
|
_visibility:
|
||||||
|
publicDescription: "みんなに公開"
|
||||||
|
home: "ホーム"
|
||||||
|
followers: "フォロワー"
|
||||||
|
_profile:
|
||||||
|
name: "名前"
|
||||||
|
username: "ユーザー名"
|
||||||
|
_exportOrImport:
|
||||||
|
allNotes: "全てのノート"
|
||||||
|
followingList: "フォロー"
|
||||||
|
muteList: "ミュート"
|
||||||
|
blockingList: "ブロック"
|
||||||
|
userLists: "リスト"
|
||||||
|
_charts:
|
||||||
|
federation: "連合"
|
||||||
|
apRequest: "リクエスト"
|
||||||
|
usersTotal: "ユーザーの合計"
|
||||||
|
activeUsers: "アクティブユーザー数"
|
||||||
|
notesIncDec: "ノートの増減"
|
||||||
|
localNotesIncDec: "ローカルのノートの増減"
|
||||||
|
remoteNotesIncDec: "リモートのノートの増減"
|
||||||
|
notesTotal: "ノートの合計"
|
||||||
|
filesIncDec: "ファイルの増減"
|
||||||
|
filesTotal: "ファイルの合計"
|
||||||
|
storageUsageIncDec: "ストレージ使用量の増減"
|
||||||
|
storageUsageTotal: "ストレージ使用量の合計"
|
||||||
|
_instanceCharts:
|
||||||
|
requests: "リクエスト"
|
||||||
|
users: "ユーザーの増減"
|
||||||
|
usersTotal: "ユーザーの累積"
|
||||||
|
notes: "ノートの増減"
|
||||||
|
notesTotal: "ノートの累積"
|
||||||
|
ff: "フォロー/フォロワーの増減"
|
||||||
|
ffTotal: "フォロー/フォロワーの累積"
|
||||||
|
cacheSize: "キャッシュサイズの増減"
|
||||||
|
cacheSizeTotal: "キャッシュサイズの累積"
|
||||||
|
files: "ファイル数の増減"
|
||||||
|
filesTotal: "ファイル数の累積"
|
||||||
|
_timelines:
|
||||||
|
home: "ホーム"
|
||||||
|
local: "ローカル"
|
||||||
|
social: "ソーシャル"
|
||||||
|
global: "グローバル"
|
||||||
|
_pages:
|
||||||
|
newPage: "ページを作る"
|
||||||
|
editPage: "ページの編集"
|
||||||
|
readPage: "ソースを表示中"
|
||||||
|
created: "ページを作成したで"
|
||||||
|
updated: "ページを更新したで"
|
||||||
|
deleted: "ページを削除したで"
|
||||||
|
pageSetting: "ページ設定"
|
||||||
|
viewPage: "ページを見る"
|
||||||
|
like: "ええやん"
|
||||||
|
unlike: "良くないわ"
|
||||||
|
liked: "ええと思ったページ"
|
||||||
|
contents: "コンテンツ"
|
||||||
|
summary: "ページの要約"
|
||||||
|
alignCenter: "中央寄せ"
|
||||||
|
font: "フォント"
|
||||||
|
fontSerif: "セリフ"
|
||||||
|
fontSansSerif: "サンセリフ"
|
||||||
|
eyeCatchingImageSet: "アイキャッチ画像を設定"
|
||||||
|
eyeCatchingImageRemove: "アイキャッチ画像を削除"
|
||||||
|
_notification:
|
||||||
|
youGotMention: "{name}からのメンション"
|
||||||
|
youGotReply: "{name}からのリプライ"
|
||||||
|
youWereFollowed: "フォローされたで"
|
||||||
|
youReceivedFollowRequest: "フォロー許可してほしいみたいやな"
|
||||||
|
yourFollowRequestAccepted: "フォローさせてもろたで"
|
||||||
|
youWereInvitedToGroup: "グループに招待されとるで"
|
||||||
|
_types:
|
||||||
|
follow: "フォロー"
|
||||||
|
mention: "メンション"
|
||||||
|
renote: "Renote"
|
||||||
|
quote: "引用"
|
||||||
|
reaction: "リアクション"
|
||||||
|
receiveFollowRequest: "フォロー許可してほしいみたいやで"
|
||||||
|
followRequestAccepted: "フォローが受理されたで"
|
||||||
|
_actions:
|
||||||
|
reply: "返事"
|
||||||
|
renote: "Renote"
|
||||||
|
_deck:
|
||||||
|
alwaysShowMainColumn: "いつもメインカラムを表示"
|
||||||
|
columnAlign: "カラムの寄せ"
|
||||||
|
columnMargin: "カラム間のマージン"
|
||||||
|
columnHeaderHeight: "カラムのヘッダー幅"
|
||||||
|
addColumn: "カラムを追加"
|
||||||
|
swapLeft: "左に移動"
|
||||||
|
swapRight: "右に移動"
|
||||||
|
swapUp: "上に移動"
|
||||||
|
swapDown: "下に移動"
|
||||||
|
stackLeft: "左に重ねる"
|
||||||
|
popRight: "右に出す"
|
||||||
|
profile: "プロファイル"
|
||||||
|
_columns:
|
||||||
|
main: "メイン"
|
||||||
|
widgets: "ウィジェット"
|
||||||
|
notifications: "通知"
|
||||||
|
tl: "タイムライン"
|
||||||
|
antenna: "アンテナ"
|
||||||
|
list: "リスト"
|
||||||
|
mentions: "あんた宛て"
|
||||||
|
direct: "ダイレクト"
|
||||||
|
_services: {}
|
|
@ -219,6 +219,7 @@ uploadFromUrl: "URL 업로드"
|
||||||
uploadFromUrlDescription: "업로드하려는 파일의 URL"
|
uploadFromUrlDescription: "업로드하려는 파일의 URL"
|
||||||
uploadFromUrlRequested: "업로드를 요청했습니다"
|
uploadFromUrlRequested: "업로드를 요청했습니다"
|
||||||
uploadFromUrlMayTakeTime: "업로드가 완료될 때까지 시간이 소요될 수 있습니다."
|
uploadFromUrlMayTakeTime: "업로드가 완료될 때까지 시간이 소요될 수 있습니다."
|
||||||
|
explore: "발견하기"
|
||||||
messageRead: "읽음"
|
messageRead: "읽음"
|
||||||
noMoreHistory: "이것보다 과거의 기록이 없습니다"
|
noMoreHistory: "이것보다 과거의 기록이 없습니다"
|
||||||
startMessaging: "대화 시작하기"
|
startMessaging: "대화 시작하기"
|
||||||
|
@ -297,6 +298,8 @@ inMb: "메가바이트 단위"
|
||||||
iconUrl: "아이콘 URL"
|
iconUrl: "아이콘 URL"
|
||||||
bannerUrl: "배너 이미지 URL"
|
bannerUrl: "배너 이미지 URL"
|
||||||
backgroundImageUrl: "배경 이미지 URL"
|
backgroundImageUrl: "배경 이미지 URL"
|
||||||
|
pinnedUsers: "고정된 유저"
|
||||||
|
pinnedUsersDescription: "\"발견하기\" 페이지 등에 고정하고 싶은 유저를 한 줄에 한 명씩 적습니다."
|
||||||
hcaptchaSiteKey: "사이트 키"
|
hcaptchaSiteKey: "사이트 키"
|
||||||
hcaptchaSecretKey: "시크릿 키"
|
hcaptchaSecretKey: "시크릿 키"
|
||||||
recaptchaSiteKey: "사이트 키"
|
recaptchaSiteKey: "사이트 키"
|
||||||
|
@ -320,6 +323,11 @@ silence: "사일런스"
|
||||||
silenceConfirm: "이 계정을 사일런스로 설정하시겠습니까?"
|
silenceConfirm: "이 계정을 사일런스로 설정하시겠습니까?"
|
||||||
unsilence: "사일런스 해제"
|
unsilence: "사일런스 해제"
|
||||||
unsilenceConfirm: "이 계정의 사일런스를 해제하시겠습니까?"
|
unsilenceConfirm: "이 계정의 사일런스를 해제하시겠습니까?"
|
||||||
|
popularUsers: "인기 유저"
|
||||||
|
recentlyUpdatedUsers: "최근 활동한 유저"
|
||||||
|
recentlyRegisteredUsers: "최근 가입한 유저"
|
||||||
|
recentlyDiscoveredUsers: "최근 발견한 유저"
|
||||||
|
popularTags: "인기 태그"
|
||||||
userList: "리스트"
|
userList: "리스트"
|
||||||
aboutMisskey: "FoundKey에 대하여"
|
aboutMisskey: "FoundKey에 대하여"
|
||||||
administrator: "관리자"
|
administrator: "관리자"
|
||||||
|
@ -360,6 +368,7 @@ messagingWithGroup: "그룹끼리 대화하기"
|
||||||
title: "제목"
|
title: "제목"
|
||||||
text: "텍스트"
|
text: "텍스트"
|
||||||
enable: "사용"
|
enable: "사용"
|
||||||
|
next: "다음"
|
||||||
retype: "다시 입력"
|
retype: "다시 입력"
|
||||||
noteOf: "{user}의 노트"
|
noteOf: "{user}의 노트"
|
||||||
inviteToGroup: "그룹에 초대하기"
|
inviteToGroup: "그룹에 초대하기"
|
||||||
|
@ -543,6 +552,7 @@ abuseReports: "신고"
|
||||||
reportAbuse: "신고"
|
reportAbuse: "신고"
|
||||||
reportAbuseOf: "{name}을 신고하기"
|
reportAbuseOf: "{name}을 신고하기"
|
||||||
fillAbuseReportDescription: "신고하려는 이유를 자세히 알려주세요."
|
fillAbuseReportDescription: "신고하려는 이유를 자세히 알려주세요."
|
||||||
|
abuseReported: "신고를 보냈습니다. 신고해 주셔서 감사합니다."
|
||||||
reporter: "신고자"
|
reporter: "신고자"
|
||||||
reporteeOrigin: "피신고자"
|
reporteeOrigin: "피신고자"
|
||||||
reporterOrigin: "신고자"
|
reporterOrigin: "신고자"
|
||||||
|
@ -911,6 +921,30 @@ _time:
|
||||||
minute: "분"
|
minute: "분"
|
||||||
hour: "시간"
|
hour: "시간"
|
||||||
day: "일"
|
day: "일"
|
||||||
|
_tutorial:
|
||||||
|
title: "FoundKey의 사용 방법"
|
||||||
|
step1_1: "환영합니다!"
|
||||||
|
step1_2: "이 페이지는 \"타임라인\"이라고 불립니다. 당신이 \"팔로우\"하고 있는 사람들의 \"노트\"가 시간순으로 나타납니다."
|
||||||
|
step1_3: "아직 아무 유저도 팔로우하고 있지 않기에 타임라인은 비어 있을 것입니다."
|
||||||
|
step2_1: "새 노트를 작성하거나 다른 사람을 팔로우하기 전에, 먼저 프로필을 완성해보도록 합시다."
|
||||||
|
step2_2: "당신이 어떤 사람인지를 알린다면, 다른 사람들이 당신을 팔로우할 확률이 올라갈 것입니다."
|
||||||
|
step3_1: "프로필 설정은 잘 끝내셨나요?"
|
||||||
|
step3_2: "그럼 시험삼아 노트를 작성해 보세요. 화면에 있는 연필 버튼을 누르면 작성 폼이 열립니다."
|
||||||
|
step3_3: "내용을 작성한 후, 폼 오른쪽 상단의 버튼을 눌러 노트를 올릴 수 있습니다."
|
||||||
|
step3_4: "쓸 말이 없나요? \"Misskey 시작했어요!\" 같은 건 어떨까요? :>"
|
||||||
|
step4_1: "노트 작성을 끝내셨나요?"
|
||||||
|
step4_2: "당신의 노트가 타임라인에 표시되어 있다면 성공입니다."
|
||||||
|
step5_1: "이제, 다른 사람을 팔로우하여 타임라인을 활기차게 만들어보도록 합시다."
|
||||||
|
step5_2: "{featured}에서 이 인스턴스의 인기 노트를 보실 수 있습니다. {explore}에서는 인기 사용자를 찾을 수 있구요.\
|
||||||
|
\ 마음에 드는 사람을 골라 팔로우해 보세요!"
|
||||||
|
step5_3: "다른 유저를 팔로우하려면 해당 유저의 아이콘을 클릭하여 프로필 페이지를 띄운 후, 팔로우 버튼을 눌러 주세요."
|
||||||
|
step5_4: "사용자에 따라 팔로우가 승인될 때까지 시간이 걸릴 수 있습니다."
|
||||||
|
step6_1: "타임라인에 다른 사용자의 노트가 나타난다면 성공입니다."
|
||||||
|
step6_2: "다른 유저의 노트에 \"리액션\"을 붙여 간단하게 당신의 반응을 전달할 수도 있습니다."
|
||||||
|
step6_3: "리액션을 붙이려면, 노트의 \"+\" 버튼을 클릭하고 원하는 이모지를 선택합니다."
|
||||||
|
step7_1: "이것으로 FoundKey의 기본 튜토리얼을 마치겠습니다. 수고하셨습니다!"
|
||||||
|
step7_2: "FoundKey에 대해 더 알고 싶으시다면 {help}를 참고해 주세요."
|
||||||
|
step7_3: "그럼 FoundKey를 즐기세요! \U0001F680"
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "이미 설정이 완료되었습니다."
|
alreadyRegistered: "이미 설정이 완료되었습니다."
|
||||||
registerDevice: "디바이스 등록"
|
registerDevice: "디바이스 등록"
|
||||||
|
|
|
@ -224,6 +224,7 @@ uploadFromUrl: "Uploaden vanaf een URL"
|
||||||
uploadFromUrlDescription: "URL van het bestand dat je wil uploaden"
|
uploadFromUrlDescription: "URL van het bestand dat je wil uploaden"
|
||||||
uploadFromUrlRequested: "Uploadverzoek"
|
uploadFromUrlRequested: "Uploadverzoek"
|
||||||
uploadFromUrlMayTakeTime: "Het kan even duren voordat het uploaden voltooid is."
|
uploadFromUrlMayTakeTime: "Het kan even duren voordat het uploaden voltooid is."
|
||||||
|
explore: "Verkennen"
|
||||||
messageRead: "Lezen"
|
messageRead: "Lezen"
|
||||||
noMoreHistory: "Er is geen verdere geschiedenis"
|
noMoreHistory: "Er is geen verdere geschiedenis"
|
||||||
startMessaging: "Start een gesprek"
|
startMessaging: "Start een gesprek"
|
||||||
|
|
|
@ -225,6 +225,7 @@ uploadFromUrl: "Wyślij z adresu URL"
|
||||||
uploadFromUrlDescription: "Adres URL pliku, który chcesz wysłać"
|
uploadFromUrlDescription: "Adres URL pliku, który chcesz wysłać"
|
||||||
uploadFromUrlRequested: "Zażądano wysłania"
|
uploadFromUrlRequested: "Zażądano wysłania"
|
||||||
uploadFromUrlMayTakeTime: "Wysyłanie może chwilę potrwać."
|
uploadFromUrlMayTakeTime: "Wysyłanie może chwilę potrwać."
|
||||||
|
explore: "Eksploruj"
|
||||||
messageRead: "Przeczytano"
|
messageRead: "Przeczytano"
|
||||||
noMoreHistory: "Nie ma dalszej historii"
|
noMoreHistory: "Nie ma dalszej historii"
|
||||||
startMessaging: "Rozpocznij czat"
|
startMessaging: "Rozpocznij czat"
|
||||||
|
@ -306,6 +307,9 @@ inMb: "W megabajtach"
|
||||||
iconUrl: "Adres URL ikony"
|
iconUrl: "Adres URL ikony"
|
||||||
bannerUrl: "Adres URL banera"
|
bannerUrl: "Adres URL banera"
|
||||||
backgroundImageUrl: "Adres URL tła"
|
backgroundImageUrl: "Adres URL tła"
|
||||||
|
pinnedUsers: "Przypięty użytkownik"
|
||||||
|
pinnedUsersDescription: "Wypisz po jednej nazwie użytkownika w wierszu. Podani użytkownicy\
|
||||||
|
\ zostaną przypięci pod kartą „Eksploruj”."
|
||||||
hcaptchaSiteKey: "Klucz strony"
|
hcaptchaSiteKey: "Klucz strony"
|
||||||
hcaptchaSecretKey: "Tajny klucz"
|
hcaptchaSecretKey: "Tajny klucz"
|
||||||
recaptchaSiteKey: "Klucz strony"
|
recaptchaSiteKey: "Klucz strony"
|
||||||
|
@ -330,6 +334,11 @@ silence: "Wycisz"
|
||||||
silenceConfirm: "Czy na pewno chcesz wyciszyć tego użytkownika?"
|
silenceConfirm: "Czy na pewno chcesz wyciszyć tego użytkownika?"
|
||||||
unsilence: "Cofnij wyciszenie"
|
unsilence: "Cofnij wyciszenie"
|
||||||
unsilenceConfirm: "Czy na pewno chcesz cofnąć wyciszenie tego użytkownika?"
|
unsilenceConfirm: "Czy na pewno chcesz cofnąć wyciszenie tego użytkownika?"
|
||||||
|
popularUsers: "Popularni użytkownicy"
|
||||||
|
recentlyUpdatedUsers: "Ostatnio aktywni użytkownicy"
|
||||||
|
recentlyRegisteredUsers: "Ostatnio zarejestrowani użytkownicy"
|
||||||
|
recentlyDiscoveredUsers: "Ostatnio odkryci użytkownicy"
|
||||||
|
popularTags: "Tagi na czasie"
|
||||||
userList: "Listy"
|
userList: "Listy"
|
||||||
aboutMisskey: "O Foundkey"
|
aboutMisskey: "O Foundkey"
|
||||||
administrator: "Admin"
|
administrator: "Admin"
|
||||||
|
@ -370,6 +379,7 @@ messagingWithGroup: "Rozmowy wewnątrz grupy"
|
||||||
title: "Tytuł"
|
title: "Tytuł"
|
||||||
text: "Tekst"
|
text: "Tekst"
|
||||||
enable: "Włącz"
|
enable: "Włącz"
|
||||||
|
next: "Dalej"
|
||||||
retype: "Wprowadź ponownie"
|
retype: "Wprowadź ponownie"
|
||||||
noteOf: "Wpisy {user}"
|
noteOf: "Wpisy {user}"
|
||||||
inviteToGroup: "Zaproś do grupy"
|
inviteToGroup: "Zaproś do grupy"
|
||||||
|
@ -546,6 +556,7 @@ abuseReports: "Zgłoszenia"
|
||||||
reportAbuse: "Zgłoś"
|
reportAbuse: "Zgłoś"
|
||||||
reportAbuseOf: "Zgłoś {name}"
|
reportAbuseOf: "Zgłoś {name}"
|
||||||
fillAbuseReportDescription: "Wypełnij szczegóły zgłoszenia."
|
fillAbuseReportDescription: "Wypełnij szczegóły zgłoszenia."
|
||||||
|
abuseReported: "Twoje zgłoszenie zostało wysłane. Dziękujemy."
|
||||||
reporteeOrigin: "Pochodzenie osoby zgłoszonej"
|
reporteeOrigin: "Pochodzenie osoby zgłoszonej"
|
||||||
reporterOrigin: "Pochodzenie osoby zgłaszającej"
|
reporterOrigin: "Pochodzenie osoby zgłaszającej"
|
||||||
send: "Wyślij"
|
send: "Wyślij"
|
||||||
|
@ -846,6 +857,40 @@ _time:
|
||||||
minute: "minuta"
|
minute: "minuta"
|
||||||
hour: "godz."
|
hour: "godz."
|
||||||
day: "dzień"
|
day: "dzień"
|
||||||
|
_tutorial:
|
||||||
|
title: "Jak korzystać z Foundkey"
|
||||||
|
step1_1: "Witaj!"
|
||||||
|
step1_3: "Twoja oś czasu jest jeszcze pusta, ponieważ nie opublikował*ś jeszcze\
|
||||||
|
\ żadnych wpisów i nie obserwujesz jeszcze nikogo."
|
||||||
|
step2_1: "Ukończmy konfigurację profilu zanim utworzymy wpis lub zaczniemy kogoś\
|
||||||
|
\ obserwować."
|
||||||
|
step3_1: "Zakończył*ś konfigurację profilu?"
|
||||||
|
step3_3: "Wypełnij pole i kliknij przycisk w prawym górnym rogu by wysłać wpis."
|
||||||
|
step4_1: Skończył*ś publikować swój pierwszy wpis?
|
||||||
|
step6_2: Możesz też dodawać reakcję do wpisów innych ludzi, żeby szybko odpowiedzieć
|
||||||
|
na nie.
|
||||||
|
step4_2: Hura! Teraz Twój wpis powinien być widoczny na Twojej osi czasu.
|
||||||
|
step5_2: '{featured} pokaże CI popularne wpisy z tej instancji. {explore} pozwoli
|
||||||
|
ci znaleźć popularnych użytkowników. Spróbuj tam znaleźć ludzi których chciał*byś
|
||||||
|
zaobserwować!'
|
||||||
|
step7_1: Gratulacje! Właśnie ukończył*ś podstawowy samouczek Foundkey.
|
||||||
|
step6_3: W celu dodania reakcji, kliknij na "+" przy poście innego użytkownika i
|
||||||
|
wybierz jakieś emoji jako reakcję na niego.
|
||||||
|
step6_1: Wpisy innych ludzi powinny już się pojawiać na Twojej osi czasu.
|
||||||
|
step7_2: Jeśli chcesz dowiedzieć się więcej o Foundkey, przejdź do sekcji {help}.
|
||||||
|
step7_3: A teraz, powodzenia i miłej zabawy z Foundkey! 🚀
|
||||||
|
step5_1: Teraz spróbujmy ożywić oś czasu poprzez zaobserwowanie innych ludzi.
|
||||||
|
step1_2: Ta strona jest nazywana "osią czasu". Pokazuje ona chronologicznie ułożone
|
||||||
|
wpisy ludzi których obserwujesz.
|
||||||
|
step5_4: Jeśli inny użytkownik ma kłódkę przy nazwie, ręczne zatwierdzenie Twojej
|
||||||
|
prośby o obserwację przez tego użytkownika może zająć trochę czasu.
|
||||||
|
step3_2: Spróbujmy opublikować wpis. Możesz to zrobić klikając przycisk z ikoną
|
||||||
|
ołówka.
|
||||||
|
step2_2: Dodanie informacji o sobie pomoże innym w decyzji czy chcą widzieć Twoje
|
||||||
|
wpisy, lub Ciebie obserwować.
|
||||||
|
step5_3: By zaobserwować innych użytkowników, kliknij na ich ikonę i wciśnij przycisk
|
||||||
|
"Obserwuj" na ich profilu.
|
||||||
|
step3_4: Nic nie przychodzi na myśl? Spróbuj "Właśnie wrócił*m z kościoła"!
|
||||||
_2fa:
|
_2fa:
|
||||||
registerDevice: "Zarejestruj nowe urządzenie"
|
registerDevice: "Zarejestruj nowe urządzenie"
|
||||||
step1: "Najpierw, zainstaluj aplikację uwierzytelniającą (taką jak {a} lub {b})\
|
step1: "Najpierw, zainstaluj aplikację uwierzytelniającą (taką jak {a} lub {b})\
|
||||||
|
|
|
@ -236,6 +236,7 @@ uploadFromUrl: "Încarcă dintr-un URL"
|
||||||
uploadFromUrlDescription: "URL-ul fișierului pe care dorești să îl încarci"
|
uploadFromUrlDescription: "URL-ul fișierului pe care dorești să îl încarci"
|
||||||
uploadFromUrlRequested: "Încărcare solicitată"
|
uploadFromUrlRequested: "Încărcare solicitată"
|
||||||
uploadFromUrlMayTakeTime: "S-ar putea să ia puțin până se finalizează încărcarea."
|
uploadFromUrlMayTakeTime: "S-ar putea să ia puțin până se finalizează încărcarea."
|
||||||
|
explore: "Explorează"
|
||||||
messageRead: "Citit"
|
messageRead: "Citit"
|
||||||
noMoreHistory: "Nu există mai mult istoric"
|
noMoreHistory: "Nu există mai mult istoric"
|
||||||
startMessaging: "Începe un chat nou"
|
startMessaging: "Începe un chat nou"
|
||||||
|
@ -317,6 +318,9 @@ inMb: "În megabytes"
|
||||||
iconUrl: "URL-ul iconiței"
|
iconUrl: "URL-ul iconiței"
|
||||||
bannerUrl: "URL-ul imaginii de banner"
|
bannerUrl: "URL-ul imaginii de banner"
|
||||||
backgroundImageUrl: "URL-ul imaginii de fundal"
|
backgroundImageUrl: "URL-ul imaginii de fundal"
|
||||||
|
pinnedUsers: "Utilizatori fixați"
|
||||||
|
pinnedUsersDescription: "Scrie utilizatorii, separați prin pauză de rând, care vor\
|
||||||
|
\ fi fixați pe pagina \"Explorează\"."
|
||||||
hcaptchaSiteKey: "Site key"
|
hcaptchaSiteKey: "Site key"
|
||||||
hcaptchaSecretKey: "Secret key"
|
hcaptchaSecretKey: "Secret key"
|
||||||
recaptchaSiteKey: "Site key"
|
recaptchaSiteKey: "Site key"
|
||||||
|
@ -341,6 +345,11 @@ silence: "Amuțește"
|
||||||
silenceConfirm: "Ești sigur că vrei să amuțești acest utilizator?"
|
silenceConfirm: "Ești sigur că vrei să amuțești acest utilizator?"
|
||||||
unsilence: "Anulează amuțirea"
|
unsilence: "Anulează amuțirea"
|
||||||
unsilenceConfirm: "Ești sigur că vrei să anulezi amuțirea acestui utilizator?"
|
unsilenceConfirm: "Ești sigur că vrei să anulezi amuțirea acestui utilizator?"
|
||||||
|
popularUsers: "Utilizatori populari"
|
||||||
|
recentlyUpdatedUsers: "Utilizatori activi recent"
|
||||||
|
recentlyRegisteredUsers: "Utilizatori ce s-au alăturat recent"
|
||||||
|
recentlyDiscoveredUsers: "Utilizatori descoperiți recent"
|
||||||
|
popularTags: "Taguri populare"
|
||||||
userList: "Liste"
|
userList: "Liste"
|
||||||
aboutMisskey: "Despre FoundKey"
|
aboutMisskey: "Despre FoundKey"
|
||||||
administrator: "Administrator"
|
administrator: "Administrator"
|
||||||
|
@ -381,6 +390,7 @@ messagingWithGroup: "Chat de grup"
|
||||||
title: "Titlu"
|
title: "Titlu"
|
||||||
text: "Text"
|
text: "Text"
|
||||||
enable: "Activează"
|
enable: "Activează"
|
||||||
|
next: "Următorul"
|
||||||
retype: "Introdu din nou"
|
retype: "Introdu din nou"
|
||||||
noteOf: "Notă de {user}"
|
noteOf: "Notă de {user}"
|
||||||
inviteToGroup: "Invită în grup"
|
inviteToGroup: "Invită în grup"
|
||||||
|
@ -574,6 +584,7 @@ abuseReports: "Rapoarte"
|
||||||
reportAbuse: "Raportează"
|
reportAbuse: "Raportează"
|
||||||
reportAbuseOf: "Raportează {name}"
|
reportAbuseOf: "Raportează {name}"
|
||||||
fillAbuseReportDescription: "Te rog scrie detaliile legate de acest raport."
|
fillAbuseReportDescription: "Te rog scrie detaliile legate de acest raport."
|
||||||
|
abuseReported: "Raportul tău a fost trimis. Mulțumim."
|
||||||
reporter: "Raportorul"
|
reporter: "Raportorul"
|
||||||
reporteeOrigin: "Originea raportatului"
|
reporteeOrigin: "Originea raportatului"
|
||||||
reporterOrigin: "Originea raportorului"
|
reporterOrigin: "Originea raportorului"
|
||||||
|
|
|
@ -231,6 +231,7 @@ uploadFromUrl: "Загрузить по ссылке"
|
||||||
uploadFromUrlDescription: "Ссылка на файл, который хотите загрузить"
|
uploadFromUrlDescription: "Ссылка на файл, который хотите загрузить"
|
||||||
uploadFromUrlRequested: "Загрузка выбранного"
|
uploadFromUrlRequested: "Загрузка выбранного"
|
||||||
uploadFromUrlMayTakeTime: "Загрузка может занять некоторое время."
|
uploadFromUrlMayTakeTime: "Загрузка может занять некоторое время."
|
||||||
|
explore: "Обзор"
|
||||||
messageRead: "Прочитали"
|
messageRead: "Прочитали"
|
||||||
noMoreHistory: "История закончилась"
|
noMoreHistory: "История закончилась"
|
||||||
startMessaging: "Начать общение"
|
startMessaging: "Начать общение"
|
||||||
|
@ -311,6 +312,9 @@ inMb: "В мегабайтах"
|
||||||
iconUrl: "Ссылка на аватар"
|
iconUrl: "Ссылка на аватар"
|
||||||
bannerUrl: "Ссылка на изображение в шапке"
|
bannerUrl: "Ссылка на изображение в шапке"
|
||||||
backgroundImageUrl: "Ссылка на фоновое изображение"
|
backgroundImageUrl: "Ссылка на фоновое изображение"
|
||||||
|
pinnedUsers: "Прикреплённый пользователь"
|
||||||
|
pinnedUsersDescription: "Перечислите по одному имени пользователя в строке. Пользователи,\
|
||||||
|
\ перечисленные здесь, будут привязаны к закладке \"Изучение\"."
|
||||||
hcaptchaSiteKey: "Ключ сайта"
|
hcaptchaSiteKey: "Ключ сайта"
|
||||||
hcaptchaSecretKey: "Секретный ключ"
|
hcaptchaSecretKey: "Секретный ключ"
|
||||||
recaptchaSiteKey: "Ключ сайта"
|
recaptchaSiteKey: "Ключ сайта"
|
||||||
|
@ -336,6 +340,11 @@ silence: "Заглушить"
|
||||||
silenceConfirm: "Заглушить этого пользователя? Уверены?"
|
silenceConfirm: "Заглушить этого пользователя? Уверены?"
|
||||||
unsilence: "Снять глушение"
|
unsilence: "Снять глушение"
|
||||||
unsilenceConfirm: "Снять глушение с этого пользователя? Уверены?"
|
unsilenceConfirm: "Снять глушение с этого пользователя? Уверены?"
|
||||||
|
popularUsers: "Популярные пользователи"
|
||||||
|
recentlyUpdatedUsers: "Активные последнее время"
|
||||||
|
recentlyRegisteredUsers: "Недавно зарегистрированные пользователи"
|
||||||
|
recentlyDiscoveredUsers: "Недавно обнаруженные пользователи"
|
||||||
|
popularTags: "Популярные теги"
|
||||||
userList: "Списки"
|
userList: "Списки"
|
||||||
aboutMisskey: "О FoundKey"
|
aboutMisskey: "О FoundKey"
|
||||||
administrator: "Администратор"
|
administrator: "Администратор"
|
||||||
|
@ -376,6 +385,7 @@ messagingWithGroup: "Общение в группе"
|
||||||
title: "Заголовок"
|
title: "Заголовок"
|
||||||
text: "Текст"
|
text: "Текст"
|
||||||
enable: "Включить"
|
enable: "Включить"
|
||||||
|
next: "Дальше"
|
||||||
retype: "Введите ещё раз"
|
retype: "Введите ещё раз"
|
||||||
noteOf: "Что пишет {user}"
|
noteOf: "Что пишет {user}"
|
||||||
inviteToGroup: "Пригласить в группу"
|
inviteToGroup: "Пригласить в группу"
|
||||||
|
@ -569,6 +579,7 @@ abuseReports: "Жалобы"
|
||||||
reportAbuse: "Жалоба"
|
reportAbuse: "Жалоба"
|
||||||
reportAbuseOf: "Пожаловаться на пользователя {name}"
|
reportAbuseOf: "Пожаловаться на пользователя {name}"
|
||||||
fillAbuseReportDescription: "Опишите, пожалуйста, причину жалобы подробнее."
|
fillAbuseReportDescription: "Опишите, пожалуйста, причину жалобы подробнее."
|
||||||
|
abuseReported: "Жалоба отправлена. Большое спасибо за информацию."
|
||||||
reporteeOrigin: "О ком сообщено"
|
reporteeOrigin: "О ком сообщено"
|
||||||
reporterOrigin: "Кто сообщил"
|
reporterOrigin: "Кто сообщил"
|
||||||
forwardReport: "Перенаправление отчета на инстант."
|
forwardReport: "Перенаправление отчета на инстант."
|
||||||
|
@ -952,6 +963,43 @@ _time:
|
||||||
minute: "мин"
|
minute: "мин"
|
||||||
hour: "ч"
|
hour: "ч"
|
||||||
day: "сут"
|
day: "сут"
|
||||||
|
_tutorial:
|
||||||
|
title: "Как пользоваться FoundKey"
|
||||||
|
step1_1: "Добро пожаловать!"
|
||||||
|
step1_2: "Эта страница называется «лента». Здесь будут появляться «заметки»: ваши\
|
||||||
|
\ личные и тех, на кого вы «подписаны». Они будут располагаться в порядке времени\
|
||||||
|
\ их появления."
|
||||||
|
step1_3: "Правда, ваша лента пока пуста. Она начнёт заполняться, когда вы будете\
|
||||||
|
\ писать свои заметки и подписываться на других."
|
||||||
|
step2_1: "Давайте, заполним профиль, прежде чем начать писать заметки и подписываться\
|
||||||
|
\ на других."
|
||||||
|
step2_2: "То, что вы расскажете в профиле, поможет лучше вас узнать, а значит, многим\
|
||||||
|
\ будет легче присоединиться — вы скорее получите новых подписчиков и читателей."
|
||||||
|
step3_1: "Успешно заполнили профиль?"
|
||||||
|
step3_2: "Что ж, теперь самое время опубликуовать заметку. Если нажать вверху страницы\
|
||||||
|
\ на изображение карандаша, появится форма для текста."
|
||||||
|
step3_3: "Напишите в неё, что хотите, и нажмите на кнопку в правом верхнем углу."
|
||||||
|
step3_4: "Ничего не приходит в голову? Как насчёт: «Я новенький, пока осваиваюсь\
|
||||||
|
\ в FoundKey»?"
|
||||||
|
step4_1: "С написанием первой заметки покончено?"
|
||||||
|
step4_2: "Отлично, теперь она должна появиться в вашей ленте."
|
||||||
|
step5_1: "А теперь самое время немного оживить ленту, подписавшись на других."
|
||||||
|
step5_2: "На странице «{featured}» собраны популярные сегодня заметки, читая которые,\
|
||||||
|
\ вы можете найти кого-то вам интересного, а на странице «{explore}» можно посмотреть,\
|
||||||
|
\ кто популярен у остальных."
|
||||||
|
step5_3: "Чтобы подписаться на кого-нибудь, щёлкните по его аватару и в открывшемся\
|
||||||
|
\ профиле нажмите кнопку «Подписаться»."
|
||||||
|
step5_4: "Некоторые пользователи (около их имени «висит замок») вручную подтверждают\
|
||||||
|
\ чужие подписки. Так что иногда подписка начинает работать не сразу."
|
||||||
|
step6_1: "Если теперь в ленте видны и чужие заметки, значит у вас получилось."
|
||||||
|
step6_2: "Здесь можно непринуждённо выразить свои чувства к чьей-то заметке, отметив\
|
||||||
|
\ «реакцию» под ней."
|
||||||
|
step6_3: "Отмечайте реакции, нажмая на символ «+» под заметкой и выбирая значок\
|
||||||
|
\ по душе."
|
||||||
|
step7_1: "На этом вводный урок по использованию FoundKey закончен. Спасибо, что\
|
||||||
|
\ прошли его до конца!"
|
||||||
|
step7_2: "Хотите изучить FoundKey глубже — добро пожаловать в раздел «{help}»."
|
||||||
|
step7_3: "Приятно вам провести время с FoundKey\U0001F680"
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "Двухфакторная аутентификация уже настроена."
|
alreadyRegistered: "Двухфакторная аутентификация уже настроена."
|
||||||
registerDevice: "Зарегистрируйте ваше устройство"
|
registerDevice: "Зарегистрируйте ваше устройство"
|
||||||
|
|
|
@ -230,6 +230,7 @@ uploadFromUrl: "Nahrať z URL adresy"
|
||||||
uploadFromUrlDescription: "URL adresa nahrávaného súboru"
|
uploadFromUrlDescription: "URL adresa nahrávaného súboru"
|
||||||
uploadFromUrlRequested: "Upload vyžiadaný"
|
uploadFromUrlRequested: "Upload vyžiadaný"
|
||||||
uploadFromUrlMayTakeTime: "Nahrávanie môže nejaký čas trvať."
|
uploadFromUrlMayTakeTime: "Nahrávanie môže nejaký čas trvať."
|
||||||
|
explore: "Objavovať"
|
||||||
messageRead: "Prečítané"
|
messageRead: "Prečítané"
|
||||||
noMoreHistory: "To je všetko"
|
noMoreHistory: "To je všetko"
|
||||||
startMessaging: "Začať chat"
|
startMessaging: "Začať chat"
|
||||||
|
@ -311,6 +312,9 @@ inMb: "V megabajtoch"
|
||||||
iconUrl: "Favicon URL"
|
iconUrl: "Favicon URL"
|
||||||
bannerUrl: "URL obrázku bannera"
|
bannerUrl: "URL obrázku bannera"
|
||||||
backgroundImageUrl: "URL obrázku pozadia"
|
backgroundImageUrl: "URL obrázku pozadia"
|
||||||
|
pinnedUsers: "Pripnutí používatelia"
|
||||||
|
pinnedUsersDescription: "Zoznam mien používateľov oddelených riadkami, ktorý budú\
|
||||||
|
\ pripnutí v záložke \"Objavovať\"."
|
||||||
hcaptchaSiteKey: "Site key"
|
hcaptchaSiteKey: "Site key"
|
||||||
hcaptchaSecretKey: "Secret key"
|
hcaptchaSecretKey: "Secret key"
|
||||||
recaptchaSiteKey: "Site key"
|
recaptchaSiteKey: "Site key"
|
||||||
|
@ -335,6 +339,11 @@ silence: "Ticho"
|
||||||
silenceConfirm: "Naozaj chcete utíšiť tohoto používateľa?"
|
silenceConfirm: "Naozaj chcete utíšiť tohoto používateľa?"
|
||||||
unsilence: "Vrátiť utíšenie"
|
unsilence: "Vrátiť utíšenie"
|
||||||
unsilenceConfirm: "Naozaj chcete vrátiť utíšenie tohoto používateľa?"
|
unsilenceConfirm: "Naozaj chcete vrátiť utíšenie tohoto používateľa?"
|
||||||
|
popularUsers: "Populárni používatelia"
|
||||||
|
recentlyUpdatedUsers: "Používatelia s najnovšou aktivitou"
|
||||||
|
recentlyRegisteredUsers: "Najnovší používatelia"
|
||||||
|
recentlyDiscoveredUsers: "Naposledy objavení používatelia"
|
||||||
|
popularTags: "Populárne značky"
|
||||||
userList: "Zoznamy"
|
userList: "Zoznamy"
|
||||||
aboutMisskey: "O FoundKey"
|
aboutMisskey: "O FoundKey"
|
||||||
administrator: "Administrátor"
|
administrator: "Administrátor"
|
||||||
|
@ -375,6 +384,7 @@ messagingWithGroup: "Skupinový chat"
|
||||||
title: "Nadpis"
|
title: "Nadpis"
|
||||||
text: "Text"
|
text: "Text"
|
||||||
enable: "Povoliť"
|
enable: "Povoliť"
|
||||||
|
next: "Ďalší"
|
||||||
retype: "Zadajte znovu"
|
retype: "Zadajte znovu"
|
||||||
noteOf: "Poznámky používateľa {user}"
|
noteOf: "Poznámky používateľa {user}"
|
||||||
inviteToGroup: "Pozvať do skupiny"
|
inviteToGroup: "Pozvať do skupiny"
|
||||||
|
@ -560,6 +570,7 @@ abuseReports: "Nahlásenia"
|
||||||
reportAbuse: "Nahlásiť"
|
reportAbuse: "Nahlásiť"
|
||||||
reportAbuseOf: "Nahlásiť {name}"
|
reportAbuseOf: "Nahlásiť {name}"
|
||||||
fillAbuseReportDescription: "Prosím vyplňte podrobnosti nahlásenia."
|
fillAbuseReportDescription: "Prosím vyplňte podrobnosti nahlásenia."
|
||||||
|
abuseReported: "Vaše nahlásenie je odoslané. Veľmi pekne ďakujeme."
|
||||||
reporter: "Nahlásil"
|
reporter: "Nahlásil"
|
||||||
reporteeOrigin: "Pôvod nahláseného"
|
reporteeOrigin: "Pôvod nahláseného"
|
||||||
reporterOrigin: "Pôvod nahlasovača"
|
reporterOrigin: "Pôvod nahlasovača"
|
||||||
|
@ -948,6 +959,40 @@ _time:
|
||||||
minute: "min"
|
minute: "min"
|
||||||
hour: "hod"
|
hour: "hod"
|
||||||
day: "dní"
|
day: "dní"
|
||||||
|
_tutorial:
|
||||||
|
title: "Ako používať FoundKey"
|
||||||
|
step1_1: "Vitajte!"
|
||||||
|
step1_2: "Táto stránka sa volá \"časová os\". Zobrazuje chronologicky zoradené \"\
|
||||||
|
poznámky\" od ľudí, ktorých sledujete."
|
||||||
|
step1_3: "Vaša časová os je teraz prázdna pretože ste nepridali žiadne poznámky\
|
||||||
|
\ ani nikoho zatiaľ nesledujete."
|
||||||
|
step2_1: "Podˇme dokončiť nastavenia vášho profilu pred napísaním poznámky alebo\
|
||||||
|
\ sledovaním niekoho."
|
||||||
|
step2_2: "Poskytnutím informácií o vás uľahčíte ostatným, či chcú vidieť alebo sledovať\
|
||||||
|
\ vaše poznámky."
|
||||||
|
step3_1: "Dokončili ste nastavovanie svojho profilu?"
|
||||||
|
step3_2: "Poďme vyskúšať napísať poznámku. Môžete to spraviť stlačením ikony ceruzky\
|
||||||
|
\ na vrchu obrazovky."
|
||||||
|
step3_3: "Vyplňte polia a stlačte tlačítko vpravo hore."
|
||||||
|
step3_4: "Nemáte čo povedať? Skúste \"len si nastavujem môj msky\"!"
|
||||||
|
step4_1: "Napísali ste svoju prvú poznámku?"
|
||||||
|
step4_2: "Hurá! Teraz by vaša prvá poznámka mala byť na vašej časovej osi."
|
||||||
|
step5_1: "Teraz skúsme oživiť časovú os sledovaním nejakých ľudí."
|
||||||
|
step5_2: "{featured} zobrazí populárne poznámku na tomto serveri. {explore} môžete\
|
||||||
|
\ objavovať populárnych používateľov. Skúste tam nájsť ľudí, ktorých by ste radi\
|
||||||
|
\ sledovali!"
|
||||||
|
step5_3: "Ak chcete sledovať ďalších používateľov, kliknite na ich ikonu a stlačte\
|
||||||
|
\ tlačidlo \"Sledovať\" na ich profile."
|
||||||
|
step5_4: "Ak má niektorý používateľ ikonu zámku vedľa svojho mena, znamená to, že\
|
||||||
|
\ môže trvať určitý čas, kým daný používateľ schváli vašu žiadosť o sledovanie."
|
||||||
|
step6_1: "Teraz by ste mali vidieť poznámky ďalších používateľov na svojej časovej\
|
||||||
|
\ osi."
|
||||||
|
step6_2: "Môžete dať \"reakcie\" na poznámky ďalších ľudí ako rýchlu odpoveď."
|
||||||
|
step6_3: "Reakciu pridáte kliknutím na \"+\" niekoho poznámke a vybratím emoji,\
|
||||||
|
\ ktorou chcete reagovať."
|
||||||
|
step7_1: "Gralujeme! Dokončili ste základného sprievodcu FoundKey."
|
||||||
|
step7_2: "Ak sa chcete naučiť viac o FoundKey, skúste sekciu {help}."
|
||||||
|
step7_3: "A teraz, veľa šťastia, bavte sa s FoundKey! \U0001F680"
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "Už ste zaregistrovali 2-faktorové autentifikačné zariadenie."
|
alreadyRegistered: "Už ste zaregistrovali 2-faktorové autentifikačné zariadenie."
|
||||||
registerDevice: "Registrovať nové zariadenie"
|
registerDevice: "Registrovať nové zariadenie"
|
||||||
|
|
|
@ -308,6 +308,7 @@ uploadFromUrl: Ladda upp via en URL
|
||||||
uploadFromUrlDescription: URL till filen som du vill ladda upp
|
uploadFromUrlDescription: URL till filen som du vill ladda upp
|
||||||
uploadFromUrlRequested: Förfrågade uppladningar
|
uploadFromUrlRequested: Förfrågade uppladningar
|
||||||
uploadFromUrlMayTakeTime: Det tar kanske ett tag innan uppladningen är färdig.
|
uploadFromUrlMayTakeTime: Det tar kanske ett tag innan uppladningen är färdig.
|
||||||
|
explore: Upptäck
|
||||||
messageRead: Läs
|
messageRead: Läs
|
||||||
noMoreHistory: Det finns ingen mer historik
|
noMoreHistory: Det finns ingen mer historik
|
||||||
startMessaging: Inled en ny chatt
|
startMessaging: Inled en ny chatt
|
||||||
|
|
|
@ -234,6 +234,7 @@ uploadFromUrl: "Завантажити з посилання"
|
||||||
uploadFromUrlDescription: "Посилання на файл для завантаження"
|
uploadFromUrlDescription: "Посилання на файл для завантаження"
|
||||||
uploadFromUrlRequested: "Завантаження розпочалось"
|
uploadFromUrlRequested: "Завантаження розпочалось"
|
||||||
uploadFromUrlMayTakeTime: "Завантаження може зайняти деякий час."
|
uploadFromUrlMayTakeTime: "Завантаження може зайняти деякий час."
|
||||||
|
explore: "Огляд"
|
||||||
messageRead: "Прочитано"
|
messageRead: "Прочитано"
|
||||||
noMoreHistory: "Подальшої історії немає"
|
noMoreHistory: "Подальшої історії немає"
|
||||||
startMessaging: "Розпочати діалог"
|
startMessaging: "Розпочати діалог"
|
||||||
|
@ -313,6 +314,9 @@ inMb: "В мегабайтах"
|
||||||
iconUrl: "URL аватара"
|
iconUrl: "URL аватара"
|
||||||
bannerUrl: "URL банера"
|
bannerUrl: "URL банера"
|
||||||
backgroundImageUrl: "URL-адреса фонового зображення"
|
backgroundImageUrl: "URL-адреса фонового зображення"
|
||||||
|
pinnedUsers: "Закріплені користувачі"
|
||||||
|
pinnedUsersDescription: "Впишіть в список користувачів, яких хочете закріпити на сторінці\
|
||||||
|
\ \"Знайти\", ім'я в стовпчик."
|
||||||
hcaptchaSiteKey: "Ключ сайту"
|
hcaptchaSiteKey: "Ключ сайту"
|
||||||
hcaptchaSecretKey: "Секретний ключ"
|
hcaptchaSecretKey: "Секретний ключ"
|
||||||
recaptchaSiteKey: "Ключ сайту"
|
recaptchaSiteKey: "Ключ сайту"
|
||||||
|
@ -337,6 +341,11 @@ silence: "Заглушити"
|
||||||
silenceConfirm: "Ви впевнені, що хочете заглушити цього користувача?"
|
silenceConfirm: "Ви впевнені, що хочете заглушити цього користувача?"
|
||||||
unsilence: "Не глушити"
|
unsilence: "Не глушити"
|
||||||
unsilenceConfirm: "Ви впевнені, що хочете скасувати глушіння цього користувача?"
|
unsilenceConfirm: "Ви впевнені, що хочете скасувати глушіння цього користувача?"
|
||||||
|
popularUsers: "Популярні користувачі"
|
||||||
|
recentlyUpdatedUsers: "Нещодавно активні користувачі"
|
||||||
|
recentlyRegisteredUsers: "Нещодавно зареєстровані користувачі"
|
||||||
|
recentlyDiscoveredUsers: "Нещодавно знайдені користувачі"
|
||||||
|
popularTags: "Популярні теги"
|
||||||
userList: "Списки"
|
userList: "Списки"
|
||||||
aboutMisskey: "Про FoundKey"
|
aboutMisskey: "Про FoundKey"
|
||||||
administrator: "Адмін"
|
administrator: "Адмін"
|
||||||
|
@ -377,6 +386,7 @@ messagingWithGroup: "Чат з групою"
|
||||||
title: "Тема"
|
title: "Тема"
|
||||||
text: "Текст"
|
text: "Текст"
|
||||||
enable: "Увімкнути"
|
enable: "Увімкнути"
|
||||||
|
next: "Далі"
|
||||||
retype: "Введіть ще раз"
|
retype: "Введіть ще раз"
|
||||||
noteOf: "Нотатка {user}"
|
noteOf: "Нотатка {user}"
|
||||||
inviteToGroup: "Запрошення до групи"
|
inviteToGroup: "Запрошення до групи"
|
||||||
|
@ -567,6 +577,7 @@ abuseReports: "Скарги"
|
||||||
reportAbuse: "Поскаржитись"
|
reportAbuse: "Поскаржитись"
|
||||||
reportAbuseOf: "Поскаржитись на {name}"
|
reportAbuseOf: "Поскаржитись на {name}"
|
||||||
fillAbuseReportDescription: "Будь ласка вкажіть подробиці скарги."
|
fillAbuseReportDescription: "Будь ласка вкажіть подробиці скарги."
|
||||||
|
abuseReported: "Дякуємо, вашу скаргу було відправлено."
|
||||||
reporter: "Репортер"
|
reporter: "Репортер"
|
||||||
reporteeOrigin: "Про кого повідомлено"
|
reporteeOrigin: "Про кого повідомлено"
|
||||||
reporterOrigin: "Хто повідомив"
|
reporterOrigin: "Хто повідомив"
|
||||||
|
@ -804,6 +815,40 @@ _time:
|
||||||
minute: "х"
|
minute: "х"
|
||||||
hour: "г"
|
hour: "г"
|
||||||
day: "д"
|
day: "д"
|
||||||
|
_tutorial:
|
||||||
|
title: "Як користуватись FoundKey"
|
||||||
|
step1_1: "Ласкаво просимо!"
|
||||||
|
step1_2: "Ця сторінка має назву \"стрічка подій\". На ній з'являються записи користувачів\
|
||||||
|
\ на яких ви підписані."
|
||||||
|
step1_3: "Наразі ваша стрічка порожня, оскільки ви ще не написали жодної нотатки\
|
||||||
|
\ і не підписані на інших."
|
||||||
|
step2_1: "Перш ніж зробити запис або підписатись на когось, спочатку заповніть свій\
|
||||||
|
\ обліковий запис."
|
||||||
|
step2_2: "Надання деякої інформації про себе дозволить іншим користувачам підписатись\
|
||||||
|
\ на вас."
|
||||||
|
step3_1: "Ви успішно налаштували свій обліковий запис?"
|
||||||
|
step3_2: "Наступним кроком є написання нотатки. Це можна зробити, натиснувши зображення\
|
||||||
|
\ олівця на екрані."
|
||||||
|
step3_3: "Після написання вмісту ви можете опублікувати його, натиснувши кнопку\
|
||||||
|
\ у верхньому правому куті форми."
|
||||||
|
step3_4: "Не знаєте що написати? Спробуйте \"налаштовую свій msky\"!"
|
||||||
|
step4_1: "Ви розмістили свій перший запис?"
|
||||||
|
step4_2: "Ура! Ваш перший запис відображається на вашій стрічці подій."
|
||||||
|
step5_1: "Настав час оживити вашу стрічку подій підписавшись на інших користувачів."
|
||||||
|
step5_2: "{featured} показує популярні записи , а {explore} популярних користувачів\
|
||||||
|
\ з цього інстансу. Спробуйте підписатись на користувача, який вам сподобався!"
|
||||||
|
step5_3: "Щоб підписатись на інших користувачів, нажміть на їхнє зображення, а потім\
|
||||||
|
\ на кнопку \"підписатись\"."
|
||||||
|
step5_4: "Якщо користувач має замок при імені, то йому потрібно буде вручну підтвердити\
|
||||||
|
\ вашу заявку на підписку."
|
||||||
|
step6_1: "Тепер ви повинні бачити записи інших користувачів на вашій стрічці подій."
|
||||||
|
step6_2: "Також ви можете швидко відповісти, або \"відреагувати\" на записи інших\
|
||||||
|
\ користувачів."
|
||||||
|
step6_3: "Щоб \"відреагувати\", нажміть на знак плюс \"+\" на записі і виберіть\
|
||||||
|
\ емоджі яким ви хочете \"відреагувати\"."
|
||||||
|
step7_1: "Вітаю! Ви пройшли ознайомлення з FoundKey."
|
||||||
|
step7_2: "Якщо ви хочете більше дізнатись про FoundKey, зайдіть в розділ {help}."
|
||||||
|
step7_3: "Насолоджуйтесь FoundKey! \U0001F680"
|
||||||
_2fa:
|
_2fa:
|
||||||
registerKey: "Зареєструвати новий ключ безпеки"
|
registerKey: "Зареєструвати новий ключ безпеки"
|
||||||
_permissions:
|
_permissions:
|
||||||
|
|
|
@ -230,6 +230,7 @@ uploadFromUrl: "Tải lên bằng một URL"
|
||||||
uploadFromUrlDescription: "URL của tập tin bạn muốn tải lên"
|
uploadFromUrlDescription: "URL của tập tin bạn muốn tải lên"
|
||||||
uploadFromUrlRequested: "Đã yêu cầu tải lên"
|
uploadFromUrlRequested: "Đã yêu cầu tải lên"
|
||||||
uploadFromUrlMayTakeTime: "Sẽ mất một khoảng thời gian để tải lên xong."
|
uploadFromUrlMayTakeTime: "Sẽ mất một khoảng thời gian để tải lên xong."
|
||||||
|
explore: "Khám phá"
|
||||||
messageRead: "Đã đọc"
|
messageRead: "Đã đọc"
|
||||||
noMoreHistory: "Không còn gì để đọc"
|
noMoreHistory: "Không còn gì để đọc"
|
||||||
startMessaging: "Bắt đầu trò chuyện"
|
startMessaging: "Bắt đầu trò chuyện"
|
||||||
|
@ -311,6 +312,9 @@ inMb: "Tính bằng MB"
|
||||||
iconUrl: "URL Icon"
|
iconUrl: "URL Icon"
|
||||||
bannerUrl: "URL Ảnh bìa"
|
bannerUrl: "URL Ảnh bìa"
|
||||||
backgroundImageUrl: "URL Ảnh nền"
|
backgroundImageUrl: "URL Ảnh nền"
|
||||||
|
pinnedUsers: "Những người thú vị"
|
||||||
|
pinnedUsersDescription: "Liệt kê mỗi hàng một tên người dùng xuống dòng để ghim trên\
|
||||||
|
\ tab \"Khám phá\"."
|
||||||
hcaptchaSiteKey: "Khóa của trang"
|
hcaptchaSiteKey: "Khóa của trang"
|
||||||
hcaptchaSecretKey: "Khóa bí mật"
|
hcaptchaSecretKey: "Khóa bí mật"
|
||||||
recaptchaSiteKey: "Khóa của trang"
|
recaptchaSiteKey: "Khóa của trang"
|
||||||
|
@ -335,6 +339,11 @@ silence: "Ẩn"
|
||||||
silenceConfirm: "Bạn có chắc muốn ẩn người này?"
|
silenceConfirm: "Bạn có chắc muốn ẩn người này?"
|
||||||
unsilence: "Bỏ ẩn"
|
unsilence: "Bỏ ẩn"
|
||||||
unsilenceConfirm: "Bạn có chắc muốn bỏ ẩn người này?"
|
unsilenceConfirm: "Bạn có chắc muốn bỏ ẩn người này?"
|
||||||
|
popularUsers: "Những người nổi tiếng"
|
||||||
|
recentlyUpdatedUsers: "Hoạt động gần đây"
|
||||||
|
recentlyRegisteredUsers: "Mới tham gia"
|
||||||
|
recentlyDiscoveredUsers: "Mới khám phá"
|
||||||
|
popularTags: "Hashtag thông dụng"
|
||||||
userList: "Danh sách"
|
userList: "Danh sách"
|
||||||
aboutMisskey: "Về FoundKey"
|
aboutMisskey: "Về FoundKey"
|
||||||
administrator: "Quản trị viên"
|
administrator: "Quản trị viên"
|
||||||
|
@ -375,6 +384,7 @@ messagingWithGroup: "Chat nhóm"
|
||||||
title: "Tựa đề"
|
title: "Tựa đề"
|
||||||
text: "Nội dung"
|
text: "Nội dung"
|
||||||
enable: "Bật"
|
enable: "Bật"
|
||||||
|
next: "Kế tiếp"
|
||||||
retype: "Nhập lại"
|
retype: "Nhập lại"
|
||||||
noteOf: "Tút của {user}"
|
noteOf: "Tút của {user}"
|
||||||
inviteToGroup: "Mời vào nhóm"
|
inviteToGroup: "Mời vào nhóm"
|
||||||
|
@ -565,6 +575,7 @@ abuseReports: "Lượt báo cáo"
|
||||||
reportAbuse: "Báo cáo"
|
reportAbuse: "Báo cáo"
|
||||||
reportAbuseOf: "Báo cáo {name}"
|
reportAbuseOf: "Báo cáo {name}"
|
||||||
fillAbuseReportDescription: "Vui lòng điền thông tin chi tiết về báo cáo này."
|
fillAbuseReportDescription: "Vui lòng điền thông tin chi tiết về báo cáo này."
|
||||||
|
abuseReported: "Báo cáo đã được gửi. Cảm ơn bạn nhiều."
|
||||||
reporter: "Người báo cáo"
|
reporter: "Người báo cáo"
|
||||||
reporteeOrigin: "Bị báo cáo"
|
reporteeOrigin: "Bị báo cáo"
|
||||||
reporterOrigin: "Máy chủ người báo cáo"
|
reporterOrigin: "Máy chủ người báo cáo"
|
||||||
|
@ -955,6 +966,43 @@ _time:
|
||||||
minute: "phút"
|
minute: "phút"
|
||||||
hour: "giờ"
|
hour: "giờ"
|
||||||
day: "ngày"
|
day: "ngày"
|
||||||
|
_tutorial:
|
||||||
|
title: "Cách dùng FoundKey"
|
||||||
|
step1_1: "Xin chào!"
|
||||||
|
step1_2: "Trang này gọi là \"bảng tin\". Nó hiện \"tút\" từ những người mà bạn \"\
|
||||||
|
theo dõi\" theo thứ tự thời gian."
|
||||||
|
step1_3: "Bảng tin của bạn đang trống, bởi vì bạn chưa đăng tút nào hoặc chưa theo\
|
||||||
|
\ dõi ai."
|
||||||
|
step2_1: "Hãy hoàn thành việc thiết lập hồ sơ của bạn trước khi viết tút hoặc theo\
|
||||||
|
\ dõi bất kỳ ai."
|
||||||
|
step2_2: "Cung cấp một số thông tin giới thiệu bạn là ai sẽ giúp người khác dễ dàng\
|
||||||
|
\ biết được họ muốn đọc tút hay theo dõi bạn."
|
||||||
|
step3_1: "Hoàn thành thiết lập hồ sơ của bạn?"
|
||||||
|
step3_2: "Sau đó, hãy thử đăng một tút tiếp theo. Bạn có thể làm như vậy bằng cách\
|
||||||
|
\ nhấn vào nút có biểu tượng bút chì trên màn hình."
|
||||||
|
step3_3: "Nhập nội dung vào khung soạn thảo và nhấn nút đăng ở góc trên."
|
||||||
|
step3_4: "Chưa biết nói gì? Thử \"Tôi mới tham gia FoundKey\"!"
|
||||||
|
step4_1: "Đăng xong tút đầu tiên của bạn?"
|
||||||
|
step4_2: "De! Tút đầu tiên của bạn đã hiện trên bảng tin."
|
||||||
|
step5_1: "Bây giờ, hãy thử làm cho bảng tin của bạn sinh động hơn bằng cách theo\
|
||||||
|
\ dõi những người khác."
|
||||||
|
step5_2: "{feature} sẽ hiển thị cho bạn các tút nổi bật trên máy chủ này. {explore}\
|
||||||
|
\ sẽ cho phép bạn tìm thấy những người dùng thú vị. Hãy thử tìm những người bạn\
|
||||||
|
\ muốn theo dõi ở đó!"
|
||||||
|
step5_3: "Để theo dõi những người dùng khác, hãy nhấn vào ảnh đại diện của họ và\
|
||||||
|
\ nhấn nút \"Theo dõi\" trên hồ sơ của họ."
|
||||||
|
step5_4: "Nếu người dùng khác có biểu tượng ổ khóa bên cạnh tên của họ, có thể mất\
|
||||||
|
\ một khoảng thời gian để người dùng đó phê duyệt yêu cầu theo dõi của bạn theo\
|
||||||
|
\ cách thủ công."
|
||||||
|
step6_1: "Bạn sẽ có thể xem tút của những người dùng khác trên bảng tin của mình\
|
||||||
|
\ ngay bây giờ."
|
||||||
|
step6_2: "Bạn cũng có thể đặt \"biểu cảm\" trên tút của người khác để phản hồi nhanh\
|
||||||
|
\ chúng."
|
||||||
|
step6_3: "Để đính kèm \"biểu cảm\", hãy nhấn vào dấu \"+\" trên tút của người dùng\
|
||||||
|
\ khác rồi chọn biểu tượng cảm xúc mà bạn muốn dùng."
|
||||||
|
step7_1: "Xin chúc mừng! Bây giờ bạn đã hoàn thành phần hướng dẫn cơ bản của FoundKey."
|
||||||
|
step7_2: "Nếu bạn muốn tìm hiểu thêm về FoundKey, hãy thử phần {help}."
|
||||||
|
step7_3: "Bây giờ, chúc may mắn và vui vẻ với FoundKey! \U0001F680"
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "Bạn đã đăng ký thiết bị xác minh 2 bước."
|
alreadyRegistered: "Bạn đã đăng ký thiết bị xác minh 2 bước."
|
||||||
registerDevice: "Đăng ký một thiết bị"
|
registerDevice: "Đăng ký một thiết bị"
|
||||||
|
|
|
@ -211,6 +211,7 @@ uploadFromUrl: "从网址上传"
|
||||||
uploadFromUrlDescription: "输入文件的URL"
|
uploadFromUrlDescription: "输入文件的URL"
|
||||||
uploadFromUrlRequested: "请求上传"
|
uploadFromUrlRequested: "请求上传"
|
||||||
uploadFromUrlMayTakeTime: "上传可能需要一些时间完成。"
|
uploadFromUrlMayTakeTime: "上传可能需要一些时间完成。"
|
||||||
|
explore: "发现"
|
||||||
messageRead: "已读"
|
messageRead: "已读"
|
||||||
noMoreHistory: "没有更多的历史记录"
|
noMoreHistory: "没有更多的历史记录"
|
||||||
startMessaging: "添加聊天"
|
startMessaging: "添加聊天"
|
||||||
|
@ -289,6 +290,8 @@ inMb: "以兆字节(MegaByte)为单位"
|
||||||
iconUrl: "图标URL"
|
iconUrl: "图标URL"
|
||||||
bannerUrl: "横幅URL"
|
bannerUrl: "横幅URL"
|
||||||
backgroundImageUrl: "背景图URL"
|
backgroundImageUrl: "背景图URL"
|
||||||
|
pinnedUsers: "置顶用户"
|
||||||
|
pinnedUsersDescription: "在「发现」页面中使用换行标记想要置顶的用户。"
|
||||||
hcaptchaSiteKey: "网站密钥"
|
hcaptchaSiteKey: "网站密钥"
|
||||||
hcaptchaSecretKey: "密钥"
|
hcaptchaSecretKey: "密钥"
|
||||||
recaptchaSiteKey: "网站密钥"
|
recaptchaSiteKey: "网站密钥"
|
||||||
|
@ -312,6 +315,11 @@ silence: "禁言"
|
||||||
silenceConfirm: "确认要禁言吗?"
|
silenceConfirm: "确认要禁言吗?"
|
||||||
unsilence: "解除禁言"
|
unsilence: "解除禁言"
|
||||||
unsilenceConfirm: "要解除禁言吗?"
|
unsilenceConfirm: "要解除禁言吗?"
|
||||||
|
popularUsers: "热门用户"
|
||||||
|
recentlyUpdatedUsers: "最近投稿的用户"
|
||||||
|
recentlyRegisteredUsers: "最近登录的用户"
|
||||||
|
recentlyDiscoveredUsers: "最近发现的用户"
|
||||||
|
popularTags: "热门标签"
|
||||||
userList: "列表"
|
userList: "列表"
|
||||||
aboutMisskey: "关于 FoundKey"
|
aboutMisskey: "关于 FoundKey"
|
||||||
administrator: "管理员"
|
administrator: "管理员"
|
||||||
|
@ -352,6 +360,7 @@ messagingWithGroup: "与群组聊天"
|
||||||
title: "标题"
|
title: "标题"
|
||||||
text: "文本"
|
text: "文本"
|
||||||
enable: "启用"
|
enable: "启用"
|
||||||
|
next: "下一个"
|
||||||
retype: "重新输入"
|
retype: "重新输入"
|
||||||
noteOf: "{user}的帖子"
|
noteOf: "{user}的帖子"
|
||||||
inviteToGroup: "群组邀请"
|
inviteToGroup: "群组邀请"
|
||||||
|
@ -526,6 +535,7 @@ abuseReports: "举报"
|
||||||
reportAbuse: "举报"
|
reportAbuse: "举报"
|
||||||
reportAbuseOf: "举报{name}"
|
reportAbuseOf: "举报{name}"
|
||||||
fillAbuseReportDescription: "请填写举报的详细原因。"
|
fillAbuseReportDescription: "请填写举报的详细原因。"
|
||||||
|
abuseReported: "内容已发送。感谢您提交信息。"
|
||||||
reporter: "举报者"
|
reporter: "举报者"
|
||||||
reporteeOrigin: "举报来源"
|
reporteeOrigin: "举报来源"
|
||||||
reporterOrigin: "举报者来源"
|
reporterOrigin: "举报者来源"
|
||||||
|
@ -887,6 +897,29 @@ _time:
|
||||||
minute: "分"
|
minute: "分"
|
||||||
hour: "小时"
|
hour: "小时"
|
||||||
day: "日"
|
day: "日"
|
||||||
|
_tutorial:
|
||||||
|
title: "FoundKey的使用方法"
|
||||||
|
step1_1: "欢迎!"
|
||||||
|
step1_2: "这个页面叫做「时间线」,它会按照时间顺序显示所有你「关注」的人所发的「帖子」。"
|
||||||
|
step1_3: "如果你并没有发布任何帖子,也没有关注其他的人,你的时间线页面应当什么都没有显示。"
|
||||||
|
step2_1: "在您想要发帖或关注其他人之前,请先设置一下个人资料吧。"
|
||||||
|
step2_2: "如果别人能够更加的了解你,关注你的概率也会得到提升。"
|
||||||
|
step3_1: "已经设置完个人资料了吗?"
|
||||||
|
step3_2: "那么接下来,试着写一些什么东西来发布吧。你可以通过点击屏幕上的铅笔图标来打开投稿页面。"
|
||||||
|
step3_3: "写完内容后,点击窗口右上方的按钮就可以投稿。"
|
||||||
|
step3_4: "不知道说些什么好吗?那就写下「FoundKey我来啦!」这样的话吧。"
|
||||||
|
step4_1: "将你的话语发布出去了吗?"
|
||||||
|
step4_2: "太棒了!现在你可以在你的时间线中看到你刚刚发布的帖子了。"
|
||||||
|
step5_1: "接下来,关注其他人来使时间线更生动吧。"
|
||||||
|
step5_2: "{featured}将向您展示热门趋势的帖子。 {explore}将让您找到热门用户。 尝试关注您喜欢的人!"
|
||||||
|
step5_3: "要关注其他用户,请单击他的头像,然后在他的个人资料上按下“关注”按钮。"
|
||||||
|
step5_4: "如果用户的名称旁边有锁定图标,则该用户需要手动批准您的关注请求。"
|
||||||
|
step6_1: "现在,您将可以在时间线上看到其他用户的帖子。"
|
||||||
|
step6_2: "您还可以在其他人的帖子上进行「回应」,以快速做出简单回复。"
|
||||||
|
step6_3: "在他人的贴子上按下「+」图标,即可选择想要的表情来进行「回应」。"
|
||||||
|
step7_1: "对FoundKey基本操作的简单介绍,就到此结束了。 辛苦了!"
|
||||||
|
step7_2: "如果你想了解更多有关FoundKey的信息,请参见{help}。"
|
||||||
|
step7_3: "接下来,享受FoundKey带来的乐趣吧\U0001F680"
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "此设备已被注册"
|
alreadyRegistered: "此设备已被注册"
|
||||||
registerDevice: "注册设备"
|
registerDevice: "注册设备"
|
||||||
|
|
|
@ -211,6 +211,7 @@ uploadFromUrl: "從網址上傳"
|
||||||
uploadFromUrlDescription: "您要上傳的文件的URL"
|
uploadFromUrlDescription: "您要上傳的文件的URL"
|
||||||
uploadFromUrlRequested: "已請求上傳"
|
uploadFromUrlRequested: "已請求上傳"
|
||||||
uploadFromUrlMayTakeTime: "還需要一些時間才能完成上傳。"
|
uploadFromUrlMayTakeTime: "還需要一些時間才能完成上傳。"
|
||||||
|
explore: "探索"
|
||||||
messageRead: "已讀"
|
messageRead: "已讀"
|
||||||
noMoreHistory: "沒有更多歷史紀錄"
|
noMoreHistory: "沒有更多歷史紀錄"
|
||||||
startMessaging: "開始傳送訊息"
|
startMessaging: "開始傳送訊息"
|
||||||
|
@ -289,6 +290,8 @@ inMb: "以Mbps為單位"
|
||||||
iconUrl: "圖像URL"
|
iconUrl: "圖像URL"
|
||||||
bannerUrl: "橫幅圖像URL"
|
bannerUrl: "橫幅圖像URL"
|
||||||
backgroundImageUrl: "背景圖片的來源網址"
|
backgroundImageUrl: "背景圖片的來源網址"
|
||||||
|
pinnedUsers: "置頂用戶"
|
||||||
|
pinnedUsersDescription: "在「發現」頁面中使用換行標記想要置頂的使用者。"
|
||||||
hcaptchaSiteKey: "網站金鑰"
|
hcaptchaSiteKey: "網站金鑰"
|
||||||
hcaptchaSecretKey: "金鑰"
|
hcaptchaSecretKey: "金鑰"
|
||||||
recaptchaSiteKey: "網站金鑰"
|
recaptchaSiteKey: "網站金鑰"
|
||||||
|
@ -312,6 +315,11 @@ silence: "禁言"
|
||||||
silenceConfirm: "確定要禁言此用戶嗎?"
|
silenceConfirm: "確定要禁言此用戶嗎?"
|
||||||
unsilence: "解除禁言"
|
unsilence: "解除禁言"
|
||||||
unsilenceConfirm: "確定要解除禁言嗎?"
|
unsilenceConfirm: "確定要解除禁言嗎?"
|
||||||
|
popularUsers: "熱門使用者"
|
||||||
|
recentlyUpdatedUsers: "最近發文的使用者"
|
||||||
|
recentlyRegisteredUsers: "新加入使用者"
|
||||||
|
recentlyDiscoveredUsers: "最近發現的使用者"
|
||||||
|
popularTags: "熱門標籤"
|
||||||
userList: "清單"
|
userList: "清單"
|
||||||
aboutMisskey: "關於 FoundKey"
|
aboutMisskey: "關於 FoundKey"
|
||||||
administrator: "管理員"
|
administrator: "管理員"
|
||||||
|
@ -352,6 +360,7 @@ messagingWithGroup: "發送訊息至群組"
|
||||||
title: "標題"
|
title: "標題"
|
||||||
text: "文字"
|
text: "文字"
|
||||||
enable: "啟用"
|
enable: "啟用"
|
||||||
|
next: "下一步"
|
||||||
retype: "重新輸入"
|
retype: "重新輸入"
|
||||||
noteOf: "{user}的貼文"
|
noteOf: "{user}的貼文"
|
||||||
inviteToGroup: "邀請至群組"
|
inviteToGroup: "邀請至群組"
|
||||||
|
@ -525,6 +534,7 @@ abuseReports: "檢舉"
|
||||||
reportAbuse: "檢舉"
|
reportAbuse: "檢舉"
|
||||||
reportAbuseOf: "檢舉{name}"
|
reportAbuseOf: "檢舉{name}"
|
||||||
fillAbuseReportDescription: "請填寫檢舉的詳細理由。"
|
fillAbuseReportDescription: "請填寫檢舉的詳細理由。"
|
||||||
|
abuseReported: "回報已送出。感謝您的報告。"
|
||||||
reporter: "檢舉者"
|
reporter: "檢舉者"
|
||||||
reporteeOrigin: "檢舉來源"
|
reporteeOrigin: "檢舉來源"
|
||||||
reporterOrigin: "檢舉者來源"
|
reporterOrigin: "檢舉者來源"
|
||||||
|
@ -886,6 +896,29 @@ _time:
|
||||||
minute: "分鐘"
|
minute: "分鐘"
|
||||||
hour: "小時"
|
hour: "小時"
|
||||||
day: "日"
|
day: "日"
|
||||||
|
_tutorial:
|
||||||
|
title: "FoundKey使用方法"
|
||||||
|
step1_1: "歡迎!"
|
||||||
|
step1_2: "此為「時間軸」頁面,它會按照時間順序顯示你「追隨」的人發出的「貼文」"
|
||||||
|
step1_3: "由於你沒有發佈任何貼文,也沒有追隨任何人,所以你的時間軸目前是空的。"
|
||||||
|
step2_1: "在發文或追隨其他人之前先讓我們設定一下個人資料吧。"
|
||||||
|
step2_2: "提供一些關於自己的資訊來讓其他人更有追隨你的意願。"
|
||||||
|
step3_1: "個人資料都設定好了嗎?"
|
||||||
|
step3_2: "接下來,讓我們來試試看發個文,按一下畫面上的鉛筆圖示來開始"
|
||||||
|
step3_3: "輸入完內容後,按視窗右上角的按鈕來發文"
|
||||||
|
step3_4: "不知道該寫什麼內容嗎?試試看「開始使用FoundKey了」如何。"
|
||||||
|
step4_1: "貼文發出去了嗎?"
|
||||||
|
step4_2: "如果你的貼文出現在時間軸上,就代表發文成功。"
|
||||||
|
step5_1: "現在試試看追隨其他人來讓你的時間軸變得更生動吧。"
|
||||||
|
step5_2: "你會在{featured}上看到受歡迎的貼文,你也可以從列表中追隨你喜歡的人,或者在{explore}上找到熱門使用者。"
|
||||||
|
step5_3: "想要追隨其他人,只要點擊他們的大頭貼並按「追隨」即可。"
|
||||||
|
step5_4: "如果使用者的名字旁有鎖頭的圖示,代表他們需要手動核准你的追隨請求。"
|
||||||
|
step6_1: "現在你可以在時間軸上看到其他用戶的貼文。"
|
||||||
|
step6_2: "你也可以對別人的貼文作出「情感」,作出簡單的回覆。"
|
||||||
|
step6_3: "在他人的貼文按下\"+\"圖標,即可選擇喜好的表情符號進行回應。"
|
||||||
|
step7_1: "以上為FoundKey的基本操作說明,教學在此告一段落。辛苦了。"
|
||||||
|
step7_2: "歡迎到{help}來瞭解更多FoundKey相關介紹。"
|
||||||
|
step7_3: "那麼,祝您在FoundKey玩的開心~ \U0001F680"
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "此設備已經被註冊過了"
|
alreadyRegistered: "此設備已經被註冊過了"
|
||||||
registerDevice: "註冊裝置"
|
registerDevice: "註冊裝置"
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
"build": "yarn workspaces foreach --topological run build && yarn run gulp",
|
"build": "yarn workspaces foreach --topological run build && yarn run gulp",
|
||||||
"build-parallel": "yarn workspaces foreach --parallel --topological run build && yarn run gulp",
|
"build-parallel": "yarn workspaces foreach --parallel --topological run build && yarn run gulp",
|
||||||
"start": "yarn workspace backend run start",
|
"start": "yarn workspace backend run start",
|
||||||
|
"start:test": "yarn workspace backend run start:test",
|
||||||
"init": "yarn migrate",
|
"init": "yarn migrate",
|
||||||
"migrate": "yarn workspace backend run migrate",
|
"migrate": "yarn workspace backend run migrate",
|
||||||
"migrateandstart": "yarn migrate && yarn start",
|
"migrateandstart": "yarn migrate && yarn start",
|
||||||
|
@ -20,6 +21,11 @@
|
||||||
"watch": "yarn dev",
|
"watch": "yarn dev",
|
||||||
"dev": "node ./scripts/dev.mjs",
|
"dev": "node ./scripts/dev.mjs",
|
||||||
"lint": "yarn workspaces foreach run lint",
|
"lint": "yarn workspaces foreach run lint",
|
||||||
|
"cy:open": "cypress open --browser --e2e --config-file=cypress.config.ts",
|
||||||
|
"cy:run": "cypress run",
|
||||||
|
"e2e": "start-server-and-test start:test http://localhost:61812 cy:run",
|
||||||
|
"mocha": "yarn workspace backend run mocha",
|
||||||
|
"test": "yarn mocha",
|
||||||
"format": "gulp format",
|
"format": "gulp format",
|
||||||
"clean": "node ./scripts/clean.mjs",
|
"clean": "node ./scripts/clean.mjs",
|
||||||
"clean-all": "node ./scripts/clean-all.mjs",
|
"clean-all": "node ./scripts/clean-all.mjs",
|
||||||
|
@ -30,6 +36,7 @@
|
||||||
"lodash": "^4.17.21"
|
"lodash": "^4.17.21"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"argon2": "^0.30.2",
|
||||||
"execa": "5.1.1",
|
"execa": "5.1.1",
|
||||||
"gulp": "4.0.2",
|
"gulp": "4.0.2",
|
||||||
"gulp-cssnano": "2.1.3",
|
"gulp-cssnano": "2.1.3",
|
||||||
|
@ -43,6 +50,8 @@
|
||||||
"@types/gulp-rename": "2.0.1",
|
"@types/gulp-rename": "2.0.1",
|
||||||
"@typescript-eslint/parser": "^5.46.1",
|
"@typescript-eslint/parser": "^5.46.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
|
"cypress": "10.3.0",
|
||||||
|
"start-server-and-test": "1.14.0",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.4"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@3.4.1"
|
"packageManager": "yarn@3.4.1"
|
||||||
|
|
8
packages/backend/.mocharc.json
Normal file
8
packages/backend/.mocharc.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"node-option": [
|
||||||
|
"experimental-specifier-resolution=node"
|
||||||
|
],
|
||||||
|
"slow": 1000,
|
||||||
|
"timeout": 30000,
|
||||||
|
"exit": true
|
||||||
|
}
|
|
@ -7,14 +7,6 @@ export class removeMentionedRemoteUsersColumn1661376843000 {
|
||||||
|
|
||||||
async down(queryRunner) {
|
async down(queryRunner) {
|
||||||
await queryRunner.query(`ALTER TABLE "note" ADD "mentionedRemoteUsers" TEXT NOT NULL DEFAULT '[]'::text`);
|
await queryRunner.query(`ALTER TABLE "note" ADD "mentionedRemoteUsers" TEXT NOT NULL DEFAULT '[]'::text`);
|
||||||
await queryRunner.query(`CREATE TEMP TABLE IF NOT EXISTS "temp_mentions" AS
|
await queryRunner.query(`UPDATE "note" SET "mentionedRemoteUsers" = (SELECT COALESCE(json_agg(row_to_json("data"))::text, '[]') FROM (SELECT "url", "uri", "username", "host" FROM "user" JOIN "user_profile" ON "user"."id" = "user_profile". "userId" WHERE "user"."host" IS NOT NULL AND "user"."id" = ANY("note"."mentions")) AS "data")`);
|
||||||
SELECT "id", "url", "uri", "username", "host"
|
|
||||||
FROM "user"
|
|
||||||
JOIN "user_profile" ON "user"."id" = "user_profile". "userId" WHERE "user"."host" IS NOT NULL`);
|
|
||||||
await queryRunner.query(`CREATE UNIQUE INDEX "temp_mentions_id" ON "temp_mentions"("id")`);
|
|
||||||
await queryRunner.query(`UPDATE "note" SET "mentionedRemoteUsers" = (
|
|
||||||
SELECT COALESCE(json_agg(row_to_json("data")::jsonb - 'id')::text, '[]') FROM "temp_mentions" AS "data"
|
|
||||||
WHERE "data"."id" = ANY("note"."mentions")
|
|
||||||
)`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
export class syncDatabase1689005520053 {
|
|
||||||
name = 'syncDatabase1689005520053';
|
|
||||||
|
|
||||||
async up(queryRunner) {
|
|
||||||
await queryRunner.query(`COMMENT ON COLUMN "user"."isDeleted" IS 'How many delivery jobs are outstanding before the deletion is completed.'`);
|
|
||||||
await queryRunner.query(`ALTER TYPE "public"."note_thread_muting_mutingnotificationtypes_enum" RENAME TO "note_thread_muting_mutingnotificationtypes_enum_old"`);
|
|
||||||
await queryRunner.query(`CREATE TYPE "public"."note_thread_muting_mutingnotificationtypes_enum" AS ENUM('mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'update')`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "note_thread_muting" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "note_thread_muting" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."note_thread_muting_mutingnotificationtypes_enum"[] USING "mutingNotificationTypes"::"text"::"public"."note_thread_muting_mutingnotificationtypes_enum"[]`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "note_thread_muting" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
|
||||||
await queryRunner.query(`DROP TYPE "public"."note_thread_muting_mutingnotificationtypes_enum_old"`);
|
|
||||||
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`);
|
|
||||||
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'update', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'move', 'app')`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum" USING "type"::"text"::"public"."notification_type_enum"`);
|
|
||||||
await queryRunner.query(`DROP TYPE "public"."notification_type_enum_old"`);
|
|
||||||
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" RENAME TO "user_profile_mutingnotificationtypes_enum_old"`);
|
|
||||||
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'update', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'move', 'app')`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum"[]`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
|
||||||
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum_old"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async down(queryRunner) {
|
|
||||||
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'move', 'app', 'updated')`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
|
||||||
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`);
|
|
||||||
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`);
|
|
||||||
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'move', 'app', 'updated')`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum_old" USING "type"::"text"::"public"."notification_type_enum_old"`);
|
|
||||||
await queryRunner.query(`DROP TYPE "public"."notification_type_enum"`);
|
|
||||||
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum_old" RENAME TO "notification_type_enum"`);
|
|
||||||
await queryRunner.query(`CREATE TYPE "public"."note_thread_muting_mutingnotificationtypes_enum_old" AS ENUM('mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded')`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "note_thread_muting" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "note_thread_muting" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."note_thread_muting_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."note_thread_muting_mutingnotificationtypes_enum_old"[]`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "note_thread_muting" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
|
||||||
await queryRunner.query(`DROP TYPE "public"."note_thread_muting_mutingnotificationtypes_enum"`);
|
|
||||||
await queryRunner.query(`ALTER TYPE "public"."note_thread_muting_mutingnotificationtypes_enum_old" RENAME TO "note_thread_muting_mutingnotificationtypes_enum"`);
|
|
||||||
await queryRunner.query(`COMMENT ON COLUMN "user"."isDeleted" IS NULL`);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
export class removePinnedUsers1704234742539 {
|
|
||||||
async up(queryRunner) {
|
|
||||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "pinnedUsers"`);
|
|
||||||
}
|
|
||||||
async down(queryRunner) {
|
|
||||||
await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedUsers" character varying(256) array NOT NULL DEFAULT '{}'::varchar[]`);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
export class removeNoteVisibility1704236065406 {
|
|
||||||
async up(queryRunner) {
|
|
||||||
await queryRunner.query(`ALTER TABLE "poll" DROP COLUMN "noteVisibility"`);
|
|
||||||
await queryRunner.query(`DROP TYPE "poll_notevisibility_enum"`);
|
|
||||||
}
|
|
||||||
async down(queryRunner) {
|
|
||||||
await queryRunner.query(`CREATE TYPE "poll_notevisibility_enum" AS ENUM('public', 'home', 'followers', 'specified')`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "poll" ADD "noteVisibility" "poll_notevisibility_enum" NOT NULL`);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
export class removeHashtagChart1710687673333 {
|
|
||||||
async up(queryRunner) {
|
|
||||||
await queryRunner.query(`DROP TABLE "__chart__hashtag"`);
|
|
||||||
await queryRunner.query(`DROP TABLE "__chart_day__hashtag"`);
|
|
||||||
}
|
|
||||||
async down(queryRunner) {
|
|
||||||
await queryRunner.query(`CREATE TABLE public.__chart__hashtag ("id" SERIAL NOT NULL CONSTRAINT "PK_c32f1ea2b44a5d2f7881e37f8f9" PRIMARY KEY,"date" integer NOT NULL,"group" character varying(128) NOT NULL,"___local_users" integer DEFAULT 0 NOT NULL,"___remote_users" integer DEFAULT 0 NOT NULL,"unique_temp___local_users" character varying[] DEFAULT '{}'::character varying[] NOT NULL,"unique_temp___remote_users" character varying[] DEFAULT '{}'::character varying[] NOT NULL,CONSTRAINT "UQ_25a97c02003338124b2b75fdbc8" UNIQUE ("date", "group"))`);
|
|
||||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_25a97c02003338124b2b75fdbc" ON public.__chart__hashtag USING btree (date, "group")`);
|
|
||||||
await queryRunner.query(`CREATE TABLE public.__chart_day__hashtag ("id" SERIAL NOT NULL CONSTRAINT CONSTRAINT "PK_13d5a3b089344e5557f8e0980b4" PRIMARY KEY,"date" integer NOT NULL,"group" character varying(128) NOT NULL,"___local_users" integer DEFAULT 0 NOT NULL,"___remote_users" integer DEFAULT 0 NOT NULL,"unique_temp___local_users" character varying[] DEFAULT '{}'::character varying[] NOT NULL,"unique_temp___remote_users" character varying[] DEFAULT '{}'::character varying[] NOT NULL,CONSTRAINT "UQ_8f589cf056ff51f09d6096f6450" UNIQUE ("date", "group"))`);
|
|
||||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_8f589cf056ff51f09d6096f645" ON public.__chart_day__hashtag USING btree (date, "group")`);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
export class removeSummaly1725695130950 {
|
|
||||||
name = 'removeSummaly1725695130950'
|
|
||||||
|
|
||||||
async up(queryRunner) {
|
|
||||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "summalyProxy"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async down(queryRunner) {
|
|
||||||
await queryRunner.query(`ALTER TABLE "meta" ADD "summalyProxy" character varying(128)`);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,26 +8,37 @@
|
||||||
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
|
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
|
||||||
"watch": "node watch.mjs",
|
"watch": "node watch.mjs",
|
||||||
"lint": "tsc --noEmit --skipLibCheck && eslint src --ext .ts",
|
"lint": "tsc --noEmit --skipLibCheck && eslint src --ext .ts",
|
||||||
"migrate": "yarn exec typeorm migration:run -d ormconfig.js",
|
"mocha": "NODE_ENV=test mocha",
|
||||||
"start": "node --experimental-json-modules ./built/index.js"
|
"migrate": "npx typeorm migration:run -d ormconfig.js",
|
||||||
|
"start": "node --experimental-json-modules ./built/index.js",
|
||||||
|
"start:test": "NODE_ENV=test node --experimental-json-modules ./built/index.js",
|
||||||
|
"test": "npm run mocha"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bull-board/api": "^4.3.1",
|
"@bull-board/api": "^4.3.1",
|
||||||
"@bull-board/koa": "^4.3.1",
|
"@bull-board/koa": "^4.3.1",
|
||||||
"@discordapp/twemoji": "14.0.2",
|
"@discordapp/twemoji": "14.0.2",
|
||||||
|
"@elastic/elasticsearch": "7.11.0",
|
||||||
"@koa/cors": "3.1.0",
|
"@koa/cors": "3.1.0",
|
||||||
"@koa/multer": "3.0.0",
|
"@koa/multer": "3.0.0",
|
||||||
"@koa/router": "9.0.1",
|
"@koa/router": "9.0.1",
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
|
"@sinonjs/fake-timers": "9.1.2",
|
||||||
|
"@syuilo/aiscript": "0.11.1",
|
||||||
|
"abort-controller": "3.0.0",
|
||||||
"ajv": "8.11.0",
|
"ajv": "8.11.0",
|
||||||
"archiver": "5.3.1",
|
"archiver": "5.3.1",
|
||||||
"argon2": "^0.30.2",
|
"autobind-decorator": "2.4.0",
|
||||||
"aws-sdk": "2.1165.0",
|
"aws-sdk": "2.1165.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"blurhash": "1.1.5",
|
"blurhash": "1.1.5",
|
||||||
"bull": "4.8.4",
|
"bull": "4.8.4",
|
||||||
"cacheable-lookup": "6.0.4",
|
"cacheable-lookup": "6.0.4",
|
||||||
"cbor": "8.1.0",
|
"cbor": "8.1.0",
|
||||||
|
"chalk": "5.0.1",
|
||||||
|
"chalk-template": "0.4.0",
|
||||||
|
"cli-highlight": "2.1.11",
|
||||||
|
"color-convert": "2.0.1",
|
||||||
"content-disposition": "0.5.4",
|
"content-disposition": "0.5.4",
|
||||||
"date-fns": "2.28.0",
|
"date-fns": "2.28.0",
|
||||||
"decompress": "4.2.1",
|
"decompress": "4.2.1",
|
||||||
|
@ -41,13 +52,13 @@
|
||||||
"hpagent": "0.1.2",
|
"hpagent": "0.1.2",
|
||||||
"ioredis": "4.28.5",
|
"ioredis": "4.28.5",
|
||||||
"ip-cidr": "3.0.10",
|
"ip-cidr": "3.0.10",
|
||||||
"ipaddr.js": "2.1.0",
|
|
||||||
"is-svg": "4.3.2",
|
"is-svg": "4.3.2",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"jsdom": "20.0.0",
|
"jsdom": "20.0.0",
|
||||||
|
"json5": "2.2.1",
|
||||||
|
"json5-loader": "4.0.1",
|
||||||
"jsonld": "6.0.0",
|
"jsonld": "6.0.0",
|
||||||
"jsrsasign": "10.5.25",
|
"jsrsasign": "10.5.25",
|
||||||
"katex": "^0.16.0",
|
|
||||||
"koa": "2.13.4",
|
"koa": "2.13.4",
|
||||||
"koa-bodyparser": "4.3.0",
|
"koa-bodyparser": "4.3.0",
|
||||||
"koa-favicon": "2.1.0",
|
"koa-favicon": "2.1.0",
|
||||||
|
@ -55,9 +66,11 @@
|
||||||
"koa-logger": "3.2.1",
|
"koa-logger": "3.2.1",
|
||||||
"koa-mount": "4.0.0",
|
"koa-mount": "4.0.0",
|
||||||
"koa-send": "5.0.1",
|
"koa-send": "5.0.1",
|
||||||
|
"koa-slow": "2.1.0",
|
||||||
"koa-views": "7.0.2",
|
"koa-views": "7.0.2",
|
||||||
"mfm-js": "0.23.3",
|
"mfm-js": "0.23.3",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
|
"mocha": "10.2.0",
|
||||||
"multer": "1.4.5-lts.1",
|
"multer": "1.4.5-lts.1",
|
||||||
"nested-property": "4.0.0",
|
"nested-property": "4.0.0",
|
||||||
"node-fetch": "3.2.6",
|
"node-fetch": "3.2.6",
|
||||||
|
@ -65,6 +78,7 @@
|
||||||
"os-utils": "0.0.14",
|
"os-utils": "0.0.14",
|
||||||
"parse5": "7.0.0",
|
"parse5": "7.0.0",
|
||||||
"pg": "8.7.3",
|
"pg": "8.7.3",
|
||||||
|
"private-ip": "2.3.3",
|
||||||
"probe-image-size": "7.2.3",
|
"probe-image-size": "7.2.3",
|
||||||
"promise-limit": "2.7.0",
|
"promise-limit": "2.7.0",
|
||||||
"pug": "3.0.2",
|
"pug": "3.0.2",
|
||||||
|
@ -75,7 +89,9 @@
|
||||||
"ratelimiter": "3.4.1",
|
"ratelimiter": "3.4.1",
|
||||||
"re2": "1.17.8",
|
"re2": "1.17.8",
|
||||||
"redis-lock": "0.1.4",
|
"redis-lock": "0.1.4",
|
||||||
|
"reflect-metadata": "0.1.13",
|
||||||
"rename": "1.0.4",
|
"rename": "1.0.4",
|
||||||
|
"require-all": "3.0.0",
|
||||||
"rss-parser": "3.12.0",
|
"rss-parser": "3.12.0",
|
||||||
"sanitize-html": "2.7.0",
|
"sanitize-html": "2.7.0",
|
||||||
"semver": "7.3.7",
|
"semver": "7.3.7",
|
||||||
|
@ -83,10 +99,15 @@
|
||||||
"speakeasy": "2.0.0",
|
"speakeasy": "2.0.0",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
|
"style-loader": "3.3.1",
|
||||||
|
"summaly": "2.7.0",
|
||||||
"systeminformation": "5.11.22",
|
"systeminformation": "5.11.22",
|
||||||
"tinycolor2": "1.4.2",
|
"tinycolor2": "1.4.2",
|
||||||
"tmp": "0.2.1",
|
"tmp": "0.2.1",
|
||||||
|
"ts-loader": "9.3.1",
|
||||||
|
"ts-node": "10.9.1",
|
||||||
"tsc-alias": "1.7.0",
|
"tsc-alias": "1.7.0",
|
||||||
|
"tsconfig-paths": "4.1.0",
|
||||||
"twemoji-parser": "14.0.0",
|
"twemoji-parser": "14.0.0",
|
||||||
"typeorm": "0.3.7",
|
"typeorm": "0.3.7",
|
||||||
"uuid": "8.3.2",
|
"uuid": "8.3.2",
|
||||||
|
@ -118,6 +139,7 @@
|
||||||
"@types/koa__cors": "3.1.1",
|
"@types/koa__cors": "3.1.1",
|
||||||
"@types/koa__multer": "2.0.4",
|
"@types/koa__multer": "2.0.4",
|
||||||
"@types/koa__router": "8.0.11",
|
"@types/koa__router": "8.0.11",
|
||||||
|
"@types/mocha": "9.1.1",
|
||||||
"@types/node": "18.7.16",
|
"@types/node": "18.7.16",
|
||||||
"@types/node-fetch": "3.0.3",
|
"@types/node-fetch": "3.0.3",
|
||||||
"@types/nodemailer": "6.4.5",
|
"@types/nodemailer": "6.4.5",
|
||||||
|
|
14
packages/backend/src/@types/koa-slow.d.ts
vendored
Normal file
14
packages/backend/src/@types/koa-slow.d.ts
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
declare module 'koa-slow' {
|
||||||
|
import { Middleware } from 'koa';
|
||||||
|
|
||||||
|
interface ISlowOptions {
|
||||||
|
url?: RegExp;
|
||||||
|
delay?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function slow(options?: ISlowOptions): Middleware;
|
||||||
|
|
||||||
|
namespace slow {} // Hack
|
||||||
|
|
||||||
|
export = slow;
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import cluster from 'node:cluster';
|
import cluster from 'node:cluster';
|
||||||
import net from 'node:net';
|
import chalk from 'chalk';
|
||||||
import Xev from 'xev';
|
import Xev from 'xev';
|
||||||
|
|
||||||
import Logger from '@/services/logger.js';
|
import Logger from '@/services/logger.js';
|
||||||
|
@ -10,8 +10,8 @@ import 'reflect-metadata';
|
||||||
import { masterMain } from './master.js';
|
import { masterMain } from './master.js';
|
||||||
import { workerMain } from './worker.js';
|
import { workerMain } from './worker.js';
|
||||||
|
|
||||||
const logger = new Logger('core');
|
const logger = new Logger('core', 'cyan');
|
||||||
const clusterLogger = logger.createSubLogger('cluster');
|
const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
|
||||||
const ev = new Xev();
|
const ev = new Xev();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,8 +26,6 @@ export async function boot(): Promise<void> {
|
||||||
process.title = `Foundkey (${process.env.mode})`;
|
process.title = `Foundkey (${process.env.mode})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
net.setDefaultAutoSelectFamily(true);
|
|
||||||
|
|
||||||
if (cluster.isPrimary || envOption.disableClustering) {
|
if (cluster.isPrimary || envOption.disableClustering) {
|
||||||
await masterMain();
|
await masterMain();
|
||||||
|
|
||||||
|
@ -59,6 +57,14 @@ cluster.on('online', worker => {
|
||||||
clusterLogger.debug(`Process is now online: [${worker.id}]`);
|
clusterLogger.debug(`Process is now online: [${worker.id}]`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Listen for dying workers
|
||||||
|
cluster.on('exit', worker => {
|
||||||
|
// Replace the dead worker,
|
||||||
|
// we're not sentimental
|
||||||
|
clusterLogger.error(chalk.red(`[${worker.id}] died :(`));
|
||||||
|
cluster.fork();
|
||||||
|
});
|
||||||
|
|
||||||
// Display detail of unhandled promise rejection
|
// Display detail of unhandled promise rejection
|
||||||
if (envOption.logLevel !== LOG_LEVELS.quiet) {
|
if (envOption.logLevel !== LOG_LEVELS.quiet) {
|
||||||
process.on('unhandledRejection', console.dir);
|
process.on('unhandledRejection', console.dir);
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { fileURLToPath } from 'node:url';
|
||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
import * as os from 'node:os';
|
import * as os from 'node:os';
|
||||||
import cluster from 'node:cluster';
|
import cluster from 'node:cluster';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import chalkTemplate from 'chalk-template';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
|
|
||||||
import Logger from '@/services/logger.js';
|
import Logger from '@/services/logger.js';
|
||||||
|
@ -17,27 +19,29 @@ const _dirname = dirname(_filename);
|
||||||
|
|
||||||
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
|
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
|
||||||
|
|
||||||
const logger = new Logger('core');
|
const logger = new Logger('core', 'cyan');
|
||||||
const bootLogger = logger.createSubLogger('boot');
|
const bootLogger = logger.createSubLogger('boot', 'magenta', false);
|
||||||
|
|
||||||
|
const themeColor = chalk.hex('#86b300');
|
||||||
|
|
||||||
function greet(): void {
|
function greet(): void {
|
||||||
if (envOption.logLevel !== LOG_LEVELS.quiet) {
|
if (envOption.logLevel !== LOG_LEVELS.quiet) {
|
||||||
//#region FoundKey logo
|
//#region FoundKey logo
|
||||||
console.log(' ___ _ _ __ ');
|
console.log(themeColor(' ___ _ _ __ '));
|
||||||
console.log(' | __|__ _ _ _ _ __| | |/ /___ _ _ ');
|
console.log(themeColor(' | __|__ _ _ _ _ __| | |/ /___ _ _ '));
|
||||||
console.log(' | _/ _ \\ || | \' \\/ _` | \' </ -_) || |');
|
console.log(themeColor(' | _/ _ \\ || | \' \\/ _` | \' </ -_) || |'));
|
||||||
console.log(' |_|\\___/\\_,_|_||_\\__,_|_|\\_\\___|\\_, |');
|
console.log(themeColor(' |_|\\___/\\_,_|_||_\\__,_|_|\\_\\___|\\_, |'));
|
||||||
console.log(' |__/ ');
|
console.log(themeColor(' |__/ '));
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
console.log(' FoundKey is an open-source decentralized microblogging platform.');
|
console.log(' FoundKey is an open-source decentralized microblogging platform.');
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(`--- ${os.hostname()} (PID: ${process.pid.toString()}) ---`);
|
console.log(chalkTemplate`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`);
|
||||||
}
|
}
|
||||||
|
|
||||||
bootLogger.info('Welcome to FoundKey!');
|
bootLogger.info('Welcome to FoundKey!');
|
||||||
bootLogger.info(`FoundKey v${meta.version}`);
|
bootLogger.info(`FoundKey v${meta.version}`, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -55,7 +59,7 @@ export async function masterMain(): Promise<void> {
|
||||||
config = loadConfigBoot();
|
config = loadConfigBoot();
|
||||||
await connectDb();
|
await connectDb();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
bootLogger.error('Fatal error occurred during initialization');
|
bootLogger.error('Fatal error occurred during initialization', true);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,7 +69,7 @@ export async function masterMain(): Promise<void> {
|
||||||
await spawnWorkers(config.clusterLimits);
|
await spawnWorkers(config.clusterLimits);
|
||||||
}
|
}
|
||||||
|
|
||||||
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`);
|
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, true);
|
||||||
|
|
||||||
if (!envOption.noDaemons) {
|
if (!envOption.noDaemons) {
|
||||||
import('../daemons/server-stats.js').then(x => x.serverStats());
|
import('../daemons/server-stats.js').then(x => x.serverStats());
|
||||||
|
@ -80,7 +84,7 @@ function showEnvironment(): void {
|
||||||
|
|
||||||
if (env !== 'production') {
|
if (env !== 'production') {
|
||||||
logger.warn('The environment is not in production mode.');
|
logger.warn('The environment is not in production mode.');
|
||||||
logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!');
|
logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,7 +109,7 @@ function loadConfigBoot(): Config {
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
const e = exception as Partial<NodeJS.ErrnoException> | Error;
|
const e = exception as Partial<NodeJS.ErrnoException> | Error;
|
||||||
if ('code' in e && e.code === 'ENOENT') {
|
if ('code' in e && e.code === 'ENOENT') {
|
||||||
configLogger.error('Configuration file not found');
|
configLogger.error('Configuration file not found', true);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
} else if (e instanceof Error) {
|
} else if (e instanceof Error) {
|
||||||
configLogger.error(e.message);
|
configLogger.error(e.message);
|
||||||
|
@ -129,7 +133,7 @@ async function connectDb(): Promise<void> {
|
||||||
const v = await db.query('SHOW server_version').then(x => x[0].server_version);
|
const v = await db.query('SHOW server_version').then(x => x[0].server_version);
|
||||||
dbLogger.succ(`Connected: v${v}`);
|
dbLogger.succ(`Connected: v${v}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dbLogger.error('Cannot connect');
|
dbLogger.error('Cannot connect', true);
|
||||||
dbLogger.error(e as Error | string);
|
dbLogger.error(e as Error | string);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
@ -164,10 +168,6 @@ async function spawnWorkers(clusterLimits: Required<Config['clusterLimits']>): P
|
||||||
function spawnWorker(mode: 'web' | 'queue'): Promise<void> {
|
function spawnWorker(mode: 'web' | 'queue'): Promise<void> {
|
||||||
return new Promise(res => {
|
return new Promise(res => {
|
||||||
const worker = cluster.fork({ mode });
|
const worker = cluster.fork({ mode });
|
||||||
worker.on('exit', async (code, signal) => {
|
|
||||||
logger.error(mode + ' worker died, restarting...');
|
|
||||||
await spawnWorker(mode);
|
|
||||||
});
|
|
||||||
worker.on('message', message => {
|
worker.on('message', message => {
|
||||||
switch (message) {
|
switch (message) {
|
||||||
case 'listenFailed':
|
case 'listenFailed':
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import Logger from '@/services/logger.js';
|
import Logger from '@/services/logger.js';
|
||||||
import config from './index.js';
|
import config from './index.js';
|
||||||
|
|
||||||
const logger = new Logger('config:redis');
|
const logger = new Logger('config:redis', 'gray', false);
|
||||||
|
|
||||||
function getRedisFamily(family?: string | number): number {
|
function getRedisFamily(family?: string | number): number {
|
||||||
const familyMap = {
|
const familyMap = {
|
||||||
|
|
|
@ -24,6 +24,14 @@ export type Source = {
|
||||||
db?: number;
|
db?: number;
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
};
|
};
|
||||||
|
elasticsearch?: {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
ssl?: boolean;
|
||||||
|
user?: string;
|
||||||
|
pass?: string;
|
||||||
|
index?: string;
|
||||||
|
};
|
||||||
|
|
||||||
proxy?: string;
|
proxy?: string;
|
||||||
proxySmtp?: string;
|
proxySmtp?: string;
|
||||||
|
|
56
packages/backend/src/db/elasticsearch.ts
Normal file
56
packages/backend/src/db/elasticsearch.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import * as elasticsearch from '@elastic/elasticsearch';
|
||||||
|
import config from '@/config/index.js';
|
||||||
|
|
||||||
|
const index = {
|
||||||
|
settings: {
|
||||||
|
analysis: {
|
||||||
|
analyzer: {
|
||||||
|
ngram: {
|
||||||
|
tokenizer: 'ngram',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mappings: {
|
||||||
|
properties: {
|
||||||
|
text: {
|
||||||
|
type: 'text',
|
||||||
|
index: true,
|
||||||
|
analyzer: 'ngram',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: 'keyword',
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
userHost: {
|
||||||
|
type: 'keyword',
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Init ElasticSearch connection
|
||||||
|
const client = config.elasticsearch ? new elasticsearch.Client({
|
||||||
|
node: `${config.elasticsearch.ssl ? 'https://' : 'http://'}${config.elasticsearch.host}:${config.elasticsearch.port}`,
|
||||||
|
auth: (config.elasticsearch.user && config.elasticsearch.pass) ? {
|
||||||
|
username: config.elasticsearch.user,
|
||||||
|
password: config.elasticsearch.pass,
|
||||||
|
} : undefined,
|
||||||
|
pingTimeout: 30000,
|
||||||
|
}) : null;
|
||||||
|
|
||||||
|
if (client) {
|
||||||
|
client.indices.exists({
|
||||||
|
index: config.elasticsearch.index || 'misskey_note',
|
||||||
|
}).then(exist => {
|
||||||
|
if (!exist.body) {
|
||||||
|
client.indices.create({
|
||||||
|
index: config.elasticsearch.index || 'misskey_note',
|
||||||
|
body: index,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default client;
|
|
@ -4,6 +4,7 @@ import pg from 'pg';
|
||||||
pg.types.setTypeParser(20, Number);
|
pg.types.setTypeParser(20, Number);
|
||||||
|
|
||||||
import { Logger, DataSource } from 'typeorm';
|
import { Logger, DataSource } from 'typeorm';
|
||||||
|
import * as highlight from 'cli-highlight';
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
|
|
||||||
import { SECOND } from '@/const.js';
|
import { SECOND } from '@/const.js';
|
||||||
|
@ -69,20 +70,27 @@ import { entities as charts } from '@/services/chart/entities.js';
|
||||||
import { Webhook } from '@/models/entities/webhook.js';
|
import { Webhook } from '@/models/entities/webhook.js';
|
||||||
import { getRedisOptions } from '@/config/redis.js';
|
import { getRedisOptions } from '@/config/redis.js';
|
||||||
import { dbLogger } from './logger.js';
|
import { dbLogger } from './logger.js';
|
||||||
|
import { redisClient } from './redis.js';
|
||||||
|
|
||||||
const sqlLogger = dbLogger.createSubLogger('sql');
|
const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
|
||||||
|
|
||||||
class MyCustomLogger implements Logger {
|
class MyCustomLogger implements Logger {
|
||||||
|
private highlight(sql: string): string {
|
||||||
|
return highlight.highlight(sql, {
|
||||||
|
language: 'sql', ignoreIllegals: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public logQuery(query: string): void {
|
public logQuery(query: string): void {
|
||||||
sqlLogger.info(query);
|
sqlLogger.info(this.highlight(query).substring(0, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
public logQueryError(error: string, query: string): void {
|
public logQueryError(error: string, query: string): void {
|
||||||
sqlLogger.error(query);
|
sqlLogger.error(this.highlight(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
public logQuerySlow(time: number, query: string): void {
|
public logQuerySlow(time: number, query: string): void {
|
||||||
sqlLogger.warn(query);
|
sqlLogger.warn(this.highlight(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
public logSchemaBuild(message: string): void {
|
public logSchemaBuild(message: string): void {
|
||||||
|
@ -201,3 +209,31 @@ export async function initDb(force = false) {
|
||||||
await db.initialize();
|
await db.initialize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resetDb() {
|
||||||
|
const reset = async () => {
|
||||||
|
await redisClient.flushdb();
|
||||||
|
const tables = await db.query(`SELECT relname AS "table"
|
||||||
|
FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
|
||||||
|
WHERE nspname NOT IN ('pg_catalog', 'information_schema')
|
||||||
|
AND C.relkind = 'r'
|
||||||
|
AND nspname !~ '^pg_toast';`);
|
||||||
|
for (const table of tables) {
|
||||||
|
await db.query(`DELETE FROM "${table.table}" CASCADE`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 1; i <= 3; i++) {
|
||||||
|
try {
|
||||||
|
await reset();
|
||||||
|
} catch (e) {
|
||||||
|
if (i === 3) {
|
||||||
|
throw e;
|
||||||
|
} else {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, SECOND));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ export const envOption = {
|
||||||
noDaemons: false,
|
noDaemons: false,
|
||||||
disableClustering: false,
|
disableClustering: false,
|
||||||
withLogTime: false,
|
withLogTime: false,
|
||||||
|
slow: false,
|
||||||
logLevel: LOG_LEVELS.info,
|
logLevel: LOG_LEVELS.info,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -23,9 +24,7 @@ for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) {
|
||||||
if (value.toLowerCase() in LOG_LEVELS) {
|
if (value.toLowerCase() in LOG_LEVELS) {
|
||||||
envOption.logLevel = LOG_LEVELS[value.toLowerCase()];
|
envOption.logLevel = LOG_LEVELS[value.toLowerCase()];
|
||||||
}
|
}
|
||||||
else {
|
console.log('Unknown log level ' + JSON.stringify(value.toLowerCase()) + ', defaulting to "info"');
|
||||||
console.log('Unknown log level ' + JSON.stringify(value.toLowerCase()) + ', defaulting to "info"');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
envOption[key] = true;
|
envOption[key] = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,14 +23,13 @@ export function fromHtml(html: string, quoteUri?: string | null): string {
|
||||||
html.replace(/<br\s?\/?>\r?\n/gi, '\n'),
|
html.replace(/<br\s?\/?>\r?\n/gi, '\n'),
|
||||||
);
|
);
|
||||||
|
|
||||||
// stores if we are parsing any lists.
|
let text = '';
|
||||||
// 0 for a level that is an unordered list, otherwise the counter for the ordered list
|
|
||||||
let listIndex: number[] = [];
|
|
||||||
|
|
||||||
return dom.childNodes
|
for (const n of dom.childNodes) {
|
||||||
.map(analyze)
|
analyze(n);
|
||||||
.join('')
|
}
|
||||||
.trim();
|
|
||||||
|
return text.trim();
|
||||||
|
|
||||||
function getText(node: TreeAdapter.Node): string {
|
function getText(node: TreeAdapter.Node): string {
|
||||||
if (treeAdapter.isTextNode(node)) return node.value;
|
if (treeAdapter.isTextNode(node)) return node.value;
|
||||||
|
@ -44,55 +43,58 @@ export function fromHtml(html: string, quoteUri?: string | null): string {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function analyzeMultiple(childNodes: TreeAdapter.ChildNode[]): string {
|
function appendChildren(childNodes: TreeAdapter.ChildNode[]): void {
|
||||||
return childNodes.map(analyze).join('');
|
if (childNodes.length > 0) {
|
||||||
|
for (const n of childNodes) {
|
||||||
|
analyze(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function analyze(node: TreeAdapter.Node): string {
|
function analyze(node: TreeAdapter.Node): void {
|
||||||
if (treeAdapter.isTextNode(node)) {
|
if (treeAdapter.isTextNode(node)) {
|
||||||
return node.value;
|
text += node.value;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip comment or document type node
|
// Skip comment or document type node
|
||||||
if (!treeAdapter.isElementNode(node)) return '';
|
if (!treeAdapter.isElementNode(node)) return;
|
||||||
|
|
||||||
switch (node.nodeName) {
|
switch (node.nodeName) {
|
||||||
case 'br': {
|
case 'br': {
|
||||||
return '\n';
|
text += '\n';
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'a':
|
case 'a':
|
||||||
{
|
{
|
||||||
let text = '';
|
const txt = getText(node);
|
||||||
// trim spaces away, because some AP servers (app.wafrn.net) send strange
|
|
||||||
// zero width non-break space in strange places and things like that
|
|
||||||
const linkText = getText(node).trim();
|
|
||||||
const href = getAttr(node, 'href');
|
const href = getAttr(node, 'href');
|
||||||
|
|
||||||
// hashtags
|
// hashtags
|
||||||
if (linkText.startsWith('#') && href && (attrHas(node, 'rel', 'tag') || attrHas(node, 'class', 'hashtag'))) {
|
if (txt.startsWith('#') && href && (attrHas(node, 'rel', 'tag') || attrHas(node, 'class', 'hashtag'))) {
|
||||||
text += linkText;
|
text += txt;
|
||||||
// mentions: a link that starts with `@` and does not include space
|
// mentions
|
||||||
} else if (linkText.startsWith('@') && linkText.match(/\s/) == null && !attrHas(node, 'rel', 'me')) {
|
} else if (txt.startsWith('@') && !attrHas(node, 'rel', 'me')) {
|
||||||
const part = linkText.split('@');
|
const part = txt.split('@');
|
||||||
|
|
||||||
if (part.length === 2 && href) {
|
if (part.length === 2 && href) {
|
||||||
// restore the host name part
|
// restore the host name part
|
||||||
const acct = `${linkText}@${(new URL(href)).hostname}`;
|
const acct = `${txt}@${(new URL(href)).hostname}`;
|
||||||
text += acct;
|
text += acct;
|
||||||
} else if (part.length === 3) {
|
} else if (part.length === 3) {
|
||||||
text += linkText;
|
text += txt;
|
||||||
}
|
}
|
||||||
// other
|
// other
|
||||||
} else {
|
} else {
|
||||||
const generateLink = () => {
|
const generateLink = () => {
|
||||||
if (!href && !linkText) {
|
if (!href && !txt) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
if (!href) {
|
if (!href) {
|
||||||
return linkText;
|
return txt;
|
||||||
}
|
}
|
||||||
if (!linkText || linkText === href) { // #6383: Missing text node
|
if (!txt || txt === href) { // #6383: Missing text node
|
||||||
if (href.match(urlRegexFull)) {
|
if (href.match(urlRegexFull)) {
|
||||||
return href;
|
return href;
|
||||||
} else {
|
} else {
|
||||||
|
@ -100,109 +102,87 @@ export function fromHtml(html: string, quoteUri?: string | null): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (href.match(urlRegex) && !href.match(urlRegexFull)) {
|
if (href.match(urlRegex) && !href.match(urlRegexFull)) {
|
||||||
return `[${linkText}](<${href}>)`; // #6846
|
return `[${txt}](<${href}>)`; // #6846
|
||||||
} else {
|
} else {
|
||||||
return `[${linkText}](${href})`;
|
return `[${txt}](${href})`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
text += generateLink();
|
text += generateLink();
|
||||||
}
|
}
|
||||||
return text;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'h1':
|
case 'h1':
|
||||||
{
|
{
|
||||||
return '【' + analyzeMultiple(node.childNodes) + '】\n';
|
text += '【';
|
||||||
|
appendChildren(node.childNodes);
|
||||||
|
text += '】\n';
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'b':
|
case 'b':
|
||||||
case 'strong':
|
case 'strong':
|
||||||
{
|
{
|
||||||
return '**' + analyzeMultiple(node.childNodes) + '**';
|
text += '**';
|
||||||
|
appendChildren(node.childNodes);
|
||||||
|
text += '**';
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'small':
|
case 'small':
|
||||||
{
|
{
|
||||||
return '<small>' + analyzeMultiple(node.childNodes) + '</small>';
|
text += '<small>';
|
||||||
|
appendChildren(node.childNodes);
|
||||||
|
text += '</small>';
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 's':
|
case 's':
|
||||||
case 'del':
|
case 'del':
|
||||||
{
|
{
|
||||||
return '~~' + analyzeMultiple(node.childNodes) + '~~';
|
text += '~~';
|
||||||
|
appendChildren(node.childNodes);
|
||||||
|
text += '~~';
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'i':
|
case 'i':
|
||||||
case 'em':
|
case 'em':
|
||||||
{
|
{
|
||||||
return '<i>' + analyzeMultiple(node.childNodes) + '</i>';
|
text += '<i>';
|
||||||
|
appendChildren(node.childNodes);
|
||||||
|
text += '</i>';
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// block code (<pre><code>)
|
// block code (<pre><code>)
|
||||||
case 'pre': {
|
case 'pre': {
|
||||||
if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
|
if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
|
||||||
return '\n```\n' + getText(node.childNodes[0]) + '\n```\n';
|
text += '\n```\n';
|
||||||
|
text += getText(node.childNodes[0]);
|
||||||
|
text += '\n```\n';
|
||||||
} else {
|
} else {
|
||||||
return analyzeMultiple(node.childNodes);
|
appendChildren(node.childNodes);
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// inline code (<code>)
|
// inline code (<code>)
|
||||||
case 'code': {
|
case 'code': {
|
||||||
return '`' + analyzeMultiple(node.childNodes) + '`';
|
text += '`';
|
||||||
}
|
appendChildren(node.childNodes);
|
||||||
|
text += '`';
|
||||||
// inline or block KaTeX
|
break;
|
||||||
case 'math': {
|
|
||||||
// This node should contain <semantics>[...]<annotation/>[...]</semantics> tag with the "source code".
|
|
||||||
if (node.childNodes.length !== 1 || node.childNodes[0].nodeName !== 'semantics')
|
|
||||||
break;
|
|
||||||
const semantics = node.childNodes[0];
|
|
||||||
|
|
||||||
// only select well formed annotations
|
|
||||||
const annotations = semantics.childNodes
|
|
||||||
.filter(node =>
|
|
||||||
node.nodeName === 'annotation'
|
|
||||||
&& node.childNodes.length === 1
|
|
||||||
&& node.childNodes[0].nodeName === '#text'
|
|
||||||
);
|
|
||||||
if (annotations.length === 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
let annotation = annotations[0];
|
|
||||||
// try to prefer a TeX annotation if there are multiple annotations
|
|
||||||
const filteredAnnotations = annotations.filter(node => node.attrs.some(attribute => attribute.name === 'encoding' && attribute.value === 'application/x-tex'));
|
|
||||||
if (filteredAnnotations.length > 0) {
|
|
||||||
annotation = filteredAnnotations[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
const formula = annotation.childNodes[0].value;
|
|
||||||
if (annotation.attrs.some(attribute => attribute.name === 'encoding' && attribute.value === 'application/x-tex')) {
|
|
||||||
// can be rendered as KaTeX, now decide if it is possible to render as inline or not
|
|
||||||
if (/[\r\n]/.test(formula)) {
|
|
||||||
// line break, this must be rendered as a block
|
|
||||||
return '\n\\[' + formula + '\\]\n';
|
|
||||||
} else {
|
|
||||||
// render as inline
|
|
||||||
return '\\(' + formula + '\\)';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// not KaTeX, but if there is a plaintext annotation it can still be rendered as code
|
|
||||||
if (/[\r\n]/.test(formula)) {
|
|
||||||
// line break, this must be rendered as a block
|
|
||||||
return '\n```\n' + formula + '\n```\n';
|
|
||||||
} else {
|
|
||||||
// render as inline
|
|
||||||
return '`' + formula + '`';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'blockquote': {
|
case 'blockquote': {
|
||||||
return analyzeMultiple(node.childNodes)
|
const t = getText(node);
|
||||||
.trim()
|
if (t) {
|
||||||
.replace(/^|\n/g, '\n>');
|
text += '\n> ';
|
||||||
|
text += t.split('\n').join('\n> ');
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'p':
|
case 'p':
|
||||||
|
@ -212,50 +192,9 @@ export function fromHtml(html: string, quoteUri?: string | null): string {
|
||||||
case 'h5':
|
case 'h5':
|
||||||
case 'h6':
|
case 'h6':
|
||||||
{
|
{
|
||||||
return '\n\n' + analyzeMultiple(node.childNodes);
|
text += '\n\n';
|
||||||
}
|
appendChildren(node.childNodes);
|
||||||
|
break;
|
||||||
// lists and list items
|
|
||||||
case 'ol':
|
|
||||||
case 'ul':
|
|
||||||
{
|
|
||||||
if (node.nodeName == 'ol') {
|
|
||||||
listIndex.push(1);
|
|
||||||
} else {
|
|
||||||
listIndex.push(0);
|
|
||||||
}
|
|
||||||
let text = '\n' + analyzeMultiple(node.childNodes);
|
|
||||||
listIndex.pop();
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'li':
|
|
||||||
{
|
|
||||||
if (listIndex.length == 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let text = '\n';
|
|
||||||
|
|
||||||
// pop the current operating on index for manipulation
|
|
||||||
let index = listIndex.pop();
|
|
||||||
// indent the start of the list item respecitve of the level of
|
|
||||||
// nesting of lists
|
|
||||||
//
|
|
||||||
// since we popped the current index, the length will be 0 on
|
|
||||||
// the first level, thus causing no indent on the first level
|
|
||||||
text += ' '.repeat(listIndex.length);
|
|
||||||
if (index == 0) {
|
|
||||||
text += '- ';
|
|
||||||
} else {
|
|
||||||
text += index + ') ';
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
// done with the index, put it back so nested lists with
|
|
||||||
// analyzeMultiple will work correctly
|
|
||||||
listIndex.push(index);
|
|
||||||
|
|
||||||
text += analyzeMultiple(node.childNodes);
|
|
||||||
return text;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// other block elements
|
// other block elements
|
||||||
|
@ -263,28 +202,30 @@ export function fromHtml(html: string, quoteUri?: string | null): string {
|
||||||
case 'header':
|
case 'header':
|
||||||
case 'footer':
|
case 'footer':
|
||||||
case 'article':
|
case 'article':
|
||||||
|
case 'li':
|
||||||
case 'dt':
|
case 'dt':
|
||||||
case 'dd':
|
case 'dd':
|
||||||
{
|
{
|
||||||
return '\n' + analyzeMultiple(node.childNodes);
|
text += '\n';
|
||||||
|
appendChildren(node.childNodes);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'span':
|
case 'span':
|
||||||
{
|
{
|
||||||
if (attrHas(node, 'class', 'quote-inline') && quoteUri && getText(node).trim() === `RE: ${quoteUri}`) {
|
if (attrHas(node, 'class', 'quote-inline') && quoteUri && getText(node).trim() === `RE: ${quoteUri}`) {
|
||||||
// embedded quote thingy for backwards compatibility, don't show it
|
// embedded quote thingy for backwards compatibility, don't show it
|
||||||
return '';
|
|
||||||
} else {
|
} else {
|
||||||
return analyzeMultiple(node.childNodes);
|
appendChildren(node.childNodes);
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
default: // includes inline elements
|
default: // includes inline elements
|
||||||
{
|
{
|
||||||
return analyzeMultiple(node.childNodes);
|
appendChildren(node.childNodes);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { JSDOM } from 'jsdom';
|
import { JSDOM } from 'jsdom';
|
||||||
import katex from 'katex';
|
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
import { UserProfiles } from '@/models/index.js';
|
import { UserProfiles } from '@/models/index.js';
|
||||||
|
@ -7,14 +6,6 @@ import { extractMentions } from '@/misc/extract-mentions.js';
|
||||||
import { intersperse } from '@/prelude/array.js';
|
import { intersperse } from '@/prelude/array.js';
|
||||||
import { toPunyNullable } from '@/misc/convert-host.js';
|
import { toPunyNullable } from '@/misc/convert-host.js';
|
||||||
|
|
||||||
function toMathMl(code: string): HTMLElement {
|
|
||||||
const rendered = katex.renderToString(code, {
|
|
||||||
throwOnError: false,
|
|
||||||
output: 'mathml',
|
|
||||||
});
|
|
||||||
return JSDOM.fragment(rendered).querySelector('math');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transforms MFM to HTML, given the MFM text and a list of user IDs that are
|
// Transforms MFM to HTML, given the MFM text and a list of user IDs that are
|
||||||
// mentioned in the text. If the list of mentions is not given, all mentions
|
// mentioned in the text. If the list of mentions is not given, all mentions
|
||||||
// from the text will be extracted.
|
// from the text will be extracted.
|
||||||
|
@ -107,11 +98,15 @@ export async function toHtml(mfmText: string, mentions?: string[]): Promise<stri
|
||||||
},
|
},
|
||||||
|
|
||||||
async mathInline(node) {
|
async mathInline(node) {
|
||||||
return toMathMl(node.props.formula);
|
const el = doc.createElement('code');
|
||||||
|
el.textContent = node.props.formula;
|
||||||
|
return el;
|
||||||
},
|
},
|
||||||
|
|
||||||
async mathBlock(node) {
|
async mathBlock(node) {
|
||||||
return toMathMl(node.props.formula);
|
const el = doc.createElement('code');
|
||||||
|
el.textContent = node.props.formula;
|
||||||
|
return el;
|
||||||
},
|
},
|
||||||
|
|
||||||
async link(node) {
|
async link(node) {
|
||||||
|
|
29
packages/backend/src/misc/api-permissions.ts
Normal file
29
packages/backend/src/misc/api-permissions.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
export const kinds = [
|
||||||
|
'read:account',
|
||||||
|
'write:account',
|
||||||
|
'read:blocks',
|
||||||
|
'write:blocks',
|
||||||
|
'read:drive',
|
||||||
|
'write:drive',
|
||||||
|
'read:following',
|
||||||
|
'write:following',
|
||||||
|
'read:messaging',
|
||||||
|
'write:messaging',
|
||||||
|
'read:mutes',
|
||||||
|
'write:mutes',
|
||||||
|
'write:notes',
|
||||||
|
'read:notifications',
|
||||||
|
'write:notifications',
|
||||||
|
'read:reactions',
|
||||||
|
'write:reactions',
|
||||||
|
'write:votes',
|
||||||
|
'read:pages',
|
||||||
|
'write:pages',
|
||||||
|
'write:page-likes',
|
||||||
|
'read:page-likes',
|
||||||
|
'read:user-groups',
|
||||||
|
'write:user-groups',
|
||||||
|
'read:channels',
|
||||||
|
'write:channels',
|
||||||
|
];
|
||||||
|
// IF YOU ADD KINDS(PERMISSIONS), YOU MUST ADD TRANSLATIONS (under _permissions).
|
|
@ -1,23 +1,12 @@
|
||||||
export class Cache<T> {
|
export class Cache<T> {
|
||||||
// The actual "database" that holds the cache entries, along with their
|
|
||||||
// insertion time.
|
|
||||||
// Insertion order is the same as the order of elements expiring. This is
|
|
||||||
// important because the expiration logic relies on the insertion order.
|
|
||||||
public cache: Map<string, { date: number; value: T; }>;
|
public cache: Map<string, { date: number; value: T; }>;
|
||||||
// The lifetime of each cache member.
|
|
||||||
//
|
|
||||||
// This must not be changed after setup because it may upset
|
|
||||||
// the expiration logic.
|
|
||||||
private lifetime: number;
|
private lifetime: number;
|
||||||
// Function of which the results should be cached.
|
|
||||||
public fetcher: (key: string) => Promise<T | undefined>;
|
public fetcher: (key: string) => Promise<T | undefined>;
|
||||||
private timeoutScheduled: boolean;
|
|
||||||
|
|
||||||
constructor(lifetime: number, fetcher: Cache<T>['fetcher']) {
|
constructor(lifetime: number, fetcher: Cache<T>['fetcher']) {
|
||||||
this.cache = new Map();
|
this.cache = new Map();
|
||||||
this.lifetime = lifetime;
|
this.lifetime = lifetime;
|
||||||
this.fetcher = fetcher;
|
this.fetcher = fetcher;
|
||||||
this.timeoutScheduled = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public set(key: string, value: T): void {
|
public set(key: string, value: T): void {
|
||||||
|
@ -25,36 +14,38 @@ export class Cache<T> {
|
||||||
date: Date.now(),
|
date: Date.now(),
|
||||||
value,
|
value,
|
||||||
});
|
});
|
||||||
|
|
||||||
// make sure the expiration timeout is in place
|
|
||||||
this.expire();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get(key: string): T | undefined {
|
public get(key: string): T | undefined {
|
||||||
const cached = this.cache.get(key);
|
const cached = this.cache.get(key);
|
||||||
if (cached == null) return undefined;
|
if (cached == null) return undefined;
|
||||||
else return cached.value;
|
|
||||||
|
// discard if past the cache lifetime
|
||||||
|
if ((Date.now() - cached.date) > this.lifetime) {
|
||||||
|
this.cache.delete(key);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cached.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public delete(key: string): void {
|
public delete(key: string): void {
|
||||||
this.cache.delete(key);
|
this.cache.delete(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the value is cached, it is returned. Otherwise the fetcher is
|
/**
|
||||||
// run to get the value. If the fetcher returns undefined, it is
|
* If the value is cached, it is returned. Otherwise the fetcher is
|
||||||
// returned but not cached.
|
* run to get the value. If the fetcher returns undefined, it is
|
||||||
|
* returned but not cached.
|
||||||
|
*/
|
||||||
public async fetch(key: string): Promise<T | undefined> {
|
public async fetch(key: string): Promise<T | undefined> {
|
||||||
// Check if this value is cached
|
|
||||||
const cached = this.get(key);
|
const cached = this.get(key);
|
||||||
if (cached !== undefined) {
|
if (cached !== undefined) {
|
||||||
// The value was cached, return it.
|
|
||||||
return cached;
|
return cached;
|
||||||
} else {
|
} else {
|
||||||
// The value was not cached, need to call the original function
|
|
||||||
// to get its result and then cache it.
|
|
||||||
const value = await this.fetcher(key);
|
const value = await this.fetcher(key);
|
||||||
|
|
||||||
// `undefined` is not cached
|
// don't cache undefined
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
this.set(key, value);
|
this.set(key, value);
|
||||||
}
|
}
|
||||||
|
@ -62,43 +53,4 @@ export class Cache<T> {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handling the expiration of cached values.
|
|
||||||
// This is done using a timeout.
|
|
||||||
private expire(): void {
|
|
||||||
// If there already is a timeout scheduled, it will be appropriate
|
|
||||||
// for the first inserted element of the cache.
|
|
||||||
// If the first element of the cache was removed, it will reschedule
|
|
||||||
// to the appropriate time when it runs out.
|
|
||||||
//
|
|
||||||
// If the cache is empty, there is nothing to expire either.
|
|
||||||
if (this.timeoutScheduled) return;
|
|
||||||
// Otherwise, this must mean this is the previously scheduled timeout.
|
|
||||||
// Since it is running now, it is no longer scheduled.
|
|
||||||
this.timeoutScheduled = false;
|
|
||||||
|
|
||||||
// Check if the first element is actually due for expiration.
|
|
||||||
//
|
|
||||||
// Items may have been removed in the meantime or this may be
|
|
||||||
// the initial call for the first key inserted into the cache.
|
|
||||||
const [expiredKey, expiredValue] = this.cache.entries().next().value;
|
|
||||||
if (expiredValue.date + this.lifetime <= Date.now()) {
|
|
||||||
// This item is due for expiration, so remove it.
|
|
||||||
this.cache.delete(expiredKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are no further elements in the cache, there is nothing to
|
|
||||||
// expire at a later time. The timeout will be set up again later by
|
|
||||||
// a call from `this.set`.
|
|
||||||
if (this.cache.size === 0) return;
|
|
||||||
|
|
||||||
// Check when the next key is due for removal and schedule
|
|
||||||
// an appropriate timeout.
|
|
||||||
const [nextKey, nextValue] = this.cache.entries().next().value;
|
|
||||||
setTimeout(
|
|
||||||
() => this.expire(),
|
|
||||||
nextValue.date + this.lifetime - Date.now()
|
|
||||||
);
|
|
||||||
this.timeoutScheduled = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { URLSearchParams } from 'node:url';
|
import { URLSearchParams } from 'node:url';
|
||||||
import { getResponse } from '@/misc/fetch.js';
|
import fetch from 'node-fetch';
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
|
import { getAgentByUrl } from './fetch.js';
|
||||||
|
|
||||||
export async function verifyRecaptcha(secret: string, response: string): Promise<void> {
|
export async function verifyRecaptcha(secret: string, response: string): Promise<void> {
|
||||||
const result = await getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(e => {
|
const result = await getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(e => {
|
||||||
|
@ -35,10 +36,15 @@ async function getCaptchaResponse(url: string, secret: string, response: string)
|
||||||
response,
|
response,
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await getResponse({
|
const res = await fetch(url, {
|
||||||
url,
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: params,
|
body: params,
|
||||||
|
headers: {
|
||||||
|
'User-Agent': config.userAgent,
|
||||||
|
},
|
||||||
|
// TODO
|
||||||
|
//timeout: 10 * 1000,
|
||||||
|
agent: getAgentByUrl,
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
throw new Error(`${e.message || e}`);
|
throw new Error(`${e.message || e}`);
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,24 +13,11 @@ type UserLike = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function checkWordMute(note: NoteLike, me: UserLike | null | undefined, mutedWords: Array<string | string[]>): Promise<boolean> {
|
export async function checkWordMute(note: NoteLike, me: UserLike | null | undefined, mutedWords: Array<string | string[]>): Promise<boolean> {
|
||||||
// own posts
|
// 自分自身
|
||||||
if (me && (note.userId === me.id)) return false;
|
if (me && (note.userId === me.id)) return false;
|
||||||
|
|
||||||
if (mutedWords.length > 0) {
|
if (mutedWords.length > 0) {
|
||||||
const text = [
|
const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim();
|
||||||
note.cw,
|
|
||||||
note.text,
|
|
||||||
...note.files.map(file => file.comment)
|
|
||||||
]
|
|
||||||
.map(x => {
|
|
||||||
if (x == null || x.trim() == '') {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return x.trim();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(x => x != null)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
if (text === '') return false;
|
if (text === '') return false;
|
||||||
const textLower = text.toLowerCase();
|
const textLower = text.toLowerCase();
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as stream from 'node:stream';
|
import * as stream from 'node:stream';
|
||||||
import * as util from 'node:util';
|
import * as util from 'node:util';
|
||||||
|
import chalk from 'chalk';
|
||||||
import got, * as Got from 'got';
|
import got, * as Got from 'got';
|
||||||
|
import IPCIDR from 'ip-cidr';
|
||||||
|
import PrivateIp from 'private-ip';
|
||||||
import { SECOND, MINUTE } from '@/const.js';
|
import { SECOND, MINUTE } from '@/const.js';
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
import Logger from '@/services/logger.js';
|
import Logger from '@/services/logger.js';
|
||||||
|
@ -12,7 +15,7 @@ const pipeline = util.promisify(stream.pipeline);
|
||||||
export async function downloadUrl(url: string, path: string): Promise<void> {
|
export async function downloadUrl(url: string, path: string): Promise<void> {
|
||||||
const logger = new Logger('download');
|
const logger = new Logger('download');
|
||||||
|
|
||||||
logger.info(`Downloading ${url} ...`);
|
logger.info(`Downloading ${chalk.cyan(url)} ...`);
|
||||||
|
|
||||||
const timeout = 30 * SECOND;
|
const timeout = 30 * SECOND;
|
||||||
const operationTimeout = MINUTE;
|
const operationTimeout = MINUTE;
|
||||||
|
@ -39,6 +42,13 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
|
||||||
limit: 0,
|
limit: 0,
|
||||||
},
|
},
|
||||||
}).on('response', (res: Got.Response) => {
|
}).on('response', (res: Got.Response) => {
|
||||||
|
if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !config.proxy && res.ip) {
|
||||||
|
if (isPrivateIp(res.ip)) {
|
||||||
|
logger.warn(`Blocked address: ${res.ip}`);
|
||||||
|
req.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const contentLength = res.headers['content-length'];
|
const contentLength = res.headers['content-length'];
|
||||||
if (contentLength != null) {
|
if (contentLength != null) {
|
||||||
const size = Number(contentLength);
|
const size = Number(contentLength);
|
||||||
|
@ -64,5 +74,16 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.succ(`Download finished: ${url}`);
|
logger.succ(`Download finished: ${chalk.cyan(url)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPrivateIp(ip: string): boolean {
|
||||||
|
for (const net of config.allowedPrivateNetworks || []) {
|
||||||
|
const cidr = new IPCIDR(net);
|
||||||
|
if (cidr.contains(ip)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PrivateIp(ip);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,67 +1,57 @@
|
||||||
import * as http from 'node:http';
|
import * as http from 'node:http';
|
||||||
import * as https from 'node:https';
|
import * as https from 'node:https';
|
||||||
import { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
import * as dns from 'node:dns';
|
|
||||||
import CacheableLookup from 'cacheable-lookup';
|
import CacheableLookup from 'cacheable-lookup';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||||
import ipaddr from 'ipaddr.js';
|
|
||||||
import IPCIDR from 'ip-cidr';
|
|
||||||
import { SECOND } from '@/const.js';
|
import { SECOND } from '@/const.js';
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
|
|
||||||
export async function getJson(url: string, accept = 'application/json, */*', timeout = 10 * SECOND, headers: Record<string, string> = {}) {
|
export async function getJson(url: string, accept = 'application/json, */*', timeout = 10 * SECOND, headers?: Record<string, string>) {
|
||||||
const res = await getResponse({
|
const res = await getResponse({
|
||||||
url,
|
url,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: Object.assign({
|
headers: Object.assign({
|
||||||
|
'User-Agent': config.userAgent,
|
||||||
Accept: accept,
|
Accept: accept,
|
||||||
}, headers),
|
}, headers || {}),
|
||||||
timeout,
|
timeout,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await res.json();
|
return await res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getHtml(url: string, accept = 'text/html, */*', timeout = 10 * SECOND, headers: Record<string, string> = {}) {
|
export async function getHtml(url: string, accept = 'text/html, */*', timeout = 10 * SECOND, headers?: Record<string, string>) {
|
||||||
const res = await getResponse({
|
const res = await getResponse({
|
||||||
url,
|
url,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: Object.assign({
|
headers: Object.assign({
|
||||||
|
'User-Agent': config.userAgent,
|
||||||
Accept: accept,
|
Accept: accept,
|
||||||
}, headers),
|
}, headers || {}),
|
||||||
timeout,
|
timeout,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await res.text();
|
return await res.text();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getResponse(_args: {
|
export async function getResponse(args: { url: string, method: string, body?: string, headers: Record<string, string>, timeout?: number, size?: number, redirect: 'follow' | 'manual' | 'error' = 'follow' }) {
|
||||||
url: string,
|
const timeout = args.timeout || 10 * SECOND;
|
||||||
method: string,
|
|
||||||
body?: string,
|
const controller = new AbortController();
|
||||||
headers: Record<string, string>,
|
setTimeout(() => {
|
||||||
timeout?: number,
|
controller.abort();
|
||||||
size?: number,
|
}, timeout * 6);
|
||||||
redirect?: 'follow' | 'manual' | 'error',
|
|
||||||
}) {
|
|
||||||
const args = {
|
|
||||||
timeout: 10 * SECOND,
|
|
||||||
size: 10 * 1024 * 1024, // 10 MiB
|
|
||||||
redirect: 'follow',
|
|
||||||
..._args,
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = await fetch(args.url, {
|
const res = await fetch(args.url, {
|
||||||
method: args.method,
|
method: args.method,
|
||||||
headers: Object.assign({
|
headers: args.headers,
|
||||||
'User-Agent': config.userAgent,
|
|
||||||
}, args.headers),
|
|
||||||
body: args.body,
|
body: args.body,
|
||||||
redirect: args.redirect,
|
redirect: args.redirect,
|
||||||
size: args.size,
|
timeout,
|
||||||
|
size: args.size || 10 * 1024 * 1024, // 10 MiB
|
||||||
agent: getAgentByUrl,
|
agent: getAgentByUrl,
|
||||||
signal: AbortSignal.timeout(args.timeout),
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -81,63 +71,13 @@ const cache = new CacheableLookup({
|
||||||
lookup: false, // nativeのdns.lookupにfallbackしない
|
lookup: false, // nativeのdns.lookupにfallbackしない
|
||||||
});
|
});
|
||||||
|
|
||||||
// because `cacheable-lookup` is annoying and prefers IPv4 by default,
|
|
||||||
// we need a little wrapper setting some extra options
|
|
||||||
const cacheLookup = (host, _2, _3) => {
|
|
||||||
// do all the weird parameter shenanigans that nodejs's dns.lookup does.
|
|
||||||
let options = {}, callback;
|
|
||||||
if (_3) {
|
|
||||||
options = _2;
|
|
||||||
if (typeof options === 'number') {
|
|
||||||
options = { family: options };
|
|
||||||
}
|
|
||||||
callback = _3;
|
|
||||||
} else {
|
|
||||||
callback = _2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// here come the shenanigans, trying to be careful not to mess up
|
|
||||||
// intentionally different behaviour
|
|
||||||
if (options.family == null && (options.hints ?? 0) === 0) {
|
|
||||||
options.family = 6;
|
|
||||||
options.hints = dns.V4MAPPED;
|
|
||||||
}
|
|
||||||
|
|
||||||
// callback wrapper that checks whether an IP is private.
|
|
||||||
// Private IPs will not be returned in the first place.
|
|
||||||
const wrapper = (err, address, family) => {
|
|
||||||
// mimics dns.lookup behaviour as if no result was found
|
|
||||||
const fakeErr = new Error("private IP");
|
|
||||||
fakeErr.code = 'ENOTFOUND';
|
|
||||||
|
|
||||||
if (err != null) {
|
|
||||||
return callback(err, address, family);
|
|
||||||
} else if (options.all) {
|
|
||||||
const results = address.filter(({ address, family }) => isPublicIp(address));
|
|
||||||
if (results.length === 0) {
|
|
||||||
return callback(fakeErr);
|
|
||||||
} else {
|
|
||||||
return callback(err, results);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isPublicIp(address)) {
|
|
||||||
return callback(err, address, family);
|
|
||||||
} else {
|
|
||||||
return callback(fakeErr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return cache.lookup(host, options, wrapper);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get http non-proxy agent
|
* Get http non-proxy agent
|
||||||
*/
|
*/
|
||||||
const _http = new http.Agent({
|
const _http = new http.Agent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * SECOND,
|
keepAliveMsecs: 30 * SECOND,
|
||||||
lookup: cacheLookup,
|
lookup: cache.lookup,
|
||||||
} as http.AgentOptions);
|
} as http.AgentOptions);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -146,7 +86,7 @@ const _http = new http.Agent({
|
||||||
const _https = new https.Agent({
|
const _https = new https.Agent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * SECOND,
|
keepAliveMsecs: 30 * SECOND,
|
||||||
lookup: cacheLookup,
|
lookup: cache.lookup,
|
||||||
} as https.AgentOptions);
|
} as https.AgentOptions);
|
||||||
|
|
||||||
const maxSockets = Math.max(256, config.deliverJobConcurrency);
|
const maxSockets = Math.max(256, config.deliverJobConcurrency);
|
||||||
|
@ -205,27 +145,3 @@ export class StatusError extends Error {
|
||||||
this.isClientError = typeof this.statusCode === 'number' && this.statusCode >= 400 && this.statusCode < 500;
|
this.isClientError = typeof this.statusCode === 'number' && this.statusCode >= 400 && this.statusCode < 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPublicIp(ip: string): boolean {
|
|
||||||
for (const net of config.allowedPrivateNetworks || []) {
|
|
||||||
const cidr = new IPCIDR(net);
|
|
||||||
if (cidr.contains(ip)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Workaround because ipaddr.js in the installed version cannot cope
|
|
||||||
// with v4mapped addresses. a fix has been submitted and merged, but a new
|
|
||||||
// version was not yet released.
|
|
||||||
// FIXME: update ipaddr.js and remove workaround
|
|
||||||
ip = ip.replace(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i, '$1');
|
|
||||||
|
|
||||||
try {
|
|
||||||
let range = ipaddr.process(ip).range();
|
|
||||||
// only unicast or multicast addresses are allowed by default
|
|
||||||
// this does not include e.g. loopback addresses
|
|
||||||
return ['unicast', 'multicast'].includes(range);
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -37,5 +37,14 @@ export const getNoteSummary = (note: Packed<'Note'>): string => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Renoteのとき
|
||||||
|
if (note.renoteId) {
|
||||||
|
if (note.renote) {
|
||||||
|
summary += `\n\nRN: ${getNoteSummary(note.renote)}`;
|
||||||
|
} else {
|
||||||
|
summary += '\n\nRN: ...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return summary.trim();
|
return summary.trim();
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,10 +2,9 @@ import { UserKeypairs } from '@/models/index.js';
|
||||||
import { User } from '@/models/entities/user.js';
|
import { User } from '@/models/entities/user.js';
|
||||||
import { UserKeypair } from '@/models/entities/user-keypair.js';
|
import { UserKeypair } from '@/models/entities/user-keypair.js';
|
||||||
import { Cache } from './cache.js';
|
import { Cache } from './cache.js';
|
||||||
import { MINUTE } from '@/const.js';
|
|
||||||
|
|
||||||
const cache = new Cache<UserKeypair>(
|
const cache = new Cache<UserKeypair>(
|
||||||
15 * MINUTE,
|
Infinity,
|
||||||
(userId) => UserKeypairs.findOneByOrFail({ userId }),
|
(userId) => UserKeypairs.findOneByOrFail({ userId }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -44,10 +44,12 @@ function normalizeHost(src: string | undefined, noteUserHost: string | null): st
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseEmojiStr(emojiName: string, noteUserHost: string | null) {
|
function parseEmojiStr(emojiName: string, noteUserHost: string | null) {
|
||||||
// emojiName may be of the form `emoji@host`, turn it into a suitable form
|
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
|
||||||
const match = emojiName.split("@");
|
if (!match) return { name: null, host: null };
|
||||||
const name = match[0];
|
|
||||||
const host = toPunyNullable(normalizeHost(match[1], noteUserHost));
|
const name = match[1];
|
||||||
|
|
||||||
|
const host = toPunyNullable(normalizeHost(match[2], noteUserHost));
|
||||||
|
|
||||||
return { name, host };
|
return { name, host };
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,11 @@ export class Meta {
|
||||||
})
|
})
|
||||||
public langs: string[];
|
public langs: string[];
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 256, array: true, default: '{}',
|
||||||
|
})
|
||||||
|
public pinnedUsers: string[];
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 256, array: true, default: '{}',
|
length: 256, array: true, default: '{}',
|
||||||
})
|
})
|
||||||
|
@ -175,6 +180,12 @@ export class Meta {
|
||||||
})
|
})
|
||||||
public remoteDriveCapacityMb: number;
|
public remoteDriveCapacityMb: number;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 128,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public summalyProxy: string | null;
|
||||||
|
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
})
|
})
|
||||||
|
|
|
@ -34,6 +34,12 @@ export class Poll {
|
||||||
public votes: number[];
|
public votes: number[];
|
||||||
|
|
||||||
//#region Denormalized fields
|
//#region Denormalized fields
|
||||||
|
@Column('enum', {
|
||||||
|
enum: noteVisibilities,
|
||||||
|
comment: '[Denormalized]',
|
||||||
|
})
|
||||||
|
public noteVisibility: typeof noteVisibilities[number];
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column({
|
@Column({
|
||||||
...id(),
|
...id(),
|
||||||
|
|
|
@ -8,10 +8,14 @@ import { populateEmojis } from '@/misc/populate-emojis.js';
|
||||||
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD, HOUR } from '@/const.js';
|
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD, HOUR } from '@/const.js';
|
||||||
import { Cache } from '@/misc/cache.js';
|
import { Cache } from '@/misc/cache.js';
|
||||||
import { db } from '@/db/postgre.js';
|
import { db } from '@/db/postgre.js';
|
||||||
import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js';
|
|
||||||
import { Instance } from '../entities/instance.js';
|
import { Instance } from '../entities/instance.js';
|
||||||
import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, RenoteMutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, AntennaNotes, ChannelFollowings, Instances, DriveFiles } from '../index.js';
|
import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, RenoteMutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, AntennaNotes, ChannelFollowings, Instances, DriveFiles } from '../index.js';
|
||||||
|
|
||||||
|
const userInstanceCache = new Cache<Instance | null>(
|
||||||
|
3 * HOUR,
|
||||||
|
(host) => Instances.findOneBy({ host }).then(x => x ?? undefined),
|
||||||
|
);
|
||||||
|
|
||||||
type IsUserDetailed<Detailed extends boolean> = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>;
|
type IsUserDetailed<Detailed extends boolean> = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>;
|
||||||
type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends boolean> =
|
type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends boolean> =
|
||||||
Detailed extends true ?
|
Detailed extends true ?
|
||||||
|
@ -315,7 +319,7 @@ export const UserRepository = db.getRepository(User).extend({
|
||||||
isModerator: user.isModerator,
|
isModerator: user.isModerator,
|
||||||
isBot: user.isBot,
|
isBot: user.isBot,
|
||||||
isCat: user.isCat,
|
isCat: user.isCat,
|
||||||
instance: !user.host ? undefined : registerOrFetchInstanceDoc(user.host)
|
instance: !user.host ? undefined : userInstanceCache.fetch(user.host)
|
||||||
.then(instance => !instance ? undefined : {
|
.then(instance => !instance ? undefined : {
|
||||||
name: instance.name,
|
name: instance.name,
|
||||||
softwareName: instance.softwareName,
|
softwareName: instance.softwareName,
|
||||||
|
|
|
@ -58,16 +58,8 @@ deliverQueue
|
||||||
await deletionRefCount(job);
|
await deletionRefCount(job);
|
||||||
})
|
})
|
||||||
.on('failed', async (job, err) => {
|
.on('failed', async (job, err) => {
|
||||||
if (err.type === 'aborted') {
|
deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`);
|
||||||
deliverLogger.debug(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`);
|
await deletionRefCount(job);
|
||||||
} else {
|
|
||||||
deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (job.attemptsMade >= (job.opts?.attempts ?? 1)) {
|
|
||||||
// this was the last attempt
|
|
||||||
await deletionRefCount(job);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`))
|
.on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`))
|
||||||
.on('stalled', (job) => deliverLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
|
.on('stalled', (job) => deliverLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
|
||||||
|
@ -310,22 +302,6 @@ export function webhookDeliver(webhook: Webhook, type: typeof webhookEventTypes[
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createUpdateInstanceJob(host: string) {
|
|
||||||
return systemQueue.add(
|
|
||||||
'updateInstance',
|
|
||||||
{ host },
|
|
||||||
{
|
|
||||||
delay: 5 * MINUTE,
|
|
||||||
// The docs say that trying to add a job with an ID that already exists will
|
|
||||||
// not actually add the job. This is useful to not duplicate jobs when parallel
|
|
||||||
// jobs request an update.
|
|
||||||
jobId: host,
|
|
||||||
removeOnComplete: true,
|
|
||||||
removeOnFail: 10,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function() {
|
export default function() {
|
||||||
deliverQueue.process(config.deliverJobConcurrency, processDeliver);
|
deliverQueue.process(config.deliverJobConcurrency, processDeliver);
|
||||||
inboxQueue.process(config.inboxJobConcurrency, processInbox);
|
inboxQueue.process(config.inboxJobConcurrency, processInbox);
|
||||||
|
|
|
@ -23,19 +23,6 @@ export function initialize<T>(name: string, limitPerSec = -1): Bull.Queue<T> {
|
||||||
function apBackoff(attemptsMade: number /*, err: Error */): number {
|
function apBackoff(attemptsMade: number /*, err: Error */): number {
|
||||||
const baseDelay = MINUTE;
|
const baseDelay = MINUTE;
|
||||||
const maxBackoff = 8 * HOUR;
|
const maxBackoff = 8 * HOUR;
|
||||||
/*
|
|
||||||
attempt | average seconds + up to 2% random offset
|
|
||||||
0 | 0
|
|
||||||
1 | 60 = 1min
|
|
||||||
2 | 180 = 3min
|
|
||||||
3 | 420 = 7min
|
|
||||||
4 | 900 = 15min
|
|
||||||
5 | 1860 = 31min
|
|
||||||
6 | 3780 = 63min
|
|
||||||
7 | 7620 = 127min ~= 2.1h
|
|
||||||
8 | 15300 = 4.25h
|
|
||||||
>8 | 28800 = 8h
|
|
||||||
*/
|
|
||||||
let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay;
|
let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay;
|
||||||
backoff = Math.min(backoff, maxBackoff);
|
backoff = Math.min(backoff, maxBackoff);
|
||||||
backoff += Math.round(backoff * Math.random() * 0.2);
|
backoff += Math.round(backoff * Math.random() * 0.2);
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import Logger from '@/services/logger.js';
|
import Logger from '@/services/logger.js';
|
||||||
|
|
||||||
export const queueLogger = new Logger('queue');
|
export const queueLogger = new Logger('queue', 'orange');
|
||||||
|
|
|
@ -58,10 +58,12 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const emoji of customEmojis) {
|
for (const emoji of customEmojis) {
|
||||||
|
if (!/^[a-zA-Z0-9_]+$/.test(emoji.name)) {
|
||||||
|
this.logger.error(`invalid emoji name: ${emoji.name}, skipping in emoji export`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const ext = mime.extension(emoji.type);
|
const ext = mime.extension(emoji.type);
|
||||||
// there are some restrictions on file names, so to be safe the files are
|
const fileName = emoji.name + (ext ? '.' + ext : '');
|
||||||
// named after their database id instead of the actual emoji name
|
|
||||||
const fileName = emoji.id + (ext ? '.' + ext : '');
|
|
||||||
const emojiPath = path + '/' + fileName;
|
const emojiPath = path + '/' + fileName;
|
||||||
fs.writeFileSync(emojiPath, '', 'binary');
|
fs.writeFileSync(emojiPath, '', 'binary');
|
||||||
let downloaded = false;
|
let downloaded = false;
|
||||||
|
|
|
@ -73,7 +73,7 @@ export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, don
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.queryResultCache?.remove(['meta_emojis']);
|
await db.queryResultCache!.remove(['meta_emojis']);
|
||||||
|
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
||||||
|
|
|
@ -2,40 +2,29 @@ import { URL } from 'node:url';
|
||||||
import Bull from 'bull';
|
import Bull from 'bull';
|
||||||
import { request } from '@/remote/activitypub/request.js';
|
import { request } from '@/remote/activitypub/request.js';
|
||||||
import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js';
|
import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js';
|
||||||
|
import Logger from '@/services/logger.js';
|
||||||
import { Instances } from '@/models/index.js';
|
import { Instances } from '@/models/index.js';
|
||||||
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
|
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
|
||||||
import { toPuny } from '@/misc/convert-host.js';
|
import { toPuny } from '@/misc/convert-host.js';
|
||||||
import { StatusError } from '@/misc/fetch.js';
|
import { StatusError } from '@/misc/fetch.js';
|
||||||
import { getUserKeypair } from '@/misc/keypair-store.js';
|
|
||||||
import { shouldSkipInstance } from '@/misc/skipped-instances.js';
|
import { shouldSkipInstance } from '@/misc/skipped-instances.js';
|
||||||
import { DeliverJobData } from '@/queue/types.js';
|
import { DeliverJobData } from '@/queue/types.js';
|
||||||
|
|
||||||
|
const logger = new Logger('deliver');
|
||||||
|
|
||||||
export default async (job: Bull.Job<DeliverJobData>) => {
|
export default async (job: Bull.Job<DeliverJobData>) => {
|
||||||
const { host } = new URL(job.data.to);
|
const { host } = new URL(job.data.to);
|
||||||
const puny = toPuny(host);
|
const puny = toPuny(host);
|
||||||
|
|
||||||
// for the first few tries (where most attempts will be made)
|
if (await shouldSkipInstance(puny)) return 'skip';
|
||||||
// we assume that inserting deliver jobs took care of this check
|
|
||||||
// only on later attempts do we actually do it, to ease database
|
|
||||||
// performance. this might cause a slight delay of a few minutes
|
|
||||||
// for instance blocks being applied
|
|
||||||
//
|
|
||||||
// with apBackoff, attempt 2 happens ~4min after the initial try, while
|
|
||||||
// attempt 3 happens ~11 min after the initial try, which seems like a
|
|
||||||
// good tradeoff between database and blocks being applied reasonably quick
|
|
||||||
if (job.attemptsMade >= 3 && await shouldSkipInstance(puny)) {
|
|
||||||
return 'skip';
|
|
||||||
}
|
|
||||||
|
|
||||||
const keypair = await getUserKeypair(job.data.user.id);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (Array.isArray(job.data.content)) {
|
if (Array.isArray(job.data.content)) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
job.data.content.map(x => request(job.data.user, job.data.to, x, keypair))
|
job.data.content.map(x => request(job.data.user, job.data.to, x))
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await request(job.data.user, job.data.to, job.data.content, keypair);
|
await request(job.data.user, job.data.to, job.data.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update stats
|
// Update stats
|
||||||
|
@ -47,7 +36,7 @@ export default async (job: Bull.Job<DeliverJobData>) => {
|
||||||
isNotResponding: false,
|
isNotResponding: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchInstanceMetadata(host);
|
fetchInstanceMetadata(i);
|
||||||
});
|
});
|
||||||
} catch (res) {
|
} catch (res) {
|
||||||
// Update stats
|
// Update stats
|
||||||
|
|
|
@ -2,8 +2,6 @@ import Bull from 'bull';
|
||||||
import { Notes, PollVotes } from '@/models/index.js';
|
import { Notes, PollVotes } from '@/models/index.js';
|
||||||
import { EndedPollNotificationJobData } from '@/queue/types.js';
|
import { EndedPollNotificationJobData } from '@/queue/types.js';
|
||||||
import { createNotification } from '@/services/create-notification.js';
|
import { createNotification } from '@/services/create-notification.js';
|
||||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
|
||||||
import { updateQuestion } from '@/remote/activitypub/models/question.js';
|
|
||||||
|
|
||||||
export async function endedPollNotification(job: Bull.Job<EndedPollNotificationJobData>, done: any): Promise<void> {
|
export async function endedPollNotification(job: Bull.Job<EndedPollNotificationJobData>, done: any): Promise<void> {
|
||||||
const note = await Notes.findOneBy({ id: job.data.noteId });
|
const note = await Notes.findOneBy({ id: job.data.noteId });
|
||||||
|
@ -12,9 +10,6 @@ export async function endedPollNotification(job: Bull.Job<EndedPollNotificationJ
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the (final) numbers of votes
|
|
||||||
await updateQuestion(note.uri, new Resolver());
|
|
||||||
|
|
||||||
const votes = await PollVotes.createQueryBuilder('vote')
|
const votes = await PollVotes.createQueryBuilder('vote')
|
||||||
.select('vote.userId')
|
.select('vote.userId')
|
||||||
.where('vote.noteId = :noteId', { noteId: note.id })
|
.where('vote.noteId = :noteId', { noteId: note.id })
|
||||||
|
|
|
@ -9,7 +9,8 @@ import { extractPunyHost } from '@/misc/convert-host.js';
|
||||||
import { getApId } from '@/remote/activitypub/type.js';
|
import { getApId } from '@/remote/activitypub/type.js';
|
||||||
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
|
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
|
||||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||||
import { AuthUser } from '@/remote/activitypub/misc/auth-user.js';
|
import { LdSignature } from '@/remote/activitypub/misc/ld-signature.js';
|
||||||
|
import { AuthUser, getAuthUser } from '@/remote/activitypub/misc/auth-user.js';
|
||||||
import { InboxJobData } from '@/queue/types.js';
|
import { InboxJobData } from '@/queue/types.js';
|
||||||
import { shouldBlockInstance } from '@/misc/should-block-instance.js';
|
import { shouldBlockInstance } from '@/misc/should-block-instance.js';
|
||||||
import { verifyHttpSignature } from '@/remote/http-signature.js';
|
import { verifyHttpSignature } from '@/remote/http-signature.js';
|
||||||
|
@ -34,7 +35,42 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
||||||
// The signature must be valid.
|
// The signature must be valid.
|
||||||
// The signature must also match the actor otherwise anyone could sign any activity.
|
// The signature must also match the actor otherwise anyone could sign any activity.
|
||||||
if (validated.status !== 'valid' || validated.authUser.user.uri !== activity.actor) {
|
if (validated.status !== 'valid' || validated.authUser.user.uri !== activity.actor) {
|
||||||
return `skip: http-signature verification failed. keyId=${signature.keyId}`;
|
// Last resort: LD-Signature
|
||||||
|
if (activity.signature) {
|
||||||
|
if (activity.signature.type !== 'RsaSignature2017') {
|
||||||
|
return `skip: unsupported LD-signature type ${activity.signature.type}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get user based on LD-Signature key id.
|
||||||
|
// lets assume that the creator has this common form:
|
||||||
|
// <https://example.com/users/user#main-key>
|
||||||
|
// Then we can use it as the key id and (without fragment part) user id.
|
||||||
|
authUser = await getAuthUser(activity.signature.creator, activity.signature.creator.replace(/#.*$/, ''), resolver);
|
||||||
|
|
||||||
|
if (authUser == null) {
|
||||||
|
return 'skip: failed to resolve LD-Signature user';
|
||||||
|
}
|
||||||
|
|
||||||
|
// LD-Signature verification
|
||||||
|
const ldSignature = new LdSignature();
|
||||||
|
const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
|
||||||
|
if (!verified) {
|
||||||
|
return 'skip: LD-Signatureの検証に失敗しました';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Again, the actor must match.
|
||||||
|
if (authUser.user.uri !== activity.actor) {
|
||||||
|
return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop if the host is blocked.
|
||||||
|
const ldHost = extractPunyHost(authUser.user.uri);
|
||||||
|
if (await shouldBlockInstance(ldHost)) {
|
||||||
|
return `Blocked request: ${ldHost}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return `skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// authUser cannot be null at this point:
|
// authUser cannot be null at this point:
|
||||||
|
@ -59,11 +95,6 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
||||||
if (activity.id.length > 2048) {
|
if (activity.id.length > 2048) {
|
||||||
return `skip: overly long id from ${signerHost}`;
|
return `skip: overly long id from ${signerHost}`;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// might want to allow null id's for transitive activites, but currently
|
|
||||||
// there are no known sensible such transitive activities that we could
|
|
||||||
// process
|
|
||||||
return "skip: non-string id";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update stats
|
// Update stats
|
||||||
|
@ -74,7 +105,7 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
||||||
isNotResponding: false,
|
isNotResponding: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchInstanceMetadata(i.host);
|
fetchInstanceMetadata(i);
|
||||||
|
|
||||||
instanceChart.requestReceived(i.host);
|
instanceChart.requestReceived(i.host);
|
||||||
apRequestChart.inbox();
|
apRequestChart.inbox();
|
||||||
|
|
|
@ -45,6 +45,6 @@ export default async function cleanRemoteFiles(job: Bull.Job<Record<string, unkn
|
||||||
job.progress(deletedCount / total);
|
job.progress(deletedCount / total);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.succ('All cached remote files have been deleted.');
|
logger.succ('All cahced remote files has been deleted.');
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Bull from 'bull';
|
import Bull from 'bull';
|
||||||
|
|
||||||
import { activeUsersChart, driveChart, federationChart, instanceChart, notesChart, perUserDriveChart, perUserFollowingChart, perUserNotesChart, perUserReactionsChart, usersChart, apRequestChart } from '@/services/chart/index.js';
|
import { activeUsersChart, driveChart, federationChart, hashtagChart, instanceChart, notesChart, perUserDriveChart, perUserFollowingChart, perUserNotesChart, perUserReactionsChart, usersChart, apRequestChart } from '@/services/chart/index.js';
|
||||||
import { queueLogger } from '@/queue/logger.js';
|
import { queueLogger } from '@/queue/logger.js';
|
||||||
|
|
||||||
const logger = queueLogger.createSubLogger('clean-charts');
|
const logger = queueLogger.createSubLogger('clean-charts');
|
||||||
|
@ -17,6 +17,7 @@ export async function cleanCharts(job: Bull.Job<Record<string, unknown>>, done:
|
||||||
perUserNotesChart.clean(),
|
perUserNotesChart.clean(),
|
||||||
driveChart.clean(),
|
driveChart.clean(),
|
||||||
perUserReactionsChart.clean(),
|
perUserReactionsChart.clean(),
|
||||||
|
hashtagChart.clean(),
|
||||||
perUserFollowingChart.clean(),
|
perUserFollowingChart.clean(),
|
||||||
perUserDriveChart.clean(),
|
perUserDriveChart.clean(),
|
||||||
apRequestChart.clean(),
|
apRequestChart.clean(),
|
||||||
|
|
|
@ -3,14 +3,12 @@ import { tickCharts } from './tick-charts.js';
|
||||||
import { resyncCharts } from './resync-charts.js';
|
import { resyncCharts } from './resync-charts.js';
|
||||||
import { cleanCharts } from './clean-charts.js';
|
import { cleanCharts } from './clean-charts.js';
|
||||||
import { checkExpired } from './check-expired.js';
|
import { checkExpired } from './check-expired.js';
|
||||||
import { updateInstance } from './update-instance.js';
|
|
||||||
|
|
||||||
const jobs = {
|
const jobs = {
|
||||||
tickCharts,
|
tickCharts,
|
||||||
resyncCharts,
|
resyncCharts,
|
||||||
cleanCharts,
|
cleanCharts,
|
||||||
checkExpired,
|
checkExpired,
|
||||||
updateInstance,
|
|
||||||
} as Record<string, Bull.ProcessCallbackFunction<Record<string, unknown>> | Bull.ProcessPromiseFunction<Record<string, unknown>>>;
|
} as Record<string, Bull.ProcessCallbackFunction<Record<string, unknown>> | Bull.ProcessPromiseFunction<Record<string, unknown>>>;
|
||||||
|
|
||||||
export default function(dbQueue: Bull.Queue<Record<string, unknown>>) {
|
export default function(dbQueue: Bull.Queue<Record<string, unknown>>) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Bull from 'bull';
|
import Bull from 'bull';
|
||||||
|
|
||||||
import { activeUsersChart, driveChart, federationChart, instanceChart, notesChart, perUserDriveChart, perUserFollowingChart, perUserNotesChart, perUserReactionsChart, usersChart, apRequestChart } from '@/services/chart/index.js';
|
import { activeUsersChart, driveChart, federationChart, hashtagChart, instanceChart, notesChart, perUserDriveChart, perUserFollowingChart, perUserNotesChart, perUserReactionsChart, usersChart, apRequestChart } from '@/services/chart/index.js';
|
||||||
import { queueLogger } from '@/queue/logger.js';
|
import { queueLogger } from '@/queue/logger.js';
|
||||||
|
|
||||||
const logger = queueLogger.createSubLogger('tick-charts');
|
const logger = queueLogger.createSubLogger('tick-charts');
|
||||||
|
@ -17,6 +17,7 @@ export async function tickCharts(job: Bull.Job<Record<string, unknown>>, done: a
|
||||||
perUserNotesChart.tick(false),
|
perUserNotesChart.tick(false),
|
||||||
driveChart.tick(false),
|
driveChart.tick(false),
|
||||||
perUserReactionsChart.tick(false),
|
perUserReactionsChart.tick(false),
|
||||||
|
hashtagChart.tick(false),
|
||||||
perUserFollowingChart.tick(false),
|
perUserFollowingChart.tick(false),
|
||||||
perUserDriveChart.tick(false),
|
perUserDriveChart.tick(false),
|
||||||
apRequestChart.tick(false),
|
apRequestChart.tick(false),
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
import Bull from 'bull';
|
|
||||||
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
|
|
||||||
|
|
||||||
export async function updateInstance(job: Bull.Job<Record<string, unknown>>, done: any): Promise<void> {
|
|
||||||
await fetchInstanceMetadata(job.data.host, true);
|
|
||||||
|
|
||||||
done();
|
|
||||||
}
|
|
|
@ -80,7 +80,7 @@ function groupingAudience(ids: string[], actor: IRemoteUser) {
|
||||||
function isPublic(id: string) {
|
function isPublic(id: string) {
|
||||||
return [
|
return [
|
||||||
'https://www.w3.org/ns/activitystreams#Public',
|
'https://www.w3.org/ns/activitystreams#Public',
|
||||||
'as:Public',
|
'as#Public',
|
||||||
'Public',
|
'Public',
|
||||||
].includes(id);
|
].includes(id);
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ export default async function(actor: IRemoteUser, uri: string): Promise<string>
|
||||||
return 'skip: cant delete other actors note';
|
return 'skip: cant delete other actors note';
|
||||||
}
|
}
|
||||||
|
|
||||||
await deleteNotes([note]);
|
await deleteNotes([note], actor);
|
||||||
return 'ok: note deleted';
|
return 'ok: note deleted';
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { IRemoteUser } from '@/models/entities/user.js';
|
import { IRemoteUser } from '@/models/entities/user.js';
|
||||||
|
import { toArray } from '@/prelude/array.js';
|
||||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||||
import { extractPunyHost } from '@/misc/convert-host.js';
|
import { extractPunyHost } from '@/misc/convert-host.js';
|
||||||
import { shouldBlockInstance } from '@/misc/should-block-instance.js';
|
import { shouldBlockInstance } from '@/misc/should-block-instance.js';
|
||||||
import { apLogger } from '../logger.js';
|
import { apLogger } from '../logger.js';
|
||||||
import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isFlag, isMove, getApId } from '../type.js';
|
import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag, isMove, getApId } from '../type.js';
|
||||||
import create from './create/index.js';
|
import create from './create/index.js';
|
||||||
import performDeleteActivity from './delete/index.js';
|
import performDeleteActivity from './delete/index.js';
|
||||||
import performUpdateActivity from './update/index.js';
|
import performUpdateActivity from './update/index.js';
|
||||||
|
@ -21,6 +22,23 @@ import flag from './flag/index.js';
|
||||||
import { move } from './move/index.js';
|
import { move } from './move/index.js';
|
||||||
|
|
||||||
export async function performActivity(actor: IRemoteUser, activity: IObject, resolver: Resolver): Promise<void> {
|
export async function performActivity(actor: IRemoteUser, activity: IObject, resolver: Resolver): Promise<void> {
|
||||||
|
if (isCollectionOrOrderedCollection(activity)) {
|
||||||
|
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
|
||||||
|
const act = await resolver.resolve(item);
|
||||||
|
try {
|
||||||
|
await performOneActivity(actor, act, resolver);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error || typeof err === 'string') {
|
||||||
|
apLogger.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await performOneActivity(actor, activity, resolver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performOneActivity(actor: IRemoteUser, activity: IObject, resolver: Resolver): Promise<void> {
|
||||||
if (actor.isSuspended) return;
|
if (actor.isSuspended) return;
|
||||||
|
|
||||||
if (typeof activity.id !== 'undefined') {
|
if (typeof activity.id !== 'undefined') {
|
||||||
|
|
|
@ -13,6 +13,6 @@ export const undoAnnounce = async (actor: IRemoteUser, activity: IAnnounce): Pro
|
||||||
|
|
||||||
if (!note) return 'skip: no such Announce';
|
if (!note) return 'skip: no such Announce';
|
||||||
|
|
||||||
await deleteNotes([note]);
|
await deleteNotes([note], actor);
|
||||||
return 'ok: deleted';
|
return 'ok: deleted';
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { updatePerson } from '@/remote/activitypub/models/person.js';
|
||||||
import { update as updateNote } from '@/remote/activitypub/kernel/update/note.js';
|
import { update as updateNote } from '@/remote/activitypub/kernel/update/note.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle Update activity
|
* Updateアクティビティを捌きます
|
||||||
*/
|
*/
|
||||||
export default async (actor: IRemoteUser, activity: IUpdate, resolver: Resolver): Promise<string> => {
|
export default async (actor: IRemoteUser, activity: IUpdate, resolver: Resolver): Promise<string> => {
|
||||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||||
|
@ -29,10 +29,6 @@ export default async (actor: IRemoteUser, activity: IUpdate, resolver: Resolver)
|
||||||
await updatePerson(object, resolver);
|
await updatePerson(object, resolver);
|
||||||
return 'ok: Person updated';
|
return 'ok: Person updated';
|
||||||
} else if (getApType(object) === 'Question') {
|
} else if (getApType(object) === 'Question') {
|
||||||
if (actor.uri !== object.attributedTo) {
|
|
||||||
return 'skip: actor id !== question attributedTo';
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateQuestion(object, resolver).catch(e => console.log(e));
|
await updateQuestion(object, resolver).catch(e => console.log(e));
|
||||||
return 'ok: Question updated';
|
return 'ok: Question updated';
|
||||||
} else if (isPost(object)) {
|
} else if (isPost(object)) {
|
||||||
|
|
|
@ -20,6 +20,8 @@ export async function update(actor: IRemoteUser, note: IObject, resolver: Resolv
|
||||||
try {
|
try {
|
||||||
// if creating was successful...
|
// if creating was successful...
|
||||||
const existsNow = await Notes.findOneByOrFail({ uri });
|
const existsNow = await Notes.findOneByOrFail({ uri });
|
||||||
|
// set the updatedAt timestamp since the note was changed
|
||||||
|
await Notes.update(existsNow.id, { updatedAt: new Date() });
|
||||||
return 'ok: unknown note created and marked as updated';
|
return 'ok: unknown note created and marked as updated';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return `skip: updated note unknown and creating rejected: ${e.message}`;
|
return `skip: updated note unknown and creating rejected: ${e.message}`;
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import { remoteLogger } from '../logger.js';
|
import { remoteLogger } from '../logger.js';
|
||||||
|
|
||||||
export const apLogger = remoteLogger.createSubLogger('ap');
|
export const apLogger = remoteLogger.createSubLogger('ap', 'magenta');
|
||||||
|
|
|
@ -1,15 +1,25 @@
|
||||||
|
import { Cache } from '@/misc/cache.js';
|
||||||
|
import { UserPublickeys } from '@/models/index.js';
|
||||||
import { IRemoteUser } from '@/models/entities/user.js';
|
import { IRemoteUser } from '@/models/entities/user.js';
|
||||||
import { UserPublickey } from '@/models/entities/user-publickey.js';
|
import { UserPublickey } from '@/models/entities/user-publickey.js';
|
||||||
import { uriPersonCache, userByIdCache, publicKeyCache, publicKeyByUserIdCache } from '@/services/user-cache.js';
|
import { uriPersonCache, userByIdCache } from '@/services/user-cache.js';
|
||||||
import { createPerson } from '@/remote/activitypub/models/person.js';
|
import { createPerson } from '@/remote/activitypub/models/person.js';
|
||||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||||
import { HOUR } from '@/const.js';
|
|
||||||
|
|
||||||
export type AuthUser = {
|
export type AuthUser = {
|
||||||
user: IRemoteUser;
|
user: IRemoteUser;
|
||||||
key: UserPublickey;
|
key: UserPublickey;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const publicKeyCache = new Cache<UserPublickey>(
|
||||||
|
Infinity,
|
||||||
|
(keyId) => UserPublickeys.findOneBy({ keyId }).then(x => x ?? undefined),
|
||||||
|
);
|
||||||
|
const publicKeyByUserIdCache = new Cache<UserPublickey>(
|
||||||
|
Infinity,
|
||||||
|
(userId) => UserPublickeys.findOneBy({ userId }).then(x => x ?? undefined),
|
||||||
|
);
|
||||||
|
|
||||||
function authUserFromApId(uri: string): Promise<AuthUser | null> {
|
function authUserFromApId(uri: string): Promise<AuthUser | null> {
|
||||||
return uriPersonCache.fetch(uri)
|
return uriPersonCache.fetch(uri)
|
||||||
.then(async user => {
|
.then(async user => {
|
||||||
|
@ -24,9 +34,10 @@ export async function authUserFromKeyId(keyId: string): Promise<AuthUser | null>
|
||||||
return await publicKeyCache.fetch(keyId)
|
return await publicKeyCache.fetch(keyId)
|
||||||
.then(async key => {
|
.then(async key => {
|
||||||
if (!key) return null;
|
if (!key) return null;
|
||||||
const user = await userByIdCache.fetch(key.userId);
|
else return {
|
||||||
if (!user) return null;
|
user: await userByIdCache.fetch(key.userId),
|
||||||
return { user, key };
|
key,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,20 +30,20 @@ import { extractApMentions } from './mention.js';
|
||||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||||
import { sideEffects } from '@/services/note/side-effects.js';
|
import { sideEffects } from '@/services/note/side-effects.js';
|
||||||
|
|
||||||
function validateNote(object: IObject) {
|
export function validateNote(object: IObject): Error | null {
|
||||||
if (object == null) {
|
if (object == null) {
|
||||||
throw new Error('invalid Note: object is null');
|
return new Error('invalid Note: object is null');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isPost(object)) {
|
if (!isPost(object)) {
|
||||||
throw new Error(`invalid Note: invalid object type ${getApType(object)}`);
|
return new Error(`invalid Note: invalid object type ${getApType(object)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = getApId(object);
|
const id = getApId(object);
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
// Only transient objects or anonymous objects may not have an id or an id that is explicitly null.
|
// Only transient objects or anonymous objects may not have an id or an id that is explicitly null.
|
||||||
// We consider all Notes as not transient and not anonymous so require ids for them.
|
// We consider all Notes as not transient and not anonymous so require ids for them.
|
||||||
throw new Error(`invalid Note: id required but not present`);
|
return new Error(`invalid Note: id required but not present`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that the server is authorized to act on behalf of this author.
|
// Check that the server is authorized to act on behalf of this author.
|
||||||
|
@ -52,11 +52,13 @@ function validateNote(object: IObject) {
|
||||||
? extractPunyHost(getOneApId(object.attributedTo))
|
? extractPunyHost(getOneApId(object.attributedTo))
|
||||||
: null;
|
: null;
|
||||||
if (attributedToHost !== expectHost) {
|
if (attributedToHost !== expectHost) {
|
||||||
throw new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${attributedToHost}`);
|
return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${attributedToHost}`);
|
||||||
}
|
}
|
||||||
if (attributedToHost === config.hostname) {
|
if (attributedToHost === config.hostname) {
|
||||||
throw new Error('invalid Note: by local author');
|
return new Error('invalid Note: by local author');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -125,7 +127,11 @@ export async function fetchNote(object: string | IObject): Promise<Note | null>
|
||||||
export async function createNote(value: string | IObject, resolver: Resolver, silent = false): Promise<Note | null> {
|
export async function createNote(value: string | IObject, resolver: Resolver, silent = false): Promise<Note | null> {
|
||||||
const object: IObject = await resolver.resolve(value);
|
const object: IObject = await resolver.resolve(value);
|
||||||
|
|
||||||
validateNote(object);
|
const err = validateNote(object);
|
||||||
|
if (err) {
|
||||||
|
apLogger.error(`${err.message}`);
|
||||||
|
throw new Error('invalid note');
|
||||||
|
}
|
||||||
|
|
||||||
const note: IPost = object;
|
const note: IPost = object;
|
||||||
|
|
||||||
|
@ -272,7 +278,6 @@ export async function createNote(value: string | IObject, resolver: Resolver, si
|
||||||
return await post(actor, {
|
return await post(actor, {
|
||||||
...processedContent,
|
...processedContent,
|
||||||
createdAt: note.published ? new Date(note.published) : null,
|
createdAt: note.published ? new Date(note.published) : null,
|
||||||
updatedAt: note.updated,
|
|
||||||
reply,
|
reply,
|
||||||
renote: quote,
|
renote: quote,
|
||||||
localOnly: false,
|
localOnly: false,
|
||||||
|
@ -313,14 +318,9 @@ export async function resolveNote(value: string | IObject, resolver: Resolver):
|
||||||
throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note');
|
throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note');
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
// リモートサーバーからフェッチしてきて登録
|
||||||
If the Note Object attached is specified here instead of uri
|
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
|
||||||
after fetching from a remote server and registering, the note is
|
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
|
||||||
generated without going through server fetch, but the attached
|
|
||||||
Note Object may be spoofed, so always specify uri for server
|
|
||||||
Therefore, server fetching is always performed by specifying uri.
|
|
||||||
*/
|
|
||||||
|
|
||||||
return await createNote(uri, resolver, true);
|
return await createNote(uri, resolver, true);
|
||||||
} finally {
|
} finally {
|
||||||
unlock();
|
unlock();
|
||||||
|
@ -333,7 +333,11 @@ export async function resolveNote(value: string | IObject, resolver: Resolver):
|
||||||
* If the target Note is not registered, it will be ignored.
|
* If the target Note is not registered, it will be ignored.
|
||||||
*/
|
*/
|
||||||
export async function updateNote(value: IPost, actor: User, resolver: Resolver): Promise<Note | null> {
|
export async function updateNote(value: IPost, actor: User, resolver: Resolver): Promise<Note | null> {
|
||||||
validateNote(value);
|
const err = validateNote(value);
|
||||||
|
if (err) {
|
||||||
|
apLogger.error(`${err.message}`);
|
||||||
|
throw new Error('invalid updated note');
|
||||||
|
}
|
||||||
|
|
||||||
const uri = getApId(value);
|
const uri = getApId(value);
|
||||||
const exists = await Notes.findOneBy({ uri });
|
const exists = await Notes.findOneBy({ uri });
|
||||||
|
@ -350,7 +354,7 @@ export async function updateNote(value: IPost, actor: User, resolver: Resolver):
|
||||||
|
|
||||||
// update note content itself
|
// update note content itself
|
||||||
await Notes.update(exists.id, {
|
await Notes.update(exists.id, {
|
||||||
updatedAt: value.updated ?? new Date(),
|
updatedAt: new Date(),
|
||||||
|
|
||||||
cw: processedContent.cw,
|
cw: processedContent.cw,
|
||||||
fileIds: processedContent.files.map(file => file.id),
|
fileIds: processedContent.files.map(file => file.id),
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { URL } from 'node:url';
|
|
||||||
import promiseLimit from 'promise-limit';
|
import promiseLimit from 'promise-limit';
|
||||||
import { Not, IsNull } from 'typeorm';
|
import { Not, IsNull } from 'typeorm';
|
||||||
|
|
||||||
|
@ -27,7 +26,7 @@ import { db } from '@/db/postgre.js';
|
||||||
import { fromHtml } from '@/mfm/from-html.js';
|
import { fromHtml } from '@/mfm/from-html.js';
|
||||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||||
import { apLogger } from '../logger.js';
|
import { apLogger } from '../logger.js';
|
||||||
import { isCollectionOrOrderedCollection, isCollection, IActor, getApId, getOneApHrefNullable, IObject, isPropertyValue, getApType, isActor, isPost } from '../type.js';
|
import { isCollectionOrOrderedCollection, isCollection, IActor, getApId, getOneApHrefNullable, IObject, isPropertyValue, getApType, isActor } from '../type.js';
|
||||||
import { extractApHashtags, extractEmojis } from './tag.js';
|
import { extractApHashtags, extractEmojis } from './tag.js';
|
||||||
import { resolveNote } from './note.js';
|
import { resolveNote } from './note.js';
|
||||||
import { resolveImage } from './image.js';
|
import { resolveImage } from './image.js';
|
||||||
|
@ -78,23 +77,8 @@ async function validateActor(x: IObject, resolver: Resolver): Promise<IActor> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// check that inbox is a valid and absolute URL
|
if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) {
|
||||||
// in NodeJS, the first parameter must be an absolute URL or the base URL is required
|
throw new Error('invalid Actor: wrong inbox');
|
||||||
try {
|
|
||||||
new URL(x.inbox)
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error('invalid Actor: wrong inbox', { cause: err });
|
|
||||||
}
|
|
||||||
|
|
||||||
// unify different sharedInbox places
|
|
||||||
x.sharedInbox = x.sharedInbox ?? x.endpoints?.sharedInbox ?? null;
|
|
||||||
if (x.sharedInbox != null) {
|
|
||||||
// check that sharedInbox is a valid and absolute URL
|
|
||||||
try {
|
|
||||||
new URL(x.sharedInbox);
|
|
||||||
} catch (err) {
|
|
||||||
delete x.sharedInbox;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) {
|
if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) {
|
||||||
|
@ -201,7 +185,7 @@ export async function createPerson(value: string | IObject, resolver: Resolver):
|
||||||
usernameLower: person.preferredUsername!.toLowerCase(),
|
usernameLower: person.preferredUsername!.toLowerCase(),
|
||||||
host,
|
host,
|
||||||
inbox: person.inbox,
|
inbox: person.inbox,
|
||||||
sharedInbox: person.sharedInbox,
|
sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
|
||||||
followersUri: person.followers ? getApId(person.followers) : undefined,
|
followersUri: person.followers ? getApId(person.followers) : undefined,
|
||||||
featured: person.featured ? getApId(person.featured) : undefined,
|
featured: person.featured ? getApId(person.featured) : undefined,
|
||||||
uri: person.id,
|
uri: person.id,
|
||||||
|
@ -253,7 +237,7 @@ export async function createPerson(value: string | IObject, resolver: Resolver):
|
||||||
registerOrFetchInstanceDoc(host).then(i => {
|
registerOrFetchInstanceDoc(host).then(i => {
|
||||||
Instances.increment({ id: i.id }, 'usersCount', 1);
|
Instances.increment({ id: i.id }, 'usersCount', 1);
|
||||||
instanceChart.newUser(i.host);
|
instanceChart.newUser(i.host);
|
||||||
fetchInstanceMetadata(i.host);
|
fetchInstanceMetadata(i);
|
||||||
});
|
});
|
||||||
|
|
||||||
usersChart.update(user!, true);
|
usersChart.update(user!, true);
|
||||||
|
@ -351,7 +335,7 @@ export async function updatePerson(value: IObject | string, resolver: Resolver):
|
||||||
const updates = {
|
const updates = {
|
||||||
lastFetchedAt: new Date(),
|
lastFetchedAt: new Date(),
|
||||||
inbox: person.inbox,
|
inbox: person.inbox,
|
||||||
sharedInbox: person.sharedInbox,
|
sharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined),
|
||||||
followersUri: person.followers ? getApId(person.followers) : undefined,
|
followersUri: person.followers ? getApId(person.followers) : undefined,
|
||||||
featured: person.featured,
|
featured: person.featured,
|
||||||
emojis: emojiNames,
|
emojis: emojiNames,
|
||||||
|
@ -398,7 +382,7 @@ export async function updatePerson(value: IObject | string, resolver: Resolver):
|
||||||
await Followings.update({
|
await Followings.update({
|
||||||
followerId: exist.id,
|
followerId: exist.id,
|
||||||
}, {
|
}, {
|
||||||
followerSharedInbox: person.sharedInbox,
|
followerSharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined),
|
||||||
});
|
});
|
||||||
|
|
||||||
await updateFeatured(exist.id, resolver).catch(err => apLogger.error(err));
|
await updateFeatured(exist.id, resolver).catch(err => apLogger.error(err));
|
||||||
|
@ -459,17 +443,17 @@ async function updateFeatured(userId: User['id'], resolver: Resolver) {
|
||||||
const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems;
|
const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems;
|
||||||
const items = await Promise.all(toArray(unresolvedItems).map(x => resolver.resolve(x)));
|
const items = await Promise.all(toArray(unresolvedItems).map(x => resolver.resolve(x)));
|
||||||
|
|
||||||
// Resolve and register Notes
|
// Resolve and regist Notes
|
||||||
const limit = promiseLimit<Note | null>(2);
|
const limit = promiseLimit<Note | null>(2);
|
||||||
const featuredNotes = await Promise.all(items
|
const featuredNotes = await Promise.all(items
|
||||||
.filter(item => isPost(item))
|
.filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも
|
||||||
.slice(0, 5)
|
.slice(0, 5)
|
||||||
.map(item => limit(() => resolveNote(item, resolver).catch(() => null))));
|
.map(item => limit(() => resolveNote(item, resolver))));
|
||||||
|
|
||||||
await db.transaction(async transactionalEntityManager => {
|
await db.transaction(async transactionalEntityManager => {
|
||||||
await transactionalEntityManager.delete(UserNotePining, { userId: user.id });
|
await transactionalEntityManager.delete(UserNotePining, { userId: user.id });
|
||||||
|
|
||||||
// TODO: For now, generate the id at a different time and maintain the order.
|
// とりあえずidを別の時間で生成して順番を維持
|
||||||
let td = 0;
|
let td = 0;
|
||||||
for (const note of featuredNotes.filter(note => note != null)) {
|
for (const note of featuredNotes.filter(note => note != null)) {
|
||||||
td -= 1000;
|
td -= 1000;
|
||||||
|
|
|
@ -57,7 +57,7 @@ export async function updateQuestion(value: string | IObject, resolver: Resolver
|
||||||
const question = await resolver.resolve(value) as IQuestion;
|
const question = await resolver.resolve(value) as IQuestion;
|
||||||
apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
|
apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
|
||||||
|
|
||||||
if (!isQuestion(question)) throw new Error('object is not a Question');
|
if (question.type !== 'Question') throw new Error('object is not a Question');
|
||||||
|
|
||||||
const apChoices = question.oneOf || question.anyOf;
|
const apChoices = question.oneOf || question.anyOf;
|
||||||
|
|
||||||
|
@ -67,10 +67,6 @@ export async function updateQuestion(value: string | IObject, resolver: Resolver
|
||||||
const oldCount = poll.votes[poll.choices.indexOf(choice)];
|
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 (!Number.isInteger(newCount) || newcount < 0) {
|
|
||||||
throw new Error(`invalid newCount: ${newCount}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldCount !== newCount) {
|
if (oldCount !== newCount) {
|
||||||
changed = true;
|
changed = true;
|
||||||
poll.votes[poll.choices.indexOf(choice)] = newCount;
|
poll.votes[poll.choices.indexOf(choice)] = newCount;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue