Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop

This commit is contained in:
tamaina 2022-05-30 05:53:40 +00:00
commit 465531d56c
266 changed files with 7785 additions and 5442 deletions

View file

@ -22,7 +22,10 @@ First, in order to avoid duplicate Issues, please search to see if the problem y
## 🤬 Actual Behavior ## 🤬 Actual Behavior
<!--- Tell us what happens instead of the expected behavior --> <!--
Tell us what happens instead of the expected behavior.
Please include errors from the developer console and/or server log files if you have access to them.
-->
## 📝 Steps to Reproduce ## 📝 Steps to Reproduce

8
.github/labeler.yml vendored Normal file
View file

@ -0,0 +1,8 @@
'⚙Server':
- packages/backend/**/*
'🖥Client':
- packages/client/**/*
'‼️ wrong locales':
- any: ['locales/*.yml', '!locales/ja-JP.yml']

14
.github/workflows/labeler.yml vendored Normal file
View file

@ -0,0 +1,14 @@
name: "Pull Request Labeler"
on:
- pull_request_target
jobs:
triage:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

View file

@ -3,6 +3,7 @@
"editorconfig.editorconfig", "editorconfig.editorconfig",
"eg2.vscode-npm-script", "eg2.vscode-npm-script",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"johnsoncodehk.volar", "Vue.volar",
"Vue.vscode-typescript-vue-plugin"
] ]
} }

View file

@ -18,6 +18,16 @@ You should also include the user name that made the change.
- enhance: API: notifications/readは配列でも受け付けるように #7667 @tamaina - enhance: API: notifications/readは配列でも受け付けるように #7667 @tamaina
- enhance: プッシュ通知を複数アカウント対応に #7667 @tamaina - enhance: プッシュ通知を複数アカウント対応に #7667 @tamaina
- enhance: プッシュ通知にクリックやactionを設定 #7667 @tamaina - enhance: プッシュ通知にクリックやactionを設定 #7667 @tamaina
- replaced webpack with Vite @tamaina
- update dependencies @syuilo
- enhance: display URL of QR code for TOTP registration @syuilo
- enhance: Supports Unicode Emoji 14.0 @mei23
- The theme color is now better validated. @Johann150
Your own theme color may be unset if it was in an invalid format.
Admins should check their instance settings if in doubt.
- Perform port diagnosis at startup only when Listen fails @mei23
- Rate limiting is now also usable for non-authenticated users. @Johann150
Admins should make sure the reverse proxy sets the `X-Forwarded-For` header to the original address.
### Bugfixes ### Bugfixes
- Client: fix settings page @tamaina - Client: fix settings page @tamaina
@ -25,6 +35,14 @@ You should also include the user name that made the change.
- Server: await promises when following or unfollowing users @Johann150 - Server: await promises when following or unfollowing users @Johann150
- Client: fix abuse reports page to be able to show all reports @Johann150 - Client: fix abuse reports page to be able to show all reports @Johann150
- Federation: Add rel attribute to host-meta @mei23 - Federation: Add rel attribute to host-meta @mei23
- Client: fix profile picture height in mentions @tamaina
- MFM: more animated functions support `speed` parameter @futchitwo
- Federation: Fix quote renotes containing no text being federated correctly @Johann150
- Server: fix missing foreign key for reports leading to reports page being unusable @Johann150
- Server: fix internal in-memory caching @Johann150
- Server: use correct order of attachments on notes @Johann150
- Server: prevent crash when processing certain PNGs @syuilo
- Server: Fix unable to generate video thumbnails @mei23
## 12.110.1 (2022/04/23) ## 12.110.1 (2022/04/23)

View file

@ -1,10 +1,11 @@
# Contribution guide # Contribution guide
We're glad you're interested in contributing Misskey! In this document you will find the information you need to contribute to the project. We're glad you're interested in contributing Misskey! In this document you will find the information you need to contribute to the project.
** Important:** This project uses Japanese as its major language, **but you do not need to translate and write the Issues/PRs in Japanese.** > **Note**
Also, you might receive comments on your Issue/PR in Japanese, but you do not need to reply to them in Japanese as well.\ > This project uses Japanese as its major language, **but you do not need to translate and write the Issues/PRs in Japanese.**
The accuracy of machine translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language. > Also, you might receive comments on your Issue/PR in Japanese, but you do not need to reply to them in Japanese as well.\
It will also allow the reader to use the translation tool of their preference if necessary. > The accuracy of machine translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language.
> It will also allow the reader to use the translation tool of their preference if necessary.
## Roadmap ## Roadmap
See [ROADMAP.md](./ROADMAP.md) See [ROADMAP.md](./ROADMAP.md)
@ -16,6 +17,9 @@ Before creating an issue, please check the following:
- Issues should only be used to feature requests, suggestions, and bug tracking. - Issues should only be used to feature requests, suggestions, and bug tracking.
- Please ask questions or troubleshooting in the [Misskey Forum](https://forum.misskey.io/) or [Discord](https://discord.gg/Wp8gVStHW3). - Please ask questions or troubleshooting in the [Misskey Forum](https://forum.misskey.io/) or [Discord](https://discord.gg/Wp8gVStHW3).
> **Warning**
> Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged.
## Before implementation ## Before implementation
When you want to add a feature or fix a bug, **first have the design and policy reviewed in an Issue** (if it is not there, please make one). Without this step, there is a high possibility that the PR will not be merged even if it is implemented. When you want to add a feature or fix a bug, **first have the design and policy reviewed in an Issue** (if it is not there, please make one). Without this step, there is a high possibility that the PR will not be merged even if it is implemented.

View file

@ -1,6 +1,6 @@
FROM node:18.0.0-alpine3.15 AS base FROM node:18.0.0-alpine3.15 AS base
ENV NODE_ENV=production ARG NODE_ENV=production
WORKDIR /misskey WORKDIR /misskey
@ -31,5 +31,6 @@ COPY --from=builder /misskey/packages/backend/built ./packages/backend/built
COPY --from=builder /misskey/packages/client/node_modules ./packages/client/node_modules COPY --from=builder /misskey/packages/client/node_modules ./packages/client/node_modules
COPY . ./ COPY . ./
ENV NODE_ENV=production
CMD ["npm", "run", "migrateandstart"] CMD ["npm", "run", "migrateandstart"]

View file

@ -1,27 +1,29 @@
[![Misskey](https://github.com/misskey-dev/assets/blob/main/banner.png?raw=true)](https://join.misskey.page/)
<div align="center"> <div align="center">
<a href="https://misskey-hub.net">
<img src="./assets/title_float.svg" alt="Misskey logo" style="border-radius:50%" width="400"/>
</a>
**🌎 A forever evolving, interplanetary microblogging platform. 🚀** **🌎 **[Misskey](https://misskey-hub.net/)** is an open source, decentralized social media platform that's free forever! 🚀**
**Misskey** is a distributed microblogging platform with advanced features such as Reactions and a highly customizable UI.
[Learn more](https://misskey-hub.net/)
--- ---
[✨ Find an instance](https://misskey-hub.net/instances.html) <a href="https://misskey-hub.net/instances.html">
<img src="https://custom-icon-badges.herokuapp.com/badge/find_an-instance-acea31?logoColor=acea31&style=for-the-badge&logo=misskey&labelColor=363B40" alt="find an instance"/></a>
[📦 Create your own instance](https://misskey-hub.net/docs/install.html)
<a href="https://misskey-hub.net/docs/install.html">
[🛠️ Contribute](./CONTRIBUTING.md) <img src="https://custom-icon-badges.herokuapp.com/badge/create_an-instance-FBD53C?logoColor=FBD53C&style=for-the-badge&logo=server&labelColor=363B40" alt="create an instance"/></a>
[🚀 Join the community](https://discord.gg/Wp8gVStHW3) <a href="./CONTRIBUTING.md">
<img src="https://custom-icon-badges.herokuapp.com/badge/become_a-contributor-A371F7?logoColor=A371F7&style=for-the-badge&logo=git-merge&labelColor=363B40" alt="become a contributor"/></a>
<a href="https://discord.gg/Wp8gVStHW3">
<img src="https://custom-icon-badges.herokuapp.com/badge/join_the-community-5865F2?logoColor=5865F2&style=for-the-badge&logo=discord&labelColor=363B40" alt="join the community"/></a>
<a href="https://www.patreon.com/syuilo">
<img src="https://custom-icon-badges.herokuapp.com/badge/become_a-patron-F96854?logoColor=F96854&style=for-the-badge&logo=patreon&labelColor=363B40" alt="become a patron"/></a>
--- ---
<a href="https://www.patreon.com/syuilo"><img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" alt="Become a Patron!" width="160" /></a>
</div> </div>
<div> <div>
@ -30,22 +32,25 @@
## ✨ Features ## ✨ Features
- **ActivityPub support**\ - **ActivityPub support**\
It is possible to interact with other software. Not on Misskey? No problem! Not only can Misskey instances talk to each other, but you can make friends with people on other networks like Mastodon and Pixelfed!
- **Reactions**\ - **Reactions**\
You can add "reactions" to each post, making it easy for you to express your feelings. You can add emoji reactions to any post! No longer are you bound by a like button, show everyone exactly how you feel with the tap of a button.
- **Drive**\ - **Drive**\
An interface to manage uploaded files such as images, videos, sounds, etc. With Misskey's built in drive, you get cloud storage right in your social media, where you can upload any files, make folders, and find media from posts you've made!
You can also organize your favorite content into folders, making it easy to share again.
- **Rich Web UI**\ - **Rich Web UI**\
Misskey has a rich WebUI by default. Misskey has a rich and easy to use Web UI!
It is highly customizable by flexibly changing the layout and installing various widgets and themes. It is highly customizable, from changing the layout and adding widgets to making custom themes.
Furthermore, plug-ins can be created using AiScript, a original programming language. Furthermore, plugins can be created using AiScript, an original programming language.
- and more... - And much more...
</div> </div>
<div style="clear: both;"></div> <div style="clear: both;"></div>
## Documentation
Misskey Documentation can be found at [Misskey Hub](https://misskey-hub.net/), some of the links and graphics above also lead to specific portions of it.
## Sponsors ## Sponsors
<div align="center"> <div align="center">
<a class="rss3" title="RSS3" href="https://rss3.io/" target="_blank"><img src="https://rss3.mypinata.cloud/ipfs/QmUG6H3Z7D5P511shn7sB4CPmpjH5uZWu4m5mWX7U3Gqbu" alt="RSS3" height="60"></a> <a class="rss3" title="RSS3" href="https://rss3.io/" target="_blank"><img src="https://rss3.mypinata.cloud/ipfs/QmUG6H3Z7D5P511shn7sB4CPmpjH5uZWu4m5mWX7U3Gqbu" alt="RSS3" height="60"></a>

BIN
assets/title_float.svg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

3
chart/Chart.yaml Normal file
View file

@ -0,0 +1,3 @@
apiVersion: v2
name: misskey
version: 0.0.0

165
chart/files/default.yml Normal file
View file

@ -0,0 +1,165 @@
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Misskey configuration
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ┌─────┐
#───┘ URL └─────────────────────────────────────────────────────
# Final accessible URL seen by a user.
# url: https://example.tld/
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
# URL SETTINGS AFTER THAT!
# ┌───────────────────────┐
#───┘ Port and TLS settings └───────────────────────────────────
#
# Misskey supports two deployment options for public.
#
# Option 1: With Reverse Proxy
#
# +----- https://example.tld/ ------------+
# +------+ |+-------------+ +----------------+|
# | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
# +------+ |+-------------+ +----------------+|
# +---------------------------------------+
#
# You need to setup reverse proxy. (eg. nginx)
# You do not define 'https' section.
# Option 2: Standalone
#
# +- https://example.tld/ -+
# +------+ | +---------------+ |
# | User | ---> | | Misskey (443) | |
# +------+ | +---------------+ |
# +------------------------+
#
# You need to run Misskey as root.
# You need to set Certificate in 'https' section.
# To use option 1, uncomment below line.
port: 3000 # A port that your Misskey server should listen.
# To use option 2, uncomment below lines.
#port: 443
#https:
# # path for certification
# key: /etc/letsencrypt/live/example.tld/privkey.pem
# cert: /etc/letsencrypt/live/example.tld/fullchain.pem
# ┌──────────────────────────┐
#───┘ PostgreSQL configuration └────────────────────────────────
db:
host: localhost
port: 5432
# Database name
db: misskey
# Auth
user: example-misskey-user
pass: example-misskey-pass
# Whether disable Caching queries
#disableCache: true
# Extra Connection options
#extra:
# ssl: true
# ┌─────────────────────┐
#───┘ Redis configuration └─────────────────────────────────────
redis:
host: localhost
port: 6379
#pass: example-pass
#prefix: example-prefix
#db: 1
# ┌─────────────────────────────┐
#───┘ Elasticsearch configuration └─────────────────────────────
#elasticsearch:
# host: localhost
# port: 9200
# ssl: false
# user:
# pass:
# ┌───────────────┐
#───┘ ID generation └───────────────────────────────────────────
# You can select the ID generation method.
# You don't usually need to change this setting, but you can
# change it according to your preferences.
# Available methods:
# aid ... Short, Millisecond accuracy
# meid ... Similar to ObjectID, Millisecond accuracy
# ulid ... Millisecond accuracy
# objectid ... This is left for backward compatibility
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
# ID SETTINGS AFTER THAT!
id: "aid"
# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────
# Whether disable HSTS
#disableHsts: true
# Number of worker processes
#clusterLimit: 1
# Job concurrency per worker
# deliverJobConcurrency: 128
# inboxJobConcurrency: 16
# Job rate limiter
# deliverJobPerSec: 128
# inboxJobPerSec: 16
# Job attempts
# deliverJobMaxAttempts: 12
# inboxJobMaxAttempts: 8
# IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4
# Syslog option
#syslog:
# host: localhost
# port: 514
# Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128
#proxyBypassHosts: [
# 'example.com',
# '192.0.2.8'
#]
# Proxy for SMTP/SMTPS
#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT
#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
# Media Proxy
#mediaProxy: https://example.com/proxy
# Sign to ActivityPub GET request (default: false)
#signToActivityPubGet: true
#allowedPrivateNetworks: [
# '127.0.0.1/32'
#]
# Upload or download file size limits (bytes)
#maxFileSize: 262144000

View file

@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "misskey.fullname" . }}-configuration
data:
default.yml: |-
{{ .Files.Get "files/default.yml"|nindent 4 }}
url: {{ .Values.url }}

View file

@ -0,0 +1,47 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "misskey.fullname" . }}
labels:
{{- include "misskey.labels" . | nindent 4 }}
spec:
selector:
matchLabels:
{{- include "misskey.selectorLabels" . | nindent 6 }}
replicas: 1
template:
metadata:
labels:
{{- include "misskey.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: misskey
image: {{ .Values.image }}
env:
- name: NODE_ENV
value: {{ .Values.environment }}
volumeMounts:
- name: {{ include "misskey.fullname" . }}-configuration
mountPath: /misskey/.config
readOnly: true
ports:
- containerPort: 3000
- name: postgres
image: postgres:14-alpine
env:
- name: POSTGRES_USER
value: "example-misskey-user"
- name: POSTGRES_PASSWORD
value: "example-misskey-pass"
- name: POSTGRES_DB
value: "misskey"
ports:
- containerPort: 5432
- name: redis
image: redis:alpine
ports:
- containerPort: 6379
volumes:
- name: {{ include "misskey.fullname" . }}-configuration
configMap:
name: {{ include "misskey.fullname" . }}-configuration

View file

@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "misskey.fullname" . }}
annotations:
dev.okteto.com/auto-ingress: "true"
spec:
type: ClusterIP
ports:
- port: 3000
protocol: TCP
name: http
selector:
{{- include "misskey.selectorLabels" . | nindent 4 }}

View file

@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "misskey.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "misskey.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "misskey.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "misskey.labels" -}}
helm.sh/chart: {{ include "misskey.chart" . }}
{{ include "misskey.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "misskey.selectorLabels" -}}
app.kubernetes.io/name: {{ include "misskey.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "misskey.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "misskey.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

3
chart/values.yml Normal file
View file

@ -0,0 +1,3 @@
url: https://example.tld/
image: okteto.dev/misskey
environment: production

View file

@ -1,5 +1,8 @@
describe('Before setup instance', () => { describe('Before setup instance', () => {
beforeEach(() => { beforeEach(() => {
cy.window(win => {
win.indexedDB.deleteDatabase('keyval-store');
});
cy.request('POST', '/api/reset-db').as('reset'); cy.request('POST', '/api/reset-db').as('reset');
cy.get('@reset').its('status').should('equal', 204); cy.get('@reset').its('status').should('equal', 204);
cy.reload(true); cy.reload(true);
@ -32,6 +35,9 @@ describe('Before setup instance', () => {
describe('After setup instance', () => { describe('After setup instance', () => {
beforeEach(() => { beforeEach(() => {
cy.window(win => {
win.indexedDB.deleteDatabase('keyval-store');
});
cy.request('POST', '/api/reset-db').as('reset'); cy.request('POST', '/api/reset-db').as('reset');
cy.get('@reset').its('status').should('equal', 204); cy.get('@reset').its('status').should('equal', 204);
cy.reload(true); cy.reload(true);
@ -70,6 +76,9 @@ describe('After setup instance', () => {
describe('After user signup', () => { describe('After user signup', () => {
beforeEach(() => { beforeEach(() => {
cy.window(win => {
win.indexedDB.deleteDatabase('keyval-store');
});
cy.request('POST', '/api/reset-db').as('reset'); cy.request('POST', '/api/reset-db').as('reset');
cy.get('@reset').its('status').should('equal', 204); cy.get('@reset').its('status').should('equal', 204);
cy.reload(true); cy.reload(true);
@ -129,6 +138,9 @@ describe('After user signup', () => {
describe('After user singed in', () => { describe('After user singed in', () => {
beforeEach(() => { beforeEach(() => {
cy.window(win => {
win.indexedDB.deleteDatabase('keyval-store');
});
cy.request('POST', '/api/reset-db').as('reset'); cy.request('POST', '/api/reset-db').as('reset');
cy.get('@reset').its('status').should('equal', 204); cy.get('@reset').its('status').should('equal', 204);
cy.reload(true); cy.reload(true);
@ -163,12 +175,10 @@ describe('After user singed in', () => {
}); });
it('successfully loads', () => { it('successfully loads', () => {
cy.visit('/'); cy.get('[data-cy-open-post-form]').should('be.visible');
}); });
it('note', () => { it('note', () => {
cy.visit('/');
cy.get('[data-cy-open-post-form]').click(); cy.get('[data-cy-open-post-form]').click();
cy.get('[data-cy-post-form-text]').type('Hello, Misskey!'); cy.get('[data-cy-post-form-text]').type('Hello, Misskey!');
cy.get('[data-cy-open-post-form-submit]').click(); cy.get('[data-cy-open-post-form-submit]').click();

View file

@ -0,0 +1,84 @@
describe('After user signed in', () => {
beforeEach(() => {
cy.window(win => {
win.indexedDB.deleteDatabase('keyval-store');
});
cy.viewport('macbook-16');
cy.request('POST', '/api/reset-db').as('reset');
cy.get('@reset').its('status').should('equal', 204);
cy.reload(true);
// インスタンス初期セットアップ
cy.request('POST', '/api/admin/accounts/create', {
username: 'admin',
password: 'pass',
}).its('body').as('admin');
// ユーザー作成
cy.request('POST', '/api/signup', {
username: 'alice',
password: 'alice1234',
}).its('body').as('alice');
cy.visit('/');
cy.intercept('POST', '/api/signin').as('signin');
cy.get('[data-cy-signin]').click();
cy.get('[data-cy-signin-username] input').type('alice');
cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
cy.wait('@signin').as('signedIn');
});
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');
});

View file

@ -9,7 +9,7 @@ services:
- redis - redis
# - es # - es
ports: ports:
- "127.0.0.1:3000:3000" - "3000:3000"
networks: networks:
- internal_network - internal_network
- external_network - external_network

View file

@ -141,7 +141,7 @@ flagAsBotDescription: "فعّل هذا الخيار إذا كان هذا الح
flagAsCat: "علّم هذا الحساب كحساب قط" flagAsCat: "علّم هذا الحساب كحساب قط"
flagAsCatDescription: "فعّل هذا الخيار لوضع علامة على الحساب لتوضيح أنه حساب قط." flagAsCatDescription: "فعّل هذا الخيار لوضع علامة على الحساب لتوضيح أنه حساب قط."
flagShowTimelineReplies: "أظهر التعليقات في الخيط الزمني" flagShowTimelineReplies: "أظهر التعليقات في الخيط الزمني"
flagShowTimelineRepliesDescription: "يظهر الردود في الخط الزمني" flagShowTimelineRepliesDescription: "يظهر الردود في الخيط الزمني"
autoAcceptFollowed: "اقبل طلبات المتابعة تلقائيا من الحسابات المتابَعة" autoAcceptFollowed: "اقبل طلبات المتابعة تلقائيا من الحسابات المتابَعة"
addAccount: "أضف حساباً" addAccount: "أضف حساباً"
loginFailed: "فشل الولوج" loginFailed: "فشل الولوج"
@ -312,12 +312,12 @@ dayX: "{day}"
monthX: "{month}" monthX: "{month}"
yearX: "{year}" yearX: "{year}"
pages: "الصفحات" pages: "الصفحات"
integration: "دمج" integration: "التكامل"
connectService: "اتصل" connectService: "اتصل"
disconnectService: "اقطع الاتصال" disconnectService: "اقطع الاتصال"
enableLocalTimeline: "تفعيل الخيط المحلي" enableLocalTimeline: "تفعيل الخيط المحلي"
enableGlobalTimeline: "تفعيل الخيط الزمني الشامل" enableGlobalTimeline: "تفعيل الخيط الزمني الشامل"
disablingTimelinesInfo: "سيتمكن المديرون والمشرفون من الوصول إلى كل الخطوط الزمنية حتى وإن لم تفعّل." disablingTimelinesInfo: "سيتمكن المديرون والمشرفون من الوصول إلى كل الخيوط الزمنية حتى وإن لم تفعّل."
registration: "إنشاء حساب" registration: "إنشاء حساب"
enableRegistration: "تفعيل إنشاء الحسابات الجديدة" enableRegistration: "تفعيل إنشاء الحسابات الجديدة"
invite: "دعوة" invite: "دعوة"
@ -532,6 +532,7 @@ poll: "استطلاع رأي"
useCw: "إخفاء المحتوى" useCw: "إخفاء المحتوى"
enablePlayer: "افتح مشغل الفيديو" enablePlayer: "افتح مشغل الفيديو"
disablePlayer: "أغلق مشغل الفيديو" disablePlayer: "أغلق مشغل الفيديو"
expandTweet: "وسّع التغريدة"
themeEditor: "مصمم القوالب" themeEditor: "مصمم القوالب"
description: "الوصف" description: "الوصف"
describeFile: "أضف تعليقًا توضيحيًا" describeFile: "أضف تعليقًا توضيحيًا"
@ -635,6 +636,7 @@ yes: "نعم"
no: "لا" no: "لا"
driveFilesCount: "عدد الملفات في قرص التخزين" driveFilesCount: "عدد الملفات في قرص التخزين"
driveUsage: "المستغل من قرص التخزين" driveUsage: "المستغل من قرص التخزين"
noCrawle: "ارفض فهرسة زاحف الويب"
noCrawleDescription: "يطلب من محركات البحث ألّا يُفهرسوا ملفك الشخصي وملاحظات وصفحاتك وما شابه." noCrawleDescription: "يطلب من محركات البحث ألّا يُفهرسوا ملفك الشخصي وملاحظات وصفحاتك وما شابه."
alwaysMarkSensitive: "علّم افتراضيًا جميع ملاحظاتي كذات محتوى حساس" alwaysMarkSensitive: "علّم افتراضيًا جميع ملاحظاتي كذات محتوى حساس"
loadRawImages: "حمّل الصور الأصلية بدلًا من المصغرات" loadRawImages: "حمّل الصور الأصلية بدلًا من المصغرات"
@ -878,9 +880,11 @@ _mfm:
center: "وسط" center: "وسط"
centerDescription: "يمركز المحتوى في الوَسَط." centerDescription: "يمركز المحتوى في الوَسَط."
quote: "اقتبس" quote: "اقتبس"
quoteDescription: "يعرض المحتوى كاقتباس"
emoji: "إيموجي مخصص" emoji: "إيموجي مخصص"
emojiDescription: "إحاطة اسم الإيموجي بنقطتي تفسير سيستبدله بصورة الإيموجي." emojiDescription: "إحاطة اسم الإيموجي بنقطتي تفسير سيستبدله بصورة الإيموجي."
search: "البحث" search: "البحث"
searchDescription: "يعرض نصًا في صندوق البحث"
flip: "اقلب" flip: "اقلب"
flipDescription: "يقلب المحتوى عموديًا أو أفقيًا" flipDescription: "يقلب المحتوى عموديًا أو أفقيًا"
jelly: "تأثير (هلام)" jelly: "تأثير (هلام)"
@ -1030,12 +1034,12 @@ _tutorial:
step3_3: "املأ النموذج وانقر الزرّ الموجود في أعلى اليمين للإرسال." step3_3: "املأ النموذج وانقر الزرّ الموجود في أعلى اليمين للإرسال."
step3_4: "ليس لديك ما تقوله؟ إذا اكتب \"بدأتُ استخدم ميسكي\"." step3_4: "ليس لديك ما تقوله؟ إذا اكتب \"بدأتُ استخدم ميسكي\"."
step4_1: "هل نشرت ملاحظتك الأولى؟" step4_1: "هل نشرت ملاحظتك الأولى؟"
step4_2: "مرحى! يمكنك الآن رؤية ملاحظتك في الخط الزمني." step4_2: "مرحى! يمكنك الآن رؤية ملاحظتك في الخيط الزمني."
step5_1: "والآن، لنجعل الخط الزمني أكثر حيوية وذلك بمتابعة بعض المستخدمين." step5_1: "والآن، لنجعل الخيط الزمني أكثر حيوية وذلك بمتابعة بعض المستخدمين."
step5_2: "تعرض صفحة {features} الملاحظات المتداولة في هذا المثيل ويتيح لك {Explore} العثور على المستخدمين الرائدين. اعثر على الأشخاص الذين يثيرون إهتمامك وتابعهم!" step5_2: "تعرض صفحة {features} الملاحظات المتداولة في هذا المثيل ويتيح لك {Explore} العثور على المستخدمين الرائدين. اعثر على الأشخاص الذين يثيرون إهتمامك وتابعهم!"
step5_3: "لمتابعة مستخدمين ادخل ملفهم الشخصي بالنقر على صورتهم الشخصية ثم اضغط زر 'تابع'." step5_3: "لمتابعة مستخدمين ادخل ملفهم الشخصي بالنقر على صورتهم الشخصية ثم اضغط زر 'تابع'."
step5_4: "إذا كان لدى المستخدم رمز قفل بجوار اسمه ، وجب عليك انتظاره ليقبل طلب المتابعة يدويًا." step5_4: "إذا كان لدى المستخدم رمز قفل بجوار اسمه ، وجب عليك انتظاره ليقبل طلب المتابعة يدويًا."
step6_1: "الآن ستتمكن من رؤية ملاحظات المستخدمين المتابَعين في الخط الزمني." step6_1: "الآن ستتمكن من رؤية ملاحظات المستخدمين المتابَعين في الخيط الزمني."
step6_2: "يمكنك التفاعل بسرعة مع الملاحظات عن طريق إضافة \"تفاعل\"." step6_2: "يمكنك التفاعل بسرعة مع الملاحظات عن طريق إضافة \"تفاعل\"."
step6_3: "لإضافة تفاعل لملاحظة ، انقر فوق علامة \"+\" أسفل للملاحظة واختر الإيموجي المطلوب." step6_3: "لإضافة تفاعل لملاحظة ، انقر فوق علامة \"+\" أسفل للملاحظة واختر الإيموجي المطلوب."
step7_1: "مبارك ! أنهيت الدورة التعليمية الأساسية لاستخدام ميسكي." step7_1: "مبارك ! أنهيت الدورة التعليمية الأساسية لاستخدام ميسكي."
@ -1201,8 +1205,13 @@ _charts:
_instanceCharts: _instanceCharts:
requests: "الطلبات" requests: "الطلبات"
users: "تباين عدد المستخدمين" users: "تباين عدد المستخدمين"
usersTotal: "تباين عدد المستخدمين"
notes: "تباين عدد الملاحظات" notes: "تباين عدد الملاحظات"
notesTotal: "تباين عدد الملاحظات"
ff: "تباين عدد حسابات المتابَعة/المتابِعة"
ffTotal: "تباين عدد حسابات المتابَعة/المتابِعة"
files: "تباين عدد الملفات" files: "تباين عدد الملفات"
filesTotal: "تباين عدد الملفات"
_timelines: _timelines:
home: "الرئيسي" home: "الرئيسي"
local: "المحلي" local: "المحلي"
@ -1321,6 +1330,7 @@ _pages:
random: "عشوائي" random: "عشوائي"
value: "القيم" value: "القيم"
fn: "دوال" fn: "دوال"
text: "إجراءات على النصوص"
convert: "تحويل" convert: "تحويل"
list: "القوائم" list: "القوائم"
blocks: blocks:
@ -1501,6 +1511,10 @@ _notification:
followRequestAccepted: "طلبات المتابعة المقبولة" followRequestAccepted: "طلبات المتابعة المقبولة"
groupInvited: "دعوات الفريق" groupInvited: "دعوات الفريق"
app: "إشعارات التطبيقات المرتبطة" app: "إشعارات التطبيقات المرتبطة"
_actions:
followBack: "تابعك بالمثل"
reply: "رد"
renote: "أعد النشر"
_deck: _deck:
alwaysShowMainColumn: "أظهر العمود الرئيسي دائمًا" alwaysShowMainColumn: "أظهر العمود الرئيسي دائمًا"
columnAlign: "حاذِ الأعمدة" columnAlign: "حاذِ الأعمدة"

View file

@ -1621,6 +1621,9 @@ _notification:
followRequestAccepted: "গৃহীত অনুসরণের অনুরোধসমূহ" followRequestAccepted: "গৃহীত অনুসরণের অনুরোধসমূহ"
groupInvited: "গ্রুপের আমন্ত্রনসমূহ" groupInvited: "গ্রুপের আমন্ত্রনসমূহ"
app: "লিঙ্ক করা অ্যাপ থেকে বিজ্ঞপ্তি" app: "লিঙ্ক করা অ্যাপ থেকে বিজ্ঞপ্তি"
_actions:
reply: "জবাব"
renote: "রিনোট"
_deck: _deck:
alwaysShowMainColumn: "সর্বদা মেইন কলাম দেখান" alwaysShowMainColumn: "সর্বদা মেইন কলাম দেখান"
columnAlign: "কলাম সাজান" columnAlign: "কলাম সাজান"

View file

@ -1,6 +1,8 @@
--- ---
_lang_: "Català" _lang_: "Català"
headlineMisskey: "Una xarxa connectada per notes" headlineMisskey: "Una xarxa connectada per notes"
introMisskey: "Benvingut! Misskey és un servei de microblogging descentralitzat de codi obert.\nCrea \"notes\" per compartir els teus pensaments amb tots els que t'envolten. 📡\nAmb \"reaccions\", també pots expressar ràpidament els teus sentiments sobre les notes de tothom. 👍\nExplorem un món nou! 🚀"
monthAndDay: "{day}/{month}"
search: "Cercar" search: "Cercar"
notifications: "Notificacions" notifications: "Notificacions"
username: "Nom d'usuari" username: "Nom d'usuari"
@ -10,17 +12,173 @@ fetchingAsApObject: "Cercant en el Fediverse..."
ok: "OK" ok: "OK"
gotIt: "Ho he entès!" gotIt: "Ho he entès!"
cancel: "Cancel·lar" cancel: "Cancel·lar"
enterUsername: "Introdueix el teu nom d'usuari"
renotedBy: "Resignat per {usuari}"
noNotes: "Cap nota"
noNotifications: "Cap notificació"
instance: "Instàncies"
settings: "Preferències"
basicSettings: "Configuració bàsica"
otherSettings: "Configuració avançada"
openInWindow: "Obrir en una nova finestra"
profile: "Perfil"
timeline: "Línia de temps"
noAccountDescription: "Aquest usuari encara no ha escrit la seva biografia."
login: "Iniciar sessió"
loggingIn: "Identificant-se"
logout: "Tancar la sessió"
signup: "Registrar-se"
uploading: "Pujant..."
save: "Desar"
users: "Usuaris"
addUser: "Afegir un usuari"
favorite: "Afegir a preferits"
favorites: "Favorits"
unfavorite: "Eliminar dels preferits"
favorited: "Afegit als preferits."
alreadyFavorited: "Ja s'ha afegit als preferits."
cantFavorite: "No s'ha pogut afegir als preferits."
pin: "Fixar al perfil"
unpin: "Para de fixar del perfil"
copyContent: "Copiar el contingut"
copyLink: "Copiar l'enllaç"
delete: "Eliminar"
deleteAndEdit: "Esborrar i editar"
deleteAndEditConfirm: "Estàs segur que vols suprimir aquesta nota i editar-la? Perdràs totes les reaccions, notes i respostes."
addToList: "Afegir a una llista"
sendMessage: "Enviar un missatge"
copyUsername: "Copiar nom d'usuari"
searchUser: "Cercar usuaris"
reply: "Respondre"
loadMore: "Carregar més"
showMore: "Veure més"
youGotNewFollower: "t'ha seguit"
receiveFollowRequest: "Sol·licitud de seguiment rebuda"
followRequestAccepted: "Sol·licitud de seguiment acceptada"
mention: "Menció"
mentions: "Mencions"
directNotes: "Notes directes"
importAndExport: "Importar / Exportar"
import: "Importar"
export: "Exportar"
files: "Fitxers"
download: "Baixar"
driveFileDeleteConfirm: "Estàs segur que vols suprimir el fitxer \"{name}\"? Les notes associades a aquest fitxer adjunt també se suprimiran."
unfollowConfirm: "Estàs segur que vols deixar de seguir {name}?"
exportRequested: "Has sol·licitat una exportació. Això pot trigar una estona. S'afegirà a la teva unitat un cop completat."
importRequested: "Has sol·licitat una importació. Això pot trigar una estona."
lists: "Llistes"
noLists: "No tens cap llista"
note: "Nota"
notes: "Notes"
following: "Seguint"
followers: "Seguidors"
followsYou: "Et segueix"
createList: "Crear llista"
manageLists: "Gestionar les llistes"
error: "Error"
somethingHappened: "S'ha produït un error"
retry: "Torna-ho a intentar"
pageLoadError: "S'ha produït un error en carregar la pàgina"
pageLoadErrorDescription: "Això normalment es deu a errors de xarxa o a la memòria cau del navegador. Prova d'esborrar la memòria cau i torna-ho a provar després d'esperar una estona."
serverIsDead: "Aquest servidor no respon. Espera una estona i torna-ho a provar."
youShouldUpgradeClient: "Per veure aquesta pàgina, actualitzeu-la per actualitzar el vostre client."
enterListName: "Introdueix un nom per a la llista"
privacy: "Privadesa"
makeFollowManuallyApprove: "Les sol·licituds de seguiment requereixen aprovació"
defaultNoteVisibility: "Visibilitat per defecte"
follow: "Seguint"
followRequest: "Enviar la sol·licitud de seguiment"
followRequests: "Sol·licituds de seguiment"
unfollow: "Deixar de seguir"
followRequestPending: "Sol·licituds de seguiment pendents"
enterEmoji: "Introduir un emoji"
renote: "Renotar"
unrenote: "Anul·lar renota"
renoted: "Renotat."
cantRenote: "Aquesta publicació no pot ser renotada."
cantReRenote: "Impossible renotar una renota."
quote: "Citar"
pinnedNote: "Nota fixada"
pinned: "Fixar al perfil"
you: "Tu"
clickToShow: "Fes clic per mostrar"
sensitive: "NSFW"
add: "Afegir"
reaction: "Reaccions"
reactionSetting: "Reaccions a mostrar al selector de reaccions"
reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, prem \"+\" per afegir."
rememberNoteVisibility: "Recorda la configuració de visibilitat de les notes"
attachCancel: "Eliminar el fitxer adjunt"
markAsSensitive: "Marcar com a NSFW"
instances: "Instàncies"
remove: "Eliminar"
nsfw: "NSFW"
pinnedNotes: "Nota fixada"
userList: "Llistes"
smtpUser: "Nom d'usuari" smtpUser: "Nom d'usuari"
smtpPass: "Contrasenya" smtpPass: "Contrasenya"
user: "Usuaris"
searchByGoogle: "Cercar" searchByGoogle: "Cercar"
_email:
_follow:
title: "t'ha seguit"
_mfm: _mfm:
mention: "Menció"
quote: "Citar"
search: "Cercar" search: "Cercar"
_theme:
keys:
mention: "Menció"
renote: "Renotar"
_sfx: _sfx:
note: "Notes"
notification: "Notificacions" notification: "Notificacions"
_widgets: _widgets:
notifications: "Notificacions" notifications: "Notificacions"
timeline: "Línia de temps"
_cw:
show: "Carregar més"
_visibility:
followers: "Seguidors"
_profile: _profile:
username: "Nom d'usuari" username: "Nom d'usuari"
_exportOrImport:
followingList: "Seguint"
userLists: "Llistes"
_pages:
script:
categories:
list: "Llistes"
blocks:
_join:
arg1: "Llistes"
_randomPick:
arg1: "Llistes"
_dailyRandomPick:
arg1: "Llistes"
_seedRandomPick:
arg2: "Llistes"
_pick:
arg1: "Llistes"
_listLen:
arg1: "Llistes"
types:
array: "Llistes"
_notification:
youWereFollowed: "t'ha seguit"
_types:
follow: "Seguint"
mention: "Menció"
renote: "Renotar"
quote: "Citar"
reaction: "Reaccions"
_actions:
reply: "Respondre"
renote: "Renotar"
_deck: _deck:
_columns: _columns:
notifications: "Notificacions" notifications: "Notificacions"
tl: "Línia de temps"
list: "Llistes"
mentions: "Mencions"

View file

@ -53,6 +53,8 @@ reply: "Odpovědět"
loadMore: "Zobrazit více" loadMore: "Zobrazit více"
showMore: "Zobrazit více" showMore: "Zobrazit více"
youGotNewFollower: "Máte nového následovníka" youGotNewFollower: "Máte nového následovníka"
receiveFollowRequest: "Žádost o sledování přijata"
followRequestAccepted: "Žádost o sledování přijata"
mention: "Zmínění" mention: "Zmínění"
mentions: "Zmínění" mentions: "Zmínění"
importAndExport: "Import a export" importAndExport: "Import a export"
@ -60,7 +62,9 @@ import: "Importovat"
export: "Exportovat" export: "Exportovat"
files: "Soubor(ů)" files: "Soubor(ů)"
download: "Stáhnout" download: "Stáhnout"
driveFileDeleteConfirm: "Opravdu chcete smazat soubor \"{name}\"? Poznámky, ke kterým je tento soubor připojen, budou také smazány."
unfollowConfirm: "Jste si jisti že už nechcete sledovat {name}?" unfollowConfirm: "Jste si jisti že už nechcete sledovat {name}?"
exportRequested: "Požádali jste o export. To může chvíli trvat. Přidáme ho na váš Disk až bude dokončen."
importRequested: "Požádali jste o export. To může chvilku trvat." importRequested: "Požádali jste o export. To může chvilku trvat."
lists: "Seznamy" lists: "Seznamy"
noLists: "Nemáte žádné seznamy" noLists: "Nemáte žádné seznamy"
@ -75,13 +79,25 @@ error: "Chyba"
somethingHappened: "Jejda. Něco se nepovedlo." somethingHappened: "Jejda. Něco se nepovedlo."
retry: "Opakovat" retry: "Opakovat"
pageLoadError: "Nepodařilo se načíst stránku" pageLoadError: "Nepodařilo se načíst stránku"
serverIsDead: "Server neodpovídá. Počkejte chvíli a zkuste to znovu."
youShouldUpgradeClient: "Pro zobrazení této stránky obnovte stránku pro aktualizaci klienta."
enterListName: "Jméno seznamu" enterListName: "Jméno seznamu"
privacy: "Soukromí" privacy: "Soukromí"
makeFollowManuallyApprove: "Žádosti o sledování vyžadují potvrzení"
defaultNoteVisibility: "Výchozí viditelnost"
follow: "Sledovaní" follow: "Sledovaní"
followRequest: "Odeslat žádost o sledování"
followRequests: "Žádosti o sledování"
unfollow: "Přestat sledovat" unfollow: "Přestat sledovat"
followRequestPending: "Čekající žádosti o sledování"
enterEmoji: "Vložte emoji"
renote: "Přeposlat" renote: "Přeposlat"
unrenote: "Zrušit přeposlání"
renoted: "Přeposláno"
cantRenote: "Tento příspěvek nelze přeposlat."
cantReRenote: "Odpověď nemůže být odstraněna." cantReRenote: "Odpověď nemůže být odstraněna."
quote: "Citovat" quote: "Citovat"
pinnedNote: "Připnutá poznámka"
pinned: "Připnout" pinned: "Připnout"
you: "Vy" you: "Vy"
clickToShow: "Klikněte pro zobrazení" clickToShow: "Klikněte pro zobrazení"
@ -122,6 +138,8 @@ flagAsBot: "Tento účet je bot"
flagAsBotDescription: "Pokud je tento účet kontrolován programem zaškrtněte tuto možnost. To označí tento účet jako bot pro ostatní vývojáře a zabrání tak nekonečným interakcím s ostatními boty a upraví Misskey systém aby se choval k tomuhle účtu jako bot." flagAsBotDescription: "Pokud je tento účet kontrolován programem zaškrtněte tuto možnost. To označí tento účet jako bot pro ostatní vývojáře a zabrání tak nekonečným interakcím s ostatními boty a upraví Misskey systém aby se choval k tomuhle účtu jako bot."
flagAsCat: "Tenhle účet je kočka" flagAsCat: "Tenhle účet je kočka"
flagAsCatDescription: "Vyberte tuto možnost aby tento účet byl označen jako kočka." flagAsCatDescription: "Vyberte tuto možnost aby tento účet byl označen jako kočka."
flagShowTimelineReplies: "Zobrazovat odpovědi na časové ose"
flagShowTimelineRepliesDescription: "Je-li zapnuto, zobrazí odpovědi uživatelů na poznámky jiných uživatelů na vaší časové ose."
autoAcceptFollowed: "Automaticky akceptovat následování od účtů které sledujete" autoAcceptFollowed: "Automaticky akceptovat následování od účtů které sledujete"
addAccount: "Přidat účet" addAccount: "Přidat účet"
loginFailed: "Přihlášení se nezdařilo." loginFailed: "Přihlášení se nezdařilo."
@ -130,13 +148,16 @@ general: "Obecně"
wallpaper: "Obrázek na pozadí" wallpaper: "Obrázek na pozadí"
setWallpaper: "Nastavení obrázku na pozadí" setWallpaper: "Nastavení obrázku na pozadí"
removeWallpaper: "Odstranit pozadí" removeWallpaper: "Odstranit pozadí"
searchWith: "Hledat: {q}"
youHaveNoLists: "Nemáte žádné seznamy" youHaveNoLists: "Nemáte žádné seznamy"
followConfirm: "Jste si jisti, že chcete sledovat {name}?"
proxyAccount: "Proxy účet" proxyAccount: "Proxy účet"
proxyAccountDescription: "Proxy účet je účet, který za určitých podmínek sleduje uživatele na dálku vaším jménem. Například když uživatel zařadí vzdáleného uživatele do seznamu, pokud nikdo nesleduje uživatele na seznamu, aktivita nebude doručena instanci, takže místo toho bude uživatele sledovat účet proxy." proxyAccountDescription: "Proxy účet je účet, který za určitých podmínek sleduje uživatele na dálku vaším jménem. Například když uživatel zařadí vzdáleného uživatele do seznamu, pokud nikdo nesleduje uživatele na seznamu, aktivita nebude doručena instanci, takže místo toho bude uživatele sledovat účet proxy."
host: "Hostitel" host: "Hostitel"
selectUser: "Vyberte uživatele" selectUser: "Vyberte uživatele"
recipient: "Pro" recipient: "Pro"
annotation: "Komentáře" annotation: "Komentáře"
federation: "Federace"
instances: "Instance" instances: "Instance"
registeredAt: "Registrován" registeredAt: "Registrován"
latestRequestSentAt: "Poslední požadavek poslán" latestRequestSentAt: "Poslední požadavek poslán"
@ -146,6 +167,7 @@ storageUsage: "Využití úložiště"
charts: "Grafy" charts: "Grafy"
perHour: "za hodinu" perHour: "za hodinu"
perDay: "za den" perDay: "za den"
stopActivityDelivery: "Přestat zasílat aktivitu"
blockThisInstance: "Blokovat tuto instanci" blockThisInstance: "Blokovat tuto instanci"
operations: "Operace" operations: "Operace"
software: "Software" software: "Software"
@ -283,6 +305,8 @@ iconUrl: "Favicon URL"
bannerUrl: "Baner URL" bannerUrl: "Baner URL"
backgroundImageUrl: "Adresa URL obrázku pozadí" backgroundImageUrl: "Adresa URL obrázku pozadí"
basicInfo: "Základní informace" basicInfo: "Základní informace"
pinnedUsers: "Připnutí uživatelé"
pinnedNotes: "Připnutá poznámka"
hcaptcha: "hCaptcha" hcaptcha: "hCaptcha"
enableHcaptcha: "Aktivovat hCaptchu" enableHcaptcha: "Aktivovat hCaptchu"
hcaptchaSecretKey: "Tajný Klíč (Secret Key)" hcaptchaSecretKey: "Tajný Klíč (Secret Key)"
@ -471,6 +495,7 @@ _widgets:
notifications: "Oznámení" notifications: "Oznámení"
timeline: "Časová osa" timeline: "Časová osa"
activity: "Aktivita" activity: "Aktivita"
federation: "Federace"
jobQueue: "Fronta úloh" jobQueue: "Fronta úloh"
_cw: _cw:
show: "Zobrazit více" show: "Zobrazit více"
@ -485,6 +510,8 @@ _exportOrImport:
muteList: "Ztlumit" muteList: "Ztlumit"
blockingList: "Zablokovat" blockingList: "Zablokovat"
userLists: "Seznamy" userLists: "Seznamy"
_charts:
federation: "Federace"
_timelines: _timelines:
home: "Domů" home: "Domů"
_pages: _pages:
@ -517,6 +544,9 @@ _notification:
renote: "Přeposlat" renote: "Přeposlat"
quote: "Citovat" quote: "Citovat"
reaction: "Reakce" reaction: "Reakce"
_actions:
reply: "Odpovědět"
renote: "Přeposlat"
_deck: _deck:
_columns: _columns:
notifications: "Oznámení" notifications: "Oznámení"

View file

@ -1006,7 +1006,7 @@ _instanceMute:
heading: "Liste der stummzuschaltenden Instanzen" heading: "Liste der stummzuschaltenden Instanzen"
_theme: _theme:
explore: "Farbschemata erforschen" explore: "Farbschemata erforschen"
install: "Farbschmata installieren" install: "Farbschemata installieren"
manage: "Farbschemaverwaltung" manage: "Farbschemaverwaltung"
code: "Farbschemencode" code: "Farbschemencode"
description: "Beschreibung" description: "Beschreibung"
@ -1613,8 +1613,9 @@ _notification:
youWereFollowed: "ist dir gefolgt" youWereFollowed: "ist dir gefolgt"
youReceivedFollowRequest: "Du hast eine Follow-Anfrage erhalten" youReceivedFollowRequest: "Du hast eine Follow-Anfrage erhalten"
yourFollowRequestAccepted: "Deine Follow-Anfrage wurde akzeptiert" yourFollowRequestAccepted: "Deine Follow-Anfrage wurde akzeptiert"
youWereInvitedToGroup: "Du wurdest in eine Gruppe eingeladen" youWereInvitedToGroup: "{userName} hat dich in eine Gruppe eingeladen"
pollEnded: "Umfrageergebnisse sind verfügbar" pollEnded: "Umfrageergebnisse sind verfügbar"
emptyPushNotificationMessage: "Push-Benachrichtigungen wurden aktualisiert"
_types: _types:
all: "Alle" all: "Alle"
follow: "Neue Follower" follow: "Neue Follower"
@ -1629,6 +1630,10 @@ _notification:
followRequestAccepted: "Akzeptierte Follow-Anfragen" followRequestAccepted: "Akzeptierte Follow-Anfragen"
groupInvited: "Erhaltene Gruppeneinladungen" groupInvited: "Erhaltene Gruppeneinladungen"
app: "Benachrichtigungen von Apps" app: "Benachrichtigungen von Apps"
_actions:
followBack: "folgt dir nun auch"
reply: "Antworten"
renote: "Renote"
_deck: _deck:
alwaysShowMainColumn: "Hauptspalte immer zeigen" alwaysShowMainColumn: "Hauptspalte immer zeigen"
columnAlign: "Spaltenausrichtung" columnAlign: "Spaltenausrichtung"

View file

@ -1613,8 +1613,9 @@ _notification:
youWereFollowed: "followed you" youWereFollowed: "followed you"
youReceivedFollowRequest: "You've received a follow request" youReceivedFollowRequest: "You've received a follow request"
yourFollowRequestAccepted: "Your follow request was accepted" yourFollowRequestAccepted: "Your follow request was accepted"
youWereInvitedToGroup: "You've been invited to a group" youWereInvitedToGroup: "{userName} invited you to a group"
pollEnded: "Poll results have become available" pollEnded: "Poll results have become available"
emptyPushNotificationMessage: "Push notifications have been updated"
_types: _types:
all: "All" all: "All"
follow: "New followers" follow: "New followers"
@ -1629,6 +1630,10 @@ _notification:
followRequestAccepted: "Accepted follow requests" followRequestAccepted: "Accepted follow requests"
groupInvited: "Group invitations" groupInvited: "Group invitations"
app: "Notifications from linked apps" app: "Notifications from linked apps"
_actions:
followBack: "followed you back"
reply: "Reply"
renote: "Renote"
_deck: _deck:
alwaysShowMainColumn: "Always show main column" alwaysShowMainColumn: "Always show main column"
columnAlign: "Align columns" columnAlign: "Align columns"

View file

@ -141,6 +141,8 @@ flagAsBot: "Esta cuenta es un bot"
flagAsBotDescription: "En caso de que esta cuenta fuera usada por un programa, active esta opción. Al hacerlo, esta opción servirá para otros desarrolladores para evitar cadenas infinitas de reacciones, y ajustará los sistemas internos de Misskey para que trate a esta cuenta como un bot." flagAsBotDescription: "En caso de que esta cuenta fuera usada por un programa, active esta opción. Al hacerlo, esta opción servirá para otros desarrolladores para evitar cadenas infinitas de reacciones, y ajustará los sistemas internos de Misskey para que trate a esta cuenta como un bot."
flagAsCat: "Esta cuenta es un gato" flagAsCat: "Esta cuenta es un gato"
flagAsCatDescription: "En caso de que declare que esta cuenta es de un gato, active esta opción." flagAsCatDescription: "En caso de que declare que esta cuenta es de un gato, active esta opción."
flagShowTimelineReplies: "Mostrar respuestas a las notas en la biografía"
flagShowTimelineRepliesDescription: "Cuando se marca, la línea de tiempo muestra respuestas a otras notas además de las notas del usuario"
autoAcceptFollowed: "Aceptar automáticamente las solicitudes de seguimiento de los usuarios que sigues" autoAcceptFollowed: "Aceptar automáticamente las solicitudes de seguimiento de los usuarios que sigues"
addAccount: "Agregar Cuenta" addAccount: "Agregar Cuenta"
loginFailed: "Error al iniciar sesión." loginFailed: "Error al iniciar sesión."
@ -235,6 +237,8 @@ resetAreYouSure: "¿Desea reestablecer?"
saved: "Guardado" saved: "Guardado"
messaging: "Chat" messaging: "Chat"
upload: "Subir" upload: "Subir"
keepOriginalUploading: "Mantener la imagen original"
keepOriginalUploadingDescription: "Mantener la versión original al cargar imágenes. Si está desactivado, el navegador generará imágenes para la publicación web en el momento de recargar la página"
fromDrive: "Desde el drive" fromDrive: "Desde el drive"
fromUrl: "Desde la URL" fromUrl: "Desde la URL"
uploadFromUrl: "Subir desde una URL" uploadFromUrl: "Subir desde una URL"
@ -444,6 +448,7 @@ uiLanguage: "Idioma de visualización de la interfaz"
groupInvited: "Invitado al grupo" groupInvited: "Invitado al grupo"
aboutX: "Acerca de {x}" aboutX: "Acerca de {x}"
useOsNativeEmojis: "Usa los emojis nativos de la plataforma" useOsNativeEmojis: "Usa los emojis nativos de la plataforma"
disableDrawer: "No mostrar los menús en cajones"
youHaveNoGroups: "Sin grupos" youHaveNoGroups: "Sin grupos"
joinOrCreateGroup: "Obtenga una invitación para unirse al grupos o puede crear su propio grupo." joinOrCreateGroup: "Obtenga una invitación para unirse al grupos o puede crear su propio grupo."
noHistory: "No hay datos en el historial" noHistory: "No hay datos en el historial"
@ -615,6 +620,10 @@ reportAbuse: "Reportar"
reportAbuseOf: "Reportar a {name}" reportAbuseOf: "Reportar a {name}"
fillAbuseReportDescription: "Ingrese los detalles del reporte. Si hay una nota en particular, ingrese la URL de esta." fillAbuseReportDescription: "Ingrese los detalles del reporte. Si hay una nota en particular, ingrese la URL de esta."
abuseReported: "Se ha enviado el reporte. Muchas gracias." abuseReported: "Se ha enviado el reporte. Muchas gracias."
reporteeOrigin: "Informar a"
reporterOrigin: "Origen del informe"
forwardReport: "Transferir un informe a una instancia remota"
forwardReportIsAnonymous: "No puede ver su información de la instancia remota y aparecerá como una cuenta anónima del sistema"
send: "Enviar" send: "Enviar"
abuseMarkAsResolved: "Marcar reporte como resuelto" abuseMarkAsResolved: "Marcar reporte como resuelto"
openInNewTab: "Abrir en una Nueva Pestaña" openInNewTab: "Abrir en una Nueva Pestaña"
@ -676,6 +685,7 @@ center: "Centrar"
wide: "Ancho" wide: "Ancho"
narrow: "Estrecho" narrow: "Estrecho"
reloadToApplySetting: "Esta configuración sólo se aplicará después de recargar la página. ¿Recargar ahora?" reloadToApplySetting: "Esta configuración sólo se aplicará después de recargar la página. ¿Recargar ahora?"
needReloadToApply: "Se requiere un reinicio para la aplicar los cambios"
showTitlebar: "Mostrar la barra de título" showTitlebar: "Mostrar la barra de título"
clearCache: "Limpiar caché" clearCache: "Limpiar caché"
onlineUsersCount: "{n} usuarios en línea" onlineUsersCount: "{n} usuarios en línea"
@ -706,19 +716,55 @@ capacity: "Capacidad"
inUse: "Usado" inUse: "Usado"
editCode: "Editar código" editCode: "Editar código"
apply: "Aplicar" apply: "Aplicar"
receiveAnnouncementFromInstance: "Recibir notificaciones de la instancia"
emailNotification: "Notificaciones por correo electrónico"
publish: "Publicar" publish: "Publicar"
inChannelSearch: "Buscar en el canal" inChannelSearch: "Buscar en el canal"
useReactionPickerForContextMenu: "Haga clic con el botón derecho para abrir el menu de reacciones"
typingUsers: "{users} está escribiendo"
jumpToSpecifiedDate: "Saltar a una fecha específica"
showingPastTimeline: "Mostrar líneas de tiempo antiguas"
clear: "Limpiar"
markAllAsRead: "Marcar todo como leído" markAllAsRead: "Marcar todo como leído"
goBack: "Deseleccionar" goBack: "Deseleccionar"
fullView: "Vista completa"
quitFullView: "quitar vista completa"
addDescription: "Agregar descripción"
userPagePinTip: "Puede mantener sus notas visibles aquí seleccionando Pin en el menú de notas individuales"
notSpecifiedMentionWarning: "Algunas menciones no están incluidas en el destino"
info: "Información" info: "Información"
userInfo: "Información del usuario"
unknown: "Desconocido"
onlineStatus: "En línea"
hideOnlineStatus: "mostrarse como desconectado"
hideOnlineStatusDescription: "Ocultar su estado en línea puede reducir la eficacia de algunas funciones, como la búsqueda"
online: "En línea" online: "En línea"
active: "Activo"
offline: "Sin conexión" offline: "Sin conexión"
notRecommended: "obsoleto"
botProtection: "Protección contra bots"
instanceBlocking: "Instancias bloqueadas"
selectAccount: "Elija una cuenta"
switchAccount: "Cambiar de cuenta"
enabled: "Activado"
disabled: "Desactivado"
quickAction: "Acciones rápidas"
user: "Usuarios" user: "Usuarios"
administration: "Administrar" administration: "Administrar"
accounts: "Cuentas"
switch: "Cambiar"
noMaintainerInformationWarning: "No se ha establecido la información del administrador"
noBotProtectionWarning: "La protección contra los bots no está configurada"
configure: "Configurar"
postToGallery: "Crear una nueva publicación en la galería"
gallery: "Galería" gallery: "Galería"
recentPosts: "Posts recientes" recentPosts: "Posts recientes"
popularPosts: "Más vistos" popularPosts: "Más vistos"
shareWithNote: "Compartir con una nota"
ads: "Anuncios"
expiration: "Termina el" expiration: "Termina el"
memo: "Notas"
priority: "Prioridad"
high: "Alta" high: "Alta"
middle: "Mediano" middle: "Mediano"
low: "Baja" low: "Baja"
@ -770,22 +816,50 @@ _accountDelete:
accountDelete: "Eliminar Cuenta" accountDelete: "Eliminar Cuenta"
_ad: _ad:
back: "Deseleccionar" back: "Deseleccionar"
_forgotPassword:
contactAdmin: "Esta instancia no admite el uso de direcciones de correo electrónico, póngase en contacto con el administrador de la instancia para restablecer su contraseña"
_gallery: _gallery:
my: "Mi galería" my: "Mi galería"
liked: "Publicaciones que me gustan"
like: "¡Muy bien!"
unlike: "Quitar me gusta" unlike: "Quitar me gusta"
_email: _email:
_follow: _follow:
title: "te ha seguido" title: "te ha seguido"
_receiveFollowRequest:
title: "Has recibido una solicitud de seguimiento"
_plugin:
install: "Instalar plugins"
installWarn: "Por favor no instale plugins que no son de confianza"
manage: "Gestionar plugins"
_registry: _registry:
scope: "Alcance"
key: "Clave" key: "Clave"
keys: "Clave" keys: "Clave"
domain: "Dominio"
createKey: "Crear una llave"
_aboutMisskey:
about: "Misskey es un software de código abierto, desarrollado por syuilo desde el 2014"
contributors: "Principales colaboradores"
allContributors: "Todos los colaboradores"
source: "Código fuente"
translation: "Traducir Misskey"
donate: "Donar a Misskey"
morePatrons: "Muchas más personas nos apoyan. Muchas gracias🥰"
patrons: "Patrocinadores"
_nsfw:
respect: "Ocultar medios NSFW"
ignore: "No esconder medios NSFW "
force: "Ocultar todos los medios"
_mfm: _mfm:
cheatSheet: "Hoja de referencia de MFM" cheatSheet: "Hoja de referencia de MFM"
intro: "MFM es un lenguaje de marcado dedicado que se puede usar en varios lugares dentro de Misskey. Aquí puede ver una lista de sintaxis disponibles en MFM." intro: "MFM es un lenguaje de marcado dedicado que se puede usar en varios lugares dentro de Misskey. Aquí puede ver una lista de sintaxis disponibles en MFM."
dummy: "Misskey expande el mundo de la Fediverso"
mention: "Menciones" mention: "Menciones"
mentionDescription: "El signo @ seguido de un nombre de usuario se puede utilizar para notificar a un usuario en particular." mentionDescription: "El signo @ seguido de un nombre de usuario se puede utilizar para notificar a un usuario en particular."
hashtag: "Hashtag" hashtag: "Hashtag"
url: "URL" url: "URL"
urlDescription: "Se pueden mostrar las URL"
link: "Vínculo" link: "Vínculo"
bold: "Negrita" bold: "Negrita"
center: "Centrar" center: "Centrar"
@ -1432,6 +1506,9 @@ _notification:
followRequestAccepted: "El seguimiento fue aceptado" followRequestAccepted: "El seguimiento fue aceptado"
groupInvited: "Invitado al grupo" groupInvited: "Invitado al grupo"
app: "Notificaciones desde aplicaciones" app: "Notificaciones desde aplicaciones"
_actions:
reply: "Responder"
renote: "Renotar"
_deck: _deck:
alwaysShowMainColumn: "Siempre mostrar la columna principal" alwaysShowMainColumn: "Siempre mostrar la columna principal"
columnAlign: "Alinear columnas" columnAlign: "Alinear columnas"

View file

@ -1615,6 +1615,9 @@ _notification:
followRequestAccepted: "Demande d'abonnement acceptée" followRequestAccepted: "Demande d'abonnement acceptée"
groupInvited: "Invitation à un groupe" groupInvited: "Invitation à un groupe"
app: "Notifications provenant des apps" app: "Notifications provenant des apps"
_actions:
reply: "Répondre"
renote: "Renoter"
_deck: _deck:
alwaysShowMainColumn: "Toujours afficher la colonne principale" alwaysShowMainColumn: "Toujours afficher la colonne principale"
columnAlign: "Aligner les colonnes" columnAlign: "Aligner les colonnes"

View file

@ -593,6 +593,7 @@ smtpSecureInfo: "Matikan ini ketika menggunakan STARTTLS"
testEmail: "Tes pengiriman surel" testEmail: "Tes pengiriman surel"
wordMute: "Bisukan kata" wordMute: "Bisukan kata"
regexpError: "Kesalahan ekspresi reguler" regexpError: "Kesalahan ekspresi reguler"
regexpErrorDescription: "Galat terjadi pada baris {line} ekspresi reguler dari {tab} kata yang dibisukan:"
instanceMute: "Bisuka instansi" instanceMute: "Bisuka instansi"
userSaysSomething: "{name} mengatakan sesuatu" userSaysSomething: "{name} mengatakan sesuatu"
makeActive: "Aktifkan" makeActive: "Aktifkan"
@ -839,6 +840,7 @@ tenMinutes: "10 Menit"
oneHour: "1 Jam" oneHour: "1 Jam"
oneDay: "1 Hari" oneDay: "1 Hari"
oneWeek: "1 Bulan" oneWeek: "1 Bulan"
reflectMayTakeTime: "Mungkin perlu beberapa saat untuk dicerminkan."
failedToFetchAccountInformation: "Gagal untuk mendapatkan informasi akun" failedToFetchAccountInformation: "Gagal untuk mendapatkan informasi akun"
_emailUnavailable: _emailUnavailable:
used: "Alamat surel ini telah digunakan" used: "Alamat surel ini telah digunakan"
@ -1613,6 +1615,7 @@ _notification:
yourFollowRequestAccepted: "Permintaan mengikuti kamu telah diterima" yourFollowRequestAccepted: "Permintaan mengikuti kamu telah diterima"
youWereInvitedToGroup: "Telah diundang ke grup" youWereInvitedToGroup: "Telah diundang ke grup"
pollEnded: "Hasil Kuesioner telah keluar" pollEnded: "Hasil Kuesioner telah keluar"
emptyPushNotificationMessage: "Pembaruan notifikasi dorong"
_types: _types:
all: "Semua" all: "Semua"
follow: "Ikuti" follow: "Ikuti"
@ -1627,6 +1630,10 @@ _notification:
followRequestAccepted: "Permintaan mengikuti disetujui" followRequestAccepted: "Permintaan mengikuti disetujui"
groupInvited: "Diundang ke grup" groupInvited: "Diundang ke grup"
app: "Pemberitahuan dari aplikasi" app: "Pemberitahuan dari aplikasi"
_actions:
followBack: "Ikuti Kembali"
reply: "Balas"
renote: "Renote"
_deck: _deck:
alwaysShowMainColumn: "Selalu tampilkan kolom utama" alwaysShowMainColumn: "Selalu tampilkan kolom utama"
columnAlign: "Luruskan kolom" columnAlign: "Luruskan kolom"

View file

@ -1433,6 +1433,9 @@ _notification:
followRequestAccepted: "Richiesta di follow accettata" followRequestAccepted: "Richiesta di follow accettata"
groupInvited: "Invito a un gruppo" groupInvited: "Invito a un gruppo"
app: "Notifiche da applicazioni" app: "Notifiche da applicazioni"
_actions:
reply: "Rispondi"
renote: "Rinota"
_deck: _deck:
alwaysShowMainColumn: "Mostra sempre la colonna principale" alwaysShowMainColumn: "Mostra sempre la colonna principale"
columnAlign: "Allineare colonne" columnAlign: "Allineare colonne"

View file

@ -425,7 +425,7 @@ quoteQuestion: "引用として添付しますか?"
noMessagesYet: "まだチャットはありません" noMessagesYet: "まだチャットはありません"
newMessageExists: "新しいメッセージがあります" newMessageExists: "新しいメッセージがあります"
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです" onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
signinRequired: "ログインしてください" signinRequired: "続行する前に、サインアップまたはサインインが必要です"
invitations: "招待" invitations: "招待"
invitationCode: "招待コード" invitationCode: "招待コード"
checking: "確認しています" checking: "確認しています"
@ -842,6 +842,7 @@ oneDay: "1日"
oneWeek: "1週間" oneWeek: "1週間"
reflectMayTakeTime: "反映されるまで時間がかかる場合があります。" reflectMayTakeTime: "反映されるまで時間がかかる場合があります。"
failedToFetchAccountInformation: "アカウント情報の取得に失敗しました" failedToFetchAccountInformation: "アカウント情報の取得に失敗しました"
rateLimitExceeded: "レート制限を超えました"
_emailUnavailable: _emailUnavailable:
used: "既に使用されています" used: "既に使用されています"
@ -1110,7 +1111,6 @@ _sfx:
channel: "チャンネル通知" channel: "チャンネル通知"
_ago: _ago:
unknown: "謎"
future: "未来" future: "未来"
justNow: "たった今" justNow: "たった今"
secondsAgo: "{n}秒前" secondsAgo: "{n}秒前"
@ -1157,6 +1157,7 @@ _2fa:
registerKey: "キーを登録" registerKey: "キーを登録"
step1: "まず、{a}や{b}などの認証アプリをお使いのデバイスにインストールします。" step1: "まず、{a}や{b}などの認証アプリをお使いのデバイスにインストールします。"
step2: "次に、表示されているQRコードをアプリでスキャンします。" step2: "次に、表示されているQRコードをアプリでスキャンします。"
step2Url: "デスクトップアプリでは次のURLを入力します:"
step3: "アプリに表示されているトークンを入力して完了です。" step3: "アプリに表示されているトークンを入力して完了です。"
step4: "これからログインするときも、同じようにトークンを入力します。" step4: "これからログインするときも、同じようにトークンを入力します。"
securityKeyInfo: "FIDO2をサポートするハードウェアセキュリティキーもしくは端末の指紋認証やPINを使用してログインするように設定できます。" securityKeyInfo: "FIDO2をサポートするハードウェアセキュリティキーもしくは端末の指紋認証やPINを使用してログインするように設定できます。"

View file

@ -1202,6 +1202,9 @@ _notification:
reaction: "リアクション" reaction: "リアクション"
receiveFollowRequest: "フォロー許可してほしいみたいやで" receiveFollowRequest: "フォロー許可してほしいみたいやで"
followRequestAccepted: "フォローが受理されたで" followRequestAccepted: "フォローが受理されたで"
_actions:
reply: "返事"
renote: "Renote"
_deck: _deck:
alwaysShowMainColumn: "いつもメインカラムを表示" alwaysShowMainColumn: "いつもメインカラムを表示"
columnAlign: "カラムの寄せ" columnAlign: "カラムの寄せ"

View file

@ -116,6 +116,8 @@ _notification:
_types: _types:
follow: "Ig ṭṭafaṛ" follow: "Ig ṭṭafaṛ"
mention: "Bder" mention: "Bder"
_actions:
reply: "Err"
_deck: _deck:
_columns: _columns:
notifications: "Ilɣuyen" notifications: "Ilɣuyen"

View file

@ -76,6 +76,8 @@ _profile:
username: "ಬಳಕೆಹೆಸರು" username: "ಬಳಕೆಹೆಸರು"
_notification: _notification:
youWereFollowed: "ಹಿಂಬಾಲಿಸಿದರು" youWereFollowed: "ಹಿಂಬಾಲಿಸಿದರು"
_actions:
reply: "ಉತ್ತರಿಸು"
_deck: _deck:
_columns: _columns:
notifications: "ಅಧಿಸೂಚನೆಗಳು" notifications: "ಅಧಿಸೂಚನೆಗಳು"

View file

@ -592,6 +592,8 @@ smtpSecure: "SMTP 연결에 Implicit SSL/TTS 사용"
smtpSecureInfo: "STARTTLS 사용 시에는 해제합니다." smtpSecureInfo: "STARTTLS 사용 시에는 해제합니다."
testEmail: "이메일 전송 테스트" testEmail: "이메일 전송 테스트"
wordMute: "단어 뮤트" wordMute: "단어 뮤트"
regexpError: "정규 표현식 오류"
regexpErrorDescription: "{tab}단어 뮤트 {line}행의 정규 표현식에 오류가 발생했습니다:"
instanceMute: "인스턴스 뮤트" instanceMute: "인스턴스 뮤트"
userSaysSomething: "{name}님이 무언가를 말했습니다" userSaysSomething: "{name}님이 무언가를 말했습니다"
makeActive: "활성화" makeActive: "활성화"
@ -825,8 +827,21 @@ overridedDeviceKind: "장치 유형"
smartphone: "스마트폰" smartphone: "스마트폰"
tablet: "태블릿" tablet: "태블릿"
auto: "자동" auto: "자동"
themeColor: "테마 컬러"
size: "크기"
numberOfColumn: "한 줄에 보일 리액션의 수"
searchByGoogle: "검색" searchByGoogle: "검색"
instanceDefaultLightTheme: "인스턴스 기본 라이트 테마"
instanceDefaultDarkTheme: "인스턴스 기본 다크 테마"
instanceDefaultThemeDescription: "객체 형식의 테마 코드를 입력해 주세요."
mutePeriod: "뮤트할 기간"
indefinitely: "무기한" indefinitely: "무기한"
tenMinutes: "10분"
oneHour: "1시간"
oneDay: "1일"
oneWeek: "일주일"
reflectMayTakeTime: "반영되기까지 시간이 걸릴 수 있습니다."
failedToFetchAccountInformation: "계정 정보를 가져오지 못했습니다"
_emailUnavailable: _emailUnavailable:
used: "이 메일 주소는 사용중입니다" used: "이 메일 주소는 사용중입니다"
format: "형식이 올바르지 않습니다" format: "형식이 올바르지 않습니다"
@ -1249,7 +1264,7 @@ _profile:
youCanIncludeHashtags: "해시 태그를 포함할 수 있습니다." youCanIncludeHashtags: "해시 태그를 포함할 수 있습니다."
metadata: "추가 정보" metadata: "추가 정보"
metadataEdit: "추가 정보 편집" metadataEdit: "추가 정보 편집"
metadataDescription: "프로필에 최대 4개의 추가 정보를 표시할 수 있어요" metadataDescription: "프로필에 추가 정보를 표시할 수 있어요"
metadataLabel: "라벨" metadataLabel: "라벨"
metadataContent: "내용" metadataContent: "내용"
changeAvatar: "아바타 이미지 변경" changeAvatar: "아바타 이미지 변경"
@ -1599,6 +1614,8 @@ _notification:
youReceivedFollowRequest: "새로운 팔로우 요청이 있습니다" youReceivedFollowRequest: "새로운 팔로우 요청이 있습니다"
yourFollowRequestAccepted: "팔로우 요청이 수락되었습니다" yourFollowRequestAccepted: "팔로우 요청이 수락되었습니다"
youWereInvitedToGroup: "그룹에 초대되었습니다" youWereInvitedToGroup: "그룹에 초대되었습니다"
pollEnded: "투표 결과가 발표되었습니다"
emptyPushNotificationMessage: "푸시 알림이 갱신되었습니다"
_types: _types:
all: "전부" all: "전부"
follow: "팔로잉" follow: "팔로잉"
@ -1608,10 +1625,15 @@ _notification:
quote: "인용" quote: "인용"
reaction: "리액션" reaction: "리액션"
pollVote: "투표 참여" pollVote: "투표 참여"
pollEnded: "투표가 종료됨"
receiveFollowRequest: "팔로우 요청을 받았을 때" receiveFollowRequest: "팔로우 요청을 받았을 때"
followRequestAccepted: "팔로우 요청이 승인되었을 때" followRequestAccepted: "팔로우 요청이 승인되었을 때"
groupInvited: "그룹에 초대되었을 때" groupInvited: "그룹에 초대되었을 때"
app: "연동된 앱을 통한 알림" app: "연동된 앱을 통한 알림"
_actions:
followBack: "팔로우"
reply: "답글"
renote: "Renote"
_deck: _deck:
alwaysShowMainColumn: "메인 칼럼 항상 표시" alwaysShowMainColumn: "메인 칼럼 항상 표시"
columnAlign: "칼럼 정렬" columnAlign: "칼럼 정렬"

View file

@ -371,6 +371,9 @@ _notification:
renote: "Herdelen" renote: "Herdelen"
quote: "Quote" quote: "Quote"
reaction: "Reacties" reaction: "Reacties"
_actions:
reply: "Antwoord"
renote: "Herdelen"
_deck: _deck:
_columns: _columns:
notifications: "Meldingen" notifications: "Meldingen"

View file

@ -1401,6 +1401,9 @@ _notification:
followRequestAccepted: "Przyjęto prośbę o możliwość obserwacji" followRequestAccepted: "Przyjęto prośbę o możliwość obserwacji"
groupInvited: "Zaproszono do grup" groupInvited: "Zaproszono do grup"
app: "Powiadomienia z aplikacji" app: "Powiadomienia z aplikacji"
_actions:
reply: "Odpowiedz"
renote: "Udostępnij"
_deck: _deck:
alwaysShowMainColumn: "Zawsze pokazuj główną kolumnę" alwaysShowMainColumn: "Zawsze pokazuj główną kolumnę"
columnAlign: "Wyrównaj kolumny" columnAlign: "Wyrównaj kolumny"

View file

@ -37,17 +37,58 @@ favorites: "Favoritar"
unfavorite: "Remover dos favoritos" unfavorite: "Remover dos favoritos"
favorited: "Adicionado aos favoritos." favorited: "Adicionado aos favoritos."
alreadyFavorited: "Já adicionado aos favoritos." alreadyFavorited: "Já adicionado aos favoritos."
cantFavorite: "Não foi possível adicionar aos favoritos."
pin: "Afixar no perfil"
unpin: "Desafixar do perfil"
copyContent: "Copiar conteúdos"
copyLink: "Copiar hiperligação"
delete: "Eliminar"
deleteAndEdit: "Eliminar e editar"
deleteAndEditConfirm: "Tens a certeza que pretendes eliminar esta nota e editá-la? Irás perder todas as suas reações, renotas e respostas."
addToList: "Adicionar a lista"
sendMessage: "Enviar uma mensagem"
copyUsername: "Copiar nome de utilizador"
searchUser: "Pesquisar utilizador"
reply: "Responder"
loadMore: "Carregar mais"
showMore: "Ver mais" showMore: "Ver mais"
youGotNewFollower: "Você tem um novo seguidor" youGotNewFollower: "Você tem um novo seguidor"
receiveFollowRequest: "Pedido de seguimento recebido"
followRequestAccepted: "Pedido de seguir aceito" followRequestAccepted: "Pedido de seguir aceito"
mention: "Menção"
mentions: "Menções"
directNotes: "Notas diretas"
importAndExport: "Importar/Exportar"
import: "Importar"
export: "Exportar"
files: "Ficheiros"
download: "Descarregar"
driveFileDeleteConfirm: "Tens a certeza que pretendes apagar o ficheiro \"{name}\"? As notas que tenham este ficheiro anexado serão também apagadas."
unfollowConfirm: "Tens a certeza que queres deixar de seguir {name}?"
exportRequested: "Pediste uma exportação. Este processo pode demorar algum tempo. Será adicionado à tua Drive após a conclusão do processo."
importRequested: "Pediste uma importação. Este processo pode demorar algum tempo."
lists: "Listas"
noLists: "Não tens nenhuma lista"
note: "Post" note: "Post"
notes: "Posts" notes: "Posts"
following: "Seguindo"
followers: "Seguidores"
followsYou: "Segue-te"
createList: "Criar lista"
manageLists: "Gerir listas"
error: "Erro"
somethingHappened: "Ocorreu um erro"
retry: "Tentar novamente"
pageLoadError: "Ocorreu um erro ao carregar a página."
pageLoadErrorDescription: "Isto é normalmente causado por erros de rede ou pela cache do browser. Experimenta limpar a cache e tenta novamente após algum tempo."
follow: "Seguindo"
enterEmoji: "Inserir emoji" enterEmoji: "Inserir emoji"
renote: "Repostar" renote: "Repostar"
renoted: "Repostado" renoted: "Repostado"
cantRenote: "Não pode repostar" cantRenote: "Não pode repostar"
cantReRenote: "Não pode repostar este repost" cantReRenote: "Não pode repostar este repost"
pinnedNote: "Post fixado" pinnedNote: "Post fixado"
pinned: "Afixar no perfil"
sensitive: "Conteúdo sensível" sensitive: "Conteúdo sensível"
mute: "Silenciar" mute: "Silenciar"
unmute: "Dessilenciar" unmute: "Dessilenciar"
@ -57,6 +98,7 @@ registeredAt: "Registrado em"
perHour: "por hora" perHour: "por hora"
perDay: "por dia" perDay: "por dia"
noUsers: "Sem usuários" noUsers: "Sem usuários"
remove: "Eliminar"
messageRead: "Lida" messageRead: "Lida"
lightThemes: "Tema claro" lightThemes: "Tema claro"
darkThemes: "Tema escuro" darkThemes: "Tema escuro"
@ -64,6 +106,7 @@ addFile: "Adicionar arquivo"
nsfw: "Conteúdo sensível" nsfw: "Conteúdo sensível"
monthX: "mês de {month}" monthX: "mês de {month}"
pinnedNotes: "Post fixado" pinnedNotes: "Post fixado"
userList: "Listas"
smtpUser: "Nome de usuário" smtpUser: "Nome de usuário"
smtpPass: "Senha" smtpPass: "Senha"
user: "Usuários" user: "Usuários"
@ -72,9 +115,11 @@ _email:
_follow: _follow:
title: "Você tem um novo seguidor" title: "Você tem um novo seguidor"
_mfm: _mfm:
mention: "Menção"
search: "Pesquisar" search: "Pesquisar"
_theme: _theme:
keys: keys:
mention: "Menção"
renote: "Repostar" renote: "Repostar"
_sfx: _sfx:
note: "Posts" note: "Posts"
@ -82,15 +127,47 @@ _sfx:
_widgets: _widgets:
notifications: "Notificações" notifications: "Notificações"
timeline: "Timeline" timeline: "Timeline"
_cw:
show: "Carregar mais"
_visibility:
followers: "Seguidores"
_profile: _profile:
username: "Nome de usuário" username: "Nome de usuário"
_exportOrImport: _exportOrImport:
followingList: "Seguindo"
muteList: "Silenciar" muteList: "Silenciar"
userLists: "Listas"
_pages:
script:
categories:
list: "Listas"
blocks:
_join:
arg1: "Listas"
_randomPick:
arg1: "Listas"
_dailyRandomPick:
arg1: "Listas"
_seedRandomPick:
arg2: "Listas"
_pick:
arg1: "Listas"
_listLen:
arg1: "Listas"
types:
array: "Listas"
_notification: _notification:
youWereFollowed: "Você tem um novo seguidor" youWereFollowed: "Você tem um novo seguidor"
_types: _types:
follow: "Seguindo"
mention: "Menção"
renote: "Repostar"
_actions:
reply: "Responder"
renote: "Repostar" renote: "Repostar"
_deck: _deck:
_columns: _columns:
notifications: "Notificações" notifications: "Notificações"
tl: "Timeline" tl: "Timeline"
list: "Listas"
mentions: "Menções"

View file

@ -562,13 +562,87 @@ plugins: "Pluginuri"
deck: "Deck" deck: "Deck"
undeck: "Părăsește Deck" undeck: "Părăsește Deck"
useBlurEffectForModal: "Folosește efect de blur pentru modale" useBlurEffectForModal: "Folosește efect de blur pentru modale"
width: "Lăţime"
height: "Înălţime"
large: "Mare"
medium: "Mediu"
small: "Mic"
generateAccessToken: "Generează token de acces"
permission: "Permisiuni"
enableAll: "Actevează tot"
disableAll: "Dezactivează tot"
tokenRequested: "Acordă acces la cont"
pluginTokenRequestedDescription: "Acest plugin va putea să folosească permisiunile setate aici."
notificationType: "Tipul notificării"
edit: "Editează"
useStarForReactionFallback: "Folosește ★ ca fallback dacă emoji-ul este necunoscut"
emailServer: "Server email"
enableEmail: "Activează distribuția de emailuri"
emailConfigInfo: "Folosit pentru a confirma emailul tău în timpul logări dacă îți uiți parola"
email: "Email"
emailAddress: "Adresă de email"
smtpConfig: "Configurare Server SMTP"
smtpHost: "Gazdă" smtpHost: "Gazdă"
smtpPort: "Port"
smtpUser: "Nume de utilizator" smtpUser: "Nume de utilizator"
smtpPass: "Parolă" smtpPass: "Parolă"
emptyToDisableSmtpAuth: "Lasă username-ul și parola necompletate pentru a dezactiva verificarea SMTP"
smtpSecure: "Folosește SSL/TLS implicit pentru conecțiunile SMTP"
smtpSecureInfo: "Oprește opțiunea asta dacă STARTTLS este folosit"
testEmail: "Testează livrarea emailurilor"
wordMute: "Cuvinte pe mut"
regexpError: "Eroare de Expresie Regulată"
regexpErrorDescription: "A apărut o eroare în expresia regulată pe linia {line} al cuvintelor {tab} setate pe mut:"
instanceMute: "Instanțe pe mut"
userSaysSomething: "{name} a spus ceva"
makeActive: "Activează"
display: "Arată"
copy: "Copiază"
metrics: "Metrici"
overview: "Privire de ansamblu"
logs: "Log-uri"
delayed: "Întârziate"
database: "Baza de date"
channel: "Canale"
create: "Crează"
notificationSetting: "Setări notificări"
notificationSettingDesc: "Selectează tipurile de notificări care să fie arătate"
useGlobalSetting: "Folosește setările globale"
useGlobalSettingDesc: "Dacă opțiunea e pornită, notificările contului tău vor fi folosite. Dacă e oprită, configurația va fi individuală."
other: "Altele"
regenerateLoginToken: "Regenerează token de login"
regenerateLoginTokenDescription: "Regenerează token-ul folosit intern în timpul logări. În mod normal asta nu este necesar. Odată regenerat, toate dispozitivele vor fi delogate."
setMultipleBySeparatingWithSpace: "Separă mai multe intrări cu spații."
fileIdOrUrl: "Introdu ID sau URL"
behavior: "Comportament"
sample: "exemplu"
abuseReports: "Rapoarte"
reportAbuse: "Raportează"
reportAbuseOf: "Raportează {name}"
fillAbuseReportDescription: "Te rog scrie detaliile legate de acest raport. Dacă este despre o notă specifică, te rog introdu URL-ul ei."
abuseReported: "Raportul tău a fost trimis. Mulțumim."
reporter: "Raportorul"
reporteeOrigin: "Originea raportatului"
reporterOrigin: "Originea raportorului"
forwardReport: "Redirecționează raportul către instanța externă"
forwardReportIsAnonymous: "În locul contului tău, va fi afișat un cont anonim, de sistem, ca raportor către instanța externă."
send: "Trimite"
abuseMarkAsResolved: "Marchează raportul ca rezolvat"
openInNewTab: "Deschide în tab nou"
openInSideView: "Deschide în vedere laterală"
defaultNavigationBehaviour: "Comportament de navigare implicit"
editTheseSettingsMayBreakAccount: "Editarea acestor setări îți pot defecta contul."
waitingFor: "Așteptând pentru {x}"
random: "Aleator"
system: "Sistem"
switchUi: "Schimbă UI"
desktop: "Desktop"
clearCache: "Golește cache-ul" clearCache: "Golește cache-ul"
info: "Despre" info: "Despre"
user: "Utilizatori" user: "Utilizatori"
administration: "Gestionare" administration: "Gestionare"
middle: "Mediu"
sent: "Trimite"
searchByGoogle: "Caută" searchByGoogle: "Caută"
_email: _email:
_follow: _follow:
@ -641,6 +715,9 @@ _notification:
renote: "Re-notează" renote: "Re-notează"
quote: "Citează" quote: "Citează"
reaction: "Reacție" reaction: "Reacție"
_actions:
reply: "Răspunde"
renote: "Re-notează"
_deck: _deck:
_columns: _columns:
notifications: "Notificări" notifications: "Notificări"

View file

@ -1599,6 +1599,9 @@ _notification:
followRequestAccepted: "Запрос на подписку одобрен" followRequestAccepted: "Запрос на подписку одобрен"
groupInvited: "Приглашение в группы" groupInvited: "Приглашение в группы"
app: "Уведомления из приложений" app: "Уведомления из приложений"
_actions:
reply: "Ответить"
renote: "Репост"
_deck: _deck:
alwaysShowMainColumn: "Всегда показывать главную колонку" alwaysShowMainColumn: "Всегда показывать главную колонку"
columnAlign: "Выравнивание колонок" columnAlign: "Выравнивание колонок"

View file

@ -1628,6 +1628,10 @@ _notification:
followRequestAccepted: "Schválené žiadosti o sledovanie" followRequestAccepted: "Schválené žiadosti o sledovanie"
groupInvited: "Pozvánky do skupín" groupInvited: "Pozvánky do skupín"
app: "Oznámenia z prepojených aplikácií" app: "Oznámenia z prepojených aplikácií"
_actions:
followBack: "Sledovať späť\n"
reply: "Odpovedať"
renote: "Preposlať"
_deck: _deck:
alwaysShowMainColumn: "Vždy zobraziť v hlavnom stĺpci" alwaysShowMainColumn: "Vždy zobraziť v hlavnom stĺpci"
columnAlign: "Zarovnať stĺpce" columnAlign: "Zarovnať stĺpce"

View file

@ -7,6 +7,7 @@ search: "Пошук"
notifications: "Сповіщення" notifications: "Сповіщення"
username: "Ім'я користувача" username: "Ім'я користувача"
password: "Пароль" password: "Пароль"
forgotPassword: "Я забув пароль"
fetchingAsApObject: "Отримуємо з федіверсу..." fetchingAsApObject: "Отримуємо з федіверсу..."
ok: "OK" ok: "OK"
gotIt: "Зрозуміло!" gotIt: "Зрозуміло!"
@ -80,6 +81,8 @@ somethingHappened: "Щось пішло не так"
retry: "Спробувати знову" retry: "Спробувати знову"
pageLoadError: "Помилка при завантаженні сторінки" pageLoadError: "Помилка при завантаженні сторінки"
pageLoadErrorDescription: "Зазвичай це пов’язано з помилками мережі або кешем браузера. Очистіть кеш або почекайте трохи й спробуйте ще раз." pageLoadErrorDescription: "Зазвичай це пов’язано з помилками мережі або кешем браузера. Очистіть кеш або почекайте трохи й спробуйте ще раз."
serverIsDead: "Відповіді від сервера немає. Зачекайте деякий час і повторіть спробу."
youShouldUpgradeClient: "Перезавантажте та використовуйте нову версію клієнта, щоб переглянути цю сторінку."
enterListName: "Введіть назву списку" enterListName: "Введіть назву списку"
privacy: "Конфіденційність" privacy: "Конфіденційність"
makeFollowManuallyApprove: "Підтверджувати підписників уручну" makeFollowManuallyApprove: "Підтверджувати підписників уручну"
@ -103,6 +106,7 @@ clickToShow: "Натисніть для перегляду"
sensitive: "NSFW" sensitive: "NSFW"
add: "Додати" add: "Додати"
reaction: "Реакції" reaction: "Реакції"
reactionSetting: "Налаштування реакцій"
reactionSettingDescription2: "Перемістити щоб змінити порядок, Клацнути мишою щоб видалити, Натиснути \"+\" щоб додати." reactionSettingDescription2: "Перемістити щоб змінити порядок, Клацнути мишою щоб видалити, Натиснути \"+\" щоб додати."
rememberNoteVisibility: "Пам’ятати параметри видимісті" rememberNoteVisibility: "Пам’ятати параметри видимісті"
attachCancel: "Видалити вкладення" attachCancel: "Видалити вкладення"
@ -137,7 +141,10 @@ flagAsBot: "Акаунт бота"
flagAsBotDescription: "Ввімкніть якщо цей обліковий запис використовується ботом. Ця опція позначить обліковий запис як бота. Це потрібно щоб виключити безкінечну інтеракцію між ботами а також відповідного підлаштування Misskey." flagAsBotDescription: "Ввімкніть якщо цей обліковий запис використовується ботом. Ця опція позначить обліковий запис як бота. Це потрібно щоб виключити безкінечну інтеракцію між ботами а також відповідного підлаштування Misskey."
flagAsCat: "Акаунт кота" flagAsCat: "Акаунт кота"
flagAsCatDescription: "Ввімкніть, щоб позначити, що обліковий запис є котиком." flagAsCatDescription: "Ввімкніть, щоб позначити, що обліковий запис є котиком."
flagShowTimelineReplies: "Показувати відповіді на нотатки на часовій шкалі"
flagShowTimelineRepliesDescription: "Показує відповіді користувачів на нотатки інших користувачів на часовій шкалі."
autoAcceptFollowed: "Автоматично приймати запити на підписку від користувачів, на яких ви підписані" autoAcceptFollowed: "Автоматично приймати запити на підписку від користувачів, на яких ви підписані"
addAccount: "Додати акаунт"
loginFailed: "Не вдалося увійти" loginFailed: "Не вдалося увійти"
showOnRemote: "Переглянути в оригіналі" showOnRemote: "Переглянути в оригіналі"
general: "Загальне" general: "Загальне"
@ -148,6 +155,7 @@ searchWith: "Пошук: {q}"
youHaveNoLists: "У вас немає списків" youHaveNoLists: "У вас немає списків"
followConfirm: "Підписатися на {name}?" followConfirm: "Підписатися на {name}?"
proxyAccount: "Проксі-акаунт" proxyAccount: "Проксі-акаунт"
proxyAccountDescription: "Обліковий запис проксі це обліковий запис, який діє як віддалений підписник для користувачів за певних умов. Наприклад, коли користувач додає віддаленого користувача до списку, активність віддаленого користувача не буде доставлена на сервер, якщо жоден локальний користувач не стежить за цим користувачем, то замість нього буде використовуватися обліковий запис проксі-сервера."
host: "Хост" host: "Хост"
selectUser: "Виберіть користувача" selectUser: "Виберіть користувача"
recipient: "Отримувач" recipient: "Отримувач"
@ -229,6 +237,8 @@ resetAreYouSure: "Справді скинути?"
saved: "Збережено" saved: "Збережено"
messaging: "Чати" messaging: "Чати"
upload: "Завантажити" upload: "Завантажити"
keepOriginalUploading: "Зберегти оригінальне зображення"
keepOriginalUploadingDescription: "Зберігає початково завантажене зображення як є. Якщо вимкнено, версія для відображення в Інтернеті буде створена під час завантаження."
fromDrive: "З диска" fromDrive: "З диска"
fromUrl: "З посилання" fromUrl: "З посилання"
uploadFromUrl: "Завантажити з посилання" uploadFromUrl: "Завантажити з посилання"
@ -275,6 +285,7 @@ emptyDrive: "Диск порожній"
emptyFolder: "Тека порожня" emptyFolder: "Тека порожня"
unableToDelete: "Видалення неможливе" unableToDelete: "Видалення неможливе"
inputNewFileName: "Введіть ім'я нового файлу" inputNewFileName: "Введіть ім'я нового файлу"
inputNewDescription: "Введіть новий заголовок"
inputNewFolderName: "Введіть ім'я нової теки" inputNewFolderName: "Введіть ім'я нової теки"
circularReferenceFolder: "Ви намагаєтесь перемістити папку в її підпапку." circularReferenceFolder: "Ви намагаєтесь перемістити папку в її підпапку."
hasChildFilesOrFolders: "Ця тека не порожня і не може бути видалена" hasChildFilesOrFolders: "Ця тека не порожня і не може бути видалена"
@ -306,6 +317,8 @@ monthX: "{month}"
yearX: "{year}" yearX: "{year}"
pages: "Сторінки" pages: "Сторінки"
integration: "Інтеграція" integration: "Інтеграція"
connectService: "Під’єднати"
disconnectService: "Відключитися"
enableLocalTimeline: "Увімкнути локальну стрічку" enableLocalTimeline: "Увімкнути локальну стрічку"
enableGlobalTimeline: "Увімкнути глобальну стрічку" enableGlobalTimeline: "Увімкнути глобальну стрічку"
disablingTimelinesInfo: "Адміністратори та модератори завжди мають доступ до всіх стрічок, навіть якщо вони вимкнуті." disablingTimelinesInfo: "Адміністратори та модератори завжди мають доступ до всіх стрічок, навіть якщо вони вимкнуті."
@ -317,6 +330,7 @@ driveCapacityPerRemoteAccount: "Об'єм диска на одного відд
inMb: "В мегабайтах" inMb: "В мегабайтах"
iconUrl: "URL аватара" iconUrl: "URL аватара"
bannerUrl: "URL банера" bannerUrl: "URL банера"
backgroundImageUrl: "URL-адреса фонового зображення"
basicInfo: "Основна інформація" basicInfo: "Основна інформація"
pinnedUsers: "Закріплені користувачі" pinnedUsers: "Закріплені користувачі"
pinnedUsersDescription: "Впишіть в список користувачів, яких хочете закріпити на сторінці \"Знайти\", ім'я в стовпчик." pinnedUsersDescription: "Впишіть в список користувачів, яких хочете закріпити на сторінці \"Знайти\", ім'я в стовпчик."
@ -332,6 +346,7 @@ recaptcha: "reCAPTCHA"
enableRecaptcha: "Увімкнути reCAPTCHA" enableRecaptcha: "Увімкнути reCAPTCHA"
recaptchaSiteKey: "Ключ сайту" recaptchaSiteKey: "Ключ сайту"
recaptchaSecretKey: "Секретний ключ" recaptchaSecretKey: "Секретний ключ"
avoidMultiCaptchaConfirm: "Використання кількох систем Captcha може спричинити перешкоди між ними. Бажаєте вимкнути інші активні системи Captcha? Якщо ви хочете, щоб вони залишалися ввімкненими, натисніть «Скасувати»."
antennas: "Антени" antennas: "Антени"
manageAntennas: "Налаштування антен" manageAntennas: "Налаштування антен"
name: "Ім'я" name: "Ім'я"
@ -428,10 +443,12 @@ signinWith: "Увійти за допомогою {x}"
signinFailed: "Не вдалося увійти. Введені ім’я користувача або пароль неправильнi." signinFailed: "Не вдалося увійти. Введені ім’я користувача або пароль неправильнi."
tapSecurityKey: "Торкніться ключа безпеки" tapSecurityKey: "Торкніться ключа безпеки"
or: "або" or: "або"
language: "Мова"
uiLanguage: "Мова інтерфейсу" uiLanguage: "Мова інтерфейсу"
groupInvited: "Запрошення до групи" groupInvited: "Запрошення до групи"
aboutX: "Про {x}" aboutX: "Про {x}"
useOsNativeEmojis: "Використовувати емодзі ОС" useOsNativeEmojis: "Використовувати емодзі ОС"
disableDrawer: "Не використовувати висувні меню"
youHaveNoGroups: "Немає груп" youHaveNoGroups: "Немає груп"
joinOrCreateGroup: "Отримуйте запрошення до груп або створюйте свої власні групи." joinOrCreateGroup: "Отримуйте запрошення до груп або створюйте свої власні групи."
noHistory: "Історія порожня" noHistory: "Історія порожня"
@ -442,6 +459,7 @@ category: "Категорія"
tags: "Теги" tags: "Теги"
docSource: "Джерело цього документа" docSource: "Джерело цього документа"
createAccount: "Створити акаунт" createAccount: "Створити акаунт"
existingAccount: "Існуючий обліковий запис"
regenerate: "Оновити" regenerate: "Оновити"
fontSize: "Розмір шрифту" fontSize: "Розмір шрифту"
noFollowRequests: "Немає запитів на підписку" noFollowRequests: "Немає запитів на підписку"
@ -463,6 +481,7 @@ showFeaturedNotesInTimeline: "Показувати популярні нотат
objectStorage: "Object Storage" objectStorage: "Object Storage"
useObjectStorage: "Використовувати object storage" useObjectStorage: "Використовувати object storage"
objectStorageBaseUrl: "Base URL" objectStorageBaseUrl: "Base URL"
objectStorageBaseUrlDesc: "Це початкова частина адреси, що використовується CDN або проксі, наприклад для S3: https://<bucket>.s3.amazonaws.com, або GCS: 'https://storage.googleapis.com/<bucket>'"
objectStorageBucket: "Bucket" objectStorageBucket: "Bucket"
objectStorageBucketDesc: "Будь ласка вкажіть назву відра в налаштованому сервісі." objectStorageBucketDesc: "Будь ласка вкажіть назву відра в налаштованому сервісі."
objectStoragePrefix: "Prefix" objectStoragePrefix: "Prefix"
@ -513,6 +532,9 @@ removeAllFollowing: "Скасувати всі підписки"
removeAllFollowingDescription: "Скасувати підписку на всі акаунти з {host}. Будь ласка, робіть це, якщо інстанс більше не існує." removeAllFollowingDescription: "Скасувати підписку на всі акаунти з {host}. Будь ласка, робіть це, якщо інстанс більше не існує."
userSuspended: "Обліковий запис заблокований." userSuspended: "Обліковий запис заблокований."
userSilenced: "Обліковий запис приглушений." userSilenced: "Обліковий запис приглушений."
yourAccountSuspendedTitle: "Цей обліковий запис заблоковано"
yourAccountSuspendedDescription: "Цей обліковий запис було заблоковано через порушення умов надання послуг сервера. Зв'яжіться з адміністратором, якщо ви хочете дізнатися докладнішу причину. Будь ласка, не створюйте новий обліковий запис."
menu: "Меню"
divider: "Розділювач" divider: "Розділювач"
addItem: "Додати елемент" addItem: "Додати елемент"
relays: "Ретранслятори" relays: "Ретранслятори"
@ -531,6 +553,8 @@ disablePlayer: "Закрити відеоплеєр"
expandTweet: "Розгорнути твіт" expandTweet: "Розгорнути твіт"
themeEditor: "Редактор тем" themeEditor: "Редактор тем"
description: "Опис" description: "Опис"
describeFile: "Додати підпис"
enterFileDescription: "Введіть підпис"
author: "Автор" author: "Автор"
leaveConfirm: "Зміни не збережені. Ви дійсно хочете скасувати зміни?" leaveConfirm: "Зміни не збережені. Ви дійсно хочете скасувати зміни?"
manage: "Управління" manage: "Управління"
@ -553,6 +577,7 @@ pluginTokenRequestedDescription: "Цей плагін зможе викорис
notificationType: "Тип сповіщення" notificationType: "Тип сповіщення"
edit: "Редагувати" edit: "Редагувати"
useStarForReactionFallback: "Використовувати ★ як запасний варіант, якщо емодзі реакції невідомий" useStarForReactionFallback: "Використовувати ★ як запасний варіант, якщо емодзі реакції невідомий"
emailServer: "Сервер електронної пошти"
enableEmail: "Увімкнути функцію доставки пошти" enableEmail: "Увімкнути функцію доставки пошти"
emailConfigInfo: "Використовується для підтвердження електронної пошти підчас реєстрації, а також для відновлення паролю." emailConfigInfo: "Використовується для підтвердження електронної пошти підчас реєстрації, а також для відновлення паролю."
email: "E-mail" email: "E-mail"
@ -567,6 +592,9 @@ smtpSecure: "Використовувати безумовне шифруван
smtpSecureInfo: "Вимкніть при використанні STARTTLS " smtpSecureInfo: "Вимкніть при використанні STARTTLS "
testEmail: "Тестовий email" testEmail: "Тестовий email"
wordMute: "Блокування слів" wordMute: "Блокування слів"
regexpError: "Помилка регулярного виразу"
regexpErrorDescription: "Сталася помилка в регулярному виразі в рядку {line} вашого слова {tab} слова що ігноруються:"
instanceMute: "Приглушення інстансів"
userSaysSomething: "{name} щось сказав(ла)" userSaysSomething: "{name} щось сказав(ла)"
makeActive: "Активувати" makeActive: "Активувати"
display: "Відображення" display: "Відображення"
@ -594,6 +622,11 @@ reportAbuse: "Поскаржитись"
reportAbuseOf: "Поскаржитись на {name}" reportAbuseOf: "Поскаржитись на {name}"
fillAbuseReportDescription: "Будь ласка вкажіть подробиці скарги. Якщо скарга стосується запису, вкажіть посилання на нього." fillAbuseReportDescription: "Будь ласка вкажіть подробиці скарги. Якщо скарга стосується запису, вкажіть посилання на нього."
abuseReported: "Дякуємо, вашу скаргу було відправлено. " abuseReported: "Дякуємо, вашу скаргу було відправлено. "
reporter: "Репортер"
reporteeOrigin: "Про кого повідомлено"
reporterOrigin: "Хто повідомив"
forwardReport: "Переслати звіт на віддалений інстанс"
forwardReportIsAnonymous: "Замість вашого облікового запису анонімний системний обліковий запис буде відображатися як доповідач на віддаленому інстансі"
send: "Відправити" send: "Відправити"
abuseMarkAsResolved: "Позначити скаргу як вирішену" abuseMarkAsResolved: "Позначити скаргу як вирішену"
openInNewTab: "Відкрити в новій вкладці" openInNewTab: "Відкрити в новій вкладці"
@ -655,6 +688,7 @@ center: "Центр"
wide: "Широкий" wide: "Широкий"
narrow: "Вузький" narrow: "Вузький"
reloadToApplySetting: "Налаштування ввійде в дію при перезавантаженні. Перезавантажити?" reloadToApplySetting: "Налаштування ввійде в дію при перезавантаженні. Перезавантажити?"
needReloadToApply: "Зміни набудуть чинності після перезавантаження сторінки."
showTitlebar: "Показати титульний рядок" showTitlebar: "Показати титульний рядок"
clearCache: "Очистити кеш" clearCache: "Очистити кеш"
onlineUsersCount: "{n} користувачів онлайн" onlineUsersCount: "{n} користувачів онлайн"
@ -669,12 +703,28 @@ textColor: "Текст"
saveAs: "Зберегти як…" saveAs: "Зберегти як…"
advanced: "Розширені" advanced: "Розширені"
value: "Значення" value: "Значення"
createdAt: "Створено"
updatedAt: "Останнє оновлення" updatedAt: "Останнє оновлення"
saveConfirm: "Зберегти зміни?" saveConfirm: "Зберегти зміни?"
deleteConfirm: "Ви дійсно бажаєте це видалити?" deleteConfirm: "Ви дійсно бажаєте це видалити?"
invalidValue: "Некоректне значення." invalidValue: "Некоректне значення."
registry: "Реєстр" registry: "Реєстр"
closeAccount: "Закрити обліковий запис" closeAccount: "Закрити обліковий запис"
currentVersion: "Версія, що використовується"
latestVersion: "Сама свіжа версія"
youAreRunningUpToDateClient: "У вас найсвіжіша версія клієнта."
newVersionOfClientAvailable: "Доступніша свіжа версія клієнта."
usageAmount: "Використане"
capacity: "Ємність"
inUse: "Зайнято"
editCode: "Редагувати вихідний текст"
apply: "Застосувати"
receiveAnnouncementFromInstance: "Отримувати оповіщення з інстансу"
emailNotification: "Сповіщення електронною поштою"
publish: "Опублікувати"
inChannelSearch: "Пошук за каналом"
useReactionPickerForContextMenu: "Відкривати палітру реакцій правою кнопкою"
typingUsers: "Стук клавіш. Це {users}…"
goBack: "Назад" goBack: "Назад"
info: "Інформація" info: "Інформація"
user: "Користувачі" user: "Користувачі"
@ -687,6 +737,8 @@ hashtags: "Хештеґ"
hide: "Сховати" hide: "Сховати"
searchByGoogle: "Пошук" searchByGoogle: "Пошук"
indefinitely: "Ніколи" indefinitely: "Ніколи"
_ffVisibility:
public: "Опублікувати"
_ad: _ad:
back: "Назад" back: "Назад"
_gallery: _gallery:
@ -1377,6 +1429,9 @@ _notification:
followRequestAccepted: "Прийняті підписки" followRequestAccepted: "Прийняті підписки"
groupInvited: "Запрошення до груп" groupInvited: "Запрошення до груп"
app: "Сповіщення від додатків" app: "Сповіщення від додатків"
_actions:
reply: "Відповісти"
renote: "Поширити"
_deck: _deck:
alwaysShowMainColumn: "Завжди показувати головну колонку" alwaysShowMainColumn: "Завжди показувати головну колонку"
columnAlign: "Вирівняти стовпці" columnAlign: "Вирівняти стовпці"

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,7 @@ notifications: "通知"
username: "用户名" username: "用户名"
password: "密码" password: "密码"
forgotPassword: "忘记密码" forgotPassword: "忘记密码"
fetchingAsApObject: "在联邦宇宙查询中..." fetchingAsApObject: "在联邦宇宙查询中..."
ok: "OK" ok: "OK"
gotIt: "我明白了" gotIt: "我明白了"
cancel: "取消" cancel: "取消"
@ -69,7 +69,7 @@ exportRequested: "导出请求已提交,这可能需要花一些时间,导
importRequested: "导入请求已提交,这可能需要花一点时间。" importRequested: "导入请求已提交,这可能需要花一点时间。"
lists: "列表" lists: "列表"
noLists: "列表为空" noLists: "列表为空"
note: "帖" note: ""
notes: "帖子" notes: "帖子"
following: "关注中" following: "关注中"
followers: "关注者" followers: "关注者"
@ -96,7 +96,7 @@ enterEmoji: "输入表情符号"
renote: "转发" renote: "转发"
unrenote: "取消转发" unrenote: "取消转发"
renoted: "已转发。" renoted: "已转发。"
cantRenote: "该帖无法转发。" cantRenote: "该帖无法转发。"
cantReRenote: "转发无法被再次转发。" cantReRenote: "转发无法被再次转发。"
quote: "引用" quote: "引用"
pinnedNote: "已置顶的帖子" pinnedNote: "已置顶的帖子"
@ -155,7 +155,7 @@ searchWith: "搜索:{q}"
youHaveNoLists: "列表为空" youHaveNoLists: "列表为空"
followConfirm: "你确定要关注{name}吗?" followConfirm: "你确定要关注{name}吗?"
proxyAccount: "代理账户" proxyAccount: "代理账户"
proxyAccountDescription: "代理帐户是在某些情况下充当用户的远程关注者的帐户。 例如,当一个用户列出一个远程用户时,如果没有人跟随该列出的用户,则该活动将不会传递到该实例,因此将代之以代理户。" proxyAccountDescription: "代理账户是在某些情况下充当用户的远程关注者的账户。 例如,当一个用户列出一个远程用户时,如果没有人跟随该列出的用户,则该活动将不会传递到该实例,因此将代之以代理户。"
host: "主机名" host: "主机名"
selectUser: "选择用户" selectUser: "选择用户"
recipient: "收件人" recipient: "收件人"
@ -171,7 +171,7 @@ charts: "图表"
perHour: "每小时" perHour: "每小时"
perDay: "每天" perDay: "每天"
stopActivityDelivery: "停止发送活动" stopActivityDelivery: "停止发送活动"
blockThisInstance: "阻止此实例" blockThisInstance: "阻止此实例向本实例推流"
operations: "操作" operations: "操作"
software: "软件" software: "软件"
version: "版本" version: "版本"
@ -250,7 +250,7 @@ messageRead: "已读"
noMoreHistory: "没有更多的历史记录" noMoreHistory: "没有更多的历史记录"
startMessaging: "添加聊天" startMessaging: "添加聊天"
nUsersRead: "{n}人已读" nUsersRead: "{n}人已读"
agreeTo: "{0}同意" agreeTo: "{0}勾选则表示已阅读并同意"
tos: "服务条款" tos: "服务条款"
start: "开始" start: "开始"
home: "首页" home: "首页"
@ -321,7 +321,7 @@ connectService: "连接"
disconnectService: "断开连接" disconnectService: "断开连接"
enableLocalTimeline: "启用本地时间线功能" enableLocalTimeline: "启用本地时间线功能"
enableGlobalTimeline: "启用全局时间线" enableGlobalTimeline: "启用全局时间线"
disablingTimelinesInfo: "即使时间线功能被禁用,出于便利性的原因,管理员和数据图表也可以继续使用。" disablingTimelinesInfo: "即使时间线功能被禁用,出于便,管理员和数据图表也可以继续使用。"
registration: "注册" registration: "注册"
enableRegistration: "允许新用户注册" enableRegistration: "允许新用户注册"
invite: "邀请" invite: "邀请"
@ -440,7 +440,7 @@ strongPassword: "密码强度:强"
passwordMatched: "密码一致" passwordMatched: "密码一致"
passwordNotMatched: "密码不一致" passwordNotMatched: "密码不一致"
signinWith: "以{x}登录" signinWith: "以{x}登录"
signinFailed: "无法登录,请检查您的用户名和密码。" signinFailed: "无法登录,请检查您的用户名和密码是否正确。"
tapSecurityKey: "轻触硬件安全密钥" tapSecurityKey: "轻触硬件安全密钥"
or: "或者" or: "或者"
language: "语言" language: "语言"
@ -459,7 +459,7 @@ category: "类别"
tags: "标签" tags: "标签"
docSource: "文件来源" docSource: "文件来源"
createAccount: "注册账户" createAccount: "注册账户"
existingAccount: "现有的户" existingAccount: "现有的户"
regenerate: "重新生成" regenerate: "重新生成"
fontSize: "字体大小" fontSize: "字体大小"
noFollowRequests: "没有关注申请" noFollowRequests: "没有关注申请"
@ -533,7 +533,7 @@ removeAllFollowingDescription: "取消{host}的所有关注者。当实例不存
userSuspended: "该用户已被冻结。" userSuspended: "该用户已被冻结。"
userSilenced: "该用户已被禁言。" userSilenced: "该用户已被禁言。"
yourAccountSuspendedTitle: "账户已被冻结" yourAccountSuspendedTitle: "账户已被冻结"
yourAccountSuspendedDescription: "由于违反了服务器的服务条款或其他原因,该账户已被冻结。 您可以与管理员联系以了解更多信息。 请不要创建一个新的户。" yourAccountSuspendedDescription: "由于违反了服务器的服务条款或其他原因,该账户已被冻结。 您可以与管理员联系以了解更多信息。 请不要创建一个新的户。"
menu: "菜单" menu: "菜单"
divider: "分割线" divider: "分割线"
addItem: "添加项目" addItem: "添加项目"
@ -609,7 +609,7 @@ create: "创建"
notificationSetting: "通知设置" notificationSetting: "通知设置"
notificationSettingDesc: "选择要显示的通知类型。" notificationSettingDesc: "选择要显示的通知类型。"
useGlobalSetting: "使用全局设置" useGlobalSetting: "使用全局设置"
useGlobalSettingDesc: "启用时,将使用户通知设置。关闭时,则可以单独设置。" useGlobalSettingDesc: "启用时,将使用户通知设置。关闭时,则可以单独设置。"
other: "其他" other: "其他"
regenerateLoginToken: "重新生成登录令牌" regenerateLoginToken: "重新生成登录令牌"
regenerateLoginTokenDescription: "重新生成用于登录的内部令牌。通常您不需要这样做。重新生成后,您将在所有设备上登出。" regenerateLoginTokenDescription: "重新生成用于登录的内部令牌。通常您不需要这样做。重新生成后,您将在所有设备上登出。"
@ -621,12 +621,12 @@ abuseReports: "举报"
reportAbuse: "举报" reportAbuse: "举报"
reportAbuseOf: "举报{name}" reportAbuseOf: "举报{name}"
fillAbuseReportDescription: "请填写举报的详细原因。如果有对方发的帖子请同时填写URL地址。" fillAbuseReportDescription: "请填写举报的详细原因。如果有对方发的帖子请同时填写URL地址。"
abuseReported: "内容已发送。感谢您的报告。" abuseReported: "内容已发送。感谢您提交信息。"
reporter: "者" reporter: "报者"
reporteeOrigin: "举报来源" reporteeOrigin: "举报来源"
reporterOrigin: "举报者来源" reporterOrigin: "举报者来源"
forwardReport: "将报告转发给远程实例" forwardReport: "将该举报信息转发给远程实例"
forwardReportIsAnonymous: "在远程实例上显示的报者是匿名的系统账号,而不是您的账号。" forwardReportIsAnonymous: "勾选则在远程实例上显示的报者是匿名的系统账号,而不是您的账号。"
send: "发送" send: "发送"
abuseMarkAsResolved: "处理完毕" abuseMarkAsResolved: "处理完毕"
openInNewTab: "在新标签页中打开" openInNewTab: "在新标签页中打开"
@ -644,9 +644,9 @@ createNew: "新建"
optional: "可选" optional: "可选"
createNewClip: "新建书签" createNewClip: "新建书签"
public: "公开" public: "公开"
i18nInfo: "Misskey已经被志愿者们翻译了各种语言。如果你也有兴趣,可以通过{link}帮助翻译。" i18nInfo: "Misskey已经被志愿者们翻译了各种语言。如果你也有兴趣,可以通过{link}帮助翻译。"
manageAccessTokens: "管理 Access Tokens" manageAccessTokens: "管理 Access Tokens"
accountInfo: "户信息" accountInfo: "户信息"
notesCount: "帖子数量" notesCount: "帖子数量"
repliesCount: "回复数量" repliesCount: "回复数量"
renotesCount: "转帖数量" renotesCount: "转帖数量"
@ -662,7 +662,7 @@ yes: "是"
no: "否" no: "否"
driveFilesCount: "网盘的文件数" driveFilesCount: "网盘的文件数"
driveUsage: "网盘的空间用量" driveUsage: "网盘的空间用量"
noCrawle: "拒绝搜索引擎的索引" noCrawle: "要求搜索引擎不索引该站点"
noCrawleDescription: "要求搜索引擎不要收录(索引)您的用户页面,帖子,页面等。" noCrawleDescription: "要求搜索引擎不要收录(索引)您的用户页面,帖子,页面等。"
lockedAccountInfo: "即使通过了关注请求,只要您不将帖子可见范围设置成“关注者”,任何人都可以看到您的帖子。" lockedAccountInfo: "即使通过了关注请求,只要您不将帖子可见范围设置成“关注者”,任何人都可以看到您的帖子。"
alwaysMarkSensitive: "默认将媒体文件标记为敏感内容" alwaysMarkSensitive: "默认将媒体文件标记为敏感内容"
@ -1615,6 +1615,7 @@ _notification:
yourFollowRequestAccepted: "您的关注请求已通过" yourFollowRequestAccepted: "您的关注请求已通过"
youWereInvitedToGroup: "您有新的群组邀请" youWereInvitedToGroup: "您有新的群组邀请"
pollEnded: "问卷调查结果已生成。" pollEnded: "问卷调查结果已生成。"
emptyPushNotificationMessage: "推送通知已更新"
_types: _types:
all: "全部" all: "全部"
follow: "关注中" follow: "关注中"
@ -1629,6 +1630,10 @@ _notification:
followRequestAccepted: "关注请求已通过" followRequestAccepted: "关注请求已通过"
groupInvited: "加入群组邀请" groupInvited: "加入群组邀请"
app: "关联应用的通知" app: "关联应用的通知"
_actions:
followBack: "回关"
reply: "回复"
renote: "转发"
_deck: _deck:
alwaysShowMainColumn: "总是显示主列" alwaysShowMainColumn: "总是显示主列"
columnAlign: "列对齐" columnAlign: "列对齐"

View file

@ -135,13 +135,14 @@ emojiName: "表情符號名稱"
emojiUrl: "表情符號URL" emojiUrl: "表情符號URL"
addEmoji: "加入表情符號" addEmoji: "加入表情符號"
settingGuide: "推薦設定" settingGuide: "推薦設定"
cacheRemoteFiles: "緩存非遠程檔案" cacheRemoteFiles: "快取遠端檔案"
cacheRemoteFilesDescription: "禁用此設定會停止遠端檔案的緩存,從而節省儲存空間,但資料會因直接連線從而產生額外連接數據。" cacheRemoteFilesDescription: "禁用此設定會停止遠端檔案的緩存,從而節省儲存空間,但資料會因直接連線從而產生額外連接數據。"
flagAsBot: "此使用者是機器人" flagAsBot: "此使用者是機器人"
flagAsBotDescription: "如果本帳戶是由程式控制請啟用此選項。啟用後會作為標示幫助其他開發者防止機器人之間產生無限互動的行為並會調整Misskey內部系統將本帳戶識別為機器人" flagAsBotDescription: "如果本帳戶是由程式控制請啟用此選項。啟用後會作為標示幫助其他開發者防止機器人之間產生無限互動的行為並會調整Misskey內部系統將本帳戶識別為機器人"
flagAsCat: "此使用者是貓" flagAsCat: "此使用者是貓"
flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示" flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示"
flagShowTimelineReplies: "在時間軸上顯示貼文的回覆" flagShowTimelineReplies: "在時間軸上顯示貼文的回覆"
flagShowTimelineRepliesDescription: "啟用時,時間線除了顯示用戶的貼文以外,還會顯示用戶對其他貼文的回覆。"
autoAcceptFollowed: "自動追隨中使用者的追隨請求" autoAcceptFollowed: "自動追隨中使用者的追隨請求"
addAccount: "添加帳戶" addAccount: "添加帳戶"
loginFailed: "登入失敗" loginFailed: "登入失敗"
@ -153,8 +154,8 @@ removeWallpaper: "移除桌布"
searchWith: "搜尋: {q}" searchWith: "搜尋: {q}"
youHaveNoLists: "你沒有任何清單" youHaveNoLists: "你沒有任何清單"
followConfirm: "你真的要追隨{name}嗎?" followConfirm: "你真的要追隨{name}嗎?"
proxyAccount: "代理帳" proxyAccount: "代理帳"
proxyAccountDescription: "代理帳號是在某些情況下充當其他伺服器用戶的帳號。例如,當使用者將一個來自其他伺服器的帳號放在列表中時,由於沒有其他使用者關注該帳號,該指令不會傳送到該伺服器上,因此會由代理帳戶關注。" proxyAccountDescription: "代理帳戶是在某些情況下充當其他伺服器用戶的帳戶。例如,當使用者將一個來自其他伺服器的帳戶放在列表中時,由於沒有其他使用者關注該帳戶,該指令不會傳送到該伺服器上,因此會由代理帳戶關注。"
host: "主機" host: "主機"
selectUser: "選取使用者" selectUser: "選取使用者"
recipient: "收件人" recipient: "收件人"
@ -197,7 +198,7 @@ noUsers: "沒有任何使用者"
editProfile: "編輯個人檔案" editProfile: "編輯個人檔案"
noteDeleteConfirm: "確定刪除此貼文嗎?" noteDeleteConfirm: "確定刪除此貼文嗎?"
pinLimitExceeded: "不能置頂更多貼文了" pinLimitExceeded: "不能置頂更多貼文了"
intro: "Misskey 部署完成!請建立管理員帳號!" intro: "Misskey 部署完成!請建立管理員帳戶。"
done: "完成" done: "完成"
processing: "處理中" processing: "處理中"
preview: "預覽" preview: "預覽"
@ -236,6 +237,8 @@ resetAreYouSure: "確定要重設嗎?"
saved: "已儲存" saved: "已儲存"
messaging: "傳送訊息" messaging: "傳送訊息"
upload: "上傳" upload: "上傳"
keepOriginalUploading: "保留原圖"
keepOriginalUploadingDescription: "上傳圖片時保留原始圖片。關閉時瀏覽器會在上傳時生成一張用於web發布的圖片。"
fromDrive: "從雲端空間" fromDrive: "從雲端空間"
fromUrl: "從URL" fromUrl: "從URL"
uploadFromUrl: "從網址上傳" uploadFromUrl: "從網址上傳"
@ -357,7 +360,7 @@ enableServiceworker: "開啟 ServiceWorker"
antennaUsersDescription: "指定用換行符分隔的用戶名" antennaUsersDescription: "指定用換行符分隔的用戶名"
caseSensitive: "區分大小寫" caseSensitive: "區分大小寫"
withReplies: "包含回覆" withReplies: "包含回覆"
connectedTo: "您的帳號已連接到以下社交帳號" connectedTo: "您的帳戶已連接到以下社交帳戶"
notesAndReplies: "貼文與回覆" notesAndReplies: "貼文與回覆"
withFiles: "附件" withFiles: "附件"
silence: "禁言" silence: "禁言"
@ -445,6 +448,7 @@ uiLanguage: "介面語言"
groupInvited: "您有新的群組邀請" groupInvited: "您有新的群組邀請"
aboutX: "關於{x}" aboutX: "關於{x}"
useOsNativeEmojis: "使用OS原生表情符號" useOsNativeEmojis: "使用OS原生表情符號"
disableDrawer: "不顯示下拉式選單"
youHaveNoGroups: "找不到群組" youHaveNoGroups: "找不到群組"
joinOrCreateGroup: "請加入現有群組,或創建新群組。" joinOrCreateGroup: "請加入現有群組,或創建新群組。"
noHistory: "沒有歷史紀錄" noHistory: "沒有歷史紀錄"
@ -468,7 +472,7 @@ weekOverWeekChanges: "與上週相比"
dayOverDayChanges: "與前一日相比" dayOverDayChanges: "與前一日相比"
appearance: "外觀" appearance: "外觀"
clientSettings: "用戶端設定" clientSettings: "用戶端設定"
accountSettings: "帳設定" accountSettings: "帳設定"
promotion: "推廣" promotion: "推廣"
promote: "推廣" promote: "推廣"
numberOfDays: "有效天數" numberOfDays: "有效天數"
@ -477,6 +481,7 @@ showFeaturedNotesInTimeline: "在時間軸上顯示熱門推薦"
objectStorage: "Object Storage (物件儲存)" objectStorage: "Object Storage (物件儲存)"
useObjectStorage: "使用Object Storage" useObjectStorage: "使用Object Storage"
objectStorageBaseUrl: "Base URL" objectStorageBaseUrl: "Base URL"
objectStorageBaseUrlDesc: "引用時的URL。如果您使用的是CDN或反向代理请指定其URL例如S3“https://<bucket>.s3.amazonaws.com”GCS“https://storage.googleapis.com/<bucket>”"
objectStorageBucket: "儲存空間Bucket" objectStorageBucket: "儲存空間Bucket"
objectStorageBucketDesc: "請指定您正在使用的服務的存儲桶名稱。 " objectStorageBucketDesc: "請指定您正在使用的服務的存儲桶名稱。 "
objectStoragePrefix: "前綴" objectStoragePrefix: "前綴"
@ -484,8 +489,11 @@ objectStoragePrefixDesc: "它存儲在此前綴目錄下。"
objectStorageEndpoint: "端點Endpoint" objectStorageEndpoint: "端點Endpoint"
objectStorageEndpointDesc: "如要使用AWS S3請留空。否則請依照你使用的服務商的說明書進行設定以'<host>'或 '<host>:<port>'的形式設定端點Endpoint。" objectStorageEndpointDesc: "如要使用AWS S3請留空。否則請依照你使用的服務商的說明書進行設定以'<host>'或 '<host>:<port>'的形式設定端點Endpoint。"
objectStorageRegion: "地域Region" objectStorageRegion: "地域Region"
objectStorageRegionDesc: "指定一個分區例如“xx-east-1”。 如果您使用的服務沒有分區的概念請留空或填寫“us-east-1”。"
objectStorageUseSSL: "使用SSL" objectStorageUseSSL: "使用SSL"
objectStorageUseSSLDesc: "如果不使用https進行API連接請關閉"
objectStorageUseProxy: "使用網路代理" objectStorageUseProxy: "使用網路代理"
objectStorageUseProxyDesc: "如果不使用代理進行API連接請關閉"
objectStorageSetPublicRead: "上傳時設定為\"public-read\"" objectStorageSetPublicRead: "上傳時設定為\"public-read\""
serverLogs: "伺服器日誌" serverLogs: "伺服器日誌"
deleteAll: "刪除所有記錄" deleteAll: "刪除所有記錄"
@ -513,6 +521,7 @@ sort: "排序"
ascendingOrder: "昇冪" ascendingOrder: "昇冪"
descendingOrder: "降冪" descendingOrder: "降冪"
scratchpad: "暫存記憶體" scratchpad: "暫存記憶體"
scratchpadDescription: "AiScript控制台為AiScript提供了實驗環境。您可以在此編寫、執行和確認代碼與Misskey互動的结果。"
output: "輸出" output: "輸出"
script: "腳本" script: "腳本"
disablePagesScript: "停用頁面的AiScript腳本" disablePagesScript: "停用頁面的AiScript腳本"
@ -523,6 +532,9 @@ removeAllFollowing: "解除所有追蹤"
removeAllFollowingDescription: "解除{host}所有的追蹤。在實例不再存在時執行。" removeAllFollowingDescription: "解除{host}所有的追蹤。在實例不再存在時執行。"
userSuspended: "該使用者已被停用" userSuspended: "該使用者已被停用"
userSilenced: "該用戶已被禁言。" userSilenced: "該用戶已被禁言。"
yourAccountSuspendedTitle: "帳戶已被凍結"
yourAccountSuspendedDescription: "由於違反了伺服器的服務條款或其他原因,該帳戶已被凍結。 您可以與管理員連繫以了解更多訊息。 請不要創建一個新的帳戶。"
menu: "選單"
divider: "分割線" divider: "分割線"
addItem: "新增項目" addItem: "新增項目"
relays: "中繼" relays: "中繼"
@ -546,7 +558,7 @@ enterFileDescription: "輸入標題 "
author: "作者" author: "作者"
leaveConfirm: "有未保存的更改。要放棄嗎?" leaveConfirm: "有未保存的更改。要放棄嗎?"
manage: "管理" manage: "管理"
plugins: "插件" plugins: "外掛"
deck: "多欄模式" deck: "多欄模式"
undeck: "取消多欄模式" undeck: "取消多欄模式"
useBlurEffectForModal: "在模態框使用模糊效果" useBlurEffectForModal: "在模態框使用模糊效果"
@ -556,10 +568,12 @@ height: "高度"
large: "大" large: "大"
medium: "中" medium: "中"
small: "小" small: "小"
generateAccessToken: "發行存取權杖"
permission: "權限" permission: "權限"
enableAll: "啟用全部" enableAll: "啟用全部"
disableAll: "停用全部" disableAll: "停用全部"
tokenRequested: "允許存取帳號" tokenRequested: "允許存取帳戶"
pluginTokenRequestedDescription: "此外掛將擁有在此設定的權限。"
notificationType: "通知形式" notificationType: "通知形式"
edit: "編輯" edit: "編輯"
useStarForReactionFallback: "以★代替未知的表情符號" useStarForReactionFallback: "以★代替未知的表情符號"
@ -574,8 +588,13 @@ smtpPort: "埠"
smtpUser: "使用者名稱" smtpUser: "使用者名稱"
smtpPass: "密碼" smtpPass: "密碼"
emptyToDisableSmtpAuth: "留空使用者名稱和密碼以關閉SMTP驗證。" emptyToDisableSmtpAuth: "留空使用者名稱和密碼以關閉SMTP驗證。"
smtpSecure: "在 SMTP 連接中使用隱式 SSL/TLS"
smtpSecureInfo: "使用STARTTLS時關閉。"
testEmail: "測試郵件發送" testEmail: "測試郵件發送"
wordMute: "靜音文字" wordMute: "被靜音的文字"
regexpError: "正規表達式錯誤"
regexpErrorDescription: "{tab} 靜音文字的第 {line} 行的正規表達式有錯誤:"
instanceMute: "實例的靜音"
userSaysSomething: "{name}說了什麼" userSaysSomething: "{name}說了什麼"
makeActive: "啟用" makeActive: "啟用"
display: "檢視" display: "檢視"
@ -606,6 +625,8 @@ abuseReported: "回報已送出。感謝您的報告。"
reporter: "檢舉者" reporter: "檢舉者"
reporteeOrigin: "檢舉來源" reporteeOrigin: "檢舉來源"
reporterOrigin: "檢舉者來源" reporterOrigin: "檢舉者來源"
forwardReport: "將報告轉送給遠端實例"
forwardReportIsAnonymous: "在遠端實例上看不到您的資訊,顯示的報告者是匿名的系统帳戶。"
send: "發送" send: "發送"
abuseMarkAsResolved: "處理完畢" abuseMarkAsResolved: "處理完畢"
openInNewTab: "在新分頁中開啟" openInNewTab: "在新分頁中開啟"
@ -667,6 +688,7 @@ center: "置中"
wide: "寬" wide: "寬"
narrow: "窄" narrow: "窄"
reloadToApplySetting: "設定將會在頁面重新載入之後生效。要現在就重載頁面嗎?" reloadToApplySetting: "設定將會在頁面重新載入之後生效。要現在就重載頁面嗎?"
needReloadToApply: "必須重新載入才會生效。"
showTitlebar: "顯示標題列" showTitlebar: "顯示標題列"
clearCache: "清除快取資料" clearCache: "清除快取資料"
onlineUsersCount: "{n}人正在線上" onlineUsersCount: "{n}人正在線上"
@ -727,6 +749,7 @@ notRecommended: "不推薦"
botProtection: "Bot防護" botProtection: "Bot防護"
instanceBlocking: "已封鎖的實例" instanceBlocking: "已封鎖的實例"
selectAccount: "選擇帳戶" selectAccount: "選擇帳戶"
switchAccount: "切換帳戶"
enabled: "已啟用" enabled: "已啟用"
disabled: "已停用" disabled: "已停用"
quickAction: "快捷操作" quickAction: "快捷操作"
@ -753,32 +776,92 @@ emailNotConfiguredWarning: "沒有設定電子郵件地址"
ratio: "%" ratio: "%"
previewNoteText: "預覽文本" previewNoteText: "預覽文本"
customCss: "自定義 CSS" customCss: "自定義 CSS"
customCssWarn: "這個設定必須由具備相關知識的人員操作,不當的設定可能导致客戶端無法正常使用。"
global: "公開" global: "公開"
squareAvatars: "頭像以方形顯示"
sent: "發送" sent: "發送"
received: "收取" received: "收取"
searchResult: "搜尋結果" searchResult: "搜尋結果"
hashtags: "#tag" hashtags: "#tag"
troubleshooting: "故障排除" troubleshooting: "故障排除"
useBlurEffect: "在 UI 上使用模糊效果" useBlurEffect: "在 UI 上使用模糊效果"
learnMore: "更多資訊"
misskeyUpdated: "Misskey 更新完成!" misskeyUpdated: "Misskey 更新完成!"
whatIsNew: "顯示更新資訊"
translate: "翻譯" translate: "翻譯"
translatedFrom: "從 {x} 翻譯" translatedFrom: "從 {x} 翻譯"
accountDeletionInProgress: "正在刪除帳戶" accountDeletionInProgress: "正在刪除帳戶"
usernameInfo: "在伺服器上您的帳戶是唯一的識別名稱。您可以使用字母 (a ~ z, A ~ Z)、數字 (0 ~ 9) 和下底線 (_)。之後帳戶名是不能更改的。"
aiChanMode: "小藍模式"
keepCw: "保持CW"
pubSub: "Pub/Sub 帳戶" pubSub: "Pub/Sub 帳戶"
lastCommunication: "最近的通信"
resolved: "已解決" resolved: "已解決"
unresolved: "未解決" unresolved: "未解決"
breakFollow: "移除追蹤者" breakFollow: "移除追蹤者"
itsOn: "已開啟"
itsOff: "已關閉"
emailRequiredForSignup: "註冊帳戶需要電子郵件地址"
unread: "未讀"
filter: "篩選"
controlPanel: "控制台"
manageAccounts: "管理帳戶"
makeReactionsPublic: "將回應設為公開"
makeReactionsPublicDescription: "將您做過的回應設為公開可見。"
classic: "經典"
muteThread: "將貼文串設為靜音"
unmuteThread: "將貼文串的靜音解除"
ffVisibility: "連接的公開範圍"
ffVisibilityDescription: "您可以設定您的關注/關注者資訊的公開範圍"
continueThread: "查看更多貼文"
deleteAccountConfirm: "將要刪除帳戶。是否確定?"
incorrectPassword: "密碼錯誤。"
voteConfirm: "確定投給「{choice}」?"
hide: "隱藏" hide: "隱藏"
leaveGroup: "離開群組"
leaveGroupConfirm: "確定離開「{name}」?" leaveGroupConfirm: "確定離開「{name}」?"
useDrawerReactionPickerForMobile: "在移動設備上使用抽屜顯示"
welcomeBackWithName: "歡迎回來,{name}"
clickToFinishEmailVerification: "點擊 [{ok}] 完成電子郵件地址認證。"
overridedDeviceKind: "裝置類型"
smartphone: "智慧型手機"
tablet: "平板"
auto: "自動" auto: "自動"
themeColor: "主題顏色"
size: "大小"
numberOfColumn: "列數"
searchByGoogle: "搜尋" searchByGoogle: "搜尋"
instanceDefaultLightTheme: "實例預設的淺色主題"
instanceDefaultDarkTheme: "實例預設的深色主題"
instanceDefaultThemeDescription: "輸入物件形式的主题代碼"
mutePeriod: "靜音的期限"
indefinitely: "無期限" indefinitely: "無期限"
tenMinutes: "10分鐘"
oneHour: "1小時"
oneDay: "1天"
oneWeek: "1週"
reflectMayTakeTime: "可能需要一些時間才會出現效果。"
failedToFetchAccountInformation: "取得帳戶資訊失敗"
_emailUnavailable:
used: "已經在使用中"
format: "格式無效"
disposable: "不是永久可用的地址"
mx: "郵件伺服器不正確"
smtp: "郵件伺服器沒有應答"
_ffVisibility: _ffVisibility:
public: "發佈" public: "發佈"
followers: "只有關注你的用戶能看到"
private: "私密" private: "私密"
_signup: _signup:
almostThere: "即將完成" almostThere: "即將完成"
emailAddressInfo: "請輸入您所使用的電子郵件地址。電子郵件地址不會被公開。"
emailSent: "已將確認郵件發送至您輸入的電子郵件地址 ({email})。請開啟電子郵件中的連結以完成帳戶創建。"
_accountDelete: _accountDelete:
accountDelete: "刪除帳戶"
mayTakeTime: "刪除帳戶的處理負荷較大,如果帳戶產生的內容數量上船的檔案數量較多的話,就需要花费一段時間才能完成。"
sendEmail: "帳戶删除完成後,將向註冊地電子郵件地址發送通知。"
requestAccountDelete: "刪除帳戶請求"
started: "已開始刪除作業。"
inProgress: "正在刪除" inProgress: "正在刪除"
_ad: _ad:
back: "返回" back: "返回"
@ -800,7 +883,7 @@ _email:
_plugin: _plugin:
install: "安裝外掛組件" install: "安裝外掛組件"
installWarn: "請不要安裝來源不明的外掛組件。" installWarn: "請不要安裝來源不明的外掛組件。"
manage: "管理插件" manage: "管理外掛"
_registry: _registry:
scope: "範圍" scope: "範圍"
key: "機碼" key: "機碼"
@ -833,14 +916,21 @@ _mfm:
link: "鏈接" link: "鏈接"
linkDescription: "您可以將特定範圍的文章與 URL 相關聯。 " linkDescription: "您可以將特定範圍的文章與 URL 相關聯。 "
bold: "粗體" bold: "粗體"
boldDescription: "可以將文字顯示为粗體来強調。"
small: "縮小" small: "縮小"
smallDescription: "可以使內容文字變小、變淡。"
center: "置中" center: "置中"
centerDescription: "可以將內容置中顯示。"
inlineCode: "程式碼(内嵌)" inlineCode: "程式碼(内嵌)"
inlineCodeDescription: "在行內用高亮度顯示,例如程式碼語法。"
blockCode: "程式碼(區塊)" blockCode: "程式碼(區塊)"
blockCodeDescription: "在區塊中用高亮度顯示,例如複數行的程式碼語法。"
inlineMath: "數學公式(內嵌)" inlineMath: "數學公式(內嵌)"
inlineMathDescription: "顯示內嵌的KaTex數學公式。" inlineMathDescription: "顯示內嵌的KaTex數學公式。"
blockMath: "數學公式(方塊)" blockMath: "數學公式(方塊)"
blockMathDescription: "以區塊顯示複數行的KaTex數學式。"
quote: "引用" quote: "引用"
quoteDescription: "可以用來表示引用的内容。"
emoji: "自訂表情符號" emoji: "自訂表情符號"
emojiDescription: "您可以通過將自定義表情符號名稱括在冒號中來顯示自定義表情符號。 " emojiDescription: "您可以通過將自定義表情符號名稱括在冒號中來顯示自定義表情符號。 "
search: "搜尋" search: "搜尋"
@ -849,22 +939,34 @@ _mfm:
flipDescription: "將內容上下或左右翻轉。" flipDescription: "將內容上下或左右翻轉。"
jelly: "動畫(果凍)" jelly: "動畫(果凍)"
jellyDescription: "顯示果凍一樣的動畫效果。" jellyDescription: "顯示果凍一樣的動畫效果。"
tada: "動畫(鏘~)"
tadaDescription: "顯示「鏘~!」這種感覺的動畫效果。"
jump: "動畫(跳動)" jump: "動畫(跳動)"
jumpDescription: "顯示跳動的動畫效果。"
bounce: "動畫(反彈)" bounce: "動畫(反彈)"
bounceDescription: "顯示有彈性的動畫效果。"
shake: "動畫(搖晃)" shake: "動畫(搖晃)"
shakeDescription: "顯示顫抖的動畫效果。"
twitch: "動畫(顫抖)" twitch: "動畫(顫抖)"
twitchDescription: "顯示強烈顫抖的動畫效果。" twitchDescription: "顯示強烈顫抖的動畫效果。"
spin: "動畫(旋轉)" spin: "動畫(旋轉)"
spinDescription: "顯示旋轉的動畫效果。" spinDescription: "顯示旋轉的動畫效果。"
x2: "大" x2: "大"
x2Description: "放大顯示內容。"
x3: "較大" x3: "較大"
x3Description: "放大顯示內容。" x3Description: "放大顯示內容。"
x4: "最大" x4: "最大"
x4Description: "將顯示內容放至最大。" x4Description: "將顯示內容放至最大。"
blur: "模糊" blur: "模糊"
blurDescription: "產生模糊效果。将游標放在上面即可將内容顯示出來。"
font: "字型" font: "字型"
fontDescription: "您可以設定顯示內容的字型" fontDescription: "您可以設定顯示內容的字型"
rainbow: "彩虹"
rainbowDescription: "用彩虹色來顯示內容。"
sparkle: "閃閃發光"
sparkleDescription: "添加閃閃發光的粒子效果。"
rotate: "旋轉" rotate: "旋轉"
rotateDescription: "以指定的角度旋轉。"
_instanceTicker: _instanceTicker:
none: "隱藏" none: "隱藏"
remote: "向遠端使用者顯示" remote: "向遠端使用者顯示"
@ -884,11 +986,24 @@ _channel:
usersCount: "有{n}人參與" usersCount: "有{n}人參與"
notesCount: "有{n}個貼文" notesCount: "有{n}個貼文"
_menuDisplay: _menuDisplay:
sideFull: "側向"
sideIcon: "側向(圖示)"
top: "頂部"
hide: "隱藏" hide: "隱藏"
_wordMute: _wordMute:
muteWords: "加入靜音文字" muteWords: "加入靜音文字"
muteWordsDescription: "用空格分隔指定AND用換行分隔指定OR。"
muteWordsDescription2: "將關鍵字用斜線括起來表示正規表達式。"
softDescription: "隱藏時間軸中指定條件的貼文。" softDescription: "隱藏時間軸中指定條件的貼文。"
hardDescription: "具有指定條件的貼文將不添加到時間軸。 即使您更改條件,未被添加的貼文也會被排除在外。"
soft: "軟性靜音"
hard: "硬性靜音"
mutedNotes: "已靜音的貼文" mutedNotes: "已靜音的貼文"
_instanceMute:
instanceMuteDescription: "包括對被靜音實例上的用戶的回覆,被設定的實例上所有貼文及轉發都會被靜音。"
instanceMuteDescription2: "設定時以換行進行分隔"
title: "被設定的實例,貼文將被隱藏。"
heading: "將實例靜音"
_theme: _theme:
explore: "取得佈景主題" explore: "取得佈景主題"
install: "安裝佈景主題" install: "安裝佈景主題"
@ -902,10 +1017,12 @@ _theme:
invalid: "主題格式錯誤" invalid: "主題格式錯誤"
make: "製作主題" make: "製作主題"
base: "基於" base: "基於"
addConstant: "添加常數"
constant: "常數" constant: "常數"
defaultValue: "預設值" defaultValue: "預設值"
color: "顏色" color: "顏色"
refProp: "查看屬性 " refProp: "查看屬性 "
refConst: "查看常數"
key: "按鍵" key: "按鍵"
func: "函数" func: "函数"
funcKind: "功能類型" funcKind: "功能類型"
@ -914,6 +1031,9 @@ _theme:
alpha: "透明度" alpha: "透明度"
darken: "暗度" darken: "暗度"
lighten: "亮度" lighten: "亮度"
inputConstantName: "請輸入常數的名稱"
importInfo: "您可以在此貼上主題代碼,將其匯入編輯器中"
deleteConstantConfirm: "確定要删除常數{const}嗎?"
keys: keys:
accent: "重點色彩" accent: "重點色彩"
bg: "背景" bg: "背景"
@ -933,6 +1053,7 @@ _theme:
mention: "提到" mention: "提到"
mentionMe: "提到了我" mentionMe: "提到了我"
renote: "轉發貼文" renote: "轉發貼文"
modalBg: "對話框背景"
divider: "分割線" divider: "分割線"
scrollbarHandle: "捲動條" scrollbarHandle: "捲動條"
scrollbarHandleHover: "捲動條 (漂浮)" scrollbarHandleHover: "捲動條 (漂浮)"
@ -1010,9 +1131,12 @@ _2fa:
registerKey: "註冊鍵" registerKey: "註冊鍵"
step1: "首先,在您的設備上安裝二步驗證程式,例如{a}或{b}。" step1: "首先,在您的設備上安裝二步驗證程式,例如{a}或{b}。"
step2: "然後掃描螢幕上的QR code。" step2: "然後掃描螢幕上的QR code。"
step3: "輸入您的App提供的權杖以完成設定。"
step4: "從現在開始,任何登入操作都將要求您提供權杖。"
securityKeyInfo: "您可以設定使用支援FIDO2的硬體安全鎖、終端設備的指纹認證或者PIN碼來登入。"
_permissions: _permissions:
"read:account": "查看帳戶信息" "read:account": "查看我的帳戶資訊"
"write:account": "更改帳戶信息" "write:account": "更改我的帳戶資訊"
"read:blocks": "已封鎖用戶名單" "read:blocks": "已封鎖用戶名單"
"write:blocks": "編輯已封鎖用戶名單" "write:blocks": "編輯已封鎖用戶名單"
"read:drive": "存取雲端硬碟" "read:drive": "存取雲端硬碟"
@ -1039,6 +1163,10 @@ _permissions:
"write:user-groups": "編輯使用者群組" "write:user-groups": "編輯使用者群組"
"read:channels": "已查看的頻道" "read:channels": "已查看的頻道"
"write:channels": "編輯頻道" "write:channels": "編輯頻道"
"read:gallery": "瀏覽圖庫"
"write:gallery": "操作圖庫"
"read:gallery-likes": "讀取喜歡的圖片"
"write:gallery-likes": "操作喜歡的圖片"
_auth: _auth:
shareAccess: "要授權「“{name}”」存取您的帳戶嗎?" shareAccess: "要授權「“{name}”」存取您的帳戶嗎?"
shareAccessAsk: "您確定要授權這個應用程式使用您的帳戶嗎?" shareAccessAsk: "您確定要授權這個應用程式使用您的帳戶嗎?"
@ -1078,6 +1206,8 @@ _widgets:
onlineUsers: "線上的用戶" onlineUsers: "線上的用戶"
jobQueue: "佇列" jobQueue: "佇列"
serverMetric: "服務器指標 " serverMetric: "服務器指標 "
aiscript: "AiScript控制台"
aichan: "小藍"
_cw: _cw:
hide: "隱藏" hide: "隱藏"
show: "瀏覽更多" show: "瀏覽更多"
@ -1103,12 +1233,15 @@ _poll:
closed: "已結束" closed: "已結束"
remainingDays: "{d}天{h}小時後結束" remainingDays: "{d}天{h}小時後結束"
remainingHours: "{h}小時{m}分後結束" remainingHours: "{h}小時{m}分後結束"
remainingMinutes: "{m}分{s}秒後結束"
remainingSeconds: "{s}秒後截止" remainingSeconds: "{s}秒後截止"
_visibility: _visibility:
public: "公開" public: "公開"
publicDescription: "發布給所有用戶 " publicDescription: "發布給所有用戶 "
home: "首頁" home: "首頁"
homeDescription: "僅發送至首頁的時間軸"
followers: "追隨者" followers: "追隨者"
followersDescription: "僅發送至關注者"
specified: "指定使用者" specified: "指定使用者"
specifiedDescription: "僅發送至指定使用者" specifiedDescription: "僅發送至指定使用者"
localOnly: "僅限本地" localOnly: "僅限本地"
@ -1131,6 +1264,7 @@ _profile:
youCanIncludeHashtags: "你也可以在「關於我」中加上 #tag" youCanIncludeHashtags: "你也可以在「關於我」中加上 #tag"
metadata: "進階資訊" metadata: "進階資訊"
metadataEdit: "編輯進階資訊" metadataEdit: "編輯進階資訊"
metadataDescription: "可以在個人資料中以表格形式顯示其他資訊。"
metadataLabel: "標籤" metadataLabel: "標籤"
metadataContent: "内容" metadataContent: "内容"
changeAvatar: "更換大頭貼" changeAvatar: "更換大頭貼"
@ -1141,6 +1275,8 @@ _exportOrImport:
muteList: "靜音" muteList: "靜音"
blockingList: "封鎖" blockingList: "封鎖"
userLists: "清單" userLists: "清單"
excludeMutingUsers: "排除被靜音的用戶"
excludeInactiveUsers: "排除不活躍帳戶"
_charts: _charts:
federation: "站台聯邦" federation: "站台聯邦"
apRequest: "請求" apRequest: "請求"
@ -1418,6 +1554,7 @@ _pages:
_seedRandomPick: _seedRandomPick:
arg1: "種子" arg1: "種子"
arg2: "清單" arg2: "清單"
DRPWPM: "从機率列表中隨機選擇(每個用户每天)"
_DRPWPM: _DRPWPM:
arg1: "字串串列" arg1: "字串串列"
pick: "從清單中選取" pick: "從清單中選取"
@ -1448,6 +1585,8 @@ _pages:
_for: _for:
arg1: "重複次數" arg1: "重複次數"
arg2: "處理" arg2: "處理"
typeError: "槽參數{slot}需要傳入“{expect}”,但是實際傳入為“{actual}”!"
thereIsEmptySlot: "參數{slot}是空的!"
types: types:
string: "字串" string: "字串"
number: "数值" number: "数值"
@ -1470,10 +1609,13 @@ _notification:
youRenoted: "{name} 轉發了你的貼文" youRenoted: "{name} 轉發了你的貼文"
youGotPoll: "{name}已投票" youGotPoll: "{name}已投票"
youGotMessagingMessageFromUser: "{name}發送給您的訊息" youGotMessagingMessageFromUser: "{name}發送給您的訊息"
youGotMessagingMessageFromGroup: "{name}發送給您的訊息"
youWereFollowed: "您有新的追隨者" youWereFollowed: "您有新的追隨者"
youReceivedFollowRequest: "您有新的追隨請求" youReceivedFollowRequest: "您有新的追隨請求"
yourFollowRequestAccepted: "您的追隨請求已通過" yourFollowRequestAccepted: "您的追隨請求已通過"
youWereInvitedToGroup: "您有新的群組邀請" youWereInvitedToGroup: "您有新的群組邀請"
pollEnded: "問卷調查已產生結果"
emptyPushNotificationMessage: "推送通知已更新"
_types: _types:
all: "全部 " all: "全部 "
follow: "追隨中" follow: "追隨中"
@ -1483,10 +1625,15 @@ _notification:
quote: "引用" quote: "引用"
reaction: "反應" reaction: "反應"
pollVote: "統計已投票數" pollVote: "統計已投票數"
pollEnded: "問卷調查結束"
receiveFollowRequest: "已收到追隨請求" receiveFollowRequest: "已收到追隨請求"
followRequestAccepted: "追隨請求已接受" followRequestAccepted: "追隨請求已接受"
groupInvited: "加入社群邀請" groupInvited: "加入社群邀請"
app: "應用程式通知" app: "應用程式通知"
_actions:
followBack: "回關"
reply: "回覆"
renote: "轉發"
_deck: _deck:
alwaysShowMainColumn: "總是顯示主欄" alwaysShowMainColumn: "總是顯示主欄"
columnAlign: "對齊欄位" columnAlign: "對齊欄位"

6
okteto.yml Normal file
View file

@ -0,0 +1,6 @@
build:
misskey:
args:
- NODE_ENV=development
deploy:
- helm upgrade --install misskey chart --set image=${OKTETO_BUILD_MISSKEY_IMAGE} --set url="https://misskey-$(kubectl config view --minify -o jsonpath='{..namespace}').cloud.okteto.net" --set environment=development

View file

@ -22,7 +22,7 @@
"cy:open": "cypress open", "cy:open": "cypress open",
"cy:run": "cypress run", "cy:run": "cypress run",
"e2e": "start-server-and-test start:test http://localhost:61812 cy:run", "e2e": "start-server-and-test start:test http://localhost:61812 cy:run",
"mocha": "cd packages/backend && cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" npx mocha", "mocha": "cd packages/backend && cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" npx mocha",
"test": "npm run mocha", "test": "npm run mocha",
"format": "gulp format", "format": "gulp format",
"clean": "node ./scripts/clean.js", "clean": "node ./scripts/clean.js",

View file

@ -16,6 +16,17 @@ module.exports = {
'position': 'after' 'position': 'after'
} }
], ],
}] }],
'no-restricted-globals': [
'error',
{
'name': '__dirname',
'message': 'Not in ESModule. Use `import.meta.url` instead.'
},
{
'name': '__filename',
'message': 'Not in ESModule. Use `import.meta.url` instead.'
}
]
}, },
}; };

View file

@ -5,6 +5,6 @@
"loader=./test/loader.js" "loader=./test/loader.js"
], ],
"slow": 1000, "slow": 1000,
"timeout": 35000, "timeout": 3000,
"exit": true "exit": true
} }

View file

@ -0,0 +1,36 @@
import tinycolor from 'tinycolor2';
export class uniformThemecolor1652859567549 {
name = 'uniformThemecolor1652859567549'
async up(queryRunner) {
const formatColor = (color) => {
let tc = new tinycolor(color);
if (tc.isValid()) {
return tc.toHexString();
} else {
return null;
}
};
await queryRunner.query('SELECT "id", "themeColor" FROM "instance" WHERE "themeColor" IS NOT NULL')
.then(instances => Promise.all(instances.map(instance => {
// update theme color to uniform format, e.g. #00ff00
// invalid theme colors get set to null
return queryRunner.query('UPDATE "instance" SET "themeColor" = $1 WHERE "id" = $2', [formatColor(instance.themeColor), instance.id]);
})));
// also fix own theme color
await queryRunner.query('SELECT "themeColor" FROM "meta" WHERE "themeColor" IS NOT NULL LIMIT 1')
.then(metas => {
if (metas.length > 0) {
return queryRunner.query('UPDATE "meta" SET "themeColor" = $1', [formatColor(metas[0].themeColor)]);
}
});
}
async down(queryRunner) {
// The original representation is not stored, so migrating back is not possible.
// The new format also works in older versions so this is not a problem.
}
}

View file

@ -6,7 +6,7 @@
"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": "eslint --quiet \"src/**/*.ts\"", "lint": "eslint --quiet \"src/**/*.ts\"",
"mocha": "cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha", "mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
"test": "npm run mocha" "test": "npm run mocha"
}, },
"resolutions": { "resolutions": {
@ -15,25 +15,24 @@
}, },
"dependencies": { "dependencies": {
"@bull-board/koa": "3.10.4", "@bull-board/koa": "3.10.4",
"@discordapp/twemoji": "13.1.1", "@discordapp/twemoji": "14.0.2",
"@elastic/elasticsearch": "7.11.0", "@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",
"@sinonjs/fake-timers": "9.1.1", "@peertube/http-signature": "1.6.0",
"@sinonjs/fake-timers": "9.1.2",
"@syuilo/aiscript": "0.11.1", "@syuilo/aiscript": "0.11.1",
"@typescript-eslint/eslint-plugin": "5.20.0",
"@typescript-eslint/parser": "5.20.0",
"abort-controller": "3.0.0", "abort-controller": "3.0.0",
"ajv": "8.11.0", "ajv": "8.11.0",
"archiver": "5.3.1", "archiver": "5.3.1",
"autobind-decorator": "2.4.0", "autobind-decorator": "2.4.0",
"autwh": "0.1.0", "autwh": "0.1.0",
"aws-sdk": "2.1120.0", "aws-sdk": "2.1135.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "1.1.5", "blurhash": "1.1.5",
"broadcast-channel": "4.11.0", "broadcast-channel": "4.12.0",
"bull": "4.8.2", "bull": "4.8.3",
"cacheable-lookup": "6.0.4", "cacheable-lookup": "6.0.4",
"cbor": "8.1.0", "cbor": "8.1.0",
"chalk": "5.0.1", "chalk": "5.0.1",
@ -44,22 +43,19 @@
"date-fns": "2.28.0", "date-fns": "2.28.0",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"eslint": "8.14.0",
"eslint-plugin-import": "2.26.0",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "17.1.1", "file-type": "17.1.1",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"got": "12.0.3", "got": "12.0.4",
"hpagent": "0.1.2", "hpagent": "0.1.2",
"http-signature": "1.3.6", "ip-cidr": "3.0.8",
"ip-cidr": "3.0.7",
"is-svg": "4.3.2", "is-svg": "4.3.2",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "19.0.0", "jsdom": "19.0.0",
"json5": "2.2.1", "json5": "2.2.1",
"json5-loader": "4.0.1", "json5-loader": "4.0.1",
"jsonld": "5.2.0", "jsonld": "5.2.0",
"jsrsasign": "10.5.19", "jsrsasign": "10.5.22",
"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",
@ -69,19 +65,18 @@
"koa-send": "5.0.1", "koa-send": "5.0.1",
"koa-slow": "2.1.0", "koa-slow": "2.1.0",
"koa-views": "7.0.2", "koa-views": "7.0.2",
"mfm-js": "0.21.0", "mfm-js": "0.22.1",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"misskey-js": "0.0.14", "misskey-js": "0.0.14",
"mocha": "9.2.2", "mocha": "10.0.0",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
"multer": "1.4.4", "multer": "1.4.4",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.2.3", "node-fetch": "3.2.4",
"nodemailer": "6.7.3", "nodemailer": "6.7.5",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"parse5": "6.0.1", "parse5": "6.0.1",
"pg": "8.7.3", "pg": "8.7.3",
"portscanner": "2.2.0",
"private-ip": "2.3.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",
@ -101,33 +96,32 @@
"s-age": "1.1.2", "s-age": "1.1.2",
"sanitize-html": "2.7.0", "sanitize-html": "2.7.0",
"semver": "7.3.7", "semver": "7.3.7",
"sharp": "0.30.4", "sharp": "0.29.3",
"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", "style-loader": "3.3.1",
"summaly": "2.5.0", "summaly": "2.5.0",
"syslog-pro": "1.0.0", "syslog-pro": "1.0.0",
"systeminformation": "5.11.14", "systeminformation": "5.11.15",
"tinycolor2": "1.4.2", "tinycolor2": "1.4.2",
"tmp": "0.2.1", "tmp": "0.2.1",
"ts-loader": "9.2.8", "ts-loader": "9.3.0",
"ts-node": "10.7.0", "ts-node": "10.8.0",
"tsc-alias": "1.4.1", "tsc-alias": "1.6.7",
"tsconfig-paths": "3.14.1", "tsconfig-paths": "4.0.0",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typeorm": "0.3.6", "typeorm": "0.3.6",
"typescript": "4.6.3",
"ulid": "2.3.0", "ulid": "2.3.0",
"unzipper": "0.10.11", "unzipper": "0.10.11",
"uuid": "8.3.2", "uuid": "8.3.2",
"web-push": "3.4.5", "web-push": "3.5.0",
"websocket": "1.0.34", "websocket": "1.0.34",
"ws": "8.5.0", "ws": "8.6.0",
"xev": "3.0.2" "xev": "3.0.2"
}, },
"devDependencies": { "devDependencies": {
"@redocly/openapi-core": "1.0.0-beta.93", "@redocly/openapi-core": "1.0.0-beta.97",
"@types/semver": "7.3.9", "@types/semver": "7.3.9",
"@types/bcryptjs": "2.4.2", "@types/bcryptjs": "2.4.2",
"@types/bull": "3.15.8", "@types/bull": "3.15.8",
@ -138,7 +132,7 @@
"@types/js-yaml": "4.0.5", "@types/js-yaml": "4.0.5",
"@types/jsdom": "16.2.14", "@types/jsdom": "16.2.14",
"@types/jsonld": "1.5.6", "@types/jsonld": "1.5.6",
"@types/jsrsasign": "10.2.1", "@types/jsrsasign": "10.5.1",
"@types/koa": "2.13.4", "@types/koa": "2.13.4",
"@types/koa-bodyparser": "4.3.7", "@types/koa-bodyparser": "4.3.7",
"@types/koa-cors": "0.0.2", "@types/koa-cors": "0.0.2",
@ -151,12 +145,11 @@
"@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/mocha": "9.1.1",
"@types/node": "17.0.25", "@types/node": "17.0.35",
"@types/node-fetch": "3.0.3", "@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.4", "@types/nodemailer": "6.4.4",
"@types/oauth": "0.9.1", "@types/oauth": "0.9.1",
"@types/parse5": "6.0.3", "@types/parse5": "6.0.3",
"@types/portscanner": "2.1.1",
"@types/pug": "2.0.6", "@types/pug": "2.0.6",
"@types/punycode": "2.1.0", "@types/punycode": "2.1.0",
"@types/qrcode": "1.4.2", "@types/qrcode": "1.4.2",
@ -174,6 +167,12 @@
"@types/web-push": "3.3.2", "@types/web-push": "3.3.2",
"@types/websocket": "1.0.5", "@types/websocket": "1.0.5",
"@types/ws": "8.5.3", "@types/ws": "8.5.3",
"@typescript-eslint/eslint-plugin": "5.26.0",
"@typescript-eslint/parser": "5.26.0",
"typescript": "4.7.2",
"eslint": "8.16.0",
"eslint-plugin-import": "2.26.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"execa": "6.1.0" "execa": "6.1.0"
} }

View file

@ -1,4 +1,4 @@
declare module 'http-signature' { declare module '@peertube/http-signature' {
import { IncomingMessage, ClientRequest } from 'node:http'; import { IncomingMessage, ClientRequest } from 'node:http';
interface ISignature { interface ISignature {

View file

@ -5,7 +5,6 @@ import * as os from 'node:os';
import cluster from 'node:cluster'; import cluster from 'node:cluster';
import chalk from 'chalk'; import chalk from 'chalk';
import chalkTemplate from 'chalk-template'; import chalkTemplate from 'chalk-template';
import * as portscanner from 'portscanner';
import semver from 'semver'; import semver from 'semver';
import Logger from '@/services/logger.js'; import Logger from '@/services/logger.js';
@ -48,11 +47,6 @@ function greet() {
bootLogger.info(`Misskey v${meta.version}`, null, true); bootLogger.info(`Misskey v${meta.version}`, null, true);
} }
function isRoot() {
// maybe process.getuid will be undefined under not POSIX environment (e.g. Windows)
return process.getuid != null && process.getuid() === 0;
}
/** /**
* Init master process * Init master process
*/ */
@ -67,7 +61,6 @@ export async function masterMain() {
showNodejsVersion(); showNodejsVersion();
config = loadConfigBoot(); config = loadConfigBoot();
await connectDb(); await connectDb();
await validatePort(config);
} catch (e) { } catch (e) {
bootLogger.error('Fatal error occurred during initialization', null, true); bootLogger.error('Fatal error occurred during initialization', null, true);
process.exit(1); process.exit(1);
@ -97,8 +90,6 @@ function showEnvironment(): void {
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!', null, true); logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true);
} }
logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`);
} }
function showNodejsVersion(): void { function showNodejsVersion(): void {
@ -152,29 +143,6 @@ async function connectDb(): Promise<void> {
} }
} }
async function validatePort(config: Config): Promise<void> {
const isWellKnownPort = (port: number) => port < 1024;
async function isPortAvailable(port: number): Promise<boolean> {
return await portscanner.checkPortStatus(port, '127.0.0.1') === 'closed';
}
if (config.port == null || Number.isNaN(config.port)) {
bootLogger.error('The port is not configured. Please configure port.', null, true);
process.exit(1);
}
if (process.platform === 'linux' && isWellKnownPort(config.port) && !isRoot()) {
bootLogger.error('You need root privileges to listen on well-known port on Linux', null, true);
process.exit(1);
}
if (!await isPortAvailable(config.port)) {
bootLogger.error(`Port ${config.port} is already in use`, null, true);
process.exit(1);
}
}
async function spawnWorkers(limit: number = 1) { async function spawnWorkers(limit: number = 1) {
const workers = Math.min(limit, os.cpus().length); const workers = Math.min(limit, os.cpus().length);
bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`); bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
@ -186,6 +154,10 @@ function spawnWorker(): Promise<void> {
return new Promise(res => { return new Promise(res => {
const worker = cluster.fork(); const worker = cluster.fork();
worker.on('message', message => { worker.on('message', message => {
if (message === 'listenFailed') {
bootLogger.error(`The server Listen failed due to the previous error.`);
process.exit(1);
}
if (message !== 'ready') return; if (message !== 'ready') return;
res(); res();
}); });

View file

@ -46,7 +46,7 @@ export default function load() {
mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`; mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`;
mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`; mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
mixin.userAgent = `Misskey/${meta.version} (${config.url})`; mixin.userAgent = `Misskey/${meta.version} (${config.url})`;
mixin.clientEntry = clientManifest['src/init.ts'].file.replace(/^_client_dist_\//, ''); mixin.clientEntry = clientManifest['src/init.ts'];
if (!config.redis.prefix) config.redis.prefix = mixin.host; if (!config.redis.prefix) config.redis.prefix = mixin.host;

View file

@ -5,9 +5,6 @@ pg.types.setTypeParser(20, Number);
import { Logger, DataSource } from 'typeorm'; import { Logger, DataSource } from 'typeorm';
import * as highlight from 'cli-highlight'; import * as highlight from 'cli-highlight';
import config from '@/config/index.js'; import config from '@/config/index.js';
import { envOption } from '../env.js';
import { dbLogger } from './logger.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFile } from '@/models/entities/drive-file.js';
@ -74,6 +71,8 @@ import { UserPending } from '@/models/entities/user-pending.js';
import { entities as charts } from '@/services/chart/entities.js'; import { entities as charts } from '@/services/chart/entities.js';
import { Webhook } from '@/models/entities/webhook.js'; import { Webhook } from '@/models/entities/webhook.js';
import { envOption } from '../env.js';
import { dbLogger } from './logger.js';
const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false); const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
@ -212,7 +211,7 @@ export async function initDb() {
if (db.isInitialized) { if (db.isInitialized) {
// nop // nop
} else { } else {
await db.connect(); await db.initialize();
} }
} }

View file

@ -48,6 +48,7 @@ export class Cache<T> {
// Cache MISS // Cache MISS
const value = await fetcher(); const value = await fetcher();
this.set(key, value);
return value; return value;
} }

View file

@ -1,10 +1,19 @@
import * as tmp from 'tmp'; import * as tmp from 'tmp';
export function createTemp(): Promise<[string, any]> { export function createTemp(): Promise<[string, () => void]> {
return new Promise<[string, any]>((res, rej) => { return new Promise<[string, () => void]>((res, rej) => {
tmp.file((e, path, fd, cleanup) => { tmp.file((e, path, fd, cleanup) => {
if (e) return rej(e); if (e) return rej(e);
res([path, cleanup]); res([path, cleanup]);
}); });
}); });
} }
export function createTempDir(): Promise<[string, () => void]> {
return new Promise<[string, () => void]>((res, rej) => {
tmp.dir((e, path, cleanup) => {
if (e) return rej(e);
res([path, cleanup]);
});
});
}

View file

@ -20,9 +20,16 @@ export async function fetchMeta(noCache = false): Promise<Meta> {
cache = meta; cache = meta;
return meta; return meta;
} else { } else {
const saved = await transactionalEntityManager.save(Meta, { // metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う
id: 'x', const saved = await transactionalEntityManager
}) as Meta; .upsert(
Meta,
{
id: 'x',
},
['id'],
)
.then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0]));
cache = saved; cache = saved;
return saved; return saved;

View file

@ -144,13 +144,7 @@ export const NoteRepository = db.getRepository(Note).extend({
return true; return true;
} else { } else {
// 指定されているかどうか // 指定されているかどうか
const specified = note.visibleUserIds.some((id: any) => meId === id); return note.visibleUserIds.some((id: any) => meId === id);
if (specified) {
return true;
} else {
return false;
}
} }
} }
@ -168,16 +162,25 @@ export const NoteRepository = db.getRepository(Note).extend({
return true; return true;
} else { } else {
// フォロワーかどうか // フォロワーかどうか
const following = await Followings.findOneBy({ const [following, user] = await Promise.all([
followeeId: note.userId, Followings.count({
followerId: meId, where: {
}); followeeId: note.userId,
followerId: meId,
},
take: 1,
}),
Users.findOneByOrFail({ id: meId }),
]);
if (following == null) { /* If we know the following, everyhting is fine.
return false;
} else { But if we do not know the following, it might be that both the
return true; author of the note and the author of the like are remote users,
} in which case we can never know the following. Instead we have
to assume that the users are following each other.
*/
return following > 0 || (note.userHost != null && user.host != null);
} }
} }

View file

@ -61,47 +61,58 @@ export const UserRepository = db.getRepository(User).extend({
//#endregion //#endregion
async getRelation(me: User['id'], target: User['id']) { async getRelation(me: User['id'], target: User['id']) {
const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([ return awaitAll({
Followings.findOneBy({
followerId: me,
followeeId: target,
}),
Followings.findOneBy({
followerId: target,
followeeId: me,
}),
FollowRequests.findOneBy({
followerId: me,
followeeId: target,
}),
FollowRequests.findOneBy({
followerId: target,
followeeId: me,
}),
Blockings.findOneBy({
blockerId: me,
blockeeId: target,
}),
Blockings.findOneBy({
blockerId: target,
blockeeId: me,
}),
Mutings.findOneBy({
muterId: me,
muteeId: target,
}),
]);
return {
id: target, id: target,
isFollowing: following1 != null, isFollowing: Followings.count({
hasPendingFollowRequestFromYou: followReq1 != null, where: {
hasPendingFollowRequestToYou: followReq2 != null, followerId: me,
isFollowed: following2 != null, followeeId: target,
isBlocking: toBlocking != null, },
isBlocked: fromBlocked != null, take: 1,
isMuted: mute != null, }).then(n => n > 0),
}; isFollowed: Followings.count({
where: {
followerId: target,
followeeId: me,
},
take: 1,
}).then(n => n > 0),
hasPendingFollowRequestFromYou: FollowRequests.count({
where: {
followerId: me,
followeeId: target,
},
take: 1,
}).then(n => n > 0),
hasPendingFollowRequestToYou: FollowRequests.count({
where: {
followerId: target,
followeeId: me,
},
take: 1,
}).then(n => n > 0),
isBlocking: Blockings.count({
where: {
blockerId: me,
blockeeId: target,
},
take: 1,
}).then(n => n > 0),
isBlocked: Blockings.count({
where: {
blockerId: target,
blockeeId: me,
},
take: 1,
}).then(n => n > 0),
isMuted: Mutings.count({
where: {
muterId: me,
muteeId: target,
},
take: 1,
}).then(n => n > 0),
});
}, },
async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> { async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> {

View file

@ -1,4 +1,4 @@
import httpSignature from 'http-signature'; import httpSignature from '@peertube/http-signature';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import config from '@/config/index.js'; import config from '@/config/index.js';

View file

@ -1,11 +1,11 @@
import Bull from 'bull'; import Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import { queueLogger } from '../../logger.js'; import { queueLogger } from '../../logger.js';
import { addFile } from '@/services/drive/add-file.js'; import { addFile } from '@/services/drive/add-file.js';
import { format as dateFormat } from 'date-fns'; import { format as dateFormat } from 'date-fns';
import { getFullApAccount } from '@/misc/convert-host.js'; import { getFullApAccount } from '@/misc/convert-host.js';
import { createTemp } from '@/misc/create-temp.js';
import { Users, Blockings } from '@/models/index.js'; import { Users, Blockings } from '@/models/index.js';
import { MoreThan } from 'typeorm'; import { MoreThan } from 'typeorm';
import { DbUserJobData } from '@/queue/types.js'; import { DbUserJobData } from '@/queue/types.js';
@ -22,73 +22,72 @@ export async function exportBlocking(job: Bull.Job<DbUserJobData>, done: any): P
} }
// Create temp file // Create temp file
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { const [path, cleanup] = await createTemp();
tmp.file((e, path, fd, cleanup) => {
if (e) return rej(e);
res([path, cleanup]);
});
});
logger.info(`Temp file is ${path}`); logger.info(`Temp file is ${path}`);
const stream = fs.createWriteStream(path, { flags: 'a' }); try {
const stream = fs.createWriteStream(path, { flags: 'a' });
let exportedCount = 0; let exportedCount = 0;
let cursor: any = null; let cursor: any = null;
while (true) { while (true) {
const blockings = await Blockings.find({ const blockings = await Blockings.find({
where: { where: {
blockerId: user.id, blockerId: user.id,
...(cursor ? { id: MoreThan(cursor) } : {}), ...(cursor ? { id: MoreThan(cursor) } : {}),
}, },
take: 100, take: 100,
order: { order: {
id: 1, id: 1,
}, },
}); });
if (blockings.length === 0) { if (blockings.length === 0) {
job.progress(100); job.progress(100);
break; break;
}
cursor = blockings[blockings.length - 1].id;
for (const block of blockings) {
const u = await Users.findOneBy({ id: block.blockeeId });
if (u == null) {
exportedCount++; continue;
} }
const content = getFullApAccount(u.username, u.host); cursor = blockings[blockings.length - 1].id;
await new Promise<void>((res, rej) => {
stream.write(content + '\n', err => { for (const block of blockings) {
if (err) { const u = await Users.findOneBy({ id: block.blockeeId });
logger.error(err); if (u == null) {
rej(err); exportedCount++; continue;
} else { }
res();
} const content = getFullApAccount(u.username, u.host);
await new Promise<void>((res, rej) => {
stream.write(content + '\n', err => {
if (err) {
logger.error(err);
rej(err);
} else {
res();
}
});
}); });
exportedCount++;
}
const total = await Blockings.countBy({
blockerId: user.id,
}); });
exportedCount++;
job.progress(exportedCount / total);
} }
const total = await Blockings.countBy({ stream.end();
blockerId: user.id, logger.succ(`Exported to: ${path}`);
});
job.progress(exportedCount / total); const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
const driveFile = await addFile({ user, path, name: fileName, force: true });
logger.succ(`Exported to: ${driveFile.id}`);
} finally {
cleanup();
} }
stream.end();
logger.succ(`Exported to: ${path}`);
const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
const driveFile = await addFile({ user, path, name: fileName, force: true });
logger.succ(`Exported to: ${driveFile.id}`);
cleanup();
done(); done();
} }

View file

@ -1,5 +1,4 @@
import Bull from 'bull'; import Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import { ulid } from 'ulid'; import { ulid } from 'ulid';
@ -10,6 +9,7 @@ import { addFile } from '@/services/drive/add-file.js';
import { format as dateFormat } from 'date-fns'; import { format as dateFormat } from 'date-fns';
import { Users, Emojis } from '@/models/index.js'; import { Users, Emojis } from '@/models/index.js';
import { } from '@/queue/types.js'; import { } from '@/queue/types.js';
import { createTemp, createTempDir } from '@/misc/create-temp.js';
import { downloadUrl } from '@/misc/download-url.js'; import { downloadUrl } from '@/misc/download-url.js';
import config from '@/config/index.js'; import config from '@/config/index.js';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
@ -25,13 +25,7 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi
return; return;
} }
// Create temp dir const [path, cleanup] = await createTempDir();
const [path, cleanup] = await new Promise<[string, () => void]>((res, rej) => {
tmp.dir((e, path, cleanup) => {
if (e) return rej(e);
res([path, cleanup]);
});
});
logger.info(`Temp dir is ${path}`); logger.info(`Temp dir is ${path}`);
@ -98,12 +92,7 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi
metaStream.end(); metaStream.end();
// Create archive // Create archive
const [archivePath, archiveCleanup] = await new Promise<[string, () => void]>((res, rej) => { const [archivePath, archiveCleanup] = await createTemp();
tmp.file((e, path, fd, cleanup) => {
if (e) return rej(e);
res([path, cleanup]);
});
});
const archiveStream = fs.createWriteStream(archivePath); const archiveStream = fs.createWriteStream(archivePath);
const archive = archiver('zip', { const archive = archiver('zip', {
zlib: { level: 0 }, zlib: { level: 0 },

View file

@ -1,11 +1,11 @@
import Bull from 'bull'; import Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import { queueLogger } from '../../logger.js'; import { queueLogger } from '../../logger.js';
import { addFile } from '@/services/drive/add-file.js'; import { addFile } from '@/services/drive/add-file.js';
import { format as dateFormat } from 'date-fns'; import { format as dateFormat } from 'date-fns';
import { getFullApAccount } from '@/misc/convert-host.js'; import { getFullApAccount } from '@/misc/convert-host.js';
import { createTemp } from '@/misc/create-temp.js';
import { Users, Followings, Mutings } from '@/models/index.js'; import { Users, Followings, Mutings } from '@/models/index.js';
import { In, MoreThan, Not } from 'typeorm'; import { In, MoreThan, Not } from 'typeorm';
import { DbUserJobData } from '@/queue/types.js'; import { DbUserJobData } from '@/queue/types.js';
@ -23,73 +23,72 @@ export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: () =>
} }
// Create temp file // Create temp file
const [path, cleanup] = await new Promise<[string, () => void]>((res, rej) => { const [path, cleanup] = await createTemp();
tmp.file((e, path, fd, cleanup) => {
if (e) return rej(e);
res([path, cleanup]);
});
});
logger.info(`Temp file is ${path}`); logger.info(`Temp file is ${path}`);
const stream = fs.createWriteStream(path, { flags: 'a' }); try {
const stream = fs.createWriteStream(path, { flags: 'a' });
let cursor: Following['id'] | null = null; let cursor: Following['id'] | null = null;
const mutings = job.data.excludeMuting ? await Mutings.findBy({ const mutings = job.data.excludeMuting ? await Mutings.findBy({
muterId: user.id, muterId: user.id,
}) : []; }) : [];
while (true) { while (true) {
const followings = await Followings.find({ const followings = await Followings.find({
where: { where: {
followerId: user.id, followerId: user.id,
...(mutings.length > 0 ? { followeeId: Not(In(mutings.map(x => x.muteeId))) } : {}), ...(mutings.length > 0 ? { followeeId: Not(In(mutings.map(x => x.muteeId))) } : {}),
...(cursor ? { id: MoreThan(cursor) } : {}), ...(cursor ? { id: MoreThan(cursor) } : {}),
}, },
take: 100, take: 100,
order: { order: {
id: 1, id: 1,
}, },
}) as Following[]; }) as Following[];
if (followings.length === 0) { if (followings.length === 0) {
break; break;
}
cursor = followings[followings.length - 1].id;
for (const following of followings) {
const u = await Users.findOneBy({ id: following.followeeId });
if (u == null) {
continue;
} }
if (job.data.excludeInactive && u.updatedAt && (Date.now() - u.updatedAt.getTime() > 1000 * 60 * 60 * 24 * 90)) { cursor = followings[followings.length - 1].id;
continue;
}
const content = getFullApAccount(u.username, u.host); for (const following of followings) {
await new Promise<void>((res, rej) => { const u = await Users.findOneBy({ id: following.followeeId });
stream.write(content + '\n', err => { if (u == null) {
if (err) { continue;
logger.error(err); }
rej(err);
} else { if (job.data.excludeInactive && u.updatedAt && (Date.now() - u.updatedAt.getTime() > 1000 * 60 * 60 * 24 * 90)) {
res(); continue;
} }
const content = getFullApAccount(u.username, u.host);
await new Promise<void>((res, rej) => {
stream.write(content + '\n', err => {
if (err) {
logger.error(err);
rej(err);
} else {
res();
}
});
}); });
}); }
} }
stream.end();
logger.succ(`Exported to: ${path}`);
const fileName = 'following-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
const driveFile = await addFile({ user, path, name: fileName, force: true });
logger.succ(`Exported to: ${driveFile.id}`);
} finally {
cleanup();
} }
stream.end();
logger.succ(`Exported to: ${path}`);
const fileName = 'following-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
const driveFile = await addFile({ user, path, name: fileName, force: true });
logger.succ(`Exported to: ${driveFile.id}`);
cleanup();
done(); done();
} }

View file

@ -1,11 +1,11 @@
import Bull from 'bull'; import Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import { queueLogger } from '../../logger.js'; import { queueLogger } from '../../logger.js';
import { addFile } from '@/services/drive/add-file.js'; import { addFile } from '@/services/drive/add-file.js';
import { format as dateFormat } from 'date-fns'; import { format as dateFormat } from 'date-fns';
import { getFullApAccount } from '@/misc/convert-host.js'; import { getFullApAccount } from '@/misc/convert-host.js';
import { createTemp } from '@/misc/create-temp.js';
import { Users, Mutings } from '@/models/index.js'; import { Users, Mutings } from '@/models/index.js';
import { IsNull, MoreThan } from 'typeorm'; import { IsNull, MoreThan } from 'typeorm';
import { DbUserJobData } from '@/queue/types.js'; import { DbUserJobData } from '@/queue/types.js';
@ -22,74 +22,73 @@ export async function exportMute(job: Bull.Job<DbUserJobData>, done: any): Promi
} }
// Create temp file // Create temp file
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { const [path, cleanup] = await createTemp();
tmp.file((e, path, fd, cleanup) => {
if (e) return rej(e);
res([path, cleanup]);
});
});
logger.info(`Temp file is ${path}`); logger.info(`Temp file is ${path}`);
const stream = fs.createWriteStream(path, { flags: 'a' }); try {
const stream = fs.createWriteStream(path, { flags: 'a' });
let exportedCount = 0; let exportedCount = 0;
let cursor: any = null; let cursor: any = null;
while (true) { while (true) {
const mutes = await Mutings.find({ const mutes = await Mutings.find({
where: { where: {
muterId: user.id, muterId: user.id,
expiresAt: IsNull(), expiresAt: IsNull(),
...(cursor ? { id: MoreThan(cursor) } : {}), ...(cursor ? { id: MoreThan(cursor) } : {}),
}, },
take: 100, take: 100,
order: { order: {
id: 1, id: 1,
}, },
}); });
if (mutes.length === 0) { if (mutes.length === 0) {
job.progress(100); job.progress(100);
break; break;
}
cursor = mutes[mutes.length - 1].id;
for (const mute of mutes) {
const u = await Users.findOneBy({ id: mute.muteeId });
if (u == null) {
exportedCount++; continue;
} }
const content = getFullApAccount(u.username, u.host); cursor = mutes[mutes.length - 1].id;
await new Promise<void>((res, rej) => {
stream.write(content + '\n', err => { for (const mute of mutes) {
if (err) { const u = await Users.findOneBy({ id: mute.muteeId });
logger.error(err); if (u == null) {
rej(err); exportedCount++; continue;
} else { }
res();
} const content = getFullApAccount(u.username, u.host);
await new Promise<void>((res, rej) => {
stream.write(content + '\n', err => {
if (err) {
logger.error(err);
rej(err);
} else {
res();
}
});
}); });
exportedCount++;
}
const total = await Mutings.countBy({
muterId: user.id,
}); });
exportedCount++;
job.progress(exportedCount / total);
} }
const total = await Mutings.countBy({ stream.end();
muterId: user.id, logger.succ(`Exported to: ${path}`);
});
job.progress(exportedCount / total); const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
const driveFile = await addFile({ user, path, name: fileName, force: true });
logger.succ(`Exported to: ${driveFile.id}`);
} finally {
cleanup();
} }
stream.end();
logger.succ(`Exported to: ${path}`);
const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
const driveFile = await addFile({ user, path, name: fileName, force: true });
logger.succ(`Exported to: ${driveFile.id}`);
cleanup();
done(); done();
} }

View file

@ -1,5 +1,4 @@
import Bull from 'bull'; import Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import { queueLogger } from '../../logger.js'; import { queueLogger } from '../../logger.js';
@ -10,6 +9,7 @@ import { MoreThan } from 'typeorm';
import { Note } from '@/models/entities/note.js'; import { Note } from '@/models/entities/note.js';
import { Poll } from '@/models/entities/poll.js'; import { Poll } from '@/models/entities/poll.js';
import { DbUserJobData } from '@/queue/types.js'; import { DbUserJobData } from '@/queue/types.js';
import { createTemp } from '@/misc/create-temp.js';
const logger = queueLogger.createSubLogger('export-notes'); const logger = queueLogger.createSubLogger('export-notes');
@ -23,82 +23,81 @@ export async function exportNotes(job: Bull.Job<DbUserJobData>, done: any): Prom
} }
// Create temp file // Create temp file
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { const [path, cleanup] = await createTemp();
tmp.file((e, path, fd, cleanup) => {
if (e) return rej(e);
res([path, cleanup]);
});
});
logger.info(`Temp file is ${path}`); logger.info(`Temp file is ${path}`);
const stream = fs.createWriteStream(path, { flags: 'a' }); try {
const stream = fs.createWriteStream(path, { flags: 'a' });
const write = (text: string): Promise<void> => { const write = (text: string): Promise<void> => {
return new Promise<void>((res, rej) => { return new Promise<void>((res, rej) => {
stream.write(text, err => { stream.write(text, err => {
if (err) { if (err) {
logger.error(err); logger.error(err);
rej(err); rej(err);
} else { } else {
res(); res();
} }
});
}); });
}); };
};
await write('['); await write('[');
let exportedNotesCount = 0; let exportedNotesCount = 0;
let cursor: Note['id'] | null = null; let cursor: Note['id'] | null = null;
while (true) { while (true) {
const notes = await Notes.find({ const notes = await Notes.find({
where: { where: {
userId: user.id, userId: user.id,
...(cursor ? { id: MoreThan(cursor) } : {}), ...(cursor ? { id: MoreThan(cursor) } : {}),
}, },
take: 100, take: 100,
order: { order: {
id: 1, id: 1,
}, },
}) as Note[]; }) as Note[];
if (notes.length === 0) { if (notes.length === 0) {
job.progress(100); job.progress(100);
break; break;
}
cursor = notes[notes.length - 1].id;
for (const note of notes) {
let poll: Poll | undefined;
if (note.hasPoll) {
poll = await Polls.findOneByOrFail({ noteId: note.id });
} }
const content = JSON.stringify(serialize(note, poll));
const isFirst = exportedNotesCount === 0; cursor = notes[notes.length - 1].id;
await write(isFirst ? content : ',\n' + content);
exportedNotesCount++; for (const note of notes) {
let poll: Poll | undefined;
if (note.hasPoll) {
poll = await Polls.findOneByOrFail({ noteId: note.id });
}
const content = JSON.stringify(serialize(note, poll));
const isFirst = exportedNotesCount === 0;
await write(isFirst ? content : ',\n' + content);
exportedNotesCount++;
}
const total = await Notes.countBy({
userId: user.id,
});
job.progress(exportedNotesCount / total);
} }
const total = await Notes.countBy({ await write(']');
userId: user.id,
});
job.progress(exportedNotesCount / total); stream.end();
logger.succ(`Exported to: ${path}`);
const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
const driveFile = await addFile({ user, path, name: fileName, force: true });
logger.succ(`Exported to: ${driveFile.id}`);
} finally {
cleanup();
} }
await write(']');
stream.end();
logger.succ(`Exported to: ${path}`);
const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
const driveFile = await addFile({ user, path, name: fileName, force: true });
logger.succ(`Exported to: ${driveFile.id}`);
cleanup();
done(); done();
} }

View file

@ -1,11 +1,11 @@
import Bull from 'bull'; import Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import { queueLogger } from '../../logger.js'; import { queueLogger } from '../../logger.js';
import { addFile } from '@/services/drive/add-file.js'; import { addFile } from '@/services/drive/add-file.js';
import { format as dateFormat } from 'date-fns'; import { format as dateFormat } from 'date-fns';
import { getFullApAccount } from '@/misc/convert-host.js'; import { getFullApAccount } from '@/misc/convert-host.js';
import { createTemp } from '@/misc/create-temp.js';
import { Users, UserLists, UserListJoinings } from '@/models/index.js'; import { Users, UserLists, UserListJoinings } from '@/models/index.js';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { DbUserJobData } from '@/queue/types.js'; import { DbUserJobData } from '@/queue/types.js';
@ -26,46 +26,45 @@ export async function exportUserLists(job: Bull.Job<DbUserJobData>, done: any):
}); });
// Create temp file // Create temp file
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { const [path, cleanup] = await createTemp();
tmp.file((e, path, fd, cleanup) => {
if (e) return rej(e);
res([path, cleanup]);
});
});
logger.info(`Temp file is ${path}`); logger.info(`Temp file is ${path}`);
const stream = fs.createWriteStream(path, { flags: 'a' }); try {
const stream = fs.createWriteStream(path, { flags: 'a' });
for (const list of lists) { for (const list of lists) {
const joinings = await UserListJoinings.findBy({ userListId: list.id }); const joinings = await UserListJoinings.findBy({ userListId: list.id });
const users = await Users.findBy({ const users = await Users.findBy({
id: In(joinings.map(j => j.userId)), id: In(joinings.map(j => j.userId)),
});
for (const u of users) {
const acct = getFullApAccount(u.username, u.host);
const content = `${list.name},${acct}`;
await new Promise<void>((res, rej) => {
stream.write(content + '\n', err => {
if (err) {
logger.error(err);
rej(err);
} else {
res();
}
});
}); });
for (const u of users) {
const acct = getFullApAccount(u.username, u.host);
const content = `${list.name},${acct}`;
await new Promise<void>((res, rej) => {
stream.write(content + '\n', err => {
if (err) {
logger.error(err);
rej(err);
} else {
res();
}
});
});
}
} }
stream.end();
logger.succ(`Exported to: ${path}`);
const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
const driveFile = await addFile({ user, path, name: fileName, force: true });
logger.succ(`Exported to: ${driveFile.id}`);
} finally {
cleanup();
} }
stream.end();
logger.succ(`Exported to: ${path}`);
const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
const driveFile = await addFile({ user, path, name: fileName, force: true });
logger.succ(`Exported to: ${driveFile.id}`);
cleanup();
done(); done();
} }

View file

@ -1,9 +1,9 @@
import Bull from 'bull'; import Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import unzipper from 'unzipper'; import unzipper from 'unzipper';
import { queueLogger } from '../../logger.js'; import { queueLogger } from '../../logger.js';
import { createTempDir } from '@/misc/create-temp.js';
import { downloadUrl } from '@/misc/download-url.js'; import { downloadUrl } from '@/misc/download-url.js';
import { DriveFiles, Emojis } from '@/models/index.js'; import { DriveFiles, Emojis } from '@/models/index.js';
import { DbUserImportJobData } from '@/queue/types.js'; import { DbUserImportJobData } from '@/queue/types.js';
@ -25,13 +25,7 @@ export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, don
return; return;
} }
// Create temp dir const [path, cleanup] = await createTempDir();
const [path, cleanup] = await new Promise<[string, () => void]>((res, rej) => {
tmp.dir((e, path, cleanup) => {
if (e) return rej(e);
res([path, cleanup]);
});
});
logger.info(`Temp dir is ${path}`); logger.info(`Temp dir is ${path}`);

View file

@ -1,6 +1,6 @@
import { URL } from 'node:url'; import { URL } from 'node:url';
import Bull from 'bull'; import Bull from 'bull';
import httpSignature from 'http-signature'; import httpSignature from '@peertube/http-signature';
import perform from '@/remote/activitypub/perform.js'; import perform from '@/remote/activitypub/perform.js';
import Logger from '@/services/logger.js'; import Logger from '@/services/logger.js';
import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js'; import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js';

View file

@ -3,7 +3,7 @@ import { Note } from '@/models/entities/note';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
import { Webhook } from '@/models/entities/webhook'; import { Webhook } from '@/models/entities/webhook';
import { IActivity } from '@/remote/activitypub/type.js'; import { IActivity } from '@/remote/activitypub/type.js';
import httpSignature from 'http-signature'; import httpSignature from '@peertube/http-signature';
export type DeliverJobData = { export type DeliverJobData = {
/** Actor */ /** Actor */

View file

@ -9,6 +9,7 @@ import { fetchMeta } from '@/misc/fetch-meta.js';
import { getApLock } from '@/misc/app-lock.js'; import { getApLock } from '@/misc/app-lock.js';
import { parseAudience } from '../../audience.js'; import { parseAudience } from '../../audience.js';
import { StatusError } from '@/misc/fetch.js'; import { StatusError } from '@/misc/fetch.js';
import { Notes } from '@/models/index.js';
const logger = apLogger; const logger = apLogger;
@ -52,6 +53,8 @@ export default async function(resolver: Resolver, actor: CacheableRemoteUser, ac
throw e; throw e;
} }
if (!await Notes.isVisibleForMe(renote, actor.id)) return 'skip: invalid actor for this activity';
logger.info(`Creating the (Re)Note: ${uri}`); logger.info(`Creating the (Re)Note: ${uri}`);
const activityAudience = await parseAudience(actor, activity.to, activity.cc); const activityAudience = await parseAudience(actor, activity.to, activity.cc);

View file

@ -13,37 +13,37 @@ export default async (actor: CacheableRemoteUser, activity: IDelete): Promise<st
} }
// 削除対象objectのtype // 削除対象objectのtype
let formarType: string | undefined; let formerType: string | undefined;
if (typeof activity.object === 'string') { if (typeof activity.object === 'string') {
// typeが不明だけど、どうせ消えてるのでremote resolveしない // typeが不明だけど、どうせ消えてるのでremote resolveしない
formarType = undefined; formerType = undefined;
} else { } else {
const object = activity.object as IObject; const object = activity.object as IObject;
if (isTombstone(object)) { if (isTombstone(object)) {
formarType = toSingle(object.formerType); formerType = toSingle(object.formerType);
} else { } else {
formarType = toSingle(object.type); formerType = toSingle(object.type);
} }
} }
const uri = getApId(activity.object); const uri = getApId(activity.object);
// type不明でもactorとobjectが同じならばそれはPersonに違いない // type不明でもactorとobjectが同じならばそれはPersonに違いない
if (!formarType && actor.uri === uri) { if (!formerType && actor.uri === uri) {
formarType = 'Person'; formerType = 'Person';
} }
// それでもなかったらおそらくNote // それでもなかったらおそらくNote
if (!formarType) { if (!formerType) {
formarType = 'Note'; formerType = 'Note';
} }
if (validPost.includes(formarType)) { if (validPost.includes(formerType)) {
return await deleteNote(actor, uri); return await deleteNote(actor, uri);
} else if (validActor.includes(formarType)) { } else if (validActor.includes(formerType)) {
return await deleteActor(actor, uri); return await deleteActor(actor, uri);
} else { } else {
return `Unknown type ${formarType}`; return `Unknown type ${formerType}`;
} }
}; };

View file

@ -8,6 +8,7 @@ export const undoAnnounce = async (actor: CacheableRemoteUser, activity: IAnnoun
const note = await Notes.findOneBy({ const note = await Notes.findOneBy({
uri, uri,
userId: actor.id,
}); });
if (!note) return 'skip: no such Announce'; if (!note) return 'skip: no such Announce';

View file

@ -3,9 +3,9 @@ import promiseLimit from 'promise-limit';
import config from '@/config/index.js'; import config from '@/config/index.js';
import Resolver from '../resolver.js'; import Resolver from '../resolver.js';
import post from '@/services/note/create.js'; import post from '@/services/note/create.js';
import { resolvePerson, updatePerson } from './person.js'; import { resolvePerson } from './person.js';
import { resolveImage } from './image.js'; import { resolveImage } from './image.js';
import { CacheableRemoteUser, IRemoteUser } from '@/models/entities/user.js'; import { CacheableRemoteUser } from '@/models/entities/user.js';
import { htmlToMfm } from '../misc/html-to-mfm.js'; import { htmlToMfm } from '../misc/html-to-mfm.js';
import { extractApHashtags } from './tag.js'; import { extractApHashtags } from './tag.js';
import { unique, toArray, toSingle } from '@/prelude/array.js'; import { unique, toArray, toSingle } from '@/prelude/array.js';
@ -15,7 +15,7 @@ import { apLogger } from '../logger.js';
import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFile } from '@/models/entities/drive-file.js';
import { deliverQuestionUpdate } from '@/services/note/polls/update.js'; import { deliverQuestionUpdate } from '@/services/note/polls/update.js';
import { extractDbHost, toPuny } from '@/misc/convert-host.js'; import { extractDbHost, toPuny } from '@/misc/convert-host.js';
import { Emojis, Polls, MessagingMessages, Users } from '@/models/index.js'; import { Emojis, Polls, MessagingMessages } from '@/models/index.js';
import { Note } from '@/models/entities/note.js'; import { Note } from '@/models/entities/note.js';
import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type.js'; import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type.js';
import { Emoji } from '@/models/entities/emoji.js'; import { Emoji } from '@/models/entities/emoji.js';

View file

@ -8,7 +8,7 @@ import { User } from '@/models/entities/user.js';
export const renderActivity = (x: any): IActivity | null => { export const renderActivity = (x: any): IActivity | null => {
if (x == null) return null; if (x == null) return null;
if (x !== null && typeof x === 'object' && x.id == null) { if (typeof x === 'object' && x.id == null) {
x.id = `${config.url}/${uuid()}`; x.id = `${config.url}/${uuid()}`;
} }

View file

@ -1,6 +1,6 @@
import Router from '@koa/router'; import Router from '@koa/router';
import json from 'koa-json-body'; import json from 'koa-json-body';
import httpSignature from 'http-signature'; import httpSignature from '@peertube/http-signature';
import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import renderNote from '@/remote/activitypub/renderer/note.js'; import renderNote from '@/remote/activitypub/renderer/note.js';

View file

@ -2,10 +2,11 @@ import Koa from 'koa';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import { limiter } from './limiter.js'; import { limiter } from './limiter.js';
import { CacheableLocalUser, User } from '@/models/entities/user.js'; import { CacheableLocalUser, User } from '@/models/entities/user.js';
import endpoints, { IEndpoint } from './endpoints.js'; import endpoints, { IEndpointMeta } from './endpoints.js';
import { ApiError } from './error.js'; import { ApiError } from './error.js';
import { apiLogger } from './logger.js'; import { apiLogger } from './logger.js';
import { AccessToken } from '@/models/entities/access-token.js'; import { AccessToken } from '@/models/entities/access-token.js';
import IPCIDR from 'ip-cidr';
const accessDenied = { const accessDenied = {
message: 'Access denied.', message: 'Access denied.',
@ -15,6 +16,7 @@ const accessDenied = {
export default async (endpoint: string, user: CacheableLocalUser | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => { export default async (endpoint: string, user: CacheableLocalUser | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => {
const isSecure = user != null && token == null; const isSecure = user != null && token == null;
const isModerator = user != null && (user.isModerator || user.isAdmin);
const ep = endpoints.find(e => e.name === endpoint); const ep = endpoints.find(e => e.name === endpoint);
@ -31,6 +33,37 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
throw new ApiError(accessDenied); throw new ApiError(accessDenied);
} }
if (ep.meta.requireCredential && ep.meta.limit && !isModerator) {
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
let limitActor: string;
if (user) {
limitActor = user.id;
} else {
// because a single person may control many IPv6 addresses,
// only a /64 subnet prefix of any IP will be taken into account.
// (this means for IPv4 the entire address is used)
const ip = IPCIDR.createAddress(ctx.ip).mask(64);
limitActor = 'ip-' + parseInt(ip, 2).toString(36);
}
const limit = Object.assign({}, ep.meta.limit);
if (!limit.key) {
limit.key = ep.name;
}
// Rate limit
await limiter(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor).catch(e => {
throw new ApiError({
message: 'Rate limit exceeded. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED',
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
httpStatusCode: 429,
});
});
}
if (ep.meta.requireCredential && user == null) { if (ep.meta.requireCredential && user == null) {
throw new ApiError({ throw new ApiError({
message: 'Credential required.', message: 'Credential required.',
@ -53,7 +86,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
throw new ApiError(accessDenied, { reason: 'You are not the admin.' }); throw new ApiError(accessDenied, { reason: 'You are not the admin.' });
} }
if (ep.meta.requireModerator && !user!.isAdmin && !user!.isModerator) { if (ep.meta.requireModerator && !isModerator) {
throw new ApiError(accessDenied, { reason: 'You are not a moderator.' }); throw new ApiError(accessDenied, { reason: 'You are not a moderator.' });
} }
@ -65,18 +98,6 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
}); });
} }
if (ep.meta.requireCredential && ep.meta.limit && !user!.isAdmin && !user!.isModerator) {
// Rate limit
await limiter(ep as IEndpoint & { meta: { limit: NonNullable<IEndpoint['meta']['limit']> } }, user!).catch(e => {
throw new ApiError({
message: 'Rate limit exceeded. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED',
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
httpStatusCode: 429,
});
});
}
// Cast non JSON input // Cast non JSON input
if (ep.meta.requireFile && ep.params.properties) { if (ep.meta.requireFile && ep.params.properties) {
for (const k of Object.keys(ep.params.properties)) { for (const k of Object.keys(ep.params.properties)) {

View file

@ -654,7 +654,6 @@ export interface IEndpointMeta {
/** /**
* *
* *
* withCredential false
*/ */
readonly limit?: { readonly limit?: {

View file

@ -27,7 +27,7 @@ export const paramDef = {
blockedHosts: { type: 'array', nullable: true, items: { blockedHosts: { type: 'array', nullable: true, items: {
type: 'string', type: 'string',
} }, } },
themeColor: { type: 'string', nullable: true }, themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
mascotImageUrl: { type: 'string', nullable: true }, mascotImageUrl: { type: 'string', nullable: true },
bannerUrl: { type: 'string', nullable: true }, bannerUrl: { type: 'string', nullable: true },
errorImageUrl: { type: 'string', nullable: true }, errorImageUrl: { type: 'string', nullable: true },

View file

@ -2,8 +2,8 @@ import bcrypt from 'bcryptjs';
import * as speakeasy from 'speakeasy'; import * as speakeasy from 'speakeasy';
import * as QRCode from 'qrcode'; import * as QRCode from 'qrcode';
import config from '@/config/index.js'; import config from '@/config/index.js';
import define from '../../../define.js';
import { UserProfiles } from '@/models/index.js'; import { UserProfiles } from '@/models/index.js';
import define from '../../../define.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
@ -40,15 +40,17 @@ export default define(meta, paramDef, async (ps, user) => {
}); });
// Get the data URL of the authenticator URL // Get the data URL of the authenticator URL
const dataUrl = await QRCode.toDataURL(speakeasy.otpauthURL({ const url = speakeasy.otpauthURL({
secret: secret.base32, secret: secret.base32,
encoding: 'base32', encoding: 'base32',
label: user.username, label: user.username,
issuer: config.host, issuer: config.host,
})); });
const dataUrl = await QRCode.toDataURL(url);
return { return {
qr: dataUrl, qr: dataUrl,
url,
secret: secret.base32, secret: secret.base32,
label: user.username, label: user.username,
issuer: config.host, issuer: config.host,

View file

@ -134,7 +134,7 @@ export const paramDef = {
{ {
// (re)note with text, files and poll are optional // (re)note with text, files and poll are optional
properties: { properties: {
text: { type: 'string', maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false }, text: { type: 'string', minLength: 1, maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false },
}, },
required: ['text'], required: ['text'],
}, },
@ -172,10 +172,14 @@ export default define(meta, paramDef, async (ps, user) => {
let files: DriveFile[] = []; let files: DriveFile[] = [];
const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null; const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null;
if (fileIds != null) { if (fileIds != null) {
files = await DriveFiles.findBy({ files = await DriveFiles.createQueryBuilder('file')
userId: user.id, .where('file.userId = :userId AND file.id IN (:...fileIds)', {
id: In(fileIds), userId: user.id,
}); fileIds,
})
.orderBy('array_position(ARRAY[:...fileIds], "id"::text)')
.setParameters({ fileIds })
.getMany();
} }
let renote: Note | null = null; let renote: Note | null = null;

View file

@ -61,7 +61,14 @@ export default define(meta, paramDef, async (ps, me) => {
.getMany(); .getMany();
} else { } else {
const nameQuery = Users.createQueryBuilder('user') const nameQuery = Users.createQueryBuilder('user')
.where('user.name ILIKE :query', { query: '%' + ps.query + '%' }) .where(new Brackets(qb => {
qb.where('user.name ILIKE :query', { query: '%' + ps.query + '%' });
// Also search username if it qualifies as username
if (Users.validateLocalUsername(ps.query)) {
qb.orWhere('user.usernameLower LIKE :username', { username: '%' + ps.query.toLowerCase() + '%' });
}
}))
.andWhere(new Brackets(qb => { qb .andWhere(new Brackets(qb => { qb
.where('user.updatedAt IS NULL') .where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });

View file

@ -1,25 +1,17 @@
import Limiter from 'ratelimiter'; import Limiter from 'ratelimiter';
import { redisClient } from '../../db/redis.js'; import { redisClient } from '../../db/redis.js';
import { IEndpoint } from './endpoints.js'; import { IEndpointMeta } from './endpoints.js';
import * as Acct from '@/misc/acct.js';
import { CacheableLocalUser, User } from '@/models/entities/user.js'; import { CacheableLocalUser, User } from '@/models/entities/user.js';
import Logger from '@/services/logger.js'; import Logger from '@/services/logger.js';
const logger = new Logger('limiter'); const logger = new Logger('limiter');
export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndpoint['meta']['limit']> } }, user: CacheableLocalUser) => new Promise<void>((ok, reject) => { export const limiter = (limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string) => new Promise<void>((ok, reject) => {
const limitation = endpoint.meta.limit; const hasShortTermLimit = typeof limitation.minInterval === 'number';
const key = Object.prototype.hasOwnProperty.call(limitation, 'key')
? limitation.key
: endpoint.name;
const hasShortTermLimit =
Object.prototype.hasOwnProperty.call(limitation, 'minInterval');
const hasLongTermLimit = const hasLongTermLimit =
Object.prototype.hasOwnProperty.call(limitation, 'duration') && typeof limitation.duration === 'number' &&
Object.prototype.hasOwnProperty.call(limitation, 'max'); typeof limitation.max === 'number';
if (hasShortTermLimit) { if (hasShortTermLimit) {
min(); min();
@ -32,7 +24,7 @@ export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndp
// Short-term limit // Short-term limit
function min(): void { function min(): void {
const minIntervalLimiter = new Limiter({ const minIntervalLimiter = new Limiter({
id: `${user.id}:${key}:min`, id: `${actor}:${limitation.key}:min`,
duration: limitation.minInterval, duration: limitation.minInterval,
max: 1, max: 1,
db: redisClient, db: redisClient,
@ -43,7 +35,7 @@ export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndp
return reject('ERR'); return reject('ERR');
} }
logger.debug(`@${Acct.toString(user)} ${endpoint.name} min remaining: ${info.remaining}`); logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
if (info.remaining === 0) { if (info.remaining === 0) {
reject('BRIEF_REQUEST_INTERVAL'); reject('BRIEF_REQUEST_INTERVAL');
@ -60,7 +52,7 @@ export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndp
// Long term limit // Long term limit
function max(): void { function max(): void {
const limiter = new Limiter({ const limiter = new Limiter({
id: `${user.id}:${key}`, id: `${actor}:${limitation.key}`,
duration: limitation.duration, duration: limitation.duration,
max: limitation.max, max: limitation.max,
db: redisClient, db: redisClient,
@ -71,7 +63,7 @@ export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndp
return reject('ERR'); return reject('ERR');
} }
logger.debug(`@${Acct.toString(user)} ${endpoint.name} max remaining: ${info.remaining}`); logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
if (info.remaining === 0) { if (info.remaining === 0) {
reject('RATE_LIMIT_EXCEEDED'); reject('RATE_LIMIT_EXCEEDED');

View file

@ -59,6 +59,18 @@ export function genOpenapiSpec(lang = 'ja-JP') {
desc += ` / **Permission**: *${kind}*`; desc += ` / **Permission**: *${kind}*`;
} }
const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json';
const schema = endpoint.params;
if (endpoint.meta.requireFile) {
schema.properties.file = {
type: 'string',
format: 'binary',
description: 'The file contents.',
};
schema.required.push('file');
}
const info = { const info = {
operationId: endpoint.name, operationId: endpoint.name,
summary: endpoint.name, summary: endpoint.name,
@ -78,8 +90,8 @@ export function genOpenapiSpec(lang = 'ja-JP') {
requestBody: { requestBody: {
required: true, required: true,
content: { content: {
'application/json': { [requestType]: {
schema: endpoint.params, schema,
}, },
}, },
}, },

View file

@ -9,6 +9,7 @@ import { genId } from '@/misc/gen-id.js';
import { verifyLogin, hash } from '../2fa.js'; import { verifyLogin, hash } from '../2fa.js';
import { randomBytes } from 'node:crypto'; import { randomBytes } from 'node:crypto';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import { limiter } from '../limiter.js';
export default async (ctx: Koa.Context) => { export default async (ctx: Koa.Context) => {
ctx.set('Access-Control-Allow-Origin', config.url); ctx.set('Access-Control-Allow-Origin', config.url);
@ -24,6 +25,21 @@ export default async (ctx: Koa.Context) => {
ctx.body = { error }; ctx.body = { error };
} }
try {
// not more than 1 attempt per second and not more than 10 attempts per hour
await limiter({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, ctx.ip);
} catch (err) {
ctx.status = 429;
ctx.body = {
error: {
message: 'Too many failed attempts to sign in. Try again later.',
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
},
};
return;
}
if (typeof username !== 'string') { if (typeof username !== 'string') {
ctx.status = 400; ctx.status = 400;
return; return;

View file

@ -4,11 +4,11 @@ import { dirname } from 'node:path';
import Koa from 'koa'; import Koa from 'koa';
import send from 'koa-send'; import send from 'koa-send';
import rename from 'rename'; import rename from 'rename';
import * as tmp from 'tmp';
import { serverLogger } from '../index.js'; import { serverLogger } from '../index.js';
import { contentDisposition } from '@/misc/content-disposition.js'; import { contentDisposition } from '@/misc/content-disposition.js';
import { DriveFiles } from '@/models/index.js'; import { DriveFiles } from '@/models/index.js';
import { InternalStorage } from '@/services/drive/internal-storage.js'; import { InternalStorage } from '@/services/drive/internal-storage.js';
import { createTemp } from '@/misc/create-temp.js';
import { downloadUrl } from '@/misc/download-url.js'; import { downloadUrl } from '@/misc/download-url.js';
import { detectType } from '@/misc/get-file-info.js'; import { detectType } from '@/misc/get-file-info.js';
import { convertToWebp, convertToJpeg, convertToPng } from '@/services/drive/image-processor.js'; import { convertToWebp, convertToJpeg, convertToPng } from '@/services/drive/image-processor.js';
@ -50,12 +50,7 @@ export default async function(ctx: Koa.Context) {
if (!file.storedInternal) { if (!file.storedInternal) {
if (file.isLink && file.uri) { // 期限切れリモートファイル if (file.isLink && file.uri) { // 期限切れリモートファイル
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { const [path, cleanup] = await createTemp();
tmp.file((e, path, fd, cleanup) => {
if (e) return rej(e);
res([path, cleanup]);
});
});
try { try {
await downloadUrl(file.uri, path); await downloadUrl(file.uri, path);

View file

@ -2,6 +2,7 @@
* Core Server * Core Server
*/ */
import cluster from 'node:cluster';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as http from 'node:http'; import * as http from 'node:http';
import Koa from 'koa'; import Koa from 'koa';
@ -88,10 +89,10 @@ router.get('/avatar/@:acct', async ctx => {
}); });
router.get('/identicon/:x', async ctx => { router.get('/identicon/:x', async ctx => {
const [temp] = await createTemp(); const [temp, cleanup] = await createTemp();
await genIdenticon(ctx.params.x, fs.createWriteStream(temp)); await genIdenticon(ctx.params.x, fs.createWriteStream(temp));
ctx.set('Content-Type', 'image/png'); ctx.set('Content-Type', 'image/png');
ctx.body = fs.createReadStream(temp); ctx.body = fs.createReadStream(temp).on('close', () => cleanup());
}); });
router.get('/verify-email/:code', async ctx => { router.get('/verify-email/:code', async ctx => {
@ -142,5 +143,26 @@ export default () => new Promise(resolve => {
initializeStreamingServer(server); initializeStreamingServer(server);
server.on('error', e => {
switch ((e as any).code) {
case 'EACCES':
serverLogger.error(`You do not have permission to listen on port ${config.port}.`);
break;
case 'EADDRINUSE':
serverLogger.error(`Port ${config.port} is already in use by another process.`);
break;
default:
serverLogger.error(e);
break;
}
if (cluster.isWorker) {
process.send!('listenFailed');
} else {
// disableClustering
process.exit(1);
}
});
server.listen(config.port, resolve); server.listen(config.port, resolve);
}); });

View file

@ -54,14 +54,10 @@
//#endregion //#endregion
//#region Script //#region Script
const salt = localStorage.getItem('salt') import(`/assets/${CLIENT_ENTRY}`)
? `?salt=${localStorage.getItem('salt')}` .catch(async e => {
: '';
import(`/assets/${CLIENT_ENTRY}${salt}`)
.catch(async () => {
await checkUpdate(); await checkUpdate();
renderError('APP_FETCH_FAILED'); renderError('APP_FETCH_FAILED', JSON.stringify(e));
}) })
//#endregion //#endregion
@ -142,9 +138,6 @@
// eslint-disable-next-line no-inner-declarations // eslint-disable-next-line no-inner-declarations
function refresh() { function refresh() {
// Random
localStorage.setItem('salt', Math.random().toString().substr(2, 8));
// Clear cache (service worker) // Clear cache (service worker)
try { try {
navigator.serviceWorker.controller.postMessage('clear'); navigator.serviceWorker.controller.postMessage('clear');

View file

@ -74,9 +74,9 @@ app.use(views(_dirname + '/views', {
extension: 'pug', extension: 'pug',
options: { options: {
version: config.version, version: config.version,
clientEntry: () => process.env.NODE_ENV === 'production' ? getClientEntry: () => process.env.NODE_ENV === 'production' ?
config.clientEntry : config.clientEntry :
JSON.parse(readFileSync(`${_dirname}/../../../../../built/_client_dist_/manifest.json`, 'utf-8'))['src/init.ts'].file.replace(/^_client_dist_\//, ''), JSON.parse(readFileSync(`${_dirname}/../../../../../built/_client_dist_/manifest.json`, 'utf-8'))['src/init.ts'],
config, config,
}, },
})); }));
@ -247,7 +247,7 @@ router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => {
icon: meta.iconUrl, icon: meta.iconUrl,
themeColor: meta.themeColor, themeColor: meta.themeColor,
}); });
ctx.set('Cache-Control', 'public, max-age=30'); ctx.set('Cache-Control', 'public, max-age=15');
} else { } else {
// リモートユーザーなので // リモートユーザーなので
// モデレータがAPI経由で参照可能にするために404にはしない // モデレータがAPI経由で参照可能にするために404にはしない
@ -292,7 +292,7 @@ router.get('/notes/:note', async (ctx, next) => {
themeColor: meta.themeColor, themeColor: meta.themeColor,
}); });
ctx.set('Cache-Control', 'public, max-age=180'); ctx.set('Cache-Control', 'public, max-age=15');
return; return;
} }
@ -329,7 +329,7 @@ router.get('/@:user/pages/:page', async (ctx, next) => {
}); });
if (['public'].includes(page.visibility)) { if (['public'].includes(page.visibility)) {
ctx.set('Cache-Control', 'public, max-age=180'); ctx.set('Cache-Control', 'public, max-age=15');
} else { } else {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate'); ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
} }
@ -360,7 +360,7 @@ router.get('/clips/:clip', async (ctx, next) => {
themeColor: meta.themeColor, themeColor: meta.themeColor,
}); });
ctx.set('Cache-Control', 'public, max-age=180'); ctx.set('Cache-Control', 'public, max-age=15');
return; return;
} }
@ -385,7 +385,7 @@ router.get('/gallery/:post', async (ctx, next) => {
themeColor: meta.themeColor, themeColor: meta.themeColor,
}); });
ctx.set('Cache-Control', 'public, max-age=180'); ctx.set('Cache-Control', 'public, max-age=15');
return; return;
} }
@ -409,7 +409,7 @@ router.get('/channels/:channel', async (ctx, next) => {
themeColor: meta.themeColor, themeColor: meta.themeColor,
}); });
ctx.set('Cache-Control', 'public, max-age=180'); ctx.set('Cache-Control', 'public, max-age=15');
return; return;
} }
@ -468,7 +468,7 @@ router.get('(.*)', async ctx => {
icon: meta.iconUrl, icon: meta.iconUrl,
themeColor: meta.themeColor, themeColor: meta.themeColor,
}); });
ctx.set('Cache-Control', 'public, max-age=300'); ctx.set('Cache-Control', 'public, max-age=15');
}); });
// Register router // Register router

View file

@ -39,28 +39,24 @@ html {
width: 28px; width: 28px;
height: 28px; height: 28px;
transform: translateY(70px); transform: translateY(70px);
color: var(--accent);
} }
#splashSpinner > .spinner {
#splashSpinner:before,
#splashSpinner:after {
content: " ";
display: block;
box-sizing: border-box;
width: 28px;
height: 28px;
border-radius: 50%;
border: solid 4px;
}
#splashSpinner:before {
border-color: currentColor;
opacity: 0.3;
}
#splashSpinner:after {
position: absolute; position: absolute;
top: 0; top: 0;
border-color: currentColor transparent transparent transparent; left: 0;
width: 28px;
height: 28px;
fill-rule: evenodd;
clip-rule: evenodd;
stroke-linecap: round;
stroke-linejoin: round;
stroke-miterlimit: 1.5;
}
#splashSpinner > .spinner.bg {
opacity: 0.275;
}
#splashSpinner > .spinner.fg {
animation: splashSpinner 0.5s linear infinite; animation: splashSpinner 0.5s linear infinite;
} }

View file

@ -1,17 +1,23 @@
block vars block vars
block loadClientEntry
- const clientEntry = getClientEntry();
doctype html doctype html
!= '<!--\n' //
!= ' _____ _ _ \n' -
!= ' | |_|___ ___| |_ ___ _ _ \n'
!= ' | | | | |_ -|_ -| \'_| -_| | |\n' _____ _ _
!= ' |_|_|_|_|___|___|_,_|___|_ |\n' | |_|___ ___| |_ ___ _ _
!= ' |___|\n' | | | | |_ -|_ -| \'_| -_| | |
!= ' Thank you for using Misskey!\n' |_|_|_|_|___|___|_,_|___|_ |
!= ' If you are reading this message... how about joining the development?\n' |___|
!= ' https://github.com/misskey-dev/misskey' Thank you for using Misskey!
!= '\n-->\n' If you are reading this message... how about joining the development?
https://github.com/misskey-dev/misskey
html html
@ -30,8 +36,14 @@ html
link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg') link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg')
link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg') link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg') link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
link(rel='preload' href='/assets/fontawesome/css/all.css' as='style')
link(rel='stylesheet' href='/assets/fontawesome/css/all.css') link(rel='stylesheet' href='/assets/fontawesome/css/all.css')
link(rel='modulepreload' href=`/assets/${clientEntry.file}`)
each href in clientEntry.css
link(rel='preload' href=`/assets/${href}` as='style')
each href in clientEntry.css
link(rel='preload' href=`/assets/${href}` as='style')
title title
block title block title
@ -52,7 +64,7 @@ html
script. script.
var VERSION = "#{version}"; var VERSION = "#{version}";
var CLIENT_ENTRY = "#{clientEntry()}"; var CLIENT_ENTRY = "#{clientEntry.file}";
script script
include ../boot.js include ../boot.js
@ -65,4 +77,14 @@ html
div#splash div#splash
img#splashIcon(src= icon || '/static-assets/splash.png') img#splashIcon(src= icon || '/static-assets/splash.png')
div#splashSpinner div#splashSpinner
<svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1,0,0,1,12,12)">
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
</g>
</svg>
<svg class="spinner fg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1,0,0,1,12,12)">
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
</g>
</svg>
block content block content

View file

@ -91,27 +91,20 @@ type ToJsonSchema<S> = {
}; };
export function getJsonSchema<S extends Schema>(schema: S): ToJsonSchema<Unflatten<ChartResult<S>>> { export function getJsonSchema<S extends Schema>(schema: S): ToJsonSchema<Unflatten<ChartResult<S>>> {
const object = {}; const jsonSchema = {
for (const [k, v] of Object.entries(schema)) { type: 'object',
nestedProperty.set(object, k, null); properties: {} as Record<string, unknown>,
} required: [],
};
function f(obj: Record<string, null | Record<string, unknown>>) { for (const k in schema) {
const jsonSchema = { jsonSchema.properties[k] = {
type: 'object', type: 'array',
properties: {} as Record<string, unknown>, items: { type: 'number' },
required: [],
}; };
for (const [k, v] of Object.entries(obj)) {
jsonSchema.properties[k] = v === null ? {
type: 'array',
items: { type: 'number' },
} : f(v as Record<string, null | Record<string, unknown>>);
}
return jsonSchema;
} }
return f(object) as ToJsonSchema<Unflatten<ChartResult<S>>>; return jsonSchema as ToJsonSchema<Unflatten<ChartResult<S>>>;
} }
/** /**

View file

@ -1,38 +1,31 @@
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as tmp from 'tmp'; import * as path from 'node:path';
import { createTemp } from '@/misc/create-temp.js';
import { IImage, convertToJpeg } from './image-processor.js'; import { IImage, convertToJpeg } from './image-processor.js';
import * as FFmpeg from 'fluent-ffmpeg'; import FFmpeg from 'fluent-ffmpeg';
export async function GenerateVideoThumbnail(path: string): Promise<IImage> { export async function GenerateVideoThumbnail(source: string): Promise<IImage> {
const [outDir, cleanup] = await new Promise<[string, any]>((res, rej) => { const [file, cleanup] = await createTemp();
tmp.dir((e, path, cleanup) => { const parsed = path.parse(file);
if (e) return rej(e);
res([path, cleanup]); try {
await new Promise((res, rej) => {
FFmpeg({
source,
})
.on('end', res)
.on('error', rej)
.screenshot({
folder: parsed.dir,
filename: parsed.base,
count: 1,
timestamps: ['5%'],
});
}); });
});
await new Promise((res, rej) => { // JPEGに変換 (Webpでもいいが、MastodonはWebpをサポートせず表示できなくなる)
FFmpeg({ return await convertToJpeg(498, 280);
source: path, } finally {
}) cleanup();
.on('end', res) }
.on('error', rej)
.screenshot({
folder: outDir,
filename: 'output.png',
count: 1,
timestamps: ['5%'],
});
});
const outPath = `${outDir}/output.png`;
// JPEGに変換 (Webpでもいいが、MastodonはWebpをサポートせず表示できなくなる)
const thumbnail = await convertToJpeg(outPath, 498, 280);
// cleanup
await fs.promises.unlink(outPath);
cleanup();
return thumbnail;
} }

View file

@ -45,29 +45,20 @@ export async function uploadFromUrl({
// Create temp file // Create temp file
const [path, cleanup] = await createTemp(); const [path, cleanup] = await createTemp();
// write content at URL to temp file
await downloadUrl(url, path);
let driveFile: DriveFile;
let error;
try { try {
driveFile = await addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive }); // write content at URL to temp file
await downloadUrl(url, path);
const driveFile = await addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive });
logger.succ(`Got: ${driveFile.id}`); logger.succ(`Got: ${driveFile.id}`);
return driveFile!;
} catch (e) { } catch (e) {
error = e;
logger.error(`Failed to create drive file: ${e}`, { logger.error(`Failed to create drive file: ${e}`, {
url: url, url: url,
e: e, e: e,
}); });
} throw e;
} finally {
// clean-up cleanup();
cleanup();
if (error) {
throw error;
} else {
return driveFile!;
} }
} }

View file

@ -1,5 +1,6 @@
import { DOMWindow, JSDOM } from 'jsdom'; import { DOMWindow, JSDOM } from 'jsdom';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import tinycolor from 'tinycolor2';
import { getJson, getHtml, getAgentByUrl } from '@/misc/fetch.js'; import { getJson, getHtml, getAgentByUrl } from '@/misc/fetch.js';
import { Instance } from '@/models/entities/instance.js'; import { Instance } from '@/models/entities/instance.js';
import { Instances } from '@/models/index.js'; import { Instances } from '@/models/index.js';
@ -208,16 +209,11 @@ async function fetchIconUrl(instance: Instance, doc: DOMWindow['document'] | nul
} }
async function getThemeColor(doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> { async function getThemeColor(doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
if (doc) { const themeColor = doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') || manifest?.theme_color;
const themeColor = doc.querySelector('meta[name="theme-color"]')?.getAttribute('content');
if (themeColor) { if (themeColor) {
return themeColor; const color = new tinycolor(themeColor);
} if (color.isValid()) return color.toHexString();
}
if (manifest) {
return manifest.theme_color;
} }
return null; return null;

View file

@ -27,6 +27,11 @@ export default async (user: { id: User['id']; host: User['host']; }, note: Note,
} }
} }
// check visibility
if (!await Notes.isVisibleForMe(note, user.id)) {
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
}
// TODO: cache // TODO: cache
reaction = await toDbReaction(reaction, user.host); reaction = await toDbReaction(reaction, user.host);

View file

@ -1,7 +0,0 @@
{
"env": {
"node": true,
"mocha": true,
"commonjs": true
}
}

View file

@ -0,0 +1,11 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
extends: ['../.eslintrc.cjs'],
env: {
node: true,
mocha: true,
},
};

View file

@ -1,7 +1,7 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import rndstr from 'rndstr';
import * as assert from 'assert'; import * as assert from 'assert';
import rndstr from 'rndstr';
import { initTestDb } from './utils.js'; import { initTestDb } from './utils.js';
describe('ActivityPub', () => { describe('ActivityPub', () => {
@ -57,8 +57,8 @@ describe('ActivityPub', () => {
const note = await createNote(post.id, resolver, true); const note = await createNote(post.id, resolver, true);
assert.deepStrictEqual(note?.uri, post.id); assert.deepStrictEqual(note?.uri, post.id);
assert.deepStrictEqual(note?.visibility, 'public'); assert.deepStrictEqual(note.visibility, 'public');
assert.deepStrictEqual(note?.text, post.content); assert.deepStrictEqual(note.text, post.content);
}); });
}); });

View file

@ -1,7 +1,7 @@
import * as assert from 'assert'; import * as assert from 'assert';
import httpSignature from 'http-signature';
import { genRsaKeyPair } from '../src/misc/gen-key-pair.js'; import { genRsaKeyPair } from '../src/misc/gen-key-pair.js';
import { createSignedPost, createSignedGet } from '../src/remote/activitypub/ap-request.js'; import { createSignedPost, createSignedGet } from '../src/remote/activitypub/ap-request.js';
import httpSignature from 'http-signature';
export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => { export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => {
return { return {
@ -13,7 +13,7 @@ export const buildParsedSignature = (signingString: string, signature: string, a
signature: signature, signature: signature,
}, },
signingString: signingString, signingString: signingString,
algorithm: algorithm?.toUpperCase(), algorithm: algorithm.toUpperCase(),
keyId: 'KeyID', // dummy, not used for verify keyId: 'KeyID', // dummy, not used for verify
}; };
}; };
@ -26,7 +26,7 @@ describe('ap-request', () => {
const activity = { a: 1 }; const activity = { a: 1 };
const body = JSON.stringify(activity); const body = JSON.stringify(activity);
const headers = { const headers = {
'User-Agent': 'UA' 'User-Agent': 'UA',
}; };
const req = createSignedPost({ key, url, body, additionalHeaders: headers }); const req = createSignedPost({ key, url, body, additionalHeaders: headers });
@ -42,7 +42,7 @@ describe('ap-request', () => {
const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey };
const url = 'https://example.com/outbox'; const url = 'https://example.com/outbox';
const headers = { const headers = {
'User-Agent': 'UA' 'User-Agent': 'UA',
}; };
const req = createSignedGet({ key, url, additionalHeaders: headers }); const req = createSignedGet({ key, url, additionalHeaders: headers });

View file

@ -61,40 +61,40 @@ describe('API visibility', () => {
const show = async (noteId: any, by: any) => { const show = async (noteId: any, by: any) => {
return await request('/notes/show', { return await request('/notes/show', {
noteId noteId,
}, by); }, by);
}; };
before(async () => { before(async () => {
//#region prepare //#region prepare
// signup // signup
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
follower = await signup({ username: 'follower' }); follower = await signup({ username: 'follower' });
other = await signup({ username: 'other' }); other = await signup({ username: 'other' });
target = await signup({ username: 'target' }); target = await signup({ username: 'target' });
target2 = await signup({ username: 'target2' }); target2 = await signup({ username: 'target2' });
// follow alice <= follower // follow alice <= follower
await request('/following/create', { userId: alice.id }, follower); await request('/following/create', { userId: alice.id }, follower);
// normal posts // normal posts
pub = await post(alice, { text: 'x', visibility: 'public' }); pub = await post(alice, { text: 'x', visibility: 'public' });
home = await post(alice, { text: 'x', visibility: 'home' }); home = await post(alice, { text: 'x', visibility: 'home' });
fol = await post(alice, { text: 'x', visibility: 'followers' }); fol = await post(alice, { text: 'x', visibility: 'followers' });
spe = await post(alice, { text: 'x', visibility: 'specified', visibleUserIds: [target.id] }); spe = await post(alice, { text: 'x', visibility: 'specified', visibleUserIds: [target.id] });
// replies // replies
tgt = await post(target, { text: 'y', visibility: 'public' }); tgt = await post(target, { text: 'y', visibility: 'public' });
pubR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'public' }); pubR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'public' });
homeR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'home' }); homeR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'home' });
folR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'followers' }); folR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'followers' });
speR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'specified' }); speR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'specified' });
// mentions // mentions
pubM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'public' }); pubM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'public' });
homeM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'home' }); homeM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'home' });
folM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'followers' }); folM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'followers' });
speM = await post(alice, { text: '@target2 x', replyId: tgt.id, visibility: 'specified' }); speM = await post(alice, { text: '@target2 x', replyId: tgt.id, visibility: 'specified' });
//#endregion //#endregion
}); });

Some files were not shown because too many files have changed in this diff Show more