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

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",
"eg2.vscode-npm-script",
"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: プッシュ通知を複数アカウント対応に #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
- 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
- Client: fix abuse reports page to be able to show all reports @Johann150
- 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)

View file

@ -1,10 +1,11 @@
# 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.
** Important:** This project uses Japanese as its major language, **but you do not need to translate and write the Issues/PRs in Japanese.**
Also, you might receive comments on your Issue/PR in Japanese, but you do not need to reply to them in Japanese as well.\
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.
> **Note**
> This project uses Japanese as its major language, **but you do not need to translate and write the Issues/PRs in Japanese.**
> Also, you might receive comments on your Issue/PR in Japanese, but you do not need to reply to them in Japanese as well.\
> 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
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.
- 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
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
ENV NODE_ENV=production
ARG NODE_ENV=production
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 . ./
ENV NODE_ENV=production
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">
**🌎 A forever evolving, interplanetary microblogging platform. 🚀**
**Misskey** is a distributed microblogging platform with advanced features such as Reactions and a highly customizable UI.
[Learn more](https://misskey-hub.net/)
<a href="https://misskey-hub.net">
<img src="./assets/title_float.svg" alt="Misskey logo" style="border-radius:50%" width="400"/>
</a>
**🌎 **[Misskey](https://misskey-hub.net/)** is an open source, decentralized social media platform that's free forever! 🚀**
---
[✨ Find an instance](https://misskey-hub.net/instances.html)
[📦 Create your own instance](https://misskey-hub.net/docs/install.html)
[🛠️ Contribute](./CONTRIBUTING.md)
[🚀 Join the community](https://discord.gg/Wp8gVStHW3)
<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>
<a href="https://misskey-hub.net/docs/install.html">
<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>
<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>
@ -30,22 +32,25 @@
## ✨ Features
- **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**\
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**\
An interface to manage uploaded files such as images, videos, sounds, etc.
You can also organize your favorite content into folders, making it easy to share again.
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!
- **Rich Web UI**\
Misskey has a rich WebUI by default.
It is highly customizable by flexibly changing the layout and installing various widgets and themes.
Furthermore, plug-ins can be created using AiScript, a original programming language.
- and more...
Misskey has a rich and easy to use Web UI!
It is highly customizable, from changing the layout and adding widgets to making custom themes.
Furthermore, plugins can be created using AiScript, an original programming language.
- And much more...
</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
<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>

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

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
# - es
ports:
- "127.0.0.1:3000:3000"
- "3000:3000"
networks:
- internal_network
- external_network

View file

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

View file

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

View file

@ -1,6 +1,8 @@
---
_lang_: "Català"
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"
notifications: "Notificacions"
username: "Nom d'usuari"
@ -10,17 +12,173 @@ fetchingAsApObject: "Cercant en el Fediverse..."
ok: "OK"
gotIt: "Ho he entès!"
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"
smtpPass: "Contrasenya"
user: "Usuaris"
searchByGoogle: "Cercar"
_email:
_follow:
title: "t'ha seguit"
_mfm:
mention: "Menció"
quote: "Citar"
search: "Cercar"
_theme:
keys:
mention: "Menció"
renote: "Renotar"
_sfx:
note: "Notes"
notification: "Notificacions"
_widgets:
notifications: "Notificacions"
timeline: "Línia de temps"
_cw:
show: "Carregar més"
_visibility:
followers: "Seguidors"
_profile:
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:
_columns:
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"
showMore: "Zobrazit více"
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í"
mentions: "Zmínění"
importAndExport: "Import a export"
@ -60,7 +62,9 @@ import: "Importovat"
export: "Exportovat"
files: "Soubor(ů)"
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}?"
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."
lists: "Seznamy"
noLists: "Nemáte žádné seznamy"
@ -75,13 +79,25 @@ error: "Chyba"
somethingHappened: "Jejda. Něco se nepovedlo."
retry: "Opakovat"
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"
privacy: "Soukromí"
makeFollowManuallyApprove: "Žádosti o sledování vyžadují potvrzení"
defaultNoteVisibility: "Výchozí viditelnost"
follow: "Sledovaní"
followRequest: "Odeslat žádost o sledování"
followRequests: "Žádosti o sledování"
unfollow: "Přestat sledovat"
followRequestPending: "Čekající žádosti o sledování"
enterEmoji: "Vložte emoji"
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."
quote: "Citovat"
pinnedNote: "Připnutá poznámka"
pinned: "Připnout"
you: "Vy"
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."
flagAsCat: "Tenhle účet je 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"
addAccount: "Přidat účet"
loginFailed: "Přihlášení se nezdařilo."
@ -130,13 +148,16 @@ general: "Obecně"
wallpaper: "Obrázek na pozadí"
setWallpaper: "Nastavení obrázku na pozadí"
removeWallpaper: "Odstranit pozadí"
searchWith: "Hledat: {q}"
youHaveNoLists: "Nemáte žádné seznamy"
followConfirm: "Jste si jisti, že chcete sledovat {name}?"
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."
host: "Hostitel"
selectUser: "Vyberte uživatele"
recipient: "Pro"
annotation: "Komentáře"
federation: "Federace"
instances: "Instance"
registeredAt: "Registrován"
latestRequestSentAt: "Poslední požadavek poslán"
@ -146,6 +167,7 @@ storageUsage: "Využití úložiště"
charts: "Grafy"
perHour: "za hodinu"
perDay: "za den"
stopActivityDelivery: "Přestat zasílat aktivitu"
blockThisInstance: "Blokovat tuto instanci"
operations: "Operace"
software: "Software"
@ -283,6 +305,8 @@ iconUrl: "Favicon URL"
bannerUrl: "Baner URL"
backgroundImageUrl: "Adresa URL obrázku pozadí"
basicInfo: "Základní informace"
pinnedUsers: "Připnutí uživatelé"
pinnedNotes: "Připnutá poznámka"
hcaptcha: "hCaptcha"
enableHcaptcha: "Aktivovat hCaptchu"
hcaptchaSecretKey: "Tajný Klíč (Secret Key)"
@ -471,6 +495,7 @@ _widgets:
notifications: "Oznámení"
timeline: "Časová osa"
activity: "Aktivita"
federation: "Federace"
jobQueue: "Fronta úloh"
_cw:
show: "Zobrazit více"
@ -485,6 +510,8 @@ _exportOrImport:
muteList: "Ztlumit"
blockingList: "Zablokovat"
userLists: "Seznamy"
_charts:
federation: "Federace"
_timelines:
home: "Domů"
_pages:
@ -517,6 +544,9 @@ _notification:
renote: "Přeposlat"
quote: "Citovat"
reaction: "Reakce"
_actions:
reply: "Odpovědět"
renote: "Přeposlat"
_deck:
_columns:
notifications: "Oznámení"

View file

@ -1006,7 +1006,7 @@ _instanceMute:
heading: "Liste der stummzuschaltenden Instanzen"
_theme:
explore: "Farbschemata erforschen"
install: "Farbschmata installieren"
install: "Farbschemata installieren"
manage: "Farbschemaverwaltung"
code: "Farbschemencode"
description: "Beschreibung"
@ -1613,8 +1613,9 @@ _notification:
youWereFollowed: "ist dir gefolgt"
youReceivedFollowRequest: "Du hast eine Follow-Anfrage erhalten"
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"
emptyPushNotificationMessage: "Push-Benachrichtigungen wurden aktualisiert"
_types:
all: "Alle"
follow: "Neue Follower"
@ -1629,6 +1630,10 @@ _notification:
followRequestAccepted: "Akzeptierte Follow-Anfragen"
groupInvited: "Erhaltene Gruppeneinladungen"
app: "Benachrichtigungen von Apps"
_actions:
followBack: "folgt dir nun auch"
reply: "Antworten"
renote: "Renote"
_deck:
alwaysShowMainColumn: "Hauptspalte immer zeigen"
columnAlign: "Spaltenausrichtung"

View file

@ -1613,8 +1613,9 @@ _notification:
youWereFollowed: "followed you"
youReceivedFollowRequest: "You've received a follow request"
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"
emptyPushNotificationMessage: "Push notifications have been updated"
_types:
all: "All"
follow: "New followers"
@ -1629,6 +1630,10 @@ _notification:
followRequestAccepted: "Accepted follow requests"
groupInvited: "Group invitations"
app: "Notifications from linked apps"
_actions:
followBack: "followed you back"
reply: "Reply"
renote: "Renote"
_deck:
alwaysShowMainColumn: "Always show main column"
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."
flagAsCat: "Esta cuenta es un gato"
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"
addAccount: "Agregar Cuenta"
loginFailed: "Error al iniciar sesión."
@ -235,6 +237,8 @@ resetAreYouSure: "¿Desea reestablecer?"
saved: "Guardado"
messaging: "Chat"
upload: "Subir"
keepOriginalUploading: "Mantener la imagen original"
keepOriginalUploadingDescription: "Mantener la versión original al cargar imágenes. Si está desactivado, el navegador generará imágenes para la publicación web en el momento de recargar la página"
fromDrive: "Desde el drive"
fromUrl: "Desde la URL"
uploadFromUrl: "Subir desde una URL"
@ -444,6 +448,7 @@ uiLanguage: "Idioma de visualización de la interfaz"
groupInvited: "Invitado al grupo"
aboutX: "Acerca de {x}"
useOsNativeEmojis: "Usa los emojis nativos de la plataforma"
disableDrawer: "No mostrar los menús en cajones"
youHaveNoGroups: "Sin grupos"
joinOrCreateGroup: "Obtenga una invitación para unirse al grupos o puede crear su propio grupo."
noHistory: "No hay datos en el historial"
@ -615,6 +620,10 @@ reportAbuse: "Reportar"
reportAbuseOf: "Reportar a {name}"
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."
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"
abuseMarkAsResolved: "Marcar reporte como resuelto"
openInNewTab: "Abrir en una Nueva Pestaña"
@ -676,6 +685,7 @@ center: "Centrar"
wide: "Ancho"
narrow: "Estrecho"
reloadToApplySetting: "Esta configuración sólo se aplicará después de recargar la página. ¿Recargar ahora?"
needReloadToApply: "Se requiere un reinicio para la aplicar los cambios"
showTitlebar: "Mostrar la barra de título"
clearCache: "Limpiar caché"
onlineUsersCount: "{n} usuarios en línea"
@ -706,19 +716,55 @@ capacity: "Capacidad"
inUse: "Usado"
editCode: "Editar código"
apply: "Aplicar"
receiveAnnouncementFromInstance: "Recibir notificaciones de la instancia"
emailNotification: "Notificaciones por correo electrónico"
publish: "Publicar"
inChannelSearch: "Buscar en el canal"
useReactionPickerForContextMenu: "Haga clic con el botón derecho para abrir el menu de reacciones"
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"
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"
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"
active: "Activo"
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"
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"
recentPosts: "Posts recientes"
popularPosts: "Más vistos"
shareWithNote: "Compartir con una nota"
ads: "Anuncios"
expiration: "Termina el"
memo: "Notas"
priority: "Prioridad"
high: "Alta"
middle: "Mediano"
low: "Baja"
@ -770,22 +816,50 @@ _accountDelete:
accountDelete: "Eliminar Cuenta"
_ad:
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:
my: "Mi galería"
liked: "Publicaciones que me gustan"
like: "¡Muy bien!"
unlike: "Quitar me gusta"
_email:
_follow:
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:
scope: "Alcance"
key: "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:
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."
dummy: "Misskey expande el mundo de la Fediverso"
mention: "Menciones"
mentionDescription: "El signo @ seguido de un nombre de usuario se puede utilizar para notificar a un usuario en particular."
hashtag: "Hashtag"
url: "URL"
urlDescription: "Se pueden mostrar las URL"
link: "Vínculo"
bold: "Negrita"
center: "Centrar"
@ -1432,6 +1506,9 @@ _notification:
followRequestAccepted: "El seguimiento fue aceptado"
groupInvited: "Invitado al grupo"
app: "Notificaciones desde aplicaciones"
_actions:
reply: "Responder"
renote: "Renotar"
_deck:
alwaysShowMainColumn: "Siempre mostrar la columna principal"
columnAlign: "Alinear columnas"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -37,17 +37,58 @@ favorites: "Favoritar"
unfavorite: "Remover dos favoritos"
favorited: "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"
youGotNewFollower: "Você tem um novo seguidor"
receiveFollowRequest: "Pedido de seguimento recebido"
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"
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"
renote: "Repostar"
renoted: "Repostado"
cantRenote: "Não pode repostar"
cantReRenote: "Não pode repostar este repost"
pinnedNote: "Post fixado"
pinned: "Afixar no perfil"
sensitive: "Conteúdo sensível"
mute: "Silenciar"
unmute: "Dessilenciar"
@ -57,6 +98,7 @@ registeredAt: "Registrado em"
perHour: "por hora"
perDay: "por dia"
noUsers: "Sem usuários"
remove: "Eliminar"
messageRead: "Lida"
lightThemes: "Tema claro"
darkThemes: "Tema escuro"
@ -64,6 +106,7 @@ addFile: "Adicionar arquivo"
nsfw: "Conteúdo sensível"
monthX: "mês de {month}"
pinnedNotes: "Post fixado"
userList: "Listas"
smtpUser: "Nome de usuário"
smtpPass: "Senha"
user: "Usuários"
@ -72,9 +115,11 @@ _email:
_follow:
title: "Você tem um novo seguidor"
_mfm:
mention: "Menção"
search: "Pesquisar"
_theme:
keys:
mention: "Menção"
renote: "Repostar"
_sfx:
note: "Posts"
@ -82,15 +127,47 @@ _sfx:
_widgets:
notifications: "Notificações"
timeline: "Timeline"
_cw:
show: "Carregar mais"
_visibility:
followers: "Seguidores"
_profile:
username: "Nome de usuário"
_exportOrImport:
followingList: "Seguindo"
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:
youWereFollowed: "Você tem um novo seguidor"
_types:
follow: "Seguindo"
mention: "Menção"
renote: "Repostar"
_actions:
reply: "Responder"
renote: "Repostar"
_deck:
_columns:
notifications: "Notificações"
tl: "Timeline"
list: "Listas"
mentions: "Menções"

View file

@ -562,13 +562,87 @@ plugins: "Pluginuri"
deck: "Deck"
undeck: "Părăsește Deck"
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ă"
smtpPort: "Port"
smtpUser: "Nume de utilizator"
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"
info: "Despre"
user: "Utilizatori"
administration: "Gestionare"
middle: "Mediu"
sent: "Trimite"
searchByGoogle: "Caută"
_email:
_follow:
@ -641,6 +715,9 @@ _notification:
renote: "Re-notează"
quote: "Citează"
reaction: "Reacție"
_actions:
reply: "Răspunde"
renote: "Re-notează"
_deck:
_columns:
notifications: "Notificări"

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -5,7 +5,6 @@ import * as os from 'node:os';
import cluster from 'node:cluster';
import chalk from 'chalk';
import chalkTemplate from 'chalk-template';
import * as portscanner from 'portscanner';
import semver from 'semver';
import Logger from '@/services/logger.js';
@ -48,11 +47,6 @@ function greet() {
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
*/
@ -67,7 +61,6 @@ export async function masterMain() {
showNodejsVersion();
config = loadConfigBoot();
await connectDb();
await validatePort(config);
} catch (e) {
bootLogger.error('Fatal error occurred during initialization', null, true);
process.exit(1);
@ -97,8 +90,6 @@ function showEnvironment(): void {
logger.warn('The environment is not in production mode.');
logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true);
}
logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`);
}
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) {
const workers = Math.min(limit, os.cpus().length);
bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
@ -186,6 +154,10 @@ function spawnWorker(): Promise<void> {
return new Promise(res => {
const worker = cluster.fork();
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;
res();
});

View file

@ -46,7 +46,7 @@ export default function load() {
mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`;
mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
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;

View file

@ -5,9 +5,6 @@ pg.types.setTypeParser(20, Number);
import { Logger, DataSource } from 'typeorm';
import * as highlight from 'cli-highlight';
import config from '@/config/index.js';
import { envOption } from '../env.js';
import { dbLogger } from './logger.js';
import { User } from '@/models/entities/user.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 { Webhook } from '@/models/entities/webhook.js';
import { envOption } from '../env.js';
import { dbLogger } from './logger.js';
const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
@ -212,7 +211,7 @@ export async function initDb() {
if (db.isInitialized) {
// nop
} else {
await db.connect();
await db.initialize();
}
}

View file

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

View file

@ -1,10 +1,19 @@
import * as tmp from 'tmp';
export function createTemp(): Promise<[string, any]> {
return new Promise<[string, any]>((res, rej) => {
export function createTemp(): Promise<[string, () => void]> {
return new Promise<[string, () => void]>((res, rej) => {
tmp.file((e, path, fd, cleanup) => {
if (e) return rej(e);
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;
return meta;
} else {
const saved = await transactionalEntityManager.save(Meta, {
id: 'x',
}) as Meta;
// metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う
const saved = await transactionalEntityManager
.upsert(
Meta,
{
id: 'x',
},
['id'],
)
.then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0]));
cache = saved;
return saved;

View file

@ -144,13 +144,7 @@ export const NoteRepository = db.getRepository(Note).extend({
return true;
} else {
// 指定されているかどうか
const specified = note.visibleUserIds.some((id: any) => meId === id);
if (specified) {
return true;
} else {
return false;
}
return note.visibleUserIds.some((id: any) => meId === id);
}
}
@ -168,16 +162,25 @@ export const NoteRepository = db.getRepository(Note).extend({
return true;
} else {
// フォロワーかどうか
const following = await Followings.findOneBy({
followeeId: note.userId,
followerId: meId,
});
const [following, user] = await Promise.all([
Followings.count({
where: {
followeeId: note.userId,
followerId: meId,
},
take: 1,
}),
Users.findOneByOrFail({ id: meId }),
]);
if (following == null) {
return false;
} else {
return true;
}
/* If we know the following, everyhting is fine.
But if we do not know the following, it might be that both the
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
async getRelation(me: User['id'], target: User['id']) {
const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([
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 {
return awaitAll({
id: target,
isFollowing: following1 != null,
hasPendingFollowRequestFromYou: followReq1 != null,
hasPendingFollowRequestToYou: followReq2 != null,
isFollowed: following2 != null,
isBlocking: toBlocking != null,
isBlocked: fromBlocked != null,
isMuted: mute != null,
};
isFollowing: Followings.count({
where: {
followerId: me,
followeeId: target,
},
take: 1,
}).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> {

View file

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

View file

@ -1,11 +1,11 @@
import Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'node:fs';
import { queueLogger } from '../../logger.js';
import { addFile } from '@/services/drive/add-file.js';
import { format as dateFormat } from 'date-fns';
import { getFullApAccount } from '@/misc/convert-host.js';
import { createTemp } from '@/misc/create-temp.js';
import { Users, Blockings } from '@/models/index.js';
import { MoreThan } from 'typeorm';
import { DbUserJobData } from '@/queue/types.js';
@ -22,73 +22,72 @@ export async function exportBlocking(job: Bull.Job<DbUserJobData>, done: any): P
}
// Create temp file
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
tmp.file((e, path, fd, cleanup) => {
if (e) return rej(e);
res([path, cleanup]);
});
});
const [path, cleanup] = await createTemp();
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 cursor: any = null;
let exportedCount = 0;
let cursor: any = null;
while (true) {
const blockings = await Blockings.find({
where: {
blockerId: user.id,
...(cursor ? { id: MoreThan(cursor) } : {}),
},
take: 100,
order: {
id: 1,
},
});
while (true) {
const blockings = await Blockings.find({
where: {
blockerId: user.id,
...(cursor ? { id: MoreThan(cursor) } : {}),
},
take: 100,
order: {
id: 1,
},
});
if (blockings.length === 0) {
job.progress(100);
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;
if (blockings.length === 0) {
job.progress(100);
break;
}
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();
}
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);
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({
blockerId: user.id,
});
stream.end();
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();
}

View file

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

View file

@ -1,11 +1,11 @@
import Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'node:fs';
import { queueLogger } from '../../logger.js';
import { addFile } from '@/services/drive/add-file.js';
import { format as dateFormat } from 'date-fns';
import { getFullApAccount } from '@/misc/convert-host.js';
import { createTemp } from '@/misc/create-temp.js';
import { Users, Followings, Mutings } from '@/models/index.js';
import { In, MoreThan, Not } from 'typeorm';
import { DbUserJobData } from '@/queue/types.js';
@ -23,73 +23,72 @@ export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: () =>
}
// Create temp file
const [path, cleanup] = await new Promise<[string, () => void]>((res, rej) => {
tmp.file((e, path, fd, cleanup) => {
if (e) return rej(e);
res([path, cleanup]);
});
});
const [path, cleanup] = await createTemp();
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({
muterId: user.id,
}) : [];
const mutings = job.data.excludeMuting ? await Mutings.findBy({
muterId: user.id,
}) : [];
while (true) {
const followings = await Followings.find({
where: {
followerId: user.id,
...(mutings.length > 0 ? { followeeId: Not(In(mutings.map(x => x.muteeId))) } : {}),
...(cursor ? { id: MoreThan(cursor) } : {}),
},
take: 100,
order: {
id: 1,
},
}) as Following[];
while (true) {
const followings = await Followings.find({
where: {
followerId: user.id,
...(mutings.length > 0 ? { followeeId: Not(In(mutings.map(x => x.muteeId))) } : {}),
...(cursor ? { id: MoreThan(cursor) } : {}),
},
take: 100,
order: {
id: 1,
},
}) as Following[];
if (followings.length === 0) {
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 (followings.length === 0) {
break;
}
if (job.data.excludeInactive && u.updatedAt && (Date.now() - u.updatedAt.getTime() > 1000 * 60 * 60 * 24 * 90)) {
continue;
}
cursor = followings[followings.length - 1].id;
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();
}
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)) {
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();
}

View file

@ -1,11 +1,11 @@
import Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'node:fs';
import { queueLogger } from '../../logger.js';
import { addFile } from '@/services/drive/add-file.js';
import { format as dateFormat } from 'date-fns';
import { getFullApAccount } from '@/misc/convert-host.js';
import { createTemp } from '@/misc/create-temp.js';
import { Users, Mutings } from '@/models/index.js';
import { IsNull, MoreThan } from 'typeorm';
import { DbUserJobData } from '@/queue/types.js';
@ -22,74 +22,73 @@ export async function exportMute(job: Bull.Job<DbUserJobData>, done: any): Promi
}
// Create temp file
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
tmp.file((e, path, fd, cleanup) => {
if (e) return rej(e);
res([path, cleanup]);
});
});
const [path, cleanup] = await createTemp();
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 cursor: any = null;
let exportedCount = 0;
let cursor: any = null;
while (true) {
const mutes = await Mutings.find({
where: {
muterId: user.id,
expiresAt: IsNull(),
...(cursor ? { id: MoreThan(cursor) } : {}),
},
take: 100,
order: {
id: 1,
},
});
while (true) {
const mutes = await Mutings.find({
where: {
muterId: user.id,
expiresAt: IsNull(),
...(cursor ? { id: MoreThan(cursor) } : {}),
},
take: 100,
order: {
id: 1,
},
});
if (mutes.length === 0) {
job.progress(100);
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;
if (mutes.length === 0) {
job.progress(100);
break;
}
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();
}
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);
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({
muterId: user.id,
});
stream.end();
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();
}

View file

@ -1,5 +1,4 @@
import Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'node:fs';
import { queueLogger } from '../../logger.js';
@ -10,6 +9,7 @@ import { MoreThan } from 'typeorm';
import { Note } from '@/models/entities/note.js';
import { Poll } from '@/models/entities/poll.js';
import { DbUserJobData } from '@/queue/types.js';
import { createTemp } from '@/misc/create-temp.js';
const logger = queueLogger.createSubLogger('export-notes');
@ -23,82 +23,81 @@ export async function exportNotes(job: Bull.Job<DbUserJobData>, done: any): Prom
}
// Create temp file
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
tmp.file((e, path, fd, cleanup) => {
if (e) return rej(e);
res([path, cleanup]);
});
});
const [path, cleanup] = await createTemp();
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> => {
return new Promise<void>((res, rej) => {
stream.write(text, err => {
if (err) {
logger.error(err);
rej(err);
} else {
res();
}
const write = (text: string): Promise<void> => {
return new Promise<void>((res, rej) => {
stream.write(text, err => {
if (err) {
logger.error(err);
rej(err);
} else {
res();
}
});
});
});
};
};
await write('[');
await write('[');
let exportedNotesCount = 0;
let cursor: Note['id'] | null = null;
let exportedNotesCount = 0;
let cursor: Note['id'] | null = null;
while (true) {
const notes = await Notes.find({
where: {
userId: user.id,
...(cursor ? { id: MoreThan(cursor) } : {}),
},
take: 100,
order: {
id: 1,
},
}) as Note[];
while (true) {
const notes = await Notes.find({
where: {
userId: user.id,
...(cursor ? { id: MoreThan(cursor) } : {}),
},
take: 100,
order: {
id: 1,
},
}) as Note[];
if (notes.length === 0) {
job.progress(100);
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 });
if (notes.length === 0) {
job.progress(100);
break;
}
const content = JSON.stringify(serialize(note, poll));
const isFirst = exportedNotesCount === 0;
await write(isFirst ? content : ',\n' + content);
exportedNotesCount++;
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;
await write(isFirst ? content : ',\n' + content);
exportedNotesCount++;
}
const total = await Notes.countBy({
userId: user.id,
});
job.progress(exportedNotesCount / total);
}
const total = await Notes.countBy({
userId: user.id,
});
await write(']');
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();
}

View file

@ -1,11 +1,11 @@
import Bull from 'bull';
import * as tmp from 'tmp';
import * as fs from 'node:fs';
import { queueLogger } from '../../logger.js';
import { addFile } from '@/services/drive/add-file.js';
import { format as dateFormat } from 'date-fns';
import { getFullApAccount } from '@/misc/convert-host.js';
import { createTemp } from '@/misc/create-temp.js';
import { Users, UserLists, UserListJoinings } from '@/models/index.js';
import { In } from 'typeorm';
import { DbUserJobData } from '@/queue/types.js';
@ -26,46 +26,45 @@ export async function exportUserLists(job: Bull.Job<DbUserJobData>, done: any):
});
// Create temp file
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
tmp.file((e, path, fd, cleanup) => {
if (e) return rej(e);
res([path, cleanup]);
});
});
const [path, cleanup] = await createTemp();
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) {
const joinings = await UserListJoinings.findBy({ userListId: list.id });
const users = await Users.findBy({
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 list of lists) {
const joinings = await UserListJoinings.findBy({ userListId: list.id });
const users = await Users.findBy({
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();
}
});
});
}
}
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();
}

View file

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

View file

@ -1,6 +1,6 @@
import { URL } from 'node:url';
import Bull from 'bull';
import httpSignature from 'http-signature';
import httpSignature from '@peertube/http-signature';
import perform from '@/remote/activitypub/perform.js';
import Logger from '@/services/logger.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 { Webhook } from '@/models/entities/webhook';
import { IActivity } from '@/remote/activitypub/type.js';
import httpSignature from 'http-signature';
import httpSignature from '@peertube/http-signature';
export type DeliverJobData = {
/** Actor */

View file

@ -9,6 +9,7 @@ import { fetchMeta } from '@/misc/fetch-meta.js';
import { getApLock } from '@/misc/app-lock.js';
import { parseAudience } from '../../audience.js';
import { StatusError } from '@/misc/fetch.js';
import { Notes } from '@/models/index.js';
const logger = apLogger;
@ -52,6 +53,8 @@ export default async function(resolver: Resolver, actor: CacheableRemoteUser, ac
throw e;
}
if (!await Notes.isVisibleForMe(renote, actor.id)) return 'skip: invalid actor for this activity';
logger.info(`Creating the (Re)Note: ${uri}`);
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
let formarType: string | undefined;
let formerType: string | undefined;
if (typeof activity.object === 'string') {
// typeが不明だけど、どうせ消えてるのでremote resolveしない
formarType = undefined;
formerType = undefined;
} else {
const object = activity.object as IObject;
if (isTombstone(object)) {
formarType = toSingle(object.formerType);
formerType = toSingle(object.formerType);
} else {
formarType = toSingle(object.type);
formerType = toSingle(object.type);
}
}
const uri = getApId(activity.object);
// type不明でもactorとobjectが同じならばそれはPersonに違いない
if (!formarType && actor.uri === uri) {
formarType = 'Person';
if (!formerType && actor.uri === uri) {
formerType = 'Person';
}
// それでもなかったらおそらくNote
if (!formarType) {
formarType = 'Note';
if (!formerType) {
formerType = 'Note';
}
if (validPost.includes(formarType)) {
if (validPost.includes(formerType)) {
return await deleteNote(actor, uri);
} else if (validActor.includes(formarType)) {
} else if (validActor.includes(formerType)) {
return await deleteActor(actor, uri);
} 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({
uri,
userId: actor.id,
});
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 Resolver from '../resolver.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 { CacheableRemoteUser, IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { htmlToMfm } from '../misc/html-to-mfm.js';
import { extractApHashtags } from './tag.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 { deliverQuestionUpdate } from '@/services/note/polls/update.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 { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type.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 => {
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()}`;
}

View file

@ -1,6 +1,6 @@
import Router from '@koa/router';
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 renderNote from '@/remote/activitypub/renderer/note.js';

View file

@ -2,10 +2,11 @@ import Koa from 'koa';
import { performance } from 'perf_hooks';
import { limiter } from './limiter.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 { apiLogger } from './logger.js';
import { AccessToken } from '@/models/entities/access-token.js';
import IPCIDR from 'ip-cidr';
const accessDenied = {
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) => {
const isSecure = user != null && token == null;
const isModerator = user != null && (user.isModerator || user.isAdmin);
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);
}
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) {
throw new ApiError({
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.' });
}
if (ep.meta.requireModerator && !user!.isAdmin && !user!.isModerator) {
if (ep.meta.requireModerator && !isModerator) {
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
if (ep.meta.requireFile && 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?: {

View file

@ -27,7 +27,7 @@ export const paramDef = {
blockedHosts: { type: 'array', nullable: true, items: {
type: 'string',
} },
themeColor: { type: 'string', nullable: true },
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
mascotImageUrl: { type: 'string', nullable: true },
bannerUrl: { 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 QRCode from 'qrcode';
import config from '@/config/index.js';
import define from '../../../define.js';
import { UserProfiles } from '@/models/index.js';
import define from '../../../define.js';
export const meta = {
requireCredential: true,
@ -40,15 +40,17 @@ export default define(meta, paramDef, async (ps, user) => {
});
// Get the data URL of the authenticator URL
const dataUrl = await QRCode.toDataURL(speakeasy.otpauthURL({
const url = speakeasy.otpauthURL({
secret: secret.base32,
encoding: 'base32',
label: user.username,
issuer: config.host,
}));
});
const dataUrl = await QRCode.toDataURL(url);
return {
qr: dataUrl,
url,
secret: secret.base32,
label: user.username,
issuer: config.host,

View file

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

View file

@ -61,7 +61,14 @@ export default define(meta, paramDef, async (ps, me) => {
.getMany();
} else {
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
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });

View file

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

View file

@ -59,6 +59,18 @@ export function genOpenapiSpec(lang = 'ja-JP') {
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 = {
operationId: endpoint.name,
summary: endpoint.name,
@ -78,8 +90,8 @@ export function genOpenapiSpec(lang = 'ja-JP') {
requestBody: {
required: true,
content: {
'application/json': {
schema: endpoint.params,
[requestType]: {
schema,
},
},
},

View file

@ -9,6 +9,7 @@ import { genId } from '@/misc/gen-id.js';
import { verifyLogin, hash } from '../2fa.js';
import { randomBytes } from 'node:crypto';
import { IsNull } from 'typeorm';
import { limiter } from '../limiter.js';
export default async (ctx: Koa.Context) => {
ctx.set('Access-Control-Allow-Origin', config.url);
@ -24,6 +25,21 @@ export default async (ctx: Koa.Context) => {
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') {
ctx.status = 400;
return;

View file

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

View file

@ -2,6 +2,7 @@
* Core Server
*/
import cluster from 'node:cluster';
import * as fs from 'node:fs';
import * as http from 'node:http';
import Koa from 'koa';
@ -88,10 +89,10 @@ router.get('/avatar/@:acct', 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));
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 => {
@ -142,5 +143,26 @@ export default () => new Promise(resolve => {
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);
});

View file

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

View file

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

View file

@ -39,28 +39,24 @@ html {
width: 28px;
height: 28px;
transform: translateY(70px);
color: var(--accent);
}
#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 {
#splashSpinner > .spinner {
position: absolute;
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;
}

View file

@ -1,17 +1,23 @@
block vars
block loadClientEntry
- const clientEntry = getClientEntry();
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'
!= '\n-->\n'
//
-
_____ _ _
| |_|___ ___| |_ ___ _ _
| | | | |_ -|_ -| \'_| -_| | |
|_|_|_|_|___|___|_,_|___|_ |
|___|
Thank you for using Misskey!
If you are reading this message... how about joining the development?
https://github.com/misskey-dev/misskey
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/not-found.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='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
block title
@ -52,7 +64,7 @@ html
script.
var VERSION = "#{version}";
var CLIENT_ENTRY = "#{clientEntry()}";
var CLIENT_ENTRY = "#{clientEntry.file}";
script
include ../boot.js
@ -65,4 +77,14 @@ html
div#splash
img#splashIcon(src= icon || '/static-assets/splash.png')
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

View file

@ -91,27 +91,20 @@ type ToJsonSchema<S> = {
};
export function getJsonSchema<S extends Schema>(schema: S): ToJsonSchema<Unflatten<ChartResult<S>>> {
const object = {};
for (const [k, v] of Object.entries(schema)) {
nestedProperty.set(object, k, null);
}
const jsonSchema = {
type: 'object',
properties: {} as Record<string, unknown>,
required: [],
};
function f(obj: Record<string, null | Record<string, unknown>>) {
const jsonSchema = {
type: 'object',
properties: {} as Record<string, unknown>,
required: [],
for (const k in schema) {
jsonSchema.properties[k] = {
type: 'array',
items: { type: 'number' },
};
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 tmp from 'tmp';
import * as path from 'node:path';
import { createTemp } from '@/misc/create-temp.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> {
const [outDir, cleanup] = await new Promise<[string, any]>((res, rej) => {
tmp.dir((e, path, cleanup) => {
if (e) return rej(e);
res([path, cleanup]);
export async function GenerateVideoThumbnail(source: string): Promise<IImage> {
const [file, cleanup] = await createTemp();
const parsed = path.parse(file);
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) => {
FFmpeg({
source: path,
})
.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;
// JPEGに変換 (Webpでもいいが、MastodonはWebpをサポートせず表示できなくなる)
return await convertToJpeg(498, 280);
} finally {
cleanup();
}
}

View file

@ -45,29 +45,20 @@ export async function uploadFromUrl({
// Create temp file
const [path, cleanup] = await createTemp();
// write content at URL to temp file
await downloadUrl(url, path);
let driveFile: DriveFile;
let error;
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}`);
return driveFile!;
} catch (e) {
error = e;
logger.error(`Failed to create drive file: ${e}`, {
url: url,
e: e,
});
}
// clean-up
cleanup();
if (error) {
throw error;
} else {
return driveFile!;
throw e;
} finally {
cleanup();
}
}

View file

@ -1,5 +1,6 @@
import { DOMWindow, JSDOM } from 'jsdom';
import fetch from 'node-fetch';
import tinycolor from 'tinycolor2';
import { getJson, getHtml, getAgentByUrl } from '@/misc/fetch.js';
import { Instance } from '@/models/entities/instance.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> {
if (doc) {
const themeColor = doc.querySelector('meta[name="theme-color"]')?.getAttribute('content');
const themeColor = doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') || manifest?.theme_color;
if (themeColor) {
return themeColor;
}
}
if (manifest) {
return manifest.theme_color;
if (themeColor) {
const color = new tinycolor(themeColor);
if (color.isValid()) return color.toHexString();
}
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
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';
import rndstr from 'rndstr';
import * as assert from 'assert';
import rndstr from 'rndstr';
import { initTestDb } from './utils.js';
describe('ActivityPub', () => {
@ -57,8 +57,8 @@ describe('ActivityPub', () => {
const note = await createNote(post.id, resolver, true);
assert.deepStrictEqual(note?.uri, post.id);
assert.deepStrictEqual(note?.visibility, 'public');
assert.deepStrictEqual(note?.text, post.content);
assert.deepStrictEqual(note.visibility, 'public');
assert.deepStrictEqual(note.text, post.content);
});
});

View file

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

View file

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

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