Compare commits
195 Commits
13.1.0-bet
...
13.3.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ceab34f5f3 | ||
![]() |
3a62625bbc | ||
![]() |
ad6844ac4a | ||
![]() |
a8c252a613 | ||
![]() |
1d39f785f1 | ||
![]() |
4b8b29b862 | ||
![]() |
d195406fdc | ||
![]() |
5173ed37f9 | ||
![]() |
825551d64f | ||
![]() |
449761bada | ||
![]() |
5859df389f | ||
![]() |
562b02310f | ||
![]() |
65ed702d87 | ||
![]() |
c559a9843f | ||
![]() |
88c3957085 | ||
![]() |
01778e11dc | ||
![]() |
9d9e8a3c4e | ||
![]() |
ed3e035ad6 | ||
![]() |
07f885fea8 | ||
![]() |
2cc98226ca | ||
![]() |
8a6f73c5ff | ||
![]() |
00e3453ce1 | ||
![]() |
16646dd77a | ||
![]() |
1f39d1fe26 | ||
![]() |
e8f3c587c9 | ||
![]() |
4b43745e7c | ||
![]() |
9db2f60053 | ||
![]() |
4610d8dfe3 | ||
![]() |
fa296efdf6 | ||
![]() |
d9d98f84bf | ||
![]() |
7c3143b8e5 | ||
![]() |
387fcd5c5d | ||
![]() |
ebc6437977 | ||
![]() |
dbc23b5d20 | ||
![]() |
843f1aed4f | ||
![]() |
e42938cad6 | ||
![]() |
2a41f6c383 | ||
![]() |
671d21a2c1 | ||
![]() |
515692d7a6 | ||
![]() |
00d28826b9 | ||
![]() |
5b38f76254 | ||
![]() |
ca7dbd6010 | ||
![]() |
133644e5a9 | ||
![]() |
04d60426c7 | ||
![]() |
8282bbd07c | ||
![]() |
7190bd00c9 | ||
![]() |
44b9539818 | ||
![]() |
b2ed4c9508 | ||
![]() |
c7b5c8b19e | ||
![]() |
f4bee24ccf | ||
![]() |
e9cb18c5aa | ||
![]() |
d8f33bc0af | ||
![]() |
663999556f | ||
![]() |
c5a12ca2c7 | ||
![]() |
7af0e38dd3 | ||
![]() |
7d9d1ae7c2 | ||
![]() |
cef448f0f2 | ||
![]() |
67d64c9365 | ||
![]() |
269af9d6b9 | ||
![]() |
d37a734379 | ||
![]() |
7cb13cf839 | ||
![]() |
d7dda8f6e3 | ||
![]() |
6670c72f8b | ||
![]() |
b21064ffa4 | ||
![]() |
1959cb462b | ||
![]() |
1d6767ef0c | ||
![]() |
4735ae6451 | ||
![]() |
452bd6db25 | ||
![]() |
f5d6b84381 | ||
![]() |
34f5d81d1f | ||
![]() |
aa8adc07aa | ||
![]() |
d87bb807c3 | ||
![]() |
7646d6ed47 | ||
![]() |
41a6ed0de0 | ||
![]() |
ec8074cd49 | ||
![]() |
7131eb1827 | ||
![]() |
605b0f27e4 | ||
![]() |
80d2e157f6 | ||
![]() |
1e3447bccb | ||
![]() |
5ffa106cc1 | ||
![]() |
fc641c9b96 | ||
![]() |
5f49ac1b11 | ||
![]() |
9ffecf25dc | ||
![]() |
35fd523edf | ||
![]() |
6721d4216c | ||
![]() |
e3275e916b | ||
![]() |
3ba5541a66 | ||
![]() |
945c50db1f | ||
![]() |
30dce42e03 | ||
![]() |
d4fb201d05 | ||
![]() |
2a2e8d0cf6 | ||
![]() |
520ed8cb4d | ||
![]() |
8cab16c824 | ||
![]() |
ae63a1f494 | ||
![]() |
117ac53505 | ||
![]() |
2c379732d2 | ||
![]() |
9ca1197759 | ||
![]() |
8d3283e2a5 | ||
![]() |
6589e8a390 | ||
![]() |
b62894ff56 | ||
![]() |
da274cd458 | ||
![]() |
a2268a95be | ||
![]() |
9fd1b35d95 | ||
![]() |
869854eae7 | ||
![]() |
238f923b41 | ||
![]() |
a5df2b0293 | ||
![]() |
e6eae558d3 | ||
![]() |
083fa53d9c | ||
![]() |
7b73dd2d62 | ||
![]() |
7028b7331b | ||
![]() |
eefebab530 | ||
![]() |
683ddbef3e | ||
![]() |
bd23522c76 | ||
![]() |
c1dfbe2623 | ||
![]() |
ed9facbb33 | ||
![]() |
26fbb3a560 | ||
![]() |
93dd0638ad | ||
![]() |
0d44129ae3 | ||
![]() |
0cffe60abc | ||
![]() |
8a6750278e | ||
![]() |
d347f0a087 | ||
![]() |
226e0c4714 | ||
![]() |
0b2f945bb6 | ||
![]() |
2f6c45e118 | ||
![]() |
a5f54580a9 | ||
![]() |
70df8c77fa | ||
![]() |
2c52655b17 | ||
![]() |
6c4c071ae9 | ||
![]() |
b19dba80f4 | ||
![]() |
a8b19f4aa8 | ||
![]() |
09f4b9e546 | ||
![]() |
2e6d8c792b | ||
![]() |
e6338a555d | ||
![]() |
313a489ba0 | ||
![]() |
b906ff3fed | ||
![]() |
ede96eca28 | ||
![]() |
42f3d9188b | ||
![]() |
a35e0e9261 | ||
![]() |
80a400a67c | ||
![]() |
7a6534f30b | ||
![]() |
68a523ec6d | ||
![]() |
97d6c1ee86 | ||
![]() |
19c93151ce | ||
![]() |
039a2af3ab | ||
![]() |
945129c371 | ||
![]() |
da32be3ef3 | ||
![]() |
468ec36830 | ||
![]() |
492fb9a115 | ||
![]() |
bd8b624bae | ||
![]() |
9dacf11702 | ||
![]() |
26ae2dfc0f | ||
![]() |
a7f43d5312 | ||
![]() |
7fdf298bd4 | ||
![]() |
7d7167df6d | ||
![]() |
aa339be2ab | ||
![]() |
1217d6fbb4 | ||
![]() |
ccb22539e1 | ||
![]() |
957eff0e63 | ||
![]() |
363d727c55 | ||
![]() |
31dcf713cc | ||
![]() |
7800a12e52 | ||
![]() |
d6ff50a30b | ||
![]() |
ead931211c | ||
![]() |
a3aafa03ad | ||
![]() |
307a882649 | ||
![]() |
3e112da486 | ||
![]() |
bd469420fa | ||
![]() |
38fde26d60 | ||
![]() |
dc4fd3e505 | ||
![]() |
4dc00ee72a | ||
![]() |
bd3d75df6b | ||
![]() |
890564e1da | ||
![]() |
002f98987d | ||
![]() |
43956f3ffb | ||
![]() |
f2a9194c79 | ||
![]() |
4cd70df7f4 | ||
![]() |
21e4c3dfe9 | ||
![]() |
d56fc41865 | ||
![]() |
fcabc99303 | ||
![]() |
fccd9c32e8 | ||
![]() |
58a3a0b7d4 | ||
![]() |
a2a1636c10 | ||
![]() |
46ec0303b7 | ||
![]() |
3b1669fb6b | ||
![]() |
09591fa4ae | ||
![]() |
85ce00adc0 | ||
![]() |
f25518af91 | ||
![]() |
b796aacf7f | ||
![]() |
ff24811676 | ||
![]() |
4c8a1867f0 | ||
![]() |
bce48dfee9 | ||
![]() |
c20311b8a7 | ||
![]() |
fb14ac50b8 | ||
![]() |
84d984bd31 | ||
![]() |
1bc856c451 |
@@ -114,11 +114,6 @@ id: 'aid'
|
||||
# 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
|
||||
|
||||
|
@@ -114,11 +114,6 @@ id: 'aid'
|
||||
# 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
|
||||
|
||||
|
2
.github/workflows/docker-develop.yml
vendored
2
.github/workflows/docker-develop.yml
vendored
@@ -31,3 +31,5 @@ jobs:
|
||||
push: true
|
||||
tags: misskey/misskey:develop
|
||||
labels: develop
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -109,8 +109,12 @@ jobs:
|
||||
# https://github.com/cypress-io/cypress/issues/4351#issuecomment-559489091
|
||||
- name: ALSA Env
|
||||
run: echo -e 'pcm.!default {\n type hw\n card 0\n}\n\nctl.!default {\n type hw\n card 0\n}' > ~/.asoundrc
|
||||
# XXX: This tries reinstalling Cypress if the binary is not cached
|
||||
# Remove this when the cache issue is fixed
|
||||
- name: Cypress install
|
||||
run: pnpm exec cypress install
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v4
|
||||
uses: cypress-io/github-action@v5
|
||||
with:
|
||||
install: false
|
||||
start: pnpm start:test
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -32,6 +32,7 @@ coverage
|
||||
!/.config/example.yml
|
||||
!/.config/docker_example.yml
|
||||
!/.config/docker_example.env
|
||||
docker-compose.yml
|
||||
|
||||
# misskey
|
||||
/build
|
||||
|
@@ -1 +1 @@
|
||||
v18.12.1
|
||||
v18.13.0
|
||||
|
102
CHANGELOG.md
102
CHANGELOG.md
@@ -9,7 +9,106 @@
|
||||
You should also include the user name that made the change.
|
||||
-->
|
||||
|
||||
## 13.x.x (unreleased)
|
||||
## 13.3.0 (2023/02/03)
|
||||
### Changes
|
||||
- twitter/github/discord連携機能が削除されました
|
||||
- ハッシュタグごとのチャートが削除されました
|
||||
- syslogのサポートが削除されました
|
||||
|
||||
### Improvements
|
||||
- ロールで広告の非表示が有効になっている場合は最初から広告を非表示にするように
|
||||
|
||||
## 13.2.6 (2023/02/01)
|
||||
### Changes
|
||||
- docker-compose.ymlをdocker-compose.yml.exampleにしました。docker-compose.ymlとしてコピーしてから使用してください。
|
||||
|
||||
### Improvements
|
||||
- 絵文字ピッカーのパフォーマンスを改善
|
||||
- AiScriptを0.12.4に更新
|
||||
|
||||
### Bugfixes
|
||||
- Server: リレーと通信できない問題を修正
|
||||
- Client: classicモード使用時にwindowサイズによってdefaultに変更された後に、windowサイズが元に戻ったらclassicに戻すように修正 #9669
|
||||
- Client: Chromeで検索ダイアログで変換確定するとそのまま検索されてしまう問題を修正
|
||||
|
||||
## 13.2.4 (2023/01/27)
|
||||
### Improvements
|
||||
- リモートカスタム絵文字表示時のパフォーマンスを改善
|
||||
- Default to `animation: false` when prefers-reduced-motion is set
|
||||
- リアクション履歴が公開なら、ログインしていなくても表示できるように
|
||||
- tweak blur setting
|
||||
- tweak custom emoji cache
|
||||
|
||||
### Bugfixes
|
||||
- fix aggregation of retention
|
||||
- ダッシュボードでオンラインユーザー数が表示されない問題を修正
|
||||
- フォロー申請・フォローのボタンが、通知から消えている問題を修正
|
||||
|
||||
## 13.2.3 (2023/01/26)
|
||||
### Improvements
|
||||
- カスタム絵文字の更新をリアルタイムで反映するように
|
||||
|
||||
### Bugfixes
|
||||
- turnstile-failed: missing-input-secret
|
||||
|
||||
## 13.2.2 (2023/01/25)
|
||||
### Improvements
|
||||
- サーバーのパフォーマンスを改善
|
||||
|
||||
### Bugfixes
|
||||
- サインイン時に誤ったレートリミットがかかることがある問題を修正
|
||||
- MFMのposition、rotate、scaleで小数が使えない問題を修正
|
||||
|
||||
## 13.2.1 (2023/01/24)
|
||||
### Improvements
|
||||
- デザインの調整
|
||||
- サーバーのパフォーマンスを改善
|
||||
|
||||
## 13.2.0 (2023/01/23)
|
||||
|
||||
### Improvements
|
||||
- onlyServer / onlyQueue オプションを復活
|
||||
- 他人の実績閲覧時は獲得条件を表示しないように
|
||||
- アニメーション減らすオプション有効時はリアクションのアニメーションを無効に
|
||||
- カスタム絵文字一覧のパフォーマンスを改善
|
||||
|
||||
### Bugfixes
|
||||
- Aiscript: button is not defined
|
||||
|
||||
## 13.1.7 (2023/01/22)
|
||||
|
||||
### Improvements
|
||||
- 新たな実績を追加
|
||||
- MFMにscaleタグを追加
|
||||
|
||||
## 13.1.4 (2023/01/22)
|
||||
|
||||
### Improvements
|
||||
- 新たな実績を追加
|
||||
|
||||
### Bugfixes
|
||||
- Client: ローカリゼーション更新時にリロードが繰り返されることがあるのを修正
|
||||
|
||||
## 13.1.3 (2023/01/22)
|
||||
|
||||
### Bugfixes
|
||||
- Client: リアクションのカスタム絵文字の表示の問題を修正
|
||||
|
||||
## 13.1.2 (2023/01/22)
|
||||
|
||||
### Bugfixes
|
||||
- Client: リアクションのカスタム絵文字の表示の問題を修正
|
||||
|
||||
## 13.1.1 (2023/01/22)
|
||||
|
||||
### Improvements
|
||||
- ローカルのカスタム絵文字を表示する際のパフォーマンスを改善
|
||||
- Client: 瞬間的に大量の実績を解除した際の挙動を改善
|
||||
|
||||
### Bugfixes
|
||||
- Client: アップデート時にローカリゼーションデータが更新されないことがあるのを修正
|
||||
|
||||
## 13.1.0 (2023/01/21)
|
||||
|
||||
### Improvements
|
||||
- 実績機能
|
||||
@@ -46,6 +145,7 @@ You should also include the user name that made the change.
|
||||
- Node.js 18.x or later is required
|
||||
- PostgreSQL 15.x is required
|
||||
- Misskey not using 15 specific features at 13.0.0, but may do so in the future.
|
||||
- Docker環境でPostgreSQLのアップデートを行う際のガイドはこちら: https://github.com/misskey-dev/misskey/pull/9641#issue-1536336620
|
||||
- Elasticsearchのサポートが削除されました
|
||||
- 代わりに今後任意の検索プロバイダを設定できる仕組みを構想しています。その仕組みを使えば今まで通りElasticsearchも利用できます
|
||||
- Yarnからpnpmに移行されました
|
||||
|
@@ -44,7 +44,7 @@ Thank you for your PR! Before creating a PR, please check the following:
|
||||
- Check if there are any documents that need to be created or updated due to this change.
|
||||
- If you have added a feature or fixed a bug, please add a test case if possible.
|
||||
- Please make sure that tests and Lint are passed in advance.
|
||||
- You can run it with `yarn test` and `yarn lint`. [See more info](#testing)
|
||||
- You can run it with `pnpm test` and `pnpm lint`. [See more info](#testing)
|
||||
- If this PR includes UI changes, please attach a screenshot in the text.
|
||||
|
||||
Thanks for your cooperation 🤗
|
||||
@@ -102,7 +102,7 @@ If your language is not listed in Crowdin, please open an issue.
|
||||
During development, it is useful to use the
|
||||
|
||||
```
|
||||
yarn dev
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
command.
|
||||
@@ -112,7 +112,7 @@ command.
|
||||
- Service Worker is watched by esbuild.
|
||||
|
||||
## Testing
|
||||
- Test codes are located in [`/test`](/test).
|
||||
- Test codes are located in [`/packages/backend/test`](/packages/backend/test).
|
||||
|
||||
### Run test
|
||||
Create a config file.
|
||||
@@ -127,12 +127,12 @@ Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.y
|
||||
|
||||
Run all test.
|
||||
```
|
||||
yarn test
|
||||
pnpm test
|
||||
```
|
||||
|
||||
#### Run specify test
|
||||
```
|
||||
yarn jest -- foo.ts
|
||||
pnpm jest -- foo.ts
|
||||
```
|
||||
|
||||
### e2e tests
|
||||
@@ -177,9 +177,9 @@ vue-routerとの最大の違いは、niraxは複数のルーターが存在す
|
||||
これにより、アプリ内ウィンドウでブラウザとは個別にルーティングすることなどが可能になります。
|
||||
|
||||
## Notes
|
||||
### How to resolve conflictions occurred at yarn.lock?
|
||||
### How to resolve conflictions occurred at pnpm-lock.yaml?
|
||||
|
||||
Just execute `yarn` to fix it.
|
||||
Just execute `pnpm` to fix it.
|
||||
|
||||
### INSERTするときにはsaveではなくinsertを使用する
|
||||
#6441
|
||||
@@ -265,7 +265,7 @@ MongoDBは`null`で返してきてたので、その感覚で`if (x === null)`
|
||||
### Migration作成方法
|
||||
packages/backendで:
|
||||
```sh
|
||||
yarn dlx typeorm migration:generate -d ormconfig.js -o <migration name>
|
||||
pnpm dlx typeorm migration:generate -d ormconfig.js -o <migration name>
|
||||
```
|
||||
|
||||
- 生成後、ファイルをmigration下に移してください
|
||||
|
19
Dockerfile
19
Dockerfile
@@ -2,8 +2,12 @@ ARG NODE_VERSION=18.13.0-bullseye
|
||||
|
||||
FROM node:${NODE_VERSION} AS builder
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
|
||||
&& apt-get update \
|
||||
&& apt-get install -yqq --no-install-recommends \
|
||||
build-essential
|
||||
|
||||
RUN corepack enable
|
||||
@@ -16,7 +20,8 @@ COPY ["packages/backend/package.json", "./packages/backend/"]
|
||||
COPY ["packages/frontend/package.json", "./packages/frontend/"]
|
||||
COPY ["packages/sw/package.json", "./packages/sw/"]
|
||||
|
||||
RUN pnpm i --frozen-lockfile
|
||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
|
||||
pnpm i --frozen-lockfile --aggregate-output
|
||||
|
||||
COPY . ./
|
||||
|
||||
@@ -30,11 +35,13 @@ FROM node:${NODE_VERSION}-slim AS runner
|
||||
ARG UID="991"
|
||||
ARG GID="991"
|
||||
|
||||
RUN apt-get update \
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ffmpeg tini \
|
||||
&& apt-get -y clean \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& corepack enable \
|
||||
&& groupadd -g "${GID}" misskey \
|
||||
&& useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey
|
||||
|
@@ -24,6 +24,8 @@
|
||||
|
||||
---
|
||||
|
||||
[](https://codecov.io/gh/misskey-dev/misskey)
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
@@ -133,11 +133,6 @@ id: "aid"
|
||||
# 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
|
||||
|
||||
|
@@ -20,7 +20,7 @@ gulp.task('copy:frontend:fonts', () =>
|
||||
);
|
||||
|
||||
gulp.task('copy:frontend:tabler-icons', () =>
|
||||
gulp.src('./packages/frontend/node_modules/@tabler/icons/iconfont/**/*').pipe(gulp.dest('./built/_frontend_dist_/tabler-icons/'))
|
||||
gulp.src('./packages/frontend/node_modules/@tabler/icons-webfont/**/*').pipe(gulp.dest('./built/_frontend_dist_/tabler-icons/'))
|
||||
);
|
||||
|
||||
gulp.task('copy:frontend:locales', cb => {
|
||||
|
@@ -938,6 +938,243 @@ cannotPerformTemporary: "Vorübergehend nicht verfügbar"
|
||||
cannotPerformTemporaryDescription: "Diese Aktion ist wegen des Überschreitenes des Ausführungslimits temporär nicht verfügbar. Bitte versuche es nach einiger Zeit erneut."
|
||||
preset: "Vorlage"
|
||||
selectFromPresets: "Aus Vorlagen wählen"
|
||||
achievements: "Errungenschaften"
|
||||
_achievements:
|
||||
earnedAt: "Freigeschaltet am"
|
||||
_types:
|
||||
_notes1:
|
||||
title: "Hallo Misskey!"
|
||||
description: "Sende deine erste Notiz"
|
||||
flavor: "Hab eine schöne Zeit mit Misskey!"
|
||||
_notes10:
|
||||
title: "Ein paar Notizen"
|
||||
description: "10 Notizen gesendet"
|
||||
_notes100:
|
||||
title: "Viele Notizen"
|
||||
description: "100 Notizen gesendet"
|
||||
_notes500:
|
||||
title: "Überschüttet mit Notizen"
|
||||
description: "500 Notizen gesendet"
|
||||
_notes1000:
|
||||
title: "Berg an Notizen"
|
||||
description: "1.000 Notizen gesendet"
|
||||
_notes5000:
|
||||
title: "Überquellende Notizen"
|
||||
description: "5.000 Notizen gesendet"
|
||||
_notes10000:
|
||||
title: "Supernotiz"
|
||||
description: "10.000 Notizen gesendet"
|
||||
_notes20000:
|
||||
title: "Brauche... mehr... Notizen"
|
||||
description: "20.000 Notizen gesendet"
|
||||
_notes30000:
|
||||
title: "Notizen, Notizen, Notizen"
|
||||
description: "30.000 Notizen gesendet"
|
||||
_notes40000:
|
||||
title: "Notizfabrik"
|
||||
description: "40.000 Notizen gesendet"
|
||||
_notes50000:
|
||||
title: "Planet der Notizen"
|
||||
description: "50.000 Notizen gesendet"
|
||||
_notes60000:
|
||||
title: "Notizquasar"
|
||||
description: "60.000 Notizen gesendet"
|
||||
_notes70000:
|
||||
title: "Schwarzes Notizloch"
|
||||
description: "70.000 Notizen gesendet"
|
||||
_notes80000:
|
||||
title: "Notizgalaxie"
|
||||
description: "80.000 Notizen gesendet"
|
||||
_notes90000:
|
||||
title: "Notizversum"
|
||||
description: "90.000 Notizen gesendet"
|
||||
_notes100000:
|
||||
title: "ALL YOUR NOTE ARE BELONG TO US"
|
||||
description: "100.000 Notizen gesendet"
|
||||
flavor: "Du hast wirklich viel zu sagen."
|
||||
_login3:
|
||||
title: "Anfänger Ⅰ"
|
||||
description: "An 3 Tagen eingeloggt"
|
||||
flavor: "Nenn' mich ab heute Misskist"
|
||||
_login7:
|
||||
title: "Anfänger Ⅱ"
|
||||
description: "An 7 Tagen eingeloggt"
|
||||
flavor: "Na, eingewöht?"
|
||||
_login15:
|
||||
title: "Anfänger Ⅲ"
|
||||
description: "An 15 Tagen eingeloggt"
|
||||
_login30:
|
||||
title: "Misskist Ⅰ"
|
||||
description: "An 30 Tagen eingeloggt"
|
||||
_login60:
|
||||
title: "Misskist Ⅱ"
|
||||
description: "An 60 Tagen eingeloggt"
|
||||
_login100:
|
||||
title: "Misskist Ⅲ"
|
||||
description: "An 100 Tagen eingeloggt"
|
||||
flavor: "Violent Misskist"
|
||||
_login200:
|
||||
title: "Stammbesucher Ⅰ"
|
||||
description: "An 200 Tagen eingeloggt"
|
||||
_login300:
|
||||
title: "Stammbesucher Ⅱ"
|
||||
description: "An 300 Tagen eingeloggt"
|
||||
_login400:
|
||||
title: "Stammbesucher Ⅲ"
|
||||
description: "An 400 Tagen eingeloggt"
|
||||
_login500:
|
||||
title: "Veteran Ⅰ"
|
||||
description: "An 500 Tagen eingeloggt"
|
||||
flavor: "Meine Kameraden, ich liebe sie, die Notizen."
|
||||
_login600:
|
||||
title: "Veteran Ⅱ"
|
||||
description: "An 600 Tagen eingeloggt"
|
||||
_login700:
|
||||
title: "Veteran Ⅲ"
|
||||
description: "An 700 Tagen eingeloggt"
|
||||
_login800:
|
||||
title: "Meister der Notizen Ⅰ"
|
||||
description: "An 800 Tagen eingeloggt"
|
||||
_login900:
|
||||
title: "Meister der Notizen Ⅱ"
|
||||
description: "An 900 Tagen eingeloggt"
|
||||
_login1000:
|
||||
title: "Meister der Notizen Ⅲ"
|
||||
description: "An 1000 Tagen eingeloggt"
|
||||
flavor: "Danke, dass du Misskey nutzt!"
|
||||
_noteClipped1:
|
||||
title: "Muss... clippen..."
|
||||
description: "Die erste Notiz geclippt"
|
||||
_noteFavorited1:
|
||||
title: "Sternengucker"
|
||||
description: "Eine Notiz als Favorit markiert"
|
||||
_myNoteFavorited1:
|
||||
title: "Sternensucher"
|
||||
description: "Ein anderer Benutzer hat eine deiner Notizen als Favoriten markiert"
|
||||
_profileFilled:
|
||||
title: "Perfekte Vorbereitung"
|
||||
description: "Fülle dein Profil aus"
|
||||
_markedAsCat:
|
||||
title: "Ich der Kater"
|
||||
description: "Markiere dein Konto als Katze"
|
||||
flavor: "Einen Namen bekommst du später. "
|
||||
_following1:
|
||||
title: "Das Folgen beginnt"
|
||||
description: "Du folgst deiner ersten Person"
|
||||
_following10:
|
||||
title: "Folge ihnen... folge ihnen..."
|
||||
description: "Du folgst über 10 Leuten"
|
||||
_following50:
|
||||
title: "Viele Freunde"
|
||||
description: "Du folgst über 50 Leuten"
|
||||
_following100:
|
||||
title: "100 Freunde"
|
||||
description: "Du folgst über 100 Leuten"
|
||||
_following300:
|
||||
title: "Freundeüberschuss"
|
||||
description: "Du folgst über 300 Leuten"
|
||||
_followers1:
|
||||
title: "Der erste Follower"
|
||||
description: "Du hast deinen ersten Follower erhalten"
|
||||
_followers10:
|
||||
title: "Mir nach!"
|
||||
description: "Die Anzahl deiner Follower hat 10 überschritten"
|
||||
_followers50:
|
||||
title: "Wirrwarr"
|
||||
description: "Die Anzahl deiner Follower hat 50 überschritten"
|
||||
_followers100:
|
||||
title: "Beliebt"
|
||||
description: "Die Anzahl deiner Follower hat 100 überschritten"
|
||||
_followers300:
|
||||
title: "Stellt euch bitte in einer Reihe auf"
|
||||
description: "Die Anzahl deiner Follower hat 300 überschritten"
|
||||
_followers500:
|
||||
title: "Funkmast"
|
||||
description: "Die Anzahl deiner Follower hat 500 überschritten"
|
||||
_followers1000:
|
||||
title: "Influencer"
|
||||
description: "Die Anzahl deiner Follower hat 1000 überschritten"
|
||||
_collectAchievements30:
|
||||
title: "Sammler der Errungenschaften"
|
||||
description: "Schalte 30 Errungenschaften frei"
|
||||
_viewAchievements3min:
|
||||
title: "Fan von Errungenschaften"
|
||||
description: "Schau dir die Liste deiner Errungenschaften für mindestens 3 Minuten an"
|
||||
_iLoveMisskey:
|
||||
title: "I Love Misskey"
|
||||
description: "Sende \"I ❤ #Misskey\""
|
||||
flavor: "Danke, dass du Misskey verwendest! - vom Entwicklerteam"
|
||||
_foundTreasure:
|
||||
title: "Schatzsuche"
|
||||
description: "Du hast einen verborgenen Schatz gefunden"
|
||||
_client30min:
|
||||
title: "Kurze Pause"
|
||||
description: "Habe Misskey für 30 Minuten geöffnet"
|
||||
_noteDeletedWithin1min:
|
||||
title: "Ups"
|
||||
description: "Lösche eine Notiz innerhalb von 1 Minute nachdem sie gesendet wurde"
|
||||
_postedAtLateNight:
|
||||
title: "Nachtaktiv"
|
||||
description: "Sende mitten in der Nacht eine Notiz"
|
||||
flavor: "Geh bald schlafen."
|
||||
_postedAt0min0sec:
|
||||
title: "Zeitansage"
|
||||
description: "Sende um 00:00 eine Notiz"
|
||||
flavor: "Klick Klick Klick Dooong"
|
||||
_selfQuote:
|
||||
title: "Selbstzitat"
|
||||
description: "Zitiere eine eigene Notiz"
|
||||
_htl20npm:
|
||||
title: "Fließende Chronik"
|
||||
description: "Deine Startseitenchronik erreicht eine Geschwindigkeit von 20 npm (Notizen pro Minute)"
|
||||
_viewInstanceChart:
|
||||
title: "Analyst"
|
||||
description: "Schau dir die Messwerte der Instanz an"
|
||||
_outputHelloWorldOnScratchpad:
|
||||
title: "Hallo Welt!"
|
||||
description: "Gib \"hello world\" in der Testumgebung aus"
|
||||
_open3windows:
|
||||
title: "Splitscreen"
|
||||
description: "Habe zur gleichen Zeit mindestens 3 Fenster offen"
|
||||
_driveFolderCircularReference:
|
||||
title: "Zyklischer Verweis"
|
||||
description: "Versuche, in Drive einen Zirkelbezug von Ordnern herzustellen"
|
||||
_reactWithoutRead:
|
||||
title: "Hast du das wirklich gelesen?"
|
||||
description: "Reagiere auf eine Notiz mit mindestens 100 Zeichen innerhalb von 3 Sekunden der Erstellung der Notiz"
|
||||
_clickedClickHere:
|
||||
title: "Klicke hier"
|
||||
description: "Du hast hier geklickt"
|
||||
_justPlainLucky:
|
||||
title: "Pures Glück"
|
||||
description: "Kann alle 10 Sekunden mit einer Warscheinlichkeit von 0.01% erhalten werden"
|
||||
_setNameToSyuilo:
|
||||
title: "Gottkomplex"
|
||||
description: "Setze deinen Namen auf \"syuilo\""
|
||||
_passedSinceAccountCreated1:
|
||||
title: "Einjahresjubiläum"
|
||||
description: "Seit der Erstellung deines Kontos ist 1 Jahr vergangen"
|
||||
_passedSinceAccountCreated2:
|
||||
title: "Zweijahresjubiläum"
|
||||
description: "Seit der Erstellung deines Kontos sind 2 Jahre vergangen"
|
||||
_passedSinceAccountCreated3:
|
||||
title: "Dreijahresjubiläum"
|
||||
description: "Seit der Erstellung deines Kontos sind 3 Jahre vergangen"
|
||||
_loggedInOnBirthday:
|
||||
title: "Alles Gute Zum Geburtstag"
|
||||
description: "Logge dich an deinem Geburtstag ein"
|
||||
_loggedInOnNewYearsDay:
|
||||
title: "Frohes Neujahr"
|
||||
description: "Logge dich am Neujahrstag ein"
|
||||
flavor: "Auf ein weiteres tolles Jahr in dieser Instanz"
|
||||
_cookieClicked:
|
||||
title: "Ein Spiel, in dem du auf einen Keks klickst"
|
||||
description: "Den Keks geklickt"
|
||||
flavor: "Bist du hier richtig?"
|
||||
_brainDiver:
|
||||
title: "Brain Diver"
|
||||
description: "Sende den Link zu Brain Diver"
|
||||
flavor: "Misskey-Misskey La-Tu-Ma"
|
||||
_role:
|
||||
new: "Rolle erstellen"
|
||||
edit: "Rolle bearbeiten"
|
||||
@@ -1587,6 +1824,7 @@ _notification:
|
||||
pollEnded: "Umfrageergebnisse sind verfügbar"
|
||||
unreadAntennaNote: "Antenne {name}"
|
||||
emptyPushNotificationMessage: "Push-Benachrichtigungen wurden aktualisiert"
|
||||
achievementEarned: "Errungenschaft freigeschaltet"
|
||||
_types:
|
||||
all: "Alle"
|
||||
follow: "Neue Follower"
|
||||
|
@@ -938,6 +938,243 @@ cannotPerformTemporary: "Temporarily unavailable"
|
||||
cannotPerformTemporaryDescription: "This action cannot be performed temporarily due to exceeding the execution limit. Please wait for a while and then try again."
|
||||
preset: "Preset"
|
||||
selectFromPresets: "Choose from presets"
|
||||
achievements: "Achievements"
|
||||
_achievements:
|
||||
earnedAt: "Unlocked at"
|
||||
_types:
|
||||
_notes1:
|
||||
title: "just setting up my msky"
|
||||
description: "Post your first note"
|
||||
flavor: "Have a good time with Misskey!"
|
||||
_notes10:
|
||||
title: "Some notes"
|
||||
description: "Post 10 notes"
|
||||
_notes100:
|
||||
title: "A lot of notes"
|
||||
description: "Post 100 notes"
|
||||
_notes500:
|
||||
title: "Covered in notes"
|
||||
description: "Post 500 notes"
|
||||
_notes1000:
|
||||
title: "A mountain of notes"
|
||||
description: "Post 1,000 notes"
|
||||
_notes5000:
|
||||
title: "Overflowing notes"
|
||||
description: "Post 5,000 notes"
|
||||
_notes10000:
|
||||
title: "Supernote"
|
||||
description: "Post 10,000 notes"
|
||||
_notes20000:
|
||||
title: "Need... more... notes..."
|
||||
description: "Post 20,000 notes"
|
||||
_notes30000:
|
||||
title: "Notes notes notes!"
|
||||
description: "Post 30,000 notes"
|
||||
_notes40000:
|
||||
title: "Note factory"
|
||||
description: "Post 40,000 notes"
|
||||
_notes50000:
|
||||
title: "Planet of notes"
|
||||
description: "Post 50,000 notes"
|
||||
_notes60000:
|
||||
title: "Note quasar"
|
||||
description: "Post 60,000 notes"
|
||||
_notes70000:
|
||||
title: "Note black hole"
|
||||
description: "Post 70,000 notes"
|
||||
_notes80000:
|
||||
title: "Note galaxy"
|
||||
description: "Post 80,000 notes"
|
||||
_notes90000:
|
||||
title: "Note universe"
|
||||
description: "Post 90,000 notes"
|
||||
_notes100000:
|
||||
title: "ALL YOUR NOTE ARE BELONG TO US"
|
||||
description: "Post 100,000 notes"
|
||||
flavor: "You sure have a lot to say."
|
||||
_login3:
|
||||
title: "Beginner I"
|
||||
description: "Log in for a total of 3 days"
|
||||
flavor: "Starting today, just call me Misskist"
|
||||
_login7:
|
||||
title: "Beginner II"
|
||||
description: "Log in for a total of 7 days"
|
||||
flavor: "Feel like you've gotten the hang of things yet?"
|
||||
_login15:
|
||||
title: "Beginner III"
|
||||
description: "Log in for a total of 15 days"
|
||||
_login30:
|
||||
title: "Misskist I"
|
||||
description: "Log in for a total of 30 days"
|
||||
_login60:
|
||||
title: "Misskist II"
|
||||
description: "Log in for a total of 60 days"
|
||||
_login100:
|
||||
title: "Misskist III"
|
||||
description: "Log in for a total of 100 days"
|
||||
flavor: "Violent Misskist"
|
||||
_login200:
|
||||
title: "Regular I"
|
||||
description: "Log in for a total of 200 days"
|
||||
_login300:
|
||||
title: "Regular II"
|
||||
description: "Log in for a total of 300 days"
|
||||
_login400:
|
||||
title: "Regular III"
|
||||
description: "Log in for a total of 400 days"
|
||||
_login500:
|
||||
title: "Expert I"
|
||||
description: "Log in for a total of 500 days"
|
||||
flavor: "My friends, it has often been said that I like notes"
|
||||
_login600:
|
||||
title: "Expert II"
|
||||
description: "Log in for a total of 600 days"
|
||||
_login700:
|
||||
title: "Expert III"
|
||||
description: "Log in for a total of 700 days"
|
||||
_login800:
|
||||
title: "Master of Notes I"
|
||||
description: "Log in for a total of 800 days"
|
||||
_login900:
|
||||
title: "Master of Notes II"
|
||||
description: "Log in for a total of 900 days"
|
||||
_login1000:
|
||||
title: "Master of Notes III"
|
||||
description: "Log in for a total of 1,000 days"
|
||||
flavor: "Thank you for using Misskey!"
|
||||
_noteClipped1:
|
||||
title: "Must... clip..."
|
||||
description: "Clip your first note"
|
||||
_noteFavorited1:
|
||||
title: "Stargazer"
|
||||
description: "Favorite your first note"
|
||||
_myNoteFavorited1:
|
||||
title: "Seeking Stars"
|
||||
description: "Have somebody else favorite one of your notes"
|
||||
_profileFilled:
|
||||
title: "Well-prepared"
|
||||
description: "Set up your profile"
|
||||
_markedAsCat:
|
||||
title: "I Am a Cat"
|
||||
description: "Mark your account as a cat"
|
||||
flavor: "I'll give you a name later."
|
||||
_following1:
|
||||
title: "Following your first user"
|
||||
description: "Follow a user"
|
||||
_following10:
|
||||
title: "Keep up... keep up..."
|
||||
description: "Follow 10 users"
|
||||
_following50:
|
||||
title: "Lots of friends"
|
||||
description: "Follow 50 accounts"
|
||||
_following100:
|
||||
title: "100 Friends"
|
||||
description: "Follow 100 accounts"
|
||||
_following300:
|
||||
title: "Friend overload"
|
||||
description: "Follow 300 accounts"
|
||||
_followers1:
|
||||
title: "First follower"
|
||||
description: "Gain 1 follower"
|
||||
_followers10:
|
||||
title: "Follow me!"
|
||||
description: "Gain 10 followers"
|
||||
_followers50:
|
||||
title: "Coming in crowds"
|
||||
description: "Gain 50 followers"
|
||||
_followers100:
|
||||
title: "Popular"
|
||||
description: "Gain 100 followers"
|
||||
_followers300:
|
||||
title: "Please form a single line"
|
||||
description: "Gain 300 followers"
|
||||
_followers500:
|
||||
title: "Radio Tower"
|
||||
description: "Gain 500 followers"
|
||||
_followers1000:
|
||||
title: "Influencer"
|
||||
description: "Gain 1,000 followers"
|
||||
_collectAchievements30:
|
||||
title: "Achievement Collector"
|
||||
description: "Earn 30 achievements"
|
||||
_viewAchievements3min:
|
||||
title: "Likes Achievements"
|
||||
description: "Look at your list of achievements for at least 3 minutes"
|
||||
_iLoveMisskey:
|
||||
title: "I Love Misskey"
|
||||
description: "Post \"I ❤ #Misskey\""
|
||||
flavor: "Misskey's development team greatly appreciates your support!"
|
||||
_foundTreasure:
|
||||
title: "Treasure Hunt"
|
||||
description: "You've found the hidden treasure"
|
||||
_client30min:
|
||||
title: "Short break"
|
||||
description: "Spend 30 minutes on Misskey"
|
||||
_noteDeletedWithin1min:
|
||||
title: "Nevermind"
|
||||
description: "Delete a note within a minute of posting it"
|
||||
_postedAtLateNight:
|
||||
title: "Nocturnal"
|
||||
description: "Post a note late at night"
|
||||
flavor: "It's about time to go to bed."
|
||||
_postedAt0min0sec:
|
||||
title: "Speaking Clock"
|
||||
description: "Post a note at 00:00"
|
||||
flavor: "Click Click Click Claaang"
|
||||
_selfQuote:
|
||||
title: "Self-Reference"
|
||||
description: "Quote your own note"
|
||||
_htl20npm:
|
||||
title: "Flowing Timeline"
|
||||
description: "Have the speed of your home timeline exceed 20 npm (notes per minute)"
|
||||
_viewInstanceChart:
|
||||
title: "Analyst"
|
||||
description: "View your instance's charts"
|
||||
_outputHelloWorldOnScratchpad:
|
||||
title: "Hello, world!"
|
||||
description: "Output \"hello world\" in the Scratchpad"
|
||||
_open3windows:
|
||||
title: "Multi-Window"
|
||||
description: "Have at least 3 windows open at the same time"
|
||||
_driveFolderCircularReference:
|
||||
title: "Circular Reference"
|
||||
description: "Attempt to create a recursively nested folder in Drive"
|
||||
_reactWithoutRead:
|
||||
title: "Did you really read that?"
|
||||
description: "React on a note that's over 100 characters long within 3 seconds of it being posted"
|
||||
_clickedClickHere:
|
||||
title: "Click here"
|
||||
description: "You've clicked here"
|
||||
_justPlainLucky:
|
||||
title: "Just Plain Lucky"
|
||||
description: "Has a chance to be obtained with a probability of 0.01% every 10 seconds"
|
||||
_setNameToSyuilo:
|
||||
title: "God Complex"
|
||||
description: "Set your name to \"syuilo\""
|
||||
_passedSinceAccountCreated1:
|
||||
title: "One Year Anniversary"
|
||||
description: "One year has passed since your account was created"
|
||||
_passedSinceAccountCreated2:
|
||||
title: "Two Year Anniversary"
|
||||
description: "Two years have passed since your account was created"
|
||||
_passedSinceAccountCreated3:
|
||||
title: "Three Year Anniversary"
|
||||
description: "Three years have passed since your account was created"
|
||||
_loggedInOnBirthday:
|
||||
title: "Happy Birthday"
|
||||
description: "Log in on your birthday"
|
||||
_loggedInOnNewYearsDay:
|
||||
title: "Happy New Year!"
|
||||
description: "Logged in on the first day of the year"
|
||||
flavor: "To another great year on this instance"
|
||||
_cookieClicked:
|
||||
title: "A game in which you click cookies"
|
||||
description: "Clicked the cookie"
|
||||
flavor: "Wait, are you on the correct website?"
|
||||
_brainDiver:
|
||||
title: "Brain Diver"
|
||||
description: "Post the link to Brain Diver"
|
||||
flavor: "Misskey-Misskey La-Tu-Ma"
|
||||
_role:
|
||||
new: "New role"
|
||||
edit: "Edit role"
|
||||
@@ -1587,6 +1824,7 @@ _notification:
|
||||
pollEnded: "Poll results have become available"
|
||||
unreadAntennaNote: "Antenna {name}"
|
||||
emptyPushNotificationMessage: "Push notifications have been updated"
|
||||
achievementEarned: "Achievement unlocked"
|
||||
_types:
|
||||
all: "All"
|
||||
follow: "New followers"
|
||||
|
@@ -509,7 +509,7 @@ objectStorageSetPublicRead: "Seleccionar \"public-read\" al subir "
|
||||
serverLogs: "Registros del servidor"
|
||||
deleteAll: "Eliminar todos"
|
||||
showFixedPostForm: "Mostrar el formulario de las entradas encima de la línea de tiempo"
|
||||
newNoteRecived: "Tienes una nota nuevo"
|
||||
newNoteRecived: "Tienes una nota nueva"
|
||||
sounds: "Sonidos"
|
||||
sound: "Sonidos"
|
||||
listen: "Escuchar"
|
||||
@@ -918,14 +918,320 @@ tools: "Utilidades"
|
||||
cannotLoad: "No se puede cargar."
|
||||
numberOfProfileView: "Número de vistas de perfil"
|
||||
like: "¡Muy bien!"
|
||||
unlike: "Quitar 'me gusta'"
|
||||
numberOfLikes: "Cantidad de 'Me gusta'"
|
||||
show: "Apariencia"
|
||||
neverShow: "No mostrar de nuevo"
|
||||
remindMeLater: "Recordar después"
|
||||
didYouLikeMisskey: "¿Te gusta Misskey?"
|
||||
pleaseDonate: "Misskey es software libre, y es usado por {host} . Por favor, ¡considera donar al proyecto principal para que podamos continuar!"
|
||||
roles: "Roles"
|
||||
role: "Roles"
|
||||
normalUser: "Usuario normal"
|
||||
undefined: "Indefinido"
|
||||
assign: "Asignar"
|
||||
unassign: "Quitar"
|
||||
color: "Color"
|
||||
manageCustomEmojis: "Administrar emojis personalizados"
|
||||
youCannotCreateAnymore: "Se alcanzó el límite de creación"
|
||||
cannotPerformTemporary: "Indisponible temporalmente"
|
||||
cannotPerformTemporaryDescription: "Esta acción no se puede realizar porque se excedió el límite de ejecución. Espera un poco y prueba de nuevo."
|
||||
preset: "Predefinido"
|
||||
selectFromPresets: "Escoger desde predefinidos"
|
||||
achievements: "Logros"
|
||||
_achievements:
|
||||
earnedAt: "Desbloqueado el"
|
||||
_types:
|
||||
_notes1:
|
||||
title: "Configurando mis espacio"
|
||||
description: "Publicar tu primera nota"
|
||||
flavor: "¡Pasándola bien con Misskey!"
|
||||
_notes10:
|
||||
title: "Algunas notas"
|
||||
description: "10 notas publicadas"
|
||||
_notes100:
|
||||
title: "¡Muchas notas!"
|
||||
description: "100 notas publicadas"
|
||||
_notes500:
|
||||
title: "¡Cubierto de notas!"
|
||||
description: "500 notas publicadas"
|
||||
_notes1000:
|
||||
title: "¡Una montaña de notas!"
|
||||
description: "1000 notas publicadas"
|
||||
_notes5000:
|
||||
title: "¡Exceso de notas!"
|
||||
description: "5000 notas publicadas"
|
||||
_notes10000:
|
||||
title: "¡Súpernota!"
|
||||
description: "10000 notas publicadas"
|
||||
_notes20000:
|
||||
title: "Necesito... Más... ¡Notas!"
|
||||
description: "20000 notas publicadas"
|
||||
_notes30000:
|
||||
title: "¡Notas! ¡Notas! ¡Notas!"
|
||||
description: "30000 notas publicadas"
|
||||
_notes40000:
|
||||
title: "Fábrica de notas"
|
||||
description: "40000 notas publicadas"
|
||||
_notes50000:
|
||||
title: "¡Un planeta de notas!"
|
||||
description: "50000 notas publicadas"
|
||||
_notes60000:
|
||||
title: "¡Un cuásar de notas!"
|
||||
description: "60000 notas publicadas"
|
||||
_notes70000:
|
||||
title: "¡Un hoyo negro de notas!"
|
||||
description: "70000 notas publicadas"
|
||||
_notes80000:
|
||||
title: "¡Una galaxia de notas!"
|
||||
description: "80000 notas publicadas"
|
||||
_notes90000:
|
||||
title: "¡Todo un universo de notas!"
|
||||
description: "90000 notas publicadas"
|
||||
_notes100000:
|
||||
title: "ALL YOUR NOTE ARE BELONG TO US"
|
||||
description: "100000 notas publicadas"
|
||||
flavor: "¿Tienes tanto para publicar?"
|
||||
_login3:
|
||||
title: "Principiante I"
|
||||
description: "Días desde el inicio de sesión: 3"
|
||||
flavor: "Desde hoy, soy Misskero"
|
||||
_login7:
|
||||
title: "Principiante II"
|
||||
description: "Días desde el inicio de sesión: 7"
|
||||
flavor: "¿Ya te acostumbraste?"
|
||||
_login15:
|
||||
title: "Principiante III"
|
||||
description: "Días desde el inicio de sesión: 15"
|
||||
_login30:
|
||||
title: "Misskero I"
|
||||
description: "Días desde el inicio de sesión: 30"
|
||||
_login60:
|
||||
title: "Misskero II"
|
||||
description: "Días desde el inicio de sesión: 60"
|
||||
_login100:
|
||||
title: "Misskero III"
|
||||
description: "Días desde el inicio de sesión: 100"
|
||||
flavor: "Para este usuario, Misskaína"
|
||||
_login200:
|
||||
title: "Regular I"
|
||||
description: "Días desde el inicio de sesión: 200"
|
||||
_login300:
|
||||
title: "Regular II"
|
||||
description: "Días desde el inicio de sesión: 300"
|
||||
_login400:
|
||||
title: "Regular III"
|
||||
description: "Días desde el inicio de sesión: 400"
|
||||
_login500:
|
||||
title: "Veterano I"
|
||||
description: "Días desde el inicio de sesión: 500"
|
||||
flavor: "Chicos, me encantan las libretas..."
|
||||
_login600:
|
||||
title: "Veterano II"
|
||||
description: "Días desde el inicio de sesión: 600"
|
||||
_login700:
|
||||
title: "Veterano III"
|
||||
description: "Días desde el inicio de sesión: 700"
|
||||
_login800:
|
||||
title: "Maestro I"
|
||||
description: "Días desde el inicio de sesión: 800"
|
||||
_login900:
|
||||
title: "Maestro II"
|
||||
description: "Días desde el inicio de sesión: 900"
|
||||
_login1000:
|
||||
title: "Maestro III"
|
||||
description: "Días desde el inicio de sesión: 1000"
|
||||
flavor: "¡Gracias por usar Misskey!"
|
||||
_noteClipped1:
|
||||
title: "No puedo evitar clipearte..."
|
||||
description: "Hacer un clip por primera vez"
|
||||
_noteFavorited1:
|
||||
title: "Contemplando las estrellas"
|
||||
description: "Poner una nota como favorito por primera vez"
|
||||
_myNoteFavorited1:
|
||||
title: "¡Quiero una estrella!"
|
||||
description: "Tu nota ha sido marcada como favorito por primera vez"
|
||||
_profileFilled:
|
||||
title: "¡Listo!"
|
||||
description: "Perfil completado"
|
||||
_markedAsCat:
|
||||
title: "Soy un gato"
|
||||
description: "Configurar la cuenta como cuenta de un gato"
|
||||
flavor: "Aún no tengo nombre"
|
||||
_following1:
|
||||
title: "Primera vez siguiendo a alguien"
|
||||
description: "Seguir a un usuario"
|
||||
_following10:
|
||||
title: "Ahí la llevas, ahí la llevas..."
|
||||
description: "10 usuarios seguidos"
|
||||
_following50:
|
||||
title: "¡Un puñado de amigos!"
|
||||
description: "50 cuentas seguidas"
|
||||
_following100:
|
||||
title: "100 amigos"
|
||||
description: "100 cuentas seguidas"
|
||||
_following300:
|
||||
title: "¡Sobrecarga de amigos!"
|
||||
description: "300 cuentas seguidas"
|
||||
_followers1:
|
||||
title: "¡Tu primer seguidor!"
|
||||
description: "1 seguidor ganado"
|
||||
_followers10:
|
||||
title: "¡Sígueme!"
|
||||
description: "10 seguidores ganados"
|
||||
_followers50:
|
||||
title: "Viniendo en manada"
|
||||
description: "50 seguidores ganados"
|
||||
_followers100:
|
||||
title: "Popular"
|
||||
description: "100 cuentas seguidas"
|
||||
_followers300:
|
||||
title: "Por favor, hagan una fila"
|
||||
description: "300 seguidores ganados"
|
||||
_followers500:
|
||||
title: "¡Toda una torre de radio!"
|
||||
description: "500 seguidores ganados"
|
||||
_followers1000:
|
||||
title: "\"Influyente\""
|
||||
description: "1000 seguidores gandos"
|
||||
_collectAchievements30:
|
||||
title: "Coleccionista"
|
||||
description: "30 logros ganados"
|
||||
_viewAchievements3min:
|
||||
title: "¡Te gustan los logros!"
|
||||
description: "Mirando tus logros por 3 minutos"
|
||||
_iLoveMisskey:
|
||||
title: "¡AMO Misskey!"
|
||||
description: "\"I ❤ #Misskey\" Publicado"
|
||||
flavor: "El equipo de desarrollo de Misskey, en verdad, ¡aprecia tu apoyo!"
|
||||
_foundTreasure:
|
||||
title: "Búsqueda del tesoro"
|
||||
description: "Encontraste un tesoro"
|
||||
_client30min:
|
||||
title: "Un descansito"
|
||||
description: "30 minutos dedicados a Misskey"
|
||||
_noteDeletedWithin1min:
|
||||
title: "Ah... Mejor no..."
|
||||
description: "Borrar una nota antes que de pase 1 minuto"
|
||||
_postedAtLateNight:
|
||||
title: "Nocturno"
|
||||
description: "Una nota publicada por la noche"
|
||||
flavor: "¡Ya casi es hora de dormir!"
|
||||
_postedAt0min0sec:
|
||||
title: "Reloj parlante"
|
||||
description: "Publicar una nota a las 00:00 de la madrugada"
|
||||
flavor: "Tic, tic, tic ¡TUUUUUN!"
|
||||
_selfQuote:
|
||||
title: "Autoreferencia"
|
||||
description: "Citar tu propia nota"
|
||||
_htl20npm:
|
||||
title: "Línea de tiempo fluyendo"
|
||||
description: "La velocidad de tu línea de tiempo excede las 20 npm (notas por minuto)"
|
||||
_viewInstanceChart:
|
||||
title: "Analista"
|
||||
description: "Gráficas de la instancia mostradas"
|
||||
_outputHelloWorldOnScratchpad:
|
||||
title: "¡Hola mundo!"
|
||||
description: "Escribir \"hello world\" en el compositor"
|
||||
_open3windows:
|
||||
title: "Multiventana"
|
||||
description: "Tener más de 3 ventanas al mismo tiempo"
|
||||
_driveFolderCircularReference:
|
||||
title: "Referencia circular"
|
||||
description: "Intento de crear carpetas recursivamente"
|
||||
_reactWithoutRead:
|
||||
title: "¡Sí lo leíste bien?"
|
||||
description: "Reaccionar a los 3 segundos de publicación de una nota con más de 100 caracteres"
|
||||
_clickedClickHere:
|
||||
title: "Pícale aquí"
|
||||
description: "Le picó ahí"
|
||||
_justPlainLucky:
|
||||
title: "Pura suerte"
|
||||
description: "Obtenido con una probabilidad del 0.01% cada 10 segundos"
|
||||
_setNameToSyuilo:
|
||||
title: "Complejo de superioridad"
|
||||
description: "Configurar el nombre como 'Syuilo'"
|
||||
_passedSinceAccountCreated1:
|
||||
title: "Primer aniversario"
|
||||
description: "Pasó un año desde la creación de la cuenta"
|
||||
_passedSinceAccountCreated2:
|
||||
title: "Segundo aniversario"
|
||||
description: "Pasaron dos años desde la creación de la cuenta"
|
||||
_passedSinceAccountCreated3:
|
||||
title: "Tercer aniversario"
|
||||
description: "Pasaron tres años desde la creación de la cuenta"
|
||||
_loggedInOnBirthday:
|
||||
title: "¡Feliz cumpleaños!"
|
||||
description: "En linea el día de tu cumpleaños"
|
||||
_loggedInOnNewYearsDay:
|
||||
title: "¡Feliz Año Nuevo!"
|
||||
description: "En linea en año nuevo"
|
||||
flavor: "¡Gracias por tu apoyo a la instancia durante todo este año!"
|
||||
_cookieClicked:
|
||||
title: "Un juego para picarle a una galleta"
|
||||
description: "Picaste una galleta"
|
||||
flavor: "¿Está mal este juego?"
|
||||
_brainDiver:
|
||||
title: "Brain Diver"
|
||||
description: "Publicaste un vínculo a \"Brain Diver\""
|
||||
flavor: "Misskey-Misskey La-Tu-Ma"
|
||||
_role:
|
||||
new: "Crear rol"
|
||||
edit: "Editar rol"
|
||||
name: "Nombre del rol"
|
||||
description: "Descripción del rol"
|
||||
permission: "Permisos del rol"
|
||||
descriptionOfPermission: "<b>Moderador</b> Te permite ejecutar acciones básicas de moderación.\n<b>Administradores</b> puede cambiar todas las configuraciones de la instancia."
|
||||
assignTarget: "Asignar objetivo"
|
||||
descriptionOfAssignTarget: "<b>Manual</b> Para cambiar manualmente lo que se incluye en este rol.\n<b>Condicional</b> configura una condición, y los usuarios que cumplan la condición serán incluídos automáticamente."
|
||||
manual: "manual"
|
||||
conditional: "condicional"
|
||||
condition: "condición"
|
||||
isConditionalRole: "Esto es un rol condicional"
|
||||
isPublic: "Publicar rol"
|
||||
descriptionOfIsPublic: "Cualquiera puede ver los usuarios asignados a este rol. También, el perfil del usuario mostrará este rol."
|
||||
options: "Opción"
|
||||
policies: "Política"
|
||||
baseRole: "Rol base"
|
||||
useBaseValue: "Usar los valores del rol base"
|
||||
chooseRoleToAssign: "Selecciona el rol para asignar"
|
||||
canEditMembersByModerator: "Permitir a los moderadores editar los miembros"
|
||||
descriptionOfCanEditMembersByModerator: "Si se activa, los moderadores, al igual que los administradores, serán capaces de asignar/quitar usuarios a éste rol. Si se desactiva, sólo los administradores podrán hacerlo."
|
||||
priority: "Prioridad"
|
||||
_priority:
|
||||
low: "Baja"
|
||||
middle: "Mediano"
|
||||
high: "Alta"
|
||||
_options:
|
||||
gtlAvailable: "Explorar la línea de tiempo global"
|
||||
ltlAvailable: "Explorar la línea de tiempo local"
|
||||
canPublicNote: "Permitir la publicación"
|
||||
canInvite: "Puede crear códigos de invitación"
|
||||
canManageCustomEmojis: "Administrar emojis personalizados"
|
||||
driveCapacity: "Capacidad de almacenamiento"
|
||||
pinMax: "Máximo de notas fijadas"
|
||||
antennaMax: "Máximo de antenas"
|
||||
wordMuteMax: "Máximo de caracteres en palabras silenciadas"
|
||||
webhookMax: "Máximo de Webhooks"
|
||||
clipMax: "Máximo de clips"
|
||||
noteEachClipsMax: "Máximo de notas con clip"
|
||||
userListMax: "Máximo de listas de usuarios"
|
||||
userEachUserListsMax: "Máximo de usuarios en una lista"
|
||||
rateLimitFactor: "Limitador"
|
||||
descriptionOfRateLimitFactor: "Límites más bajos son menos restrictivos, más altos menos restrictivos"
|
||||
canHideAds: "Puede ocultar anuncios"
|
||||
_condition:
|
||||
isLocal: "Usuario local"
|
||||
isRemote: "Usuario remoto"
|
||||
createdLessThan: "Menos de X han pasado desde la creación de la cuenta"
|
||||
createdMoreThan: "Más de X han pasado desde la creación de la cuenta"
|
||||
followersLessThanOrEq: "Tiene X o menos seguidores"
|
||||
followersMoreThanOrEq: "Tiene X o más seguidores"
|
||||
followingLessThanOrEq: "Sigue X o menos cuentas"
|
||||
followingMoreThanOrEq: "Sigue X o más cuentas"
|
||||
and: "Condicional AND"
|
||||
or: "Condicional OR"
|
||||
not: "Condicional NOT"
|
||||
_sensitiveMediaDetection:
|
||||
description: "Reduce el esfuerzo de la moderación el el servidor a través del reconocimiento automático de contenido NSFW usando 'Machine Learning'. Esto puede incrementar ligeramente la carga en el servidor."
|
||||
sensitivity: "Sensibilidad de detección"
|
||||
@@ -1328,10 +1634,12 @@ _widgets:
|
||||
jobQueue: "Cola de trabajos"
|
||||
serverMetric: "Estadísticas del servidor"
|
||||
aiscript: "Consola de AiScript"
|
||||
aiscriptApp: "Aplicación AiScript"
|
||||
aichan: "indigo"
|
||||
userList: "Lista de usuarios"
|
||||
_userList:
|
||||
chooseList: "Seleccione una lista"
|
||||
clicker: "Cliqueador"
|
||||
_cw:
|
||||
hide: "Ocultar"
|
||||
show: "Ver más"
|
||||
@@ -1434,7 +1742,16 @@ _timelines:
|
||||
social: "Social"
|
||||
global: "Global"
|
||||
_play:
|
||||
new: "Crear guión"
|
||||
edit: "Editar guión"
|
||||
created: "Guión creado"
|
||||
updated: "Guión editado"
|
||||
deleted: "Guión eliminado"
|
||||
pageSetting: "Configuración de guión"
|
||||
editThisPage: "Editar este guión"
|
||||
viewSource: "Ver la fuente"
|
||||
my: "Mis guiones"
|
||||
liked: "Guiones que te gustaron"
|
||||
featured: "Popular"
|
||||
title: "Título"
|
||||
script: "Script"
|
||||
@@ -1507,6 +1824,7 @@ _notification:
|
||||
pollEnded: "Estan disponibles los resultados de la encuesta"
|
||||
unreadAntennaNote: "Antena {name}"
|
||||
emptyPushNotificationMessage: "Se han actualizado las notificaciones push"
|
||||
achievementEarned: "Logro desbloqueado"
|
||||
_types:
|
||||
all: "Todo"
|
||||
follow: "Siguiendo"
|
||||
|
@@ -13,6 +13,7 @@ fetchingAsApObject: "Mengambil data dari Fediverse..."
|
||||
ok: "OK"
|
||||
gotIt: "Saya mengerti"
|
||||
cancel: "Batalkan"
|
||||
noThankYou: "Tidak sekarang."
|
||||
enterUsername: "Masukkan nama pengguna"
|
||||
renotedBy: "direnote oleh {user}"
|
||||
noNotes: "Tidak ada catatan"
|
||||
@@ -206,6 +207,7 @@ done: "Selesai"
|
||||
processing: "Memproses"
|
||||
preview: "Pratinjau"
|
||||
default: "Bawaan"
|
||||
defaultValueIs: "Bawaan: {value}"
|
||||
noCustomEmojis: "Tidak ada emoji kustom"
|
||||
noJobs: "Tidak ada kerja"
|
||||
federating: "memfederasi"
|
||||
@@ -349,6 +351,8 @@ recaptcha: "reCAPTCHA"
|
||||
enableRecaptcha: "Nyalakan reCAPTCHA"
|
||||
recaptchaSiteKey: "Site key"
|
||||
recaptchaSecretKey: "Secret Key"
|
||||
turnstile: "Turnstile"
|
||||
enableTurnstile: "Nyalakan Turnstile"
|
||||
turnstileSiteKey: "Site key"
|
||||
turnstileSecretKey: "Secret Key"
|
||||
avoidMultiCaptchaConfirm: "Menggunakan banyak Captcha dapat menyebabkan gangguan. Apakah kamu ingin untuk menonaktifkan Captcha yang lain? Kamu dapat membiarkan fitur ini tetap aktif dengan menekan tombol batal."
|
||||
@@ -454,6 +458,7 @@ uiLanguage: "Bahasa antarmuka pengguna"
|
||||
groupInvited: "Telah diundang ke grup"
|
||||
aboutX: "Tentang {x}"
|
||||
emojiStyle: "Gaya emoji"
|
||||
native: "Native"
|
||||
disableDrawer: "Jangan gunakan menu bergaya laci"
|
||||
youHaveNoGroups: "Kamu tidak memiliki grup"
|
||||
joinOrCreateGroup: "Bergabunglah dengan grup atau kamu dapat membuat grupmu sendiri."
|
||||
@@ -857,10 +862,21 @@ rateLimitExceeded: "Batas sudah terlampaui"
|
||||
cropImage: "potong gambar"
|
||||
cropImageAsk: "Ingin memotong gambar?"
|
||||
file: "Berkas"
|
||||
recentNHours: "{n} jam terakhir"
|
||||
recentNDays: "{n} hari terakhir"
|
||||
noEmailServerWarning: "Mail Server tidak disetel."
|
||||
thereIsUnresolvedAbuseReportWarning: "Ada laporan yang belum diselesaikan."
|
||||
recommended: "Disarankan"
|
||||
check: "Cek"
|
||||
driveCapOverrideLabel: "Ubah kapasitas drive untuk user ini"
|
||||
driveCapOverrideCaption: "Setel ulang kapasitas ke bawaan dengan memasukkan nilai 0 atau lebih rendah."
|
||||
requireAdminForView: "Kamu harus login dengan akun administrator untuk melihat ini."
|
||||
isSystemAccount: "Akun yang dibuat dan otomatis dioperasikan oleh sistem."
|
||||
typeToConfirm: "Mohon masukkan {x} untuk mengonfirmasi"
|
||||
deleteAccount: "Hapus Akun"
|
||||
document: "Dokumen"
|
||||
numberOfPageCache: "Jumlah halaman ditembolokkan"
|
||||
numberOfPageCacheDescription: "Menaikkan jumlah ini akan meningkatkan kenyamanan untuk pengguna, namun dapat menyebabkan lonjakan beban pada peladen dan juga memori yang digunakan."
|
||||
logoutConfirm: "Anda yakin ingin keluar?"
|
||||
lastActiveDate: "Terakhir digunakan"
|
||||
statusbar: "Bilah status"
|
||||
@@ -870,20 +886,189 @@ colored: "Diwarnai"
|
||||
refreshInterval: "Jeda pembaharuan"
|
||||
label: "Label"
|
||||
type: "Tipe"
|
||||
speed: "Kecepatan"
|
||||
slow: "Lambat"
|
||||
fast: "Cepat"
|
||||
sensitiveMediaDetection: "Deteksi media NSFW"
|
||||
localOnly: "Hanya lokal"
|
||||
remoteOnly: "Hanya remot"
|
||||
failedToUpload: "Gagal mengunggah"
|
||||
cannotUploadBecauseInappropriate: "Berkas ini tidak dapat diunggah karena sebagian dari berkas terdeteksi berpotensi NSFW."
|
||||
cannotUploadBecauseNoFreeSpace: "Gagal mengunggah karena kekurangan kapasitas Drive."
|
||||
beta: "Beta"
|
||||
enableAutoSensitive: "Penandaan NSFW otomatis"
|
||||
enableAutoSensitiveDescription: "Mendeteksi otomatis dan menandai media NSFW menggunakan Machine Learning jika memungkinkan. Meskipun opsi ini dimatikan, ada kemungkinan dinyalakan secara menyeluruh pada instansi peladen."
|
||||
activeEmailValidationDescription: "Membolehkan validasi alamat surel ketat dengan mengecek apakah alamat surel tersebut temporer dan bisa berkomunikasi dengan surel tersebut. Ketidak tidak dicentang, hanya format surel yang divalidasi."
|
||||
navbar: "Bilah navigasi"
|
||||
shuffle: "Acak"
|
||||
account: "Akun"
|
||||
move: "Pindah"
|
||||
pushNotification: "Pemberitahuan push"
|
||||
subscribePushNotification: "Nyalakan pemberitahuan push"
|
||||
unsubscribePushNotification: "Matikan pemberitahuan push"
|
||||
pushNotificationAlreadySubscribed: "Pemberitahuan push telah dinyalakan"
|
||||
pushNotificationNotSupported: "Browser atau instansi kamu tidak mendukung pemberitahuan push"
|
||||
sendPushNotificationReadMessage: "Hapus pemberitahuan push ketika pemberitahuan relevan atau pesan telah dibaca"
|
||||
sendPushNotificationReadMessageCaption: "Pemberitahuan berisi teks「{emptyPushNotificationMessage}」akan ditampilkan dalam waktu pendek. Ini mungkin dapat menambah pemakaian baterai pada perangkat kamu."
|
||||
windowMaximize: "Maksimalkan"
|
||||
windowRestore: "Kembalikan"
|
||||
caption: "Keterangan"
|
||||
loggedInAsBot: "Sedang login sebagai bot"
|
||||
tools: "Alat"
|
||||
cannotLoad: "Tidak dapat memuat"
|
||||
numberOfProfileView: "tayang profil"
|
||||
like: "Suka"
|
||||
unlike: "Tidak Suka"
|
||||
numberOfLikes: "Jumlah yang disukai"
|
||||
show: "Tampilkan"
|
||||
neverShow: "Jangan tampilkan lagi"
|
||||
remindMeLater: "Mungkin nanti"
|
||||
didYouLikeMisskey: "Apakah kamu mulai menyukai Misskey?"
|
||||
pleaseDonate: "{host} menggunakan perangkat lunak bebas yaitu Misskey. Kami sangat mengapresiasi sekali donasi dari kamu agar pengembangan Misskey tetap dapat berlanjut!"
|
||||
roles: "Peran"
|
||||
role: "Peran"
|
||||
color: "Warna"
|
||||
_achievements:
|
||||
_types:
|
||||
_login7:
|
||||
description: "Login selama 7 hari"
|
||||
flavor: "Sudah mulai terbiasa?"
|
||||
_login15:
|
||||
title: "Pemula III"
|
||||
description: "Login selama 15 hari"
|
||||
_login30:
|
||||
title: "Misskist I"
|
||||
description: "Login selama 30 hari"
|
||||
_login60:
|
||||
title: "Misskist II"
|
||||
description: "Login selama 60 hari"
|
||||
_login100:
|
||||
title: "Misskist III"
|
||||
description: "Login selama 100 hari"
|
||||
flavor: "Violent Misskist"
|
||||
_login200:
|
||||
title: "Reguler I"
|
||||
description: "Login selama 200 hari"
|
||||
_login300:
|
||||
title: "Reguler II"
|
||||
description: "Login selama 300 hari"
|
||||
_login400:
|
||||
title: "Reguler III"
|
||||
description: "Login selama 400 hari"
|
||||
_login500:
|
||||
title: "Veteran I"
|
||||
description: "Login selama 500 hari"
|
||||
flavor: "Kawanku, aku suka catatan."
|
||||
_login600:
|
||||
title: "Veteran II"
|
||||
description: "Login selama 600 hari"
|
||||
_login700:
|
||||
title: "Veteran III"
|
||||
description: "Login selama 700 hari"
|
||||
_login800:
|
||||
title: "Sepuh Catatan I"
|
||||
description: "Login selama 800 hari"
|
||||
_login900:
|
||||
title: "Sepuh Catatan II"
|
||||
description: "Login selama 900 hari"
|
||||
_login1000:
|
||||
title: "Sepuh Catatan III"
|
||||
description: "Login selama 1000 hari"
|
||||
flavor: "Terima kasih telah menggunakan Misskey!"
|
||||
_noteClipped1:
|
||||
title: "Harus... Ngeklip..."
|
||||
description: "Klip catatan pertamamu"
|
||||
_noteFavorited1:
|
||||
title: "Pengamat Bintang"
|
||||
description: "Favoritkan catatan pertamamu"
|
||||
_myNoteFavorited1:
|
||||
title: "Pencari Bintang"
|
||||
description: "Minta orang lain memfavoritkan salah satu catatanmu"
|
||||
_profileFilled:
|
||||
title: "Siap Sedia"
|
||||
description: "Atur profil kamu"
|
||||
_markedAsCat:
|
||||
title: "Aku Seekor Kucing"
|
||||
description: "Tandai akunmu sebagai kucing"
|
||||
flavor: "Aku beri kamu nama nanti"
|
||||
_following1:
|
||||
title: "Ikuti pengguna lain pertamamu"
|
||||
description: "Ikuti pengguna"
|
||||
_following10:
|
||||
title: "Terusin... terusin..."
|
||||
description: "Ikuti 10 pengguna lain"
|
||||
_following50:
|
||||
title: "Banyak teman"
|
||||
description: "Ikuti 50 pengguna lain"
|
||||
_following100:
|
||||
title: "100 Teman"
|
||||
description: "Ikuti 100 pengguna lain"
|
||||
_clickedClickHere:
|
||||
description: "Kamu telah mengeklik disini"
|
||||
_justPlainLucky:
|
||||
title: "Lagi Beruntung"
|
||||
description: "Mendapatkan kesempatan dengan kemungkinan 0.01% setiap 10 detik"
|
||||
_setNameToSyuilo:
|
||||
title: "God Complex"
|
||||
description: "Atur namamu jadi \"syuilo\""
|
||||
_passedSinceAccountCreated1:
|
||||
title: "Perayaan Satu Tahun"
|
||||
description: "Satu tahun telah lewat sejak akunmu dibuat"
|
||||
_passedSinceAccountCreated2:
|
||||
title: "Perayaan Dua Tahun"
|
||||
description: "Dua tahun telah lewat sejak akunmu dibuat"
|
||||
_passedSinceAccountCreated3:
|
||||
title: "Perayaan Tiga Tahun"
|
||||
description: "Tiga tahun telah lewat sejak akunmu dibuat"
|
||||
_loggedInOnBirthday:
|
||||
title: "Selamat Ulang Tahun"
|
||||
description: "Login di hari ulang tahunmu"
|
||||
_loggedInOnNewYearsDay:
|
||||
title: "Selamat Tahun Baru!"
|
||||
description: "Login di hari pertama tahun baru"
|
||||
_cookieClicked:
|
||||
title: "Permainan dimana kamu mengeklik kue"
|
||||
description: "Mengeklik kue"
|
||||
flavor: "Tunggu, apakah kamu sedang berada di website yang benar?"
|
||||
_brainDiver:
|
||||
title: "Brain Diver"
|
||||
description: "Posting tautan mengenai Brain Diver"
|
||||
flavor: "Misskey-Misskey La-Tu-Ma"
|
||||
_role:
|
||||
new: "Buat peran"
|
||||
edit: "Sunting peran"
|
||||
name: "Nama peran"
|
||||
description: "Deskripsi peran"
|
||||
permission: "Perijinan peran"
|
||||
descriptionOfPermission: "<b>Moderator</b> dapat melakukan operasi moderasi dasar.\n<b>Administrator</b> dapat mengubah seluruh pengaturan instansi."
|
||||
assignTarget: "Tipe tugas"
|
||||
descriptionOfAssignTarget: "<b>Manual</b> untuk mengganti secara manual siapa yang mendapatkan peran ini dan siapa yang tidak.\n<b>Kondisional</b> untuk pengguna secara otomatis dimasukkan atau dihapus dari peran berdasarkan kondisi yang ditentukan."
|
||||
manual: "Manual"
|
||||
conditional: "Kondisional"
|
||||
condition: "Kondisi"
|
||||
isConditionalRole: "Ini adalah peran kondisional"
|
||||
isPublic: "Publikkan Peran"
|
||||
descriptionOfIsPublic: "Siapapun dapat melihat daftar pengguna yang ditugaskan pada peran ini. Tambahan juga peran ini akan ditampilkan ke dalam profil pengguna tentang peran yang ditugaskan."
|
||||
options: "Opsi peran"
|
||||
policies: "Kebijakan"
|
||||
baseRole: "Templat peran"
|
||||
useBaseValue: "Gunakan nilai templat peran"
|
||||
chooseRoleToAssign: "Pilih peran yang ditugaskan"
|
||||
canEditMembersByModerator: "Perbolehkan moderator untuk menyunting daftar anggota untuk peran ini"
|
||||
descriptionOfCanEditMembersByModerator: "Ketika dinyalakan, moderator beserta administrator dapat menugaskan ataupun mencabut pengguna ke peran ini. Ketika dimatikan, hanya administrator saja yang dapat menugaskan pengguna ke peran ini."
|
||||
priority: "Prioritas"
|
||||
_priority:
|
||||
low: "Rendah"
|
||||
middle: "Sedang"
|
||||
high: "Tinggi"
|
||||
_options:
|
||||
gtlAvailable: "Dapat melihat linimasa global"
|
||||
ltlAvailable: "Dapat melihat linimasa lokal"
|
||||
canPublicNote: "Dapat mengirim catatan publik"
|
||||
canInvite: "Dapat membuat kode undangan instansi"
|
||||
canManageCustomEmojis: "Dapat mengelola Emoji kustom"
|
||||
driveCapacity: "Kapasitas Drive"
|
||||
pinMax: "Jumlah maksimal catatan yang disematkan"
|
||||
_emailUnavailable:
|
||||
used: "Alamat surel ini telah digunakan"
|
||||
format: "Format tidak valid."
|
||||
@@ -1167,6 +1352,7 @@ _tutorial:
|
||||
step7_1: "Yay, Selamat! Kamu sudah menyelesaikan tutorial dasar Misskey."
|
||||
step7_2: "Jika kamu ingin mempelajari lebih lanjut tentang Misskey, cobalah berkunjung ke bagian {help}."
|
||||
step7_3: "Semoga berhasil dan bersenang-senanglah! 🚀"
|
||||
step8_3: "Kamu dapat mengganti pengaturan ini nanti."
|
||||
_2fa:
|
||||
alreadyRegistered: "Kamu telah mendaftarkan perangkat otentikasi dua faktor."
|
||||
registerDevice: "Daftarkan perangkat baru"
|
||||
@@ -1241,10 +1427,13 @@ _widgets:
|
||||
trends: "Tren"
|
||||
clock: "Jam"
|
||||
rss: "Pembaca RSS"
|
||||
rssTicker: "RSS-Ticker"
|
||||
activity: "Aktivitas"
|
||||
photos: "Foto"
|
||||
digitalClock: "Jam digital"
|
||||
unixClock: "Jam UNIX"
|
||||
federation: "Federasi"
|
||||
instanceCloud: "Instansi awan"
|
||||
postForm: "Buat catatan"
|
||||
slideshow: "Slideshow"
|
||||
button: "Tombol"
|
||||
@@ -1254,8 +1443,10 @@ _widgets:
|
||||
aiscript: "Konsol AiScript"
|
||||
aiscriptApp: "Aplikasi AiScript"
|
||||
aichan: "Ai"
|
||||
userList: "Daftar pengguna"
|
||||
_userList:
|
||||
chooseList: "Pilih daftar"
|
||||
clicker: "Pengeklik"
|
||||
_cw:
|
||||
hide: "Sembunyikan"
|
||||
show: "Lihat konten"
|
||||
@@ -1319,6 +1510,7 @@ _profile:
|
||||
changeBanner: "Ubah header"
|
||||
_exportOrImport:
|
||||
allNotes: "Semua catatan"
|
||||
favoritedNotes: "Catatan favorit"
|
||||
followingList: "Ikuti"
|
||||
muteList: "Bisukan"
|
||||
blockingList: "Blokir"
|
||||
@@ -1437,7 +1629,9 @@ _notification:
|
||||
yourFollowRequestAccepted: "Permintaan mengikuti kamu telah diterima"
|
||||
youWereInvitedToGroup: "Telah diundang ke grup"
|
||||
pollEnded: "Hasil Kuesioner telah keluar"
|
||||
unreadAntennaNote: "Antena {name}"
|
||||
emptyPushNotificationMessage: "Pembaruan notifikasi dorong"
|
||||
achievementEarned: "Pencapaian didapatkan"
|
||||
_types:
|
||||
all: "Semua"
|
||||
follow: "Ikuti"
|
||||
@@ -1459,6 +1653,7 @@ _deck:
|
||||
alwaysShowMainColumn: "Selalu tampilkan kolom utama"
|
||||
columnAlign: "Luruskan kolom"
|
||||
addColumn: "Tambahkan kolom"
|
||||
configureColumn: "Atur kolom"
|
||||
swapLeft: "Pindah ke kiri"
|
||||
swapRight: "Pindah ke kanan"
|
||||
swapUp: "Pindah ke atas"
|
||||
@@ -1466,6 +1661,11 @@ _deck:
|
||||
stackLeft: "Tumpukkan di kolom kiri"
|
||||
popRight: "Keluarkan di kanan"
|
||||
profile: "Profil"
|
||||
newProfile: "Profil baru"
|
||||
deleteProfile: "Hapus profil"
|
||||
introduction: "Buat antarmuka sempurna untukmu dengan menata kolom secara bebas!"
|
||||
introduction2: "Klik \"+\" pada kanan layar untuk menambahkan kolom baru kapanpun yang kamu mau."
|
||||
widgetsIntroduction: "Mohon pilih \"Sunting gawit\" pada menu kolom dan tambahkan gawit."
|
||||
_columns:
|
||||
main: "Utama"
|
||||
widgets: "Widget"
|
||||
|
@@ -1,7 +1,7 @@
|
||||
---
|
||||
_lang_: "Italiano"
|
||||
headlineMisskey: "Rete collegata tramite note"
|
||||
introMisskey: "Eccoci! Misskey è un servizio di microblogging decentralizzato, libero e aperto. \n📡 Puoi pubblicare «Note» per condividere ciò che sta succedendo o per dire a tutti qualcosa su di te. \n👍 Puoi reagire inviando emoji rapidi alle «Note» provenienti da altri profili nel Fediverso.\n🚀 Esplora un nuovo mondo insieme a noi!"
|
||||
introMisskey: "Eccoci! Misskey è un servizio di microblogging decentralizzato, libero e aperto. \n\n📡 Puoi pubblicare «Note» per condividere ciò che sta succedendo o per dire a tutti qualcosa su di te. \n\n👍 Puoi reagire inviando emoji rapidi alle «Note» provenienti da altri profili nel Fediverso.\n\n🚀 Esplora un nuovo mondo insieme a noi!"
|
||||
poweredByMisskeyDescription: "{name} è uno dei servizi (chiamati istanze) che utilizzano la piattaforma open source <b>Misskey</b>."
|
||||
monthAndDay: "{day}/{month}"
|
||||
search: "Cerca"
|
||||
@@ -95,7 +95,7 @@ follow: "Segui"
|
||||
followRequest: "Richiesta di follow"
|
||||
followRequests: "Richieste di follow"
|
||||
unfollow: "Smetti di seguire"
|
||||
followRequestPending: "La richiesta di follow deve essere approvata"
|
||||
followRequestPending: "Richiesta in approvazione"
|
||||
enterEmoji: "Inserisci emoji"
|
||||
renote: "Rinota"
|
||||
unrenote: "Annulla rinota"
|
||||
@@ -938,6 +938,243 @@ cannotPerformTemporary: "Indisponibilità temporanea"
|
||||
cannotPerformTemporaryDescription: "L'attività non può essere svolta, poiché si è raggiunto il limite di esecuzioni possibili. Per favore, riprova più tardi."
|
||||
preset: "Preimpostato"
|
||||
selectFromPresets: "Seleziona preimpostato"
|
||||
achievements: "Obiettivi raggiunti"
|
||||
_achievements:
|
||||
earnedAt: "Data di conseguimento"
|
||||
_types:
|
||||
_notes1:
|
||||
title: "Hai iniziato a usare Misskey"
|
||||
description: "Hai pubblicato la prima Nota"
|
||||
flavor: "Goditi la vita su Misskey!"
|
||||
_notes10:
|
||||
title: "Alcune Note"
|
||||
description: "Hai inserito 10 Note"
|
||||
_notes100:
|
||||
title: "Un po' di Note"
|
||||
description: "Hai inserito 100 Note"
|
||||
_notes500:
|
||||
title: "Un bel po' di Note"
|
||||
description: "Hai inserito 500 Note"
|
||||
_notes1000:
|
||||
title: "Una montagna di Note"
|
||||
description: "Hai inserito 1.000 Note"
|
||||
_notes5000:
|
||||
title: "Un sovraccarico di Note!"
|
||||
description: "Hai inserito 5.000 Note"
|
||||
_notes10000:
|
||||
title: "SuperNote!"
|
||||
description: "Hai inserito 10.000 Note"
|
||||
_notes20000:
|
||||
title: "Voglio più... Note!"
|
||||
description: "Hai inserito 20.000 Note"
|
||||
_notes30000:
|
||||
title: "Note, Note, Note!"
|
||||
description: "Hai inserito 30.000 Note"
|
||||
_notes40000:
|
||||
title: "Una fabbrica di Note"
|
||||
description: "Hai inserito 40.000 Note"
|
||||
_notes50000:
|
||||
title: "Un pianeta di Note"
|
||||
description: "Hai inserito 50.000 Note"
|
||||
_notes60000:
|
||||
title: "Un quasar di Note"
|
||||
description: "Hai inserito 60.000 Note"
|
||||
_notes70000:
|
||||
title: "Un buco nero supermassiccio di Note"
|
||||
description: "Hai inserito 70.000 Note"
|
||||
_notes80000:
|
||||
title: "Una galassia di Note"
|
||||
description: "Hai inserito 80.000 Note"
|
||||
_notes90000:
|
||||
title: "Un universo di Note!"
|
||||
description: "Hai inserito 90.000 Note"
|
||||
_notes100000:
|
||||
title: "ALL YOUR NOTE ARE BELONG TO US"
|
||||
description: "Hai inserito 100.000 Note"
|
||||
flavor: "Hai molto da scrivere?"
|
||||
_login3:
|
||||
title: "Principiante I"
|
||||
description: "Accedi per un totale di 3 giorni"
|
||||
flavor: "Da oggi, chiamatemi Misskist"
|
||||
_login7:
|
||||
title: "Principiante II"
|
||||
description: "Accedi per un totale di 7 giorni"
|
||||
flavor: "Ti sembra di avere la situazione sotto controllo?"
|
||||
_login15:
|
||||
title: "Principiante III"
|
||||
description: "Accedi per un totale di 15 giorni"
|
||||
_login30:
|
||||
title: "Misskist I"
|
||||
description: "Accedi per un totale di 30 giorni"
|
||||
_login60:
|
||||
title: "Misskeist II"
|
||||
description: "Accedi per un totale di 60 giorni"
|
||||
_login100:
|
||||
title: "Misskeist III"
|
||||
description: "Accedi per un totale di 100 giorni"
|
||||
flavor: "Violent Misskeist"
|
||||
_login200:
|
||||
title: "Regolare I"
|
||||
description: "Accedi per un totale di 200 giorni"
|
||||
_login300:
|
||||
title: "Regolare II"
|
||||
description: "Accedi per un totale di 300 giorni"
|
||||
_login400:
|
||||
title: "Regolare III"
|
||||
description: "Accedi per un totale di 400 giorni"
|
||||
_login500:
|
||||
title: "Professionista I"
|
||||
description: "Accedi per un totale di 500 giorni"
|
||||
flavor: "Amici cari, mi piacciono le Note"
|
||||
_login600:
|
||||
title: "Professionista II"
|
||||
description: "Accedi per un totale di 600 giorni"
|
||||
_login700:
|
||||
title: "Professionista III"
|
||||
description: "Accedi per un totale di 700 giorni"
|
||||
_login800:
|
||||
title: "Maestro di Note I"
|
||||
description: "Accedi per un totale di 800 giorni"
|
||||
_login900:
|
||||
title: "Maestro di Note II"
|
||||
description: "Accedi per un totale di 900 giorni"
|
||||
_login1000:
|
||||
title: "Maestro di Note III"
|
||||
description: "Accedi per un totale di 1.000 giorni"
|
||||
flavor: "Grazie per aver usato Misskey!"
|
||||
_noteClipped1:
|
||||
title: "Devo clippare!"
|
||||
description: "Ho raccolto in Clip la prima Nota"
|
||||
_noteFavorited1:
|
||||
title: "Guarda le stelle"
|
||||
description: "Aggiungi una Nota ai preferiti per la prima volta"
|
||||
_myNoteFavorited1:
|
||||
title: "Fornitura stelline"
|
||||
description: "Qualcuno ha preferito una delle tue Note"
|
||||
_profileFilled:
|
||||
title: "Perfettamente"
|
||||
description: "Imposta il tuo profilo"
|
||||
_markedAsCat:
|
||||
title: "Io sono un gatto"
|
||||
description: "Aggiungi le orecchie da gatto al tuo profilo"
|
||||
flavor: "Ti chiamerò..."
|
||||
_following1:
|
||||
title: "Il mio primo Follow"
|
||||
description: "Hai seguito il tuo primo profilo"
|
||||
_following10:
|
||||
title: "Segui, segui!"
|
||||
description: "Hai seguito 10 profili"
|
||||
_following50:
|
||||
title: "Tanti amici"
|
||||
description: "Hai seguito 50 profili"
|
||||
_following100:
|
||||
title: "Cento amici"
|
||||
description: "Hai seguito 100 profili"
|
||||
_following300:
|
||||
title: "Sovraccarico di amici"
|
||||
description: "Hai seguito 300 profili"
|
||||
_followers1:
|
||||
title: "Il primo profilo tuo Follower"
|
||||
description: "Hai ottenuto il tuo primo Follower"
|
||||
_followers10:
|
||||
title: "Follow me!"
|
||||
description: "Hai ottenuto 10 profili Follower"
|
||||
_followers50:
|
||||
title: "Follower a frotte"
|
||||
description: "Hai ottenuto 50 Follower"
|
||||
_followers100:
|
||||
title: "Popolare"
|
||||
description: "Hai ottenuto 100 profili Follower"
|
||||
_followers300:
|
||||
title: "Mettetevi in fila"
|
||||
description: "Hai ottenuto 300 Follower"
|
||||
_followers500:
|
||||
title: "Trasmettitore"
|
||||
description: "Hai ottenuto 500 Follower"
|
||||
_followers1000:
|
||||
title: "Influenzer"
|
||||
description: "Hai superato i 1.000 profili Follower"
|
||||
_collectAchievements30:
|
||||
title: "Collezionista di successi"
|
||||
description: "Hai raggiunto 30 obiettivi"
|
||||
_viewAchievements3min:
|
||||
title: "Mi piacciono i risultati"
|
||||
description: "Guarda la tua collezione di obiettivi per almeno 3 minuti"
|
||||
_iLoveMisskey:
|
||||
title: "I LOVE Misskey"
|
||||
description: "Pubblica «I ♥ #Misskey»"
|
||||
flavor: "Grazie per aver utilizzato Misskey! Dal team di sviluppo"
|
||||
_foundTreasure:
|
||||
title: "Caccia al tesoro"
|
||||
description: "Hai trovato un tesoro nascosto"
|
||||
_client30min:
|
||||
title: "Piccola pausa"
|
||||
description: "Hai passato più di 30 minuti su Misskey"
|
||||
_noteDeletedWithin1min:
|
||||
title: "Ooops!"
|
||||
description: "Hai eliminato una nota entro un minuto dalla sua pubblicazione"
|
||||
_postedAtLateNight:
|
||||
title: "Biassanot!"
|
||||
description: "Hai pubblicato una nota in tarda notte"
|
||||
flavor: "Andiamo a dormire presto"
|
||||
_postedAt0min0sec:
|
||||
title: "Mezzanotte"
|
||||
description: "Hai pubblicato una Nota a mezzanotte in punto"
|
||||
flavor: "tic, tac, tic, tac! Gong!"
|
||||
_selfQuote:
|
||||
title: "Autoreferenziale"
|
||||
description: "Hai citato una delle tue Note"
|
||||
_htl20npm:
|
||||
title: "Timeline scorrevole"
|
||||
description: "La tua Timeline personale ha superato la velocità di 20 Note orarie (Note al minuto)"
|
||||
_viewInstanceChart:
|
||||
title: "Analista"
|
||||
description: "Visualizza i grafici dell'istanza"
|
||||
_outputHelloWorldOnScratchpad:
|
||||
title: "Hello, world!"
|
||||
description: "Hai scritto «Hello world» nel blocco appunti"
|
||||
_open3windows:
|
||||
title: "Finestrato"
|
||||
description: "Hai aperto almeno 3 finestre contemporaneamente"
|
||||
_driveFolderCircularReference:
|
||||
title: "Riferimento circolare"
|
||||
description: "Hai provato a nidificare in modo ricorsivo le cartelle del Drive"
|
||||
_reactWithoutRead:
|
||||
title: "Hai letto bene?"
|
||||
description: "Hai reagito ad una Nota più lunga di 100 caratteri entro 3 secondi dalla sua pubblicazione"
|
||||
_clickedClickHere:
|
||||
title: "Clicca qui"
|
||||
description: "Hai cliccato qui"
|
||||
_justPlainLucky:
|
||||
title: "Proprio fortunato"
|
||||
description: "Ottenuto con una probabilità dello 0,01% ogni 10 secondi"
|
||||
_setNameToSyuilo:
|
||||
title: "Complesso divino"
|
||||
description: "Hai impostati il tuo nome in «syuilo»"
|
||||
_passedSinceAccountCreated1:
|
||||
title: "Primo Anniversario"
|
||||
description: "È passato un anno da quando hai creato il profilo"
|
||||
_passedSinceAccountCreated2:
|
||||
title: "Secondo Anniversario"
|
||||
description: "Sono passati due anni da quando hai creato il profilo"
|
||||
_passedSinceAccountCreated3:
|
||||
title: "Terzo Anniversario"
|
||||
description: "Sono passati tre anni da quando hai creato il profilo"
|
||||
_loggedInOnBirthday:
|
||||
title: "Buon compleanno!"
|
||||
description: "Hai effettuato l'accesso il giorno del tuo compleanno"
|
||||
_loggedInOnNewYearsDay:
|
||||
title: "Buon anno nuovo!"
|
||||
description: "Hai usato effettuato l'accesso il giorno di capodanno"
|
||||
flavor: "Anche quest'anno, grazie per il tuo continuo supporto a questa istanza"
|
||||
_cookieClicked:
|
||||
title: "Clicca il biscotto"
|
||||
description: "Hai giocato a cliccare il cookie"
|
||||
flavor: "Hai autorizzato i cookie?"
|
||||
_brainDiver:
|
||||
title: "Brain Diver"
|
||||
description: "Pubblica un link a Brain Diver"
|
||||
flavor: "Sulle note di Brain Diver"
|
||||
_role:
|
||||
new: "Nuovo ruolo"
|
||||
edit: "Modifica ruolo"
|
||||
@@ -1270,8 +1507,8 @@ _sfx:
|
||||
channel: "Notifiche di canale"
|
||||
_ago:
|
||||
future: "Futuro"
|
||||
justNow: "Ora"
|
||||
secondsAgo: "{n}s fa"
|
||||
justNow: "Adesso"
|
||||
secondsAgo: "{n} sec fa"
|
||||
minutesAgo: "{n} min fa"
|
||||
hoursAgo: "{n} ore fa"
|
||||
daysAgo: "{n} gg fa"
|
||||
@@ -1293,7 +1530,7 @@ _tutorial:
|
||||
step3_1: "Hai finito di impostare il tuo profilo?"
|
||||
step3_2: "Ora puoi pubblicare una «Nota». Proviamo subito! Premi il bottone con l'icona «penna» per iniziare a scrivere in una finestra di dialogo. "
|
||||
step3_3: "Scritto il testo della nota, puoi pubblicarla premendo il pulsante nella parte superiore destra della finestra di dialogo."
|
||||
step3_4: "Non ti viene niente in mente? Perché non scrivi semplicemente \"Ho appena cominciato a usare Misskey\"?"
|
||||
step3_4: "Non ti viene niente in mente? Perché non scrivi semplicemente \"Ho appena iniziato a usare Misskey\"?"
|
||||
step4_1: "Hai pubblicato qualcosa?"
|
||||
step4_2: "Se puoi visualizzare la tua nota sulla timeline, ce l'hai fatta!"
|
||||
step5_1: "Adesso, cerca di seguire altre persone per vivacizzare la tua timeline. "
|
||||
@@ -1587,6 +1824,7 @@ _notification:
|
||||
pollEnded: "Risultati del sondaggio."
|
||||
unreadAntennaNote: "Antenna {name}"
|
||||
emptyPushNotificationMessage: "Le notifiche push sono state aggiornate."
|
||||
achievementEarned: "Obiettivo raggiunto"
|
||||
_types:
|
||||
all: "Tutto"
|
||||
follow: "Novità follower"
|
||||
|
@@ -1049,6 +1049,9 @@ _achievements:
|
||||
_noteFavorited1:
|
||||
title: "星をみるひと"
|
||||
description: "初めてノートをお気に入りに登録した"
|
||||
_myNoteFavorited1:
|
||||
title: "星が欲しい"
|
||||
description: "自分のノートが他の人からお気に入りに登録された"
|
||||
_profileFilled:
|
||||
title: "準備万端"
|
||||
description: "プロフィール設定を行った"
|
||||
@@ -1102,6 +1105,9 @@ _achievements:
|
||||
title: "I Love Misskey"
|
||||
description: "\"I ❤ #Misskey\"を投稿した"
|
||||
flavor: "Misskeyを使ってくださりありがとうございます! by 開発チーム"
|
||||
_foundTreasure:
|
||||
title: "宝探し"
|
||||
description: "隠されたお宝を発見した"
|
||||
_client30min:
|
||||
title: "ひとやすみ"
|
||||
description: "クライアントを起動してから30分以上経過した"
|
||||
@@ -1122,6 +1128,9 @@ _achievements:
|
||||
_htl20npm:
|
||||
title: "流れるTL"
|
||||
description: "ホームタイムラインの流速が20npmを越す"
|
||||
_viewInstanceChart:
|
||||
title: "アナリスト"
|
||||
description: "インスタンスのチャートを表示した"
|
||||
_outputHelloWorldOnScratchpad:
|
||||
title: "Hello, world!"
|
||||
description: "スクラッチパッドで hello world を出力した"
|
||||
|
@@ -938,13 +938,13 @@ cannotPerformTemporary: "일시적으로 사용할 수 없음"
|
||||
cannotPerformTemporaryDescription: "조작 횟수 제한을 초과하여 일시적으로 사용이 불가합니다. 잠시 후 다시 시도해 주세요."
|
||||
preset: "프리셋"
|
||||
selectFromPresets: "프리셋에서 선택"
|
||||
achievements: "도전과제"
|
||||
achievements: "도전 과제"
|
||||
_achievements:
|
||||
earnedAt: "달성 일시"
|
||||
_types:
|
||||
_notes1:
|
||||
title: "미스키 설정하고 있었는데요"
|
||||
description: "첫 노트를 포스트했습니다"
|
||||
title: "미스키 시작했는데요"
|
||||
description: "첫 노트를 작성했습니다"
|
||||
flavor: "Misskey에 오신 것을 환영합니다!"
|
||||
_notes10:
|
||||
title: "노트 조금"
|
||||
@@ -962,7 +962,7 @@ _achievements:
|
||||
title: "노트가 어디서 솟아?"
|
||||
description: "5,000개의 노트를 작성했습니다"
|
||||
_notes10000:
|
||||
title: "슈퍼-노트"
|
||||
title: "슈퍼 노트"
|
||||
description: "10,000개의 노트를 작성했습니다"
|
||||
_notes20000:
|
||||
title: "노트 더 없어?"
|
||||
@@ -989,7 +989,7 @@ _achievements:
|
||||
title: "노트 우주"
|
||||
description: "90,000개의 노트를 작성했습니다"
|
||||
_notes100000:
|
||||
title: "네 모든 노트는 내 거야"
|
||||
title: "ALL YOUR NOTE ARE BELONG TO US"
|
||||
description: "100,000개의 노트를 작성했습니다"
|
||||
flavor: "이만큼 쓸 일도 없겠지만... 다른 할 일이 있진 않으신가요?"
|
||||
_login3:
|
||||
@@ -1012,7 +1012,7 @@ _achievements:
|
||||
_login100:
|
||||
title: "미스키스트 III"
|
||||
description: "총 100일간 로그인했습니다"
|
||||
flavor: "그 유저, 미스키스트를 위하여"
|
||||
flavor: "그 유저, 미스키스트이다"
|
||||
_login200:
|
||||
title: "단골 I"
|
||||
description: "총 200일간 로그인했습니다"
|
||||
@@ -1025,7 +1025,7 @@ _achievements:
|
||||
_login500:
|
||||
title: "베테랑 I"
|
||||
description: "총 500일간 로그인했습니다"
|
||||
flavor: "여러분, 저 이 노트들 좋아해요"
|
||||
flavor: "제군, 나는 노트가 좋다"
|
||||
_login600:
|
||||
title: "베테랑 II"
|
||||
description: "총 600일간 로그인했습니다"
|
||||
@@ -1041,13 +1041,16 @@ _achievements:
|
||||
_login1000:
|
||||
title: "노트 마스터 III"
|
||||
description: "총 1,000일간 로그인했습니다"
|
||||
flavor: "미스키를 사용해 주셔서 감사합니다!"
|
||||
flavor: "Misskey를 사용해 주셔서 감사합니다!"
|
||||
_noteClipped1:
|
||||
title: "클립할 수밖에 없었어"
|
||||
description: "처음으로 노트를 클립했습니다"
|
||||
_noteFavorited1:
|
||||
title: "별을 바라보는 자"
|
||||
description: "처음으로 노트를 즐겨찾기했습니다"
|
||||
_myNoteFavorited1:
|
||||
title: "별을 원하는 자"
|
||||
description: "다른 사람이 당신의 노트를 즐겨찾기했습니다"
|
||||
_profileFilled:
|
||||
title: "준비 완료"
|
||||
description: "프로필 설정을 완료했습니다"
|
||||
@@ -1074,7 +1077,7 @@ _achievements:
|
||||
title: "첫 팔로워"
|
||||
description: "사용자가 처음으로 팔로잉했습니다"
|
||||
_followers10:
|
||||
title: "날 따라와!"
|
||||
title: "팔로우 미!"
|
||||
description: "10명의 사용자가 팔로우했습니다"
|
||||
_followers50:
|
||||
title: "이곳저곳"
|
||||
@@ -1092,12 +1095,18 @@ _achievements:
|
||||
title: "유명인사"
|
||||
description: "1,000명의 사용자가 팔로우했습니다"
|
||||
_collectAchievements30:
|
||||
title: "도전과제 콜렉터"
|
||||
title: "도전 과제 콜렉터"
|
||||
description: "30개의 도전과제를 획득했습니다"
|
||||
_viewAchievements3min:
|
||||
title: "저 도전과제 좋아해요"
|
||||
description: "도전 과제 목록을 3분 이상 쳐다봤습니다"
|
||||
_iLoveMisskey:
|
||||
title: "I Love Misskey"
|
||||
description: "\"I ❤ #Misskey\"를 포스트했습니다"
|
||||
flavor: "Misskey를 이용해주셔서 감사합니다! - 개발팀 일동"
|
||||
_foundTreasure:
|
||||
title: "보물찾기"
|
||||
description: "숨겨진 보물을 발견했습니다"
|
||||
_client30min:
|
||||
title: "잠깐 쉬어"
|
||||
description: "클라이언트를 시작하고 30분이 경과하였습니다"
|
||||
@@ -1110,7 +1119,7 @@ _achievements:
|
||||
flavor: "잠 좀 자세요. 걱정돼요."
|
||||
_postedAt0min0sec:
|
||||
title: "정각"
|
||||
description: "1초도 어긋나지 않은 정각에 노트를 포스트했습니다"
|
||||
description: "0분 0초 정각에 노트를 작성했습니다"
|
||||
flavor: "째깍 째깍 째깍 땡!"
|
||||
_selfQuote:
|
||||
title: "혼잣말"
|
||||
@@ -1118,15 +1127,24 @@ _achievements:
|
||||
_htl20npm:
|
||||
title: "타임라인 폭주 중"
|
||||
description: "1분 사이에 홈 타임라인에 노트가 20개 넘게 생성되었습니다"
|
||||
_viewInstanceChart:
|
||||
title: "애널리스트"
|
||||
description: "인스턴스의 차트를 열었습니다"
|
||||
_outputHelloWorldOnScratchpad:
|
||||
title: "Hello, world!"
|
||||
description: "스크래치패드에서 hello world를 출력했습니다"
|
||||
_open3windows:
|
||||
title: "멀티 윈도우"
|
||||
description: "3개 이상의 창을 열었습니다"
|
||||
_driveFolderCircularReference:
|
||||
title: "순환 참조"
|
||||
description: "드라이브 폴더를 자신을 가리키도록 만드려 시도했습니다"
|
||||
_reactWithoutRead:
|
||||
title: "읽고 답하긴 하시는 건가요?"
|
||||
description: "100자가 넘는 포스트에 3초 안에 포스트했습니다"
|
||||
description: "100자가 넘는 노트가 작성되고 3초 안에 반응했습니다"
|
||||
_clickedClickHere:
|
||||
title: "여길 눌러보세요"
|
||||
description: "이 곳을 눌러봤습니다"
|
||||
description: "여길을 눌러봤습니다"
|
||||
_justPlainLucky:
|
||||
title: "그냥 운이 좋았어"
|
||||
description: "매 10초마다 0.01%의 확률로 달성됩니다"
|
||||
@@ -1134,21 +1152,25 @@ _achievements:
|
||||
title: "신 콤플렉스"
|
||||
description: "이름을 syuilo로 설정했습니다"
|
||||
_passedSinceAccountCreated1:
|
||||
title: "1년"
|
||||
title: "1주년"
|
||||
description: "계정을 생성하고 1년이 지났습니다"
|
||||
_passedSinceAccountCreated2:
|
||||
title: "2년"
|
||||
title: "2주년"
|
||||
description: "계정을 생성하고 2년이 지났습니다"
|
||||
_passedSinceAccountCreated3:
|
||||
title: "3년"
|
||||
title: "3주년"
|
||||
description: "계정을 생성하고 3년이 지났습니다"
|
||||
_loggedInOnBirthday:
|
||||
title: "생일 축하합니다!"
|
||||
description: "설정한 생일에 로그인했습니다"
|
||||
description: "생일에 로그인했습니다"
|
||||
_loggedInOnNewYearsDay:
|
||||
title: "새해 복 많이 받으세요"
|
||||
description: "새해 첫 날에 로그인했습니다"
|
||||
flavor: "올해에도 저희 인스턴스에 관심을 가져 주셔서 감사합니다"
|
||||
_cookieClicked:
|
||||
title: "쿠키 클리커 게임"
|
||||
title: "쿠키를 클릭하는 게임"
|
||||
description: "쿠키를 클릭했습니다"
|
||||
flavor: "뭔가 문제가 있나요?"
|
||||
flavor: "소프트웨어 착각하지 않으셨나요?"
|
||||
_brainDiver:
|
||||
title: "Brain Diver"
|
||||
description: "Brain Diver로의 링크를 첨부했습니다"
|
||||
|
@@ -2,6 +2,7 @@
|
||||
_lang_: "Русский"
|
||||
headlineMisskey: "Сеть, сплетённая из заметок"
|
||||
introMisskey: "Добро пожаловать! Misskey — это децентрализованный сервис микроблогов с открытым исходным кодом.\nПишите «заметки» — делитесь со всеми происходящим вокруг или рассказывайте о себе 📡\nСтавьте «реакции» — выражайте свои чувства и эмоции от заметок других 👍\nОткройте для себя новый мир 🚀"
|
||||
poweredByMisskeyDescription: "{name} – один из инстансов (также называемый экземпляром Misskey), использующий платформу с открытым исходным кодом <b>Misskey</b>."
|
||||
monthAndDay: "{day}.{month}"
|
||||
search: "Поиск"
|
||||
notifications: "Уведомления"
|
||||
@@ -12,6 +13,7 @@ fetchingAsApObject: "Приём с других сайтов"
|
||||
ok: "Окей"
|
||||
gotIt: "Ясно!"
|
||||
cancel: "Отмена"
|
||||
noThankYou: "Нет, спасибо"
|
||||
enterUsername: "Введите имя пользователя"
|
||||
renotedBy: "{user} делится"
|
||||
noNotes: "Нет ни одной заметки"
|
||||
@@ -20,7 +22,7 @@ instance: "Инстанс"
|
||||
settings: "Настройки"
|
||||
basicSettings: "Основные настройки"
|
||||
otherSettings: "Прочие настройки"
|
||||
openInWindow: "Открывать в плавающих окнах"
|
||||
openInWindow: "Открыть в плавающем окне"
|
||||
profile: "Профиль"
|
||||
timeline: "Лента"
|
||||
noAccountDescription: "Пользователь ничего не написал про себя"
|
||||
@@ -47,6 +49,7 @@ deleteAndEdit: "Удалить и отредактировать"
|
||||
deleteAndEditConfirm: "Удалить эту заметку и создать отредактированную? Все реакции, ссылки и ответы на существующую будут будут потеряны."
|
||||
addToList: "Добавить в список"
|
||||
sendMessage: "Отправить сообщение"
|
||||
copyRSS: "Скопировать RSS"
|
||||
copyUsername: "Скопировать имя пользователя"
|
||||
searchUser: "Поиск людей"
|
||||
reply: "Ответить"
|
||||
@@ -270,7 +273,7 @@ light: "Светлый"
|
||||
dark: "Тёмный"
|
||||
lightThemes: "Светлые темы"
|
||||
darkThemes: "Тёмные темы"
|
||||
syncDeviceDarkMode: "Синхронизировать с темным режимом устройства"
|
||||
syncDeviceDarkMode: "Синхронизировать с тёмной темой системы"
|
||||
drive: "Диск"
|
||||
fileName: "Имя файла"
|
||||
selectFile: "Выберите файл"
|
||||
@@ -452,6 +455,8 @@ language: "Язык"
|
||||
uiLanguage: "Язык интерфейса"
|
||||
groupInvited: "Приглашение в группу"
|
||||
aboutX: "Описание {x}"
|
||||
emojiStyle: "Стиль эмодзи"
|
||||
native: "Системные"
|
||||
disableDrawer: "Не использовать выдвижные меню"
|
||||
youHaveNoGroups: "У вас нет ни одной группы"
|
||||
joinOrCreateGroup: "Получайте приглашения в группы или создавайте свои собственные"
|
||||
@@ -599,6 +604,7 @@ smtpSecureInfo: "Выключите при использовании STARTTLS."
|
||||
testEmail: "Проверка доставки электронной почты"
|
||||
wordMute: "Скрытие слов"
|
||||
regexpError: "Ошибка в регулярном выражении"
|
||||
regexpErrorDescription: "В списке {tab} скрытых слов, в строке {line} обнаружена синтаксическая ошибка:"
|
||||
instanceMute: "Глушение инстансов"
|
||||
userSaysSomething: "{name} что-то сообщает"
|
||||
makeActive: "Активировать"
|
||||
@@ -709,6 +715,7 @@ accentColor: "Акцент"
|
||||
textColor: "Текст"
|
||||
saveAs: "Сохранить под названием…"
|
||||
advanced: "Для продвинутых"
|
||||
advancedSettings: "Расширенные настройки "
|
||||
value: "Значения"
|
||||
createdAt: "Создано"
|
||||
updatedAt: "Обновлено"
|
||||
@@ -799,7 +806,7 @@ translate: "Перевод"
|
||||
translatedFrom: "Перевод. Язык оригинала — {x}"
|
||||
accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи"
|
||||
usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере. Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания (_). Имена пользователей не могут быть изменены позже."
|
||||
aiChanMode: "ИИ режим"
|
||||
aiChanMode: "Режим Ай"
|
||||
keepCw: "Сохраняйте Предупреждения о содержимом"
|
||||
pubSub: "Учётные записи Pub/Sub"
|
||||
lastCommunication: "Последнее сообщение"
|
||||
@@ -816,8 +823,8 @@ manageAccounts: "Управление аккаунтом"
|
||||
makeReactionsPublic: "Опубликовать список реакций"
|
||||
makeReactionsPublicDescription: "Список сделанных вами реакций доступен для просмотра всем желающим."
|
||||
classic: "Классика"
|
||||
muteThread: "Заглушить цепочку"
|
||||
unmuteThread: "Отменить глушение цепочки"
|
||||
muteThread: "Скрыть цепочку"
|
||||
unmuteThread: "Отменить сокрытие цепочки"
|
||||
ffVisibility: "Видимость подписок и подписчиков"
|
||||
ffVisibilityDescription: "Здесь можно настроить, кто будет видеть ваши подписки и подписчиков."
|
||||
continueThread: "Показать следующие ответы"
|
||||
@@ -840,40 +847,385 @@ numberOfColumn: "Количество столбцов"
|
||||
searchByGoogle: "Поиск"
|
||||
instanceDefaultLightTheme: "Светлая тема по умолчанию"
|
||||
instanceDefaultDarkTheme: "Темная тема по умолчанию"
|
||||
instanceDefaultThemeDescription: "Описание темы по умолчанию для инстанса"
|
||||
mutePeriod: "Продолжительность скрытия"
|
||||
indefinitely: "вечно"
|
||||
tenMinutes: "10 минут"
|
||||
oneHour: "1 час"
|
||||
oneDay: "1 день"
|
||||
oneWeek: "1 неделя"
|
||||
reflectMayTakeTime: "Изменения могут занять время для отображения"
|
||||
failedToFetchAccountInformation: "Не удалось получить информацию об аккаунте"
|
||||
cropImage: "Кадрирование"
|
||||
cropImageAsk: "Нужно ли кадрировать изображение?"
|
||||
file: "Файлы"
|
||||
recentNHours: "Последние {n} ч"
|
||||
recentNDays: "Последние {n} сут"
|
||||
noEmailServerWarning: "Почтовый сервер не установлен "
|
||||
thereIsUnresolvedAbuseReportWarning: "Остались нерешённые жалобы"
|
||||
recommended: "Рекомендуем"
|
||||
check: "Проверить"
|
||||
driveCapOverrideLabel: "Изменение лимита дискового пространства для этого пользователя"
|
||||
driveCapOverrideCaption: "Укажите меньше или равное нулю для отмены"
|
||||
requireAdminForView: "Для просмотра необходимо иметь аккаунт администратора"
|
||||
isSystemAccount: "Данная учётная запись создана автоматически и управляется системой"
|
||||
typeToConfirm: "Введите {x} для продолжения"
|
||||
deleteAccount: "Удаление учётной записи"
|
||||
document: "Документ"
|
||||
numberOfPageCache: "Количество сохранённых страниц в кэше"
|
||||
numberOfPageCacheDescription: "Описание количества страниц в кэше"
|
||||
logoutConfirm: "Вы хотите выйти из аккаунта?"
|
||||
lastActiveDate: "Последняя дата использования"
|
||||
statusbar: "Статусбар"
|
||||
pleaseSelect: "Пожалуйста, выберите"
|
||||
reverse: "Переворот"
|
||||
colored: "Выделена цветом"
|
||||
refreshInterval: "Интервал перезагрузки"
|
||||
label: "Метка"
|
||||
type: "Тип"
|
||||
speed: "Скорость"
|
||||
sensitiveMediaDetection: "Определение содержимого деликатного характера"
|
||||
localOnly: "Локально"
|
||||
remoteOnly: "Только удалённо"
|
||||
failedToUpload: "Сбой выгрузки"
|
||||
cannotUploadBecauseInappropriate: "Файл не может быть загружен, так как было установлено, что он может содержать неприемлемое содержимое."
|
||||
cannotUploadBecauseNoFreeSpace: "Файл не может быть загружен, так как не осталось места на диске"
|
||||
beta: "Бета"
|
||||
enableAutoSensitive: "Автоматическое определение NSFW"
|
||||
enableAutoSensitiveDescription: "Если доступно, используйте машинное обучение для автоматической установки флага NSFW на носителе. Даже если эта функция отключена, она может быть установлена автоматически в зависимости от инстанта."
|
||||
activeEmailValidationDescription: "Если включено, будет проводиться более строгая проверка адреса электронной почты, в том числе на то, что он действительный и не временный. Если же отключено, то проверяется только корректность написания адреса."
|
||||
navbar: "Панель навигации"
|
||||
shuffle: "Перемешать"
|
||||
account: "Учётные записи"
|
||||
move: "Переместить"
|
||||
pushNotification: "Push-уведомления"
|
||||
subscribePushNotification: "Включить push-уведомления"
|
||||
unsubscribePushNotification: "Выключить push-уведомления"
|
||||
pushNotificationAlreadySubscribed: "Push-уведомления уже включены"
|
||||
pushNotificationNotSupported: "Push-уведмления не поддерживаются инстансом или браузером"
|
||||
sendPushNotificationReadMessage: "Удалять push-уведомления когда сообщение или прочитано"
|
||||
sendPushNotificationReadMessageCaption: "На мгновение появится уведомление \"{emptyPushNotificationMessage}\". Расход заряда батареи может увеличиться "
|
||||
windowMaximize: "Развернуть"
|
||||
windowRestore: "Восстановить"
|
||||
caption: "Подпись (Automatic Translation)"
|
||||
loggedInAsBot: "Вы под аккаунтом бота!"
|
||||
tools: "Инструменты"
|
||||
cannotLoad: "Не удалось загрузить"
|
||||
numberOfProfileView: "Количество профилей для просмотра"
|
||||
like: "Нравится!"
|
||||
unlike: "Отменить «нравится»"
|
||||
numberOfLikes: "Количество лайков"
|
||||
show: "Отображение"
|
||||
neverShow: "Больше не показывать"
|
||||
remindMeLater: "Напомнить позже"
|
||||
didYouLikeMisskey: "Вам нравится Misskey?"
|
||||
pleaseDonate: "Сайт {host} работает на Misskey. Это бесплатное программное обеспечение, и ваши пожертвования очень бы помогли продолжать его разработку!"
|
||||
roles: "Роли"
|
||||
role: "Роль"
|
||||
normalUser: "Обычный пользователь"
|
||||
undefined: "неопределён"
|
||||
assign: "Назначить"
|
||||
unassign: "Отменить назначение"
|
||||
color: "Цвет"
|
||||
manageCustomEmojis: "Управлять пользовательскими эмодзи"
|
||||
youCannotCreateAnymore: "Вы достигли лимита создания."
|
||||
cannotPerformTemporary: "Временно недоступен"
|
||||
cannotPerformTemporaryDescription: "Это действие временно невозможно выполнить из-за превышения лимита выполнения."
|
||||
preset: "Шаблоны"
|
||||
selectFromPresets: "Выбрать из шаблонов"
|
||||
achievements: "Достижения"
|
||||
_achievements:
|
||||
earnedAt: "Разблокировано в"
|
||||
_types:
|
||||
_notes1:
|
||||
title: "Первые шаги в Misskey"
|
||||
description: "Опубликована первая заметка"
|
||||
flavor: "Приятных дней с Misskey!"
|
||||
_notes10:
|
||||
title: "Несколько заметок"
|
||||
description: "Опубликовано 10 заметок"
|
||||
_notes100:
|
||||
title: "Много заметок"
|
||||
description: "Опубликовано 100 заметок"
|
||||
_notes500:
|
||||
title: "Всё в заметках"
|
||||
description: "Опубликовано 500 заметок"
|
||||
_notes1000:
|
||||
title: "Гора заметок"
|
||||
description: "Опубликовано 1000 заметок"
|
||||
_notes5000:
|
||||
title: "Заметки льются рекой"
|
||||
description: "Опубликовано 5000 заметок"
|
||||
_notes10000:
|
||||
title: "Превосходство в заметках"
|
||||
description: "Опубликовано 10 000 заметок"
|
||||
_notes20000:
|
||||
title: "Нужно больше заметок!"
|
||||
description: "Опубликовано 20 000 заметок"
|
||||
_notes30000:
|
||||
title: "Заметки, заметки, заметки"
|
||||
description: "Опубликовано 30 000 заметок"
|
||||
_notes40000:
|
||||
title: "Фабрика заметок"
|
||||
description: "Опубликовано 40 000 заметок"
|
||||
_notes50000:
|
||||
title: "Планета заметок"
|
||||
description: "Опубликовано 50 000 заметок"
|
||||
_notes60000:
|
||||
title: "Замет-квазар"
|
||||
description: "Опубликовано 60 000 заметок"
|
||||
_notes70000:
|
||||
title: "Чёрная дыра из заметок"
|
||||
description: "Опубликовано 70 000 заметок"
|
||||
_notes80000:
|
||||
title: "Галактика заметок"
|
||||
description: "Опубликовано 80 000 заметок"
|
||||
_notes90000:
|
||||
title: "Вселенная заметок"
|
||||
description: "Опубликовано 90 000 заметок"
|
||||
_notes100000:
|
||||
title: "ALL YOUR NOTE ARE BELONG TO US"
|
||||
description: "Опубликовано 100 000 заметок"
|
||||
flavor: "Вам правда нужно столько писать?"
|
||||
_login3:
|
||||
title: "Новичок Ⅰ"
|
||||
description: "3 дня на сайте"
|
||||
flavor: "С сегодняшнего дня зовите меня просто мискиец"
|
||||
_login7:
|
||||
title: "Новичок Ⅱ"
|
||||
description: "Неделя на сайте"
|
||||
flavor: "Кажется, вы начали свыкаться с этим, нет?"
|
||||
_login15:
|
||||
title: "Новичок Ⅲ"
|
||||
description: "15 дней на сайте"
|
||||
_login30:
|
||||
title: "Мискиец Ⅰ"
|
||||
description: "30 дней на сайте"
|
||||
_login60:
|
||||
title: "Мискиец Ⅱ"
|
||||
description: "60 дней на сайте"
|
||||
_login100:
|
||||
title: "Мискиец Ⅲ"
|
||||
description: "100 дней на сайте"
|
||||
flavor: "Жестокий мискиец"
|
||||
_login200:
|
||||
title: "Завсегдатай Ⅰ"
|
||||
description: "200 дней на сайте"
|
||||
_login300:
|
||||
title: "Завсегдатай Ⅱ"
|
||||
description: "300 дней на сайте"
|
||||
_login400:
|
||||
title: "Завсегдатай Ⅲ"
|
||||
description: "400 дней на сайте"
|
||||
_login500:
|
||||
title: "Ветеран Ⅰ"
|
||||
description: "500 дней на сайте"
|
||||
flavor: "Господа, я люблю заметки"
|
||||
_login600:
|
||||
title: "Ветеран Ⅱ"
|
||||
description: "600 дней на сайте"
|
||||
_login700:
|
||||
title: "Ветеран Ⅲ"
|
||||
description: "700 дней на сайте"
|
||||
_login800:
|
||||
title: "Повелитель заметок Ⅰ"
|
||||
description: "800 дней на сайте"
|
||||
_login900:
|
||||
title: "Повелитель заметок Ⅱ"
|
||||
description: "900 дней на сайте"
|
||||
_login1000:
|
||||
title: "Повелитель заметок Ⅲ"
|
||||
description: "1000 дней на сайте"
|
||||
flavor: "Спасибо, что пользуетесь Misskey!"
|
||||
_noteClipped1:
|
||||
title: "Нельзя не сохранить"
|
||||
description: "Первая заметка в подборке"
|
||||
_noteFavorited1:
|
||||
title: "Смотрящий на звёзды"
|
||||
description: "Первое добавление в избранное"
|
||||
_myNoteFavorited1:
|
||||
title: "В поиске звёзд"
|
||||
description: "Кому-то понравилась ваша заметка"
|
||||
_profileFilled:
|
||||
title: "Приготовления закончены"
|
||||
description: "Заполнен профиль"
|
||||
_markedAsCat:
|
||||
title: "Ваш покорный слуга кот"
|
||||
description: "Включена опция «Аккаунт кота»"
|
||||
flavor: "Позвольте представиться: я — кот, просто кот, у меня еще нет имени."
|
||||
_following1:
|
||||
title: "Я не один"
|
||||
description: "Сделана первая подписка"
|
||||
_following10:
|
||||
title: "Не останавливайся… Не останавливайся…"
|
||||
description: "Количество подписок достигло 10"
|
||||
_following50:
|
||||
title: "Много друзей"
|
||||
description: "Количество подписок достигло 50"
|
||||
_following100:
|
||||
title: "Сотня друзей"
|
||||
description: "Количество подписок достигло 100"
|
||||
_following300:
|
||||
title: "Друзья в избытке"
|
||||
description: "Количество подписок достигло 300"
|
||||
_followers1:
|
||||
title: "Первый подписчик"
|
||||
description: "Появился 1 подписчик"
|
||||
_followers10:
|
||||
title: "Следуй за мной!"
|
||||
description: "Количество подписчиков достигло 10"
|
||||
_followers50:
|
||||
title: "Один за другим"
|
||||
description: "Количество подписчиков достигло 50"
|
||||
_followers100:
|
||||
title: "Всеобщий любимец"
|
||||
description: "Количество подписчиков достигло 100"
|
||||
_followers300:
|
||||
title: "В очередь!"
|
||||
description: "Количество подписчиков достигло 300"
|
||||
_followers500:
|
||||
title: "Радиостанция"
|
||||
description: "Количество подписчиков достигло 500"
|
||||
_followers1000:
|
||||
title: "Авторитет"
|
||||
description: "Количество подписчиков достигло 1000"
|
||||
_collectAchievements30:
|
||||
title: "Достигатор"
|
||||
description: "Получено 30 достижений"
|
||||
_viewAchievements3min:
|
||||
title: "Любовь к успехам"
|
||||
description: "Более 3 минут любования достижениями"
|
||||
_iLoveMisskey:
|
||||
title: "Я люблю Misskey"
|
||||
description: "Написана заметка «I ❤ #Misskey»"
|
||||
flavor: "Спасибо за поддержку Misskey! Ваша команда разработчиков"
|
||||
_foundTreasure:
|
||||
title: "Охота за сокровищами"
|
||||
description: "Найдено спрятанное сокровище"
|
||||
_client30min:
|
||||
title: "Перерыв на обед"
|
||||
description: "Прошло 30 минут с момента запуска клиента"
|
||||
_noteDeletedWithin1min:
|
||||
title: "Ой, нет!"
|
||||
description: "Заметка удалена через минуту после публикации"
|
||||
_postedAtLateNight:
|
||||
title: "Житель ночи"
|
||||
description: "Заметка опубликована в глухую ночь"
|
||||
flavor: "Вроде бы пора спать"
|
||||
_postedAt0min0sec:
|
||||
title: "Говорящие часы"
|
||||
description: "Заметка опубликована ровно в 0 минут 0 секунд"
|
||||
flavor: "Дин-дон дин-дон"
|
||||
_selfQuote:
|
||||
title: "Самовоспроизведение"
|
||||
description: "Процитирована собственная заметка"
|
||||
_htl20npm:
|
||||
title: "В потоке"
|
||||
description: "Достигнута скорость домашней ленты в 20 з/мин (заметок минуту)"
|
||||
_viewInstanceChart:
|
||||
title: "Аналитик"
|
||||
description: "Просмотрены статистические диаграммы инстанса"
|
||||
_outputHelloWorldOnScratchpad:
|
||||
title: "Привет, мир!"
|
||||
description: "Выведен текст «hello world» в Когтеточке"
|
||||
_open3windows:
|
||||
title: "Многооконный"
|
||||
description: "Открыто одновременно 3 окна"
|
||||
_driveFolderCircularReference:
|
||||
title: "Циклическая ссылка"
|
||||
description: "Попытка создать на «диске» рекурсивно вложенную папку"
|
||||
_reactWithoutRead:
|
||||
title: "Не читай @ отвечай!"
|
||||
description: "На заметку более чем 100 знаков написан ответ в первые же 3 секунды с её появления."
|
||||
_clickedClickHere:
|
||||
title: "Нажмите здесь"
|
||||
description: "Нажато здесь"
|
||||
_justPlainLucky:
|
||||
title: "Чистая удача"
|
||||
description: "Может достаться с вероятностью 0,01% каждые 10 секунд."
|
||||
_setNameToSyuilo:
|
||||
title: "Комплекс бога"
|
||||
description: "Установлено «syuilo» в качестве имени"
|
||||
_passedSinceAccountCreated1:
|
||||
title: "Первая годовщина"
|
||||
description: "Прошёл 1 год с момента регистрации"
|
||||
_passedSinceAccountCreated2:
|
||||
title: "Вторая годовщина"
|
||||
description: "Прошло 2 года с момента регистрации"
|
||||
_passedSinceAccountCreated3:
|
||||
title: "Третья годовщина"
|
||||
description: "Прошло 3 года с момента регистрации"
|
||||
_loggedInOnBirthday:
|
||||
title: "С днём рождения!"
|
||||
description: "Вход на сайт в свой день рождения"
|
||||
_loggedInOnNewYearsDay:
|
||||
title: "С Новым годом!"
|
||||
description: "Вход на сайт в первый день года"
|
||||
flavor: "Желаем отличного года на нашем сайте!"
|
||||
_cookieClicked:
|
||||
title: "Игра, в которой вы щёлкаете по печенькам"
|
||||
description: "Нажато печенье"
|
||||
flavor: "Стоп, вы вообще на том сайте-то?"
|
||||
_brainDiver:
|
||||
title: "Brain Diver"
|
||||
description: "Опубликована ссылка на песню «Brain Diver»"
|
||||
flavor: "Мисски-Мисски Ла-Ту-Ма"
|
||||
_role:
|
||||
new: "Новая роль"
|
||||
edit: "Изменить роль"
|
||||
name: "Название роли"
|
||||
description: "Описание роли"
|
||||
permission: "Ролевые полномочия"
|
||||
descriptionOfPermission: "<b>Модераторы</b> могут изменять базовые операции для модераторов.\n<b>Администраторы</b> могут изменять полностью настройки инстанса."
|
||||
assignTarget: "Метод присвоения"
|
||||
descriptionOfAssignTarget: "<b>Вручную</b> чтобы указать кому выдавать роль, а кому нет.\n<b>По условию<b> чтобы автоматически выдавать и удалять роль при условиях."
|
||||
manual: "Вручную"
|
||||
conditional: "По условию"
|
||||
condition: "Условия"
|
||||
isConditionalRole: "Эта роль выдаётся по условию."
|
||||
isPublic: "Общедоступная роль"
|
||||
descriptionOfIsPublic: "Список тех, кому назначена эта роль будет доступен всем. Кроме того эта роль будет отмечена у каждого в профиле."
|
||||
options: "Настройки ролей"
|
||||
policies: "Политики"
|
||||
baseRole: "Шаблон роли"
|
||||
useBaseValue: "Использовать значение из шаблона"
|
||||
chooseRoleToAssign: "Выберите роль, которую хотите выдать"
|
||||
canEditMembersByModerator: "Могут назначать модераторы"
|
||||
descriptionOfCanEditMembersByModerator: "Если включено, на эту роль могут назначать пользователей как администраторы, так и модераторы. Если выключено, назначать могут только администраторы."
|
||||
priority: "Приоритет"
|
||||
_priority:
|
||||
low: "Низкий"
|
||||
middle: "Средне"
|
||||
high: "Высокий"
|
||||
_options:
|
||||
gtlAvailable: "Может просматривать глобальную ленту"
|
||||
ltlAvailable: "Может просматривать местную ленту"
|
||||
canPublicNote: "Может публиковать общедоступные заметки"
|
||||
canInvite: "Может создавать пригласительные коды"
|
||||
canManageCustomEmojis: "Управлять пользовательскими эмодзи"
|
||||
driveCapacity: "Доступное пространство на «диске»"
|
||||
pinMax: "Доступное количество закреплённых заметок"
|
||||
antennaMax: "Доступное количество антенн"
|
||||
wordMuteMax: "Доступное количество знаков в списке скрытия слов"
|
||||
clipMax: "Максимальное количество подборок"
|
||||
noteEachClipsMax: "Максимальное количество заметок в подборке"
|
||||
userListMax: "Максимальное количество списков аккаунтов"
|
||||
userEachUserListsMax: "Максимальное количество аккаунтов в списке"
|
||||
rateLimitFactor: "Ограничение активности"
|
||||
descriptionOfRateLimitFactor: "Меньшее значение — слабые ограничения, большее — сильные"
|
||||
canHideAds: "Может скрыть рекламу"
|
||||
_condition:
|
||||
isLocal: "Местный"
|
||||
isRemote: "Неместный"
|
||||
createdLessThan: "Аккаунт младше, чем..."
|
||||
createdMoreThan: "Аккаунт старше, чем..."
|
||||
followersLessThanOrEq: "Количество подписчиков не превышает…"
|
||||
followersMoreThanOrEq: "Количество подписчиков не меньше чем…"
|
||||
followingLessThanOrEq: "Количество подписок не превышает…"
|
||||
followingMoreThanOrEq: "Количество подписок не меньше чем…"
|
||||
and: "Выполнено несколько условий:.."
|
||||
or: "Выполнено любое из условий:.."
|
||||
not: "Кроме тех, у кого…"
|
||||
_sensitiveMediaDetection:
|
||||
description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно."
|
||||
setSensitiveFlagAutomatically: "Установить флаг NSFW"
|
||||
@@ -920,6 +1272,24 @@ _plugin:
|
||||
install: "Установка расширений"
|
||||
installWarn: "Пожалуйста, не устанавливайте расширения, которым не доверяете."
|
||||
manage: "Управление расширениями"
|
||||
_preferencesBackups:
|
||||
list: "Существующие резервные копии"
|
||||
saveNew: "Создать резервную копию"
|
||||
loadFile: "Прочесть из файла"
|
||||
apply: "Восстановить на это устройство"
|
||||
save: "Обновить из текущих настроек"
|
||||
inputName: "Введите название для резервной копии"
|
||||
cannotSave: "Сохранить не удалось"
|
||||
nameAlreadyExists: "Резервная копия под названием «{name}» уже существует. Придумайте другое."
|
||||
applyConfirm: "Правда хотите загрузить резервную копию «{name}» на это устройство? Этим будут потеряны текущие настройки."
|
||||
saveConfirm: "Сохранить резервную копию под названием «{name}»?"
|
||||
deleteConfirm: "Удалить резервную копию «{name}»?"
|
||||
renameConfirm: "Переименовать резервную копию «{old}» в «{new}»?"
|
||||
noBackups: "Здесь ещё нет резервных копий. Вы можете создать резервную копию настроек на этом сайте с помощью кнопки «Создать резервную копию»."
|
||||
createdAt: "Создана {date} в {time}"
|
||||
updatedAt: "Обновлена {date} в {time}"
|
||||
cannotLoad: "Загрузить не удалось"
|
||||
invalidFile: "Некорректный формат файла"
|
||||
_registry:
|
||||
scope: "Область"
|
||||
key: "Ключ"
|
||||
@@ -1003,6 +1373,8 @@ _mfm:
|
||||
sparkleDescription: "Добавляет эффект искрящихся частиц."
|
||||
rotate: "Повернуть"
|
||||
rotateDescription: "Поворачивает на заданный угол."
|
||||
plain: "Буквально"
|
||||
plainDescription: "MFM внутри отключается, и текст отображается как есть"
|
||||
_instanceTicker:
|
||||
none: "Не показывать"
|
||||
remote: "Только для других сайтов"
|
||||
@@ -1032,12 +1404,14 @@ _wordMute:
|
||||
muteWordsDescription2: "Здесь можно использовать регулярные выражения — просто заключите их между двумя дробными чертами (/)."
|
||||
softDescription: "Соответствующие условиям заметки будут спрятаны из вашей ленты."
|
||||
hardDescription: "Соответстующие условиям заметки вообще не будут попадать в вашу ленту. Даже если вы поменяете условия, отсеенные таким образом заметки уже не появятся."
|
||||
soft: "Мягкий"
|
||||
hard: "Жёсткий"
|
||||
soft: "Мягко"
|
||||
hard: "Жёстко"
|
||||
mutedNotes: "Скрытые заметки"
|
||||
_instanceMute:
|
||||
instanceMuteDescription: "Заметки и репосты с указанных здесь инстансов, а также ответы пользователям оттуда же не будут отображаться."
|
||||
instanceMuteDescription2: "Пишите каждый инстанс на отдельной строке"
|
||||
title: "Скрывает заметки с заданных инстансов."
|
||||
heading: "Список заглушенных инстансов"
|
||||
heading: "Список скрытых инстансов"
|
||||
_theme:
|
||||
explore: "Обзор"
|
||||
install: "Установить тему"
|
||||
@@ -1158,12 +1532,16 @@ _tutorial:
|
||||
step7_1: "На этом вводный урок по использованию Misskey закончен. Спасибо, что прошли его до конца!"
|
||||
step7_2: "Хотите изучить Misskey глубже — добро пожаловать в раздел «{help}»."
|
||||
step7_3: "Приятно вам провести время с Misskey🚀"
|
||||
step8_1: "Ах, да, не хотите ли включить push-уведомления?"
|
||||
step8_2: "С push-уведомлениями вы будете в курсе репостов, ответов, реакций и всего такого, даже когда закрыли Misskey."
|
||||
step8_3: "Эту настройку вы всегда сможете поменять"
|
||||
_2fa:
|
||||
alreadyRegistered: "Двухфакторная аутентификация уже настроена."
|
||||
registerDevice: "Зарегистрируйте ваше устройство"
|
||||
registerKey: "Зарегистрировать ключ"
|
||||
step1: "Прежде всего, установите на устройство приложение для аутентификации, например, {a} или {b}."
|
||||
step2: "Далее отсканируйте отображаемый QR-код при помощи приложения."
|
||||
step2Url: "Если пользуетесь приложением на компьютере, можете ввести в него эту строку (URL):"
|
||||
step3: "И наконец, введите код, который покажет приложение."
|
||||
step4: "Теперь при каждом входе на сайт вам нужно будет вводить код из приложения аналогичным образом."
|
||||
securityKeyInfo: "Вы можете настроить вход с помощью аппаратного ключа безопасности, поддерживающего FIDO2, или отпечатка пальца или PIN-кода на устройстве."
|
||||
@@ -1180,7 +1558,7 @@ _permissions:
|
||||
"write:following": "Изменять спискок подписок"
|
||||
"read:messaging": "Смотреть сообщения"
|
||||
"write:messaging": "Писать и удалять сообщения"
|
||||
"read:mutes": "Смотреть спискок скрытых пользователей"
|
||||
"read:mutes": "Смотреть список скрытых пользователей"
|
||||
"write:mutes": "Изменять список скрытых пользователей"
|
||||
"write:notes": "Писать и удалять заметки"
|
||||
"read:notifications": "Смотреть уведомления"
|
||||
@@ -1231,10 +1609,13 @@ _widgets:
|
||||
trends: "Актуальное"
|
||||
clock: "Часы"
|
||||
rss: "Просмотр RSS"
|
||||
rssTicker: "Бегущая строка RSS"
|
||||
activity: "Активность"
|
||||
photos: "Фото"
|
||||
digitalClock: "Цифровые часы"
|
||||
unixClock: "Часы UNIX"
|
||||
federation: "Федерация"
|
||||
instanceCloud: "Облако инстансов"
|
||||
postForm: "Форма отправки"
|
||||
slideshow: "Показ слайдов"
|
||||
button: "Кнопка"
|
||||
@@ -1242,9 +1623,12 @@ _widgets:
|
||||
jobQueue: "Очередь заданий"
|
||||
serverMetric: "Показатели сервера"
|
||||
aiscript: "Консоль AiScript"
|
||||
aiscriptApp: "Приложение на AiScript"
|
||||
aichan: "Ай"
|
||||
userList: "Список аккаунтов"
|
||||
_userList:
|
||||
chooseList: "Выберите список"
|
||||
clicker: "Счётчик щелчков"
|
||||
_cw:
|
||||
hide: "Спрятать"
|
||||
show: "Показать еще"
|
||||
@@ -1307,12 +1691,13 @@ _profile:
|
||||
changeAvatar: "Поменять аватар"
|
||||
changeBanner: "Поменять изображение в шапке"
|
||||
_exportOrImport:
|
||||
allNotes: "Все записи\n"
|
||||
allNotes: "Все заметки\n"
|
||||
favoritedNotes: "Избранное"
|
||||
followingList: "Подписки"
|
||||
muteList: "Скрытые"
|
||||
blockingList: "Заблокированные"
|
||||
userLists: "Списки"
|
||||
excludeMutingUsers: "За исключением заглушенных пользователей"
|
||||
excludeMutingUsers: "За исключением скрытых пользователей"
|
||||
excludeInactiveUsers: "Без неактивных учётных записей"
|
||||
_charts:
|
||||
federation: "Федерация"
|
||||
@@ -1416,6 +1801,9 @@ _notification:
|
||||
youReceivedFollowRequest: "У вас новый запрос на подписку."
|
||||
yourFollowRequestAccepted: "Ваш запрос на подписку одобрен."
|
||||
youWereInvitedToGroup: "Вы приглашены в группу."
|
||||
pollEnded: "Подведены окончательные итоги опроса"
|
||||
emptyPushNotificationMessage: "Обновлены push-уведомления"
|
||||
achievementEarned: "Получено достижение"
|
||||
_types:
|
||||
all: "Все"
|
||||
follow: "Подписки"
|
||||
@@ -1424,11 +1812,13 @@ _notification:
|
||||
renote: "Репосты"
|
||||
quote: "Цитаты"
|
||||
reaction: "Реакции"
|
||||
pollEnded: "Окончания опросов"
|
||||
receiveFollowRequest: "Получен запрос на подписку"
|
||||
followRequestAccepted: "Запрос на подписку одобрен"
|
||||
groupInvited: "Приглашение в группы"
|
||||
app: "Уведомления из приложений"
|
||||
_actions:
|
||||
followBack: "отвечает взаимной подпиской"
|
||||
reply: "Ответить"
|
||||
renote: "Репост"
|
||||
_deck:
|
||||
@@ -1442,7 +1832,12 @@ _deck:
|
||||
swapDown: "Переставить ниже"
|
||||
stackLeft: "В столбик влево"
|
||||
popRight: "Из столбика вправо"
|
||||
profile: "Профиль"
|
||||
profile: "Расстановка"
|
||||
newProfile: "Новая расстановка"
|
||||
deleteProfile: "Удаление расстановки"
|
||||
introduction: "Создайте идеальный интерфейс расставляя колонки как угодно"
|
||||
introduction2: "Чтобы добавлять колонки в любом месте, жмите «+» справа экрана."
|
||||
widgetsIntroduction: "Чтобы добавлять виджеты, выбирайте «Редактировать виджеты» в меню колонки."
|
||||
_columns:
|
||||
main: "Основная"
|
||||
widgets: "Виджеты"
|
||||
|
@@ -942,14 +942,239 @@ achievements: "ความสำเร็จ"
|
||||
_achievements:
|
||||
earnedAt: "ได้รับเมื่อ"
|
||||
_types:
|
||||
_notes1:
|
||||
title: "เพียงแค่ตั้งค่า msky ของฉัน"
|
||||
description: "โพสต์โน้ตครั้งแรกของคุณ"
|
||||
flavor: "ขอให้มีช่วงเวลาที่ดีกับ Misskey นะคะ!"
|
||||
_notes10:
|
||||
title: "โน้ตบางอย่าง"
|
||||
description: "โพสต์ 10 โน้ต"
|
||||
_notes100:
|
||||
title: "โน้ตจำนวนมาก"
|
||||
description: "โพสต์ 100 โน้ต"
|
||||
_notes500:
|
||||
title: "ครอบคลุมในโน้ต"
|
||||
description: "โพสต์ 500 โน้ต"
|
||||
_notes1000:
|
||||
title: "ภูเขาแห่งโน้ต"
|
||||
description: "โพสต์ 1,000 โน้ต"
|
||||
_notes5000:
|
||||
title: "โน้ตล้น"
|
||||
description: "โพสต์ 5,000 โน้ต"
|
||||
_notes10000:
|
||||
title: "ซุปเปอร์โน้ต"
|
||||
description: "โพสต์ 10,000 โน้ต"
|
||||
_notes20000:
|
||||
title: "ต้องการ... เพิ่มเติม... โน้ต..."
|
||||
description: "โพสต์ 20,000 โน้ต"
|
||||
_notes30000:
|
||||
title: "โน้ต โน้ต โน้ต!"
|
||||
description: "โพสต์ 30,000 โน้ต"
|
||||
_notes40000:
|
||||
title: "โน้ตโรงงาน"
|
||||
description: "โพสต์ 40,000 โน้ต"
|
||||
_notes50000:
|
||||
title: "ดาวเคราะห์แห่งโน้ต"
|
||||
description: "โพสต์ 50,000 โน้ต"
|
||||
_notes60000:
|
||||
title: "โน้ตควอซาร์"
|
||||
description: "โพสต์ 60,000 โน้ต"
|
||||
_notes70000:
|
||||
title: "โน้ตหลุมดำ"
|
||||
description: "โพสต์ 70,000 โน้ต"
|
||||
_notes80000:
|
||||
title: "โน้ต กาแล็กซี่"
|
||||
description: "โพสต์ 80,000 โน้ต"
|
||||
_notes90000:
|
||||
title: "โน้ต จักรวาล"
|
||||
description: "โพสต์ 90,000 โน้ต"
|
||||
_notes100000:
|
||||
title: "ALL YOUR NOTE ARE BELONG TO US"
|
||||
description: "โพสต์ 100,000 โน้ต"
|
||||
flavor: "นายแน่ใจล่ะก็ มีอะไรพูดมาได้นะ"
|
||||
_login3:
|
||||
title: "มือใหม่ I"
|
||||
description: "เข้าสู่ระบบเป็นเวลารวม 3 วัน"
|
||||
flavor: "เริ่มตั้งแต่วันนี้ เรียกฉันว่ามิสคิสต์"
|
||||
_login7:
|
||||
title: "มือใหม่ II"
|
||||
description: "เข้าสู่ระบบเป็นเวลารวม 7 วัน"
|
||||
flavor: "รู้สึกเหมือนคุณได้แขวนของสิ่งต่างๆ หรือยังคะ?"
|
||||
_login15:
|
||||
title: "มือใหม่ III"
|
||||
description: "เข้าสู่ระบบเป็นเวลารวม 15 วัน"
|
||||
_login30:
|
||||
title: "มิสคิสท์ I"
|
||||
description: "เข้าสู่ระบบเป็นเวลารวม 30 วัน"
|
||||
_login60:
|
||||
title: "มิสคิสท์ II"
|
||||
description: "เข้าสู่ระบบเป็นเวลารวม 60 วัน"
|
||||
_login100:
|
||||
title: "มิสคิสท์ III"
|
||||
description: "เข้าสู่ระบบเป็นเวลารวม 100 วัน"
|
||||
flavor: "ความรุนแรง Misskist"
|
||||
_login200:
|
||||
title: "ลูกค้าประจำ I"
|
||||
description: "เข้าสู่ระบบเป็นเวลารวม 200 วัน"
|
||||
_login300:
|
||||
title: "ลูกค้าประจำ II"
|
||||
description: "เข้าสู่ระบบเป็นเวลารวม 300 วัน"
|
||||
_login400:
|
||||
title: "ลูกค้าประจำ III"
|
||||
description: "เข้าสู่ระบบเป็นเวลารวม 400 วัน"
|
||||
_login500:
|
||||
title: "ผู้เชี่ยวชาญ I"
|
||||
description: "เข้าสู่ระบบเป็นเวลารวม 500 วัน"
|
||||
flavor: "เพื่อนของผมนะมักจะกล่าวว่าผมนะชอบจดโน้ต"
|
||||
_login600:
|
||||
title: "ผู้เชี่ยวชาญ II"
|
||||
description: "เข้าสู่ระบบเป็นเวลารวม 600 วัน"
|
||||
_login700:
|
||||
title: "ผู้เชี่ยวชาญ III"
|
||||
description: "เข้าสู่ระบบเป็นเวลารวม 700 วัน"
|
||||
_login800:
|
||||
title: "ปรมาจารย์ด้านโน้ต I"
|
||||
description: "เข้าสู่ระบบเป็นเวลารวม 800 วัน"
|
||||
_login900:
|
||||
title: "ปรมาจารย์ด้านโน้ต II"
|
||||
description: "เข้าสู่ระบบเป็นเวลารวม 900 วัน"
|
||||
_login1000:
|
||||
title: "ปรมาจารย์ด้านโน้ต III"
|
||||
description: "เข้าสู่ระบบเป็นเวลารวม 1,000 วัน"
|
||||
flavor: "ขอบคุณที่ใช้ Misskey นะ !"
|
||||
_noteClipped1:
|
||||
title: "จะต้อง... คลิป..."
|
||||
description: "คลิปโน้ตตัวแรกของคุณ"
|
||||
_noteFavorited1:
|
||||
title: "สตาร์เกเซอร์"
|
||||
description: "ชื่นชอบโน้ตแรกของคุณ"
|
||||
_myNoteFavorited1:
|
||||
title: "แสวงหาดวงดาว"
|
||||
description: "มีคนอื่นๆที่ชื่นชอบหนึ่งในโน้ตของคุณ"
|
||||
_profileFilled:
|
||||
title: "เตรียมไว้อย่างดี"
|
||||
description: "ตั้งค่าโปรไฟล์ของคุณ"
|
||||
_markedAsCat:
|
||||
title: "ฉันเป็นแมว"
|
||||
description: "ทำเครื่องหมายบัญชีของคุณว่าเป็นแมว"
|
||||
flavor: "ฉันจะให้ชื่อคุณภายหลังนะ"
|
||||
_following1:
|
||||
title: "กำลังติดตามผู้ใช้คนแรกของคุณ"
|
||||
description: "ติดตามผู้ใช้"
|
||||
_following10:
|
||||
title: "ทำต่อไป... ทำต่อไป..."
|
||||
description: "ติดตาม 10 บัญชีผู้ใช้"
|
||||
_following50:
|
||||
title: "มีเพื่อนมากมาย"
|
||||
description: "ติดตาม 50 บัญชี"
|
||||
_following100:
|
||||
title: "เพื่อน 100 คน"
|
||||
description: "ติดตาม 100 บัญชี"
|
||||
_following300:
|
||||
title: "เพื่อนโอเวอร์โหลด"
|
||||
description: "ติดตาม 300 บัญชี"
|
||||
_followers1:
|
||||
title: "ผู้ติดตามคนแรก"
|
||||
description: "ได้รับ 1 ผู้ติดตาม"
|
||||
_followers10:
|
||||
title: "ติดตามฉัน!"
|
||||
description: "ได้รับ 10 คนผู้ติดตาม"
|
||||
_followers50:
|
||||
title: "มากันเป็นฝูง"
|
||||
description: "ได้รับ 50 ผู้ติดตาม"
|
||||
_followers100:
|
||||
title: "บุคคลที่เป็นที่นิยม"
|
||||
description: "ได้รับ 100 ผู้ติดตาม"
|
||||
_followers300:
|
||||
title: "กรุณาสร้างบรรทัดเดียวนะคะ"
|
||||
description: "ได้รับ 300 คนผู้ติดตาม"
|
||||
_followers500:
|
||||
title: "เสาสัญญาณ"
|
||||
description: "ได้รับ 500 คนผู้ติดตาม"
|
||||
_followers1000:
|
||||
title: "ผู้ทรงอิทธิพล"
|
||||
description: "ได้รับ 1,000 ผู้ติดตาม"
|
||||
_collectAchievements30:
|
||||
title: "นักสะสมความสำเร็จ"
|
||||
description: "ได้รับความสำเร็จ 30 ครั้ง"
|
||||
_viewAchievements3min:
|
||||
title: "ชอบบรรลุผลสําเร็จ"
|
||||
description: "มองดูรายการความสำเร็จของคุณเป็นเวลาอย่างน้อย 3 นาที"
|
||||
_iLoveMisskey:
|
||||
title: "ฉันรัก Misskey"
|
||||
description: "โพสต์ \"I ❤ #Misskey\""
|
||||
flavor: "ทีมผู้พัฒนา Misskey ได้ขอบคุณสำหรับการสนับสนุนของคุณ!"
|
||||
_foundTreasure:
|
||||
title: "ล่าสมบัติ"
|
||||
description: "คุณพบสมบัติที่ซ่อนอยู่"
|
||||
_client30min:
|
||||
title: "พักผ่อนสักหน่อย"
|
||||
description: "ใช้เวลา 30 นาทีบน Misskey"
|
||||
_noteDeletedWithin1min:
|
||||
title: "ไม่เป็นไร"
|
||||
description: "ลบโน้ตภายในหนึ่งนาทีหลังจากที่โพสต์"
|
||||
_postedAtLateNight:
|
||||
title: "กลางคืน"
|
||||
description: "โพสต์โน้ตตอนดึกๆ"
|
||||
flavor: "ได้เวลาเข้านอนแล้วนะ"
|
||||
_postedAt0min0sec:
|
||||
title: "นาฬิกาพูดได้"
|
||||
description: "โพสต์บนโน้ตเมื่อเวลา 00:00 น."
|
||||
flavor: "คลิก คลิก คลิก แกล๊งๆ"
|
||||
_selfQuote:
|
||||
title: "อ้างอิงตนเอง"
|
||||
description: "อ้างโน้ตย่อของคุณเอง"
|
||||
_htl20npm:
|
||||
title: "ไทม์ไลน์ไหล"
|
||||
description: "มีการทำความเร็วของไทม์ไลน์ที่บ้านของคุณเกิน 20 npm (โน้ตต่อนาที)"
|
||||
_viewInstanceChart:
|
||||
title: "วิเคราะห์"
|
||||
description: "ดูแผนภูมิอินสแตนซ์ของคุณ"
|
||||
_outputHelloWorldOnScratchpad:
|
||||
title: "หวัดดีชาวโลก!"
|
||||
description: "เอาพุต \"hello world\" ใน Scratchpad"
|
||||
_open3windows:
|
||||
title: "มัลติวินโดว์"
|
||||
description: "มีการเปิดหน้าต่างอย่างน้อย 3 หน้าต่างพร้อมกัน"
|
||||
_driveFolderCircularReference:
|
||||
title: "อ้างอิงวงจร"
|
||||
description: "พยายามสร้างโฟลเดอร์ที่ซ้อนกันแบบวนซ้ำในไดรฟ์"
|
||||
_reactWithoutRead:
|
||||
title: "คุณอ่านมันจริงๆหรือเปล่า?"
|
||||
description: "มีการโต้ตอบกับโน้ตที่มีความยาวมากกว่า 100 ตัวอักษรภายใน 3 วินาทีหลังจากที่โพสต์"
|
||||
_clickedClickHere:
|
||||
title: "คลิ๊กที่นี่"
|
||||
description: "คุณได้คลิกที่นี่"
|
||||
_justPlainLucky:
|
||||
title: "แค่ลัคกี้ธรรมดา"
|
||||
description: "มีโอกาสที่จะได้รับด้วยความน่าจะเป็นไปได้ 0.01% ทุก ๆ 10 วินาที"
|
||||
_setNameToSyuilo:
|
||||
title: "พระเจ้าคอมเพล็กซ์"
|
||||
description: "ตั้งชื่อของคุณเป็น \"syuilo\""
|
||||
_passedSinceAccountCreated1:
|
||||
title: "ครบรอบหนึ่งปี"
|
||||
description: "ผ่านไปหนึ่งปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ"
|
||||
_passedSinceAccountCreated2:
|
||||
title: "ครบรอบสองปี"
|
||||
description: "ผ่านไปสองปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ"
|
||||
_passedSinceAccountCreated3:
|
||||
title: "ครบรอบสามปี"
|
||||
description: "ผ่านไปสามปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ"
|
||||
_loggedInOnBirthday:
|
||||
title: "สุขสันต์วันเกิด"
|
||||
description: "เข้าสู่ระบบในวันเกิดของคุณ"
|
||||
_loggedInOnNewYearsDay:
|
||||
title: "สวัสดีปีใหม่!"
|
||||
description: "เข้าสู่ระบบในวันแรกของปี"
|
||||
flavor: "อีกปีที่ยอดเยี่ยมในโอกาสนี้เลย"
|
||||
_cookieClicked:
|
||||
title: "เกมที่คุณคลิกที่คุกกี้"
|
||||
description: "คลิกคุกกี้"
|
||||
flavor: "เดี๋ยวก่อนนะ คุณอยู่ในเว็บไซต์ที่ถูกต้องแน่อย่างงั้นเหรอ?"
|
||||
_brainDiver:
|
||||
title: "Brain Diver"
|
||||
description: "โพสต์ลิงก์ไปยัง Brain Diver"
|
||||
flavor: "Misskey-Misskey La-Tu-Ma"
|
||||
_role:
|
||||
new: "บทบาทใหม่"
|
||||
edit: "แก้ไขบทบาท"
|
||||
|
@@ -529,7 +529,7 @@ state: "Стан"
|
||||
sort: "Сортування"
|
||||
ascendingOrder: "За зростанням"
|
||||
descendingOrder: "За спаданням"
|
||||
scratchpad: "Чернетка"
|
||||
scratchpad: "Scratchpad"
|
||||
scratchpadDescription: "Scratchpad надає середовище для експериментів з AiScript. Ви можете писати, виконувати його і тестувати взаємодію з Misskey."
|
||||
output: "Вихід"
|
||||
script: "Скрипт"
|
||||
@@ -688,7 +688,7 @@ pageLikesCount: "Кількість отриманих вподобань сто
|
||||
pageLikedCount: "Кількість вподобаних сторінок"
|
||||
contact: "Контакт"
|
||||
useSystemFont: "Використовувати стандартний шрифт системи"
|
||||
clips: "Добірка"
|
||||
clips: "Добірки"
|
||||
experimentalFeatures: "Експериментальні функції"
|
||||
developer: "Розробник"
|
||||
makeExplorable: "Зробіть обліковий запис видимим у розділі \"Огляд\""
|
||||
@@ -899,6 +899,222 @@ unlike: "Не вподобати"
|
||||
numberOfLikes: "Вподобання"
|
||||
show: "Відображення"
|
||||
color: "Колір"
|
||||
achievements: "Досягнення"
|
||||
_achievements:
|
||||
earnedAt: "Відкрито"
|
||||
_types:
|
||||
_notes1:
|
||||
title: "Привіт, Misskey!"
|
||||
description: "Перша нотатка"
|
||||
flavor: "Приємного часу з Misskey!"
|
||||
_notes10:
|
||||
title: "Декілька нотаток"
|
||||
description: "10 нотаток відправлено"
|
||||
_notes100:
|
||||
title: "Купа нотаток"
|
||||
description: "100 нотаток відправлено"
|
||||
_notes500:
|
||||
title: "Все в нотатках"
|
||||
description: "500 нотаток відправлено"
|
||||
_notes1000:
|
||||
title: "Гора нотаток"
|
||||
description: "1 000 нотаток відправлено"
|
||||
_notes5000:
|
||||
title: "Переповнюючі нотатки"
|
||||
description: "5 000 нотаток відправлено"
|
||||
_notes10000:
|
||||
title: "Супернотатка"
|
||||
description: "10 000 нотаток відправлено"
|
||||
_notes20000:
|
||||
title: "Треба Більше Нотаток"
|
||||
description: "20 000 нотаток відправлено"
|
||||
_notes30000:
|
||||
title: "Нотатки нотатки нотатки"
|
||||
description: "30 000 нотаток відправлено"
|
||||
_notes40000:
|
||||
title: "Фабрика нотаток"
|
||||
description: "40 000 нотаток відправлено"
|
||||
_notes50000:
|
||||
title: "Планета нотаток"
|
||||
description: "50 000 нотаток відправлено"
|
||||
_notes60000:
|
||||
title: "Нотатковий квазар"
|
||||
description: "60 000 нотаток відправлено"
|
||||
_notes70000:
|
||||
title: "Чорна нотаткова діра"
|
||||
description: "70 000 нотаток відправлено"
|
||||
_notes80000:
|
||||
title: "Галактика нотаток"
|
||||
description: "80 000 нотаток відправлено"
|
||||
_notes90000:
|
||||
title: "Нотатковерс"
|
||||
description: "90 000 нотаток відправлено"
|
||||
_notes100000:
|
||||
title: "ALL YOUR NOTE ARE BELONG TO US"
|
||||
description: "100 000 нотаток відправлено"
|
||||
flavor: "Так багато потрібно сказати?"
|
||||
_login3:
|
||||
title: "Новачок I"
|
||||
description: "3 дні користування загально"
|
||||
flavor: "Відсьогодні називайте мене \"Місскіст\""
|
||||
_login7:
|
||||
title: "Новачок II"
|
||||
description: "7 днів користування загально"
|
||||
flavor: "Ви звикли до цього?"
|
||||
_login15:
|
||||
title: "Новачок III"
|
||||
description: "15 днів користування загально"
|
||||
_login30:
|
||||
title: "Міскієць I"
|
||||
description: "30 днів користування загально"
|
||||
_login60:
|
||||
title: "Міскієць II"
|
||||
description: "60 днів користування загально"
|
||||
_login100:
|
||||
title: "Міскієць III"
|
||||
description: "100 днів користування загально"
|
||||
flavor: "Цей юзер лютий місскіст"
|
||||
_login200:
|
||||
title: "Завсідник I"
|
||||
description: "200 днів користування загально"
|
||||
_login300:
|
||||
title: "Завсідник II"
|
||||
description: "300 днів користування загально"
|
||||
_login400:
|
||||
title: "Завсідник III"
|
||||
description: "400 днів користування загально"
|
||||
_login500:
|
||||
title: "Ветеран I"
|
||||
description: "500 днів користування загально"
|
||||
flavor: "Meine Kameraden, ich liebe sie, die Notizen."
|
||||
_login600:
|
||||
title: "Ветеран II"
|
||||
description: "600 днів користування загально"
|
||||
_login700:
|
||||
title: "Ветеран III"
|
||||
description: "700 днів користування загально"
|
||||
_login800:
|
||||
title: "Майстер нотаток I"
|
||||
description: "800 днів користування загально"
|
||||
_login900:
|
||||
title: "Майстер нотаток II"
|
||||
description: "900 днів користування загально"
|
||||
_login1000:
|
||||
title: "Майстер нотаток III"
|
||||
description: "1000 днів користування загально"
|
||||
flavor: "Дякуємо, що користуєтеся Misskey!"
|
||||
_noteClipped1:
|
||||
title: "Не можна не зберегти"
|
||||
description: "Перша нотатка у добірці"
|
||||
_noteFavorited1:
|
||||
title: "Дивитися на зірки"
|
||||
_myNoteFavorited1:
|
||||
title: "У пошуках зірок"
|
||||
_profileFilled:
|
||||
title: "Повна готовність"
|
||||
description: "Профіль заповнено"
|
||||
_markedAsCat:
|
||||
title: "Я кіт"
|
||||
description: "Позначено як акаунт кота"
|
||||
flavor: "Я дам тобі ім'я пізніше"
|
||||
_following1:
|
||||
title: "Перша підписка"
|
||||
_following10:
|
||||
title: "Продовжуй, продовжуй"
|
||||
_following50:
|
||||
title: "Багато друзів"
|
||||
description: "Кількість підписок сягнула 50"
|
||||
_following100:
|
||||
title: "100 друзів"
|
||||
description: "Кількість підписок сягнула 100"
|
||||
_following300:
|
||||
title: "Надлишок друзів"
|
||||
description: "Кількість підписок сягнула 300"
|
||||
_followers1:
|
||||
title: "Перший підписник"
|
||||
description: "З'явився перший підписник"
|
||||
_followers10:
|
||||
title: "Follow me!"
|
||||
description: "Кількість підписників досягла 10"
|
||||
_followers50:
|
||||
description: "Кількість підписників досягла 50"
|
||||
_followers100:
|
||||
title: "Популярна особа"
|
||||
description: "Кількість підписників досягла 100"
|
||||
_followers300:
|
||||
title: "Ставайте в чергу"
|
||||
description: "Кількість підписників досягла 300"
|
||||
_followers500:
|
||||
title: "Радіовежа"
|
||||
description: "Кількість підписників досягла 500"
|
||||
_followers1000:
|
||||
title: "Інфлюенсер"
|
||||
description: "Кількість підписників досягла 1000"
|
||||
_collectAchievements30:
|
||||
title: "Збирач досягнень"
|
||||
description: "Отримано 30 досягнень"
|
||||
_viewAchievements3min:
|
||||
title: "Шанувальник досягнень"
|
||||
description: "Переглядати список досягнень принаймні 3 хвилини"
|
||||
_iLoveMisskey:
|
||||
title: "I Love Misskey"
|
||||
description: "Відправлено \"I ❤ #Misskey\""
|
||||
flavor: "Дякуємо вам, що користуєтесь Misskey! – команда розробників"
|
||||
_foundTreasure:
|
||||
title: "Пошуки скарбів"
|
||||
description: "Ви знайшли прихований скарб"
|
||||
_client30min:
|
||||
title: "Коротка перерва"
|
||||
description: "З моменту запуску клієнта минуло 30 хвилин"
|
||||
_noteDeletedWithin1min:
|
||||
title: "Не зважай"
|
||||
description: "Допис видалено протягом 1 хвилини після публікації"
|
||||
_postedAtLateNight:
|
||||
title: "Нічне життя"
|
||||
description: "Відправити нотатку посеред ночі"
|
||||
flavor: "Час лягати спати"
|
||||
_postedAt0min0sec:
|
||||
title: "Сигнал часу"
|
||||
description: "Відправити нотатку о 00:00"
|
||||
_selfQuote:
|
||||
title: "Самопосилання"
|
||||
description: "Процитувати власну нотатку"
|
||||
_htl20npm:
|
||||
title: "Плинна стрічка"
|
||||
description: "Перевищити швидкість домашньої стрічки 20npm (нотаток на хвилину)"
|
||||
_viewInstanceChart:
|
||||
title: "Аналітик"
|
||||
_outputHelloWorldOnScratchpad:
|
||||
title: "Hello, world!"
|
||||
description: "Вивести \"hello world\" у Скретчпаді"
|
||||
_clickedClickHere:
|
||||
title: "Натисніть тут"
|
||||
description: "Натиснуто тут"
|
||||
_justPlainLucky:
|
||||
title: "Просто вдача"
|
||||
description: "Можна отримати з ймовірністю 0,01% кожні 10 секунд"
|
||||
_setNameToSyuilo:
|
||||
title: "Комплекс бога"
|
||||
description: "Встановлено ім'я \"syuilo\""
|
||||
_passedSinceAccountCreated1:
|
||||
title: "Перша річниця"
|
||||
description: "Минув рік з моменту створення акаунта"
|
||||
_passedSinceAccountCreated2:
|
||||
title: "Друга річниця"
|
||||
description: "Минуло 2 роки з моменту створення акаунта"
|
||||
_passedSinceAccountCreated3:
|
||||
title: "Третя річниця"
|
||||
description: "Минуло 3 роки з моменту створення акаунта"
|
||||
_loggedInOnBirthday:
|
||||
title: "З Днем народження!"
|
||||
description: "Увійти у свій день народження"
|
||||
_loggedInOnNewYearsDay:
|
||||
title: "З Новим роком!"
|
||||
description: "Увійшли в перший день року"
|
||||
_brainDiver:
|
||||
title: "Brain Diver"
|
||||
description: "Відправити посилання на \"Brain Diver\""
|
||||
flavor: "Misskey-Misskey La-Tu-Ma"
|
||||
_role:
|
||||
priority: "Пріоритет"
|
||||
_priority:
|
||||
@@ -1171,7 +1387,7 @@ _tutorial:
|
||||
step3_1: "Ви успішно налаштували свій обліковий запис?"
|
||||
step3_2: "Наступним кроком є написання нотатки. Це можна зробити, натиснувши зображення олівця на екрані."
|
||||
step3_3: "Після написання вмісту ви можете опублікувати його, натиснувши кнопку у верхньому правому куті форми."
|
||||
step3_4: "Не знаєте що написати? Спробуйте \"налаштовую свій msky\"!"
|
||||
step3_4: "Не знаєте що написати? Спробуйте \"Привіт, Misskey!\""
|
||||
step4_1: "Ви розмістили свій перший запис?"
|
||||
step4_2: "Ура! Ваш перший запис відображається на вашій стрічці подій."
|
||||
step5_1: "Настав час оживити вашу стрічку подій підписавшись на інших користувачів."
|
||||
@@ -1435,6 +1651,7 @@ _notification:
|
||||
youReceivedFollowRequest: "Ви отримали запит на підписку"
|
||||
yourFollowRequestAccepted: "Запит на підписку прийнято"
|
||||
youWereInvitedToGroup: "Запрошення до групи"
|
||||
achievementEarned: "Досягнення відкрито"
|
||||
_types:
|
||||
all: "Все"
|
||||
follow: "Підписки"
|
||||
|
@@ -938,6 +938,231 @@ cannotPerformTemporary: "暂时不可用"
|
||||
cannotPerformTemporaryDescription: "因操作过于频繁,暂时不可用,请稍后再试。"
|
||||
preset: "預設值"
|
||||
selectFromPresets: "從預設值中選擇"
|
||||
achievements: "成就"
|
||||
_achievements:
|
||||
earnedAt: "达成时间"
|
||||
_types:
|
||||
_notes1:
|
||||
title: "初来乍到"
|
||||
description: "第一次发帖"
|
||||
flavor: "祝您在Misskey玩的愉快~"
|
||||
_notes10:
|
||||
title: "一些帖子"
|
||||
description: "发布了10篇帖子"
|
||||
_notes100:
|
||||
title: "很多帖子"
|
||||
description: "发布了100篇帖子"
|
||||
_notes500:
|
||||
title: "满是帖子"
|
||||
description: "发布了500篇帖子"
|
||||
_notes1000:
|
||||
title: "积帖成山"
|
||||
description: "发布了1,000篇帖子"
|
||||
_notes5000:
|
||||
title: "帖如泉涌"
|
||||
description: "发布了5,000篇帖子"
|
||||
_notes10000:
|
||||
title: "超级帖"
|
||||
description: "发布了10,000篇帖子"
|
||||
_notes20000:
|
||||
title: "还想要更多帖子"
|
||||
description: "发布了20,000篇帖子"
|
||||
_notes30000:
|
||||
title: "帖子帖子帖子"
|
||||
description: "发布了30,000篇帖子"
|
||||
_notes40000:
|
||||
title: "帖子工厂"
|
||||
description: "发布了40,000篇帖子"
|
||||
_notes50000:
|
||||
title: "帖子星球"
|
||||
description: "发布了50,000篇帖子"
|
||||
_notes60000:
|
||||
title: "帖子类星体"
|
||||
description: "发布了60,000篇帖子"
|
||||
_notes70000:
|
||||
title: "帖子黑洞"
|
||||
description: "发布了70,000篇帖子"
|
||||
_notes80000:
|
||||
title: "帖子星系"
|
||||
description: "发布了80,000篇帖子"
|
||||
_notes90000:
|
||||
title: "帖子起源"
|
||||
description: "发布了90,000篇帖子"
|
||||
_notes100000:
|
||||
title: "ALL YOUR NOTE ARE BELONG TO US"
|
||||
description: "发布了100,000篇帖子"
|
||||
flavor: "真的有那么多可以写的东西吗?"
|
||||
_login3:
|
||||
title: "初学者 I"
|
||||
description: "连续登录3天"
|
||||
flavor: "今天开始我就是Misskist!"
|
||||
_login7:
|
||||
title: "初学者 II"
|
||||
description: "连续登录7天"
|
||||
flavor: "您开始习惯了吗?"
|
||||
_login15:
|
||||
title: "初学者 III"
|
||||
description: "连续登录15天"
|
||||
_login30:
|
||||
title: "Misskist Ⅰ"
|
||||
description: "连续登录30天"
|
||||
_login60:
|
||||
title: "Misskist Ⅱ"
|
||||
description: "连续登录60天"
|
||||
_login100:
|
||||
title: "Misskist Ⅲ"
|
||||
description: "总登入100天"
|
||||
flavor: "那个用户,是Misskist喔"
|
||||
_login200:
|
||||
title: "定期联系Ⅰ"
|
||||
description: "总登录天数200天"
|
||||
_login300:
|
||||
title: "定期联系Ⅱ"
|
||||
description: "总登录天数300天"
|
||||
_login400:
|
||||
title: "定期联系Ⅲ"
|
||||
description: "总登录天数400天"
|
||||
_login500:
|
||||
description: "总登录天数500天"
|
||||
flavor: "诸君,我喜欢贴文"
|
||||
_login600:
|
||||
description: "总登录天数600天"
|
||||
_login700:
|
||||
description: "总登录天数700天"
|
||||
_login800:
|
||||
description: "总登录天数800天"
|
||||
_login900:
|
||||
description: "总登录天数900天"
|
||||
_login1000:
|
||||
description: "总登录天数1000天"
|
||||
flavor: "感谢您使用Misskey!"
|
||||
_noteClipped1:
|
||||
title: "忍不住要收藏到便签"
|
||||
description: "第一次将贴文贴进便签"
|
||||
_noteFavorited1:
|
||||
title: "观星者"
|
||||
description: "第一次将帖子加入收藏"
|
||||
_myNoteFavorited1:
|
||||
title: "想要星星"
|
||||
description: "自己的帖子被其他人加入收藏了"
|
||||
_profileFilled:
|
||||
title: "整装待发"
|
||||
description: "设置了个人资料"
|
||||
_markedAsCat:
|
||||
title: "我是猫"
|
||||
description: "将账户设定为一只猫"
|
||||
flavor: "还没有名字"
|
||||
_following1:
|
||||
title: "首次关注"
|
||||
description: "第一次关注别人"
|
||||
_following10:
|
||||
title: "关注,跟随"
|
||||
description: "关注超过10人"
|
||||
_following50:
|
||||
title: "我的朋友很多"
|
||||
description: "关注超过50人"
|
||||
_following100:
|
||||
title: "我的朋友很多"
|
||||
description: "关注超过100人"
|
||||
_following300:
|
||||
title: "朋友成群"
|
||||
description: "关注数超过300"
|
||||
_followers1:
|
||||
title: "最初的关注者"
|
||||
description: "第一次被关注"
|
||||
_followers10:
|
||||
title: "关注我吧!"
|
||||
description: "关注者超过10人"
|
||||
_followers50:
|
||||
title: "三五成群"
|
||||
description: "关注者超过50人"
|
||||
_followers100:
|
||||
title: "胜友如云"
|
||||
description: "关注者超过100人"
|
||||
_followers300:
|
||||
title: "排列成行"
|
||||
description: "关注者超过300人"
|
||||
_followers500:
|
||||
title: "信号塔"
|
||||
description: "关注者超过500人"
|
||||
_collectAchievements30:
|
||||
title: "成就收藏家"
|
||||
description: "获得超过30个成就"
|
||||
_viewAchievements3min:
|
||||
title: "成就爱好者"
|
||||
description: "盯着成就看三分钟"
|
||||
_iLoveMisskey:
|
||||
title: "I Love Misskey"
|
||||
description: "发布\"I ❤ #Misskey\"帖子"
|
||||
flavor: "感谢您使用 Misskey ! by 开发团队"
|
||||
_foundTreasure:
|
||||
description: "发现了隐藏的宝藏"
|
||||
_client30min:
|
||||
title: "休息一下!"
|
||||
description: "启动客户端超过30分钟"
|
||||
_noteDeletedWithin1min:
|
||||
title: "无话可说"
|
||||
description: "发帖后一分钟内就将其删除"
|
||||
_postedAtLateNight:
|
||||
title: "夜猫子"
|
||||
description: "深夜发布帖子"
|
||||
flavor: "差不多该去睡了喔。"
|
||||
_postedAt0min0sec:
|
||||
title: "报时"
|
||||
description: "在0点发布一篇帖子"
|
||||
flavor: "嘣 嘣 嘣 Biu——!"
|
||||
_selfQuote:
|
||||
title: "自我提及"
|
||||
description: "引用了自己的帖子"
|
||||
_htl20npm:
|
||||
title: "流动的时间线"
|
||||
description: "在首页时间线的流速超过20npm"
|
||||
_viewInstanceChart:
|
||||
title: "分析师"
|
||||
description: "查看了实例信息中的图表"
|
||||
_outputHelloWorldOnScratchpad:
|
||||
title: "Hello, world!"
|
||||
_open3windows:
|
||||
title: "多窗口"
|
||||
description: "打开了三个或更多的窗口"
|
||||
_driveFolderCircularReference:
|
||||
title: "循环引用"
|
||||
_reactWithoutRead:
|
||||
title: "有好好读过吗?"
|
||||
description: "在含有100字以上的帖子被发出三秒内做出回应"
|
||||
_clickedClickHere:
|
||||
title: "点这里"
|
||||
description: "点了这里"
|
||||
_justPlainLucky:
|
||||
title: "超高校级的幸运"
|
||||
description: "每10秒有0.01的概率自动获得"
|
||||
_setNameToSyuilo:
|
||||
title: "像神一样呐"
|
||||
description: "将名称设定为syuilo"
|
||||
_passedSinceAccountCreated1:
|
||||
title: "一周年"
|
||||
description: "账户创建时间超过1年"
|
||||
_passedSinceAccountCreated2:
|
||||
title: "二周年"
|
||||
description: "账户创建时间超过2年"
|
||||
_passedSinceAccountCreated3:
|
||||
title: "三周年"
|
||||
description: "账户创建时间超过3年"
|
||||
_loggedInOnBirthday:
|
||||
title: "生日快乐"
|
||||
description: "在生日当天登录"
|
||||
_loggedInOnNewYearsDay:
|
||||
title: "恭贺新禧"
|
||||
description: "在元旦登入"
|
||||
flavor: "今年也请对本实例多多指教!"
|
||||
_cookieClicked:
|
||||
title: "点击饼干小游戏"
|
||||
description: "点击了可疑的饼干"
|
||||
flavor: "是不是软件有问题?"
|
||||
_brainDiver:
|
||||
title: "Brain Diver"
|
||||
description: "发布了包含Brain Diver链接的帖子"
|
||||
flavor: "Misskey-Misskey La-Tu-Ma"
|
||||
_role:
|
||||
new: "创建角色"
|
||||
edit: "编辑角色"
|
||||
@@ -1456,7 +1681,7 @@ _profile:
|
||||
name: "昵称"
|
||||
username: "用户名"
|
||||
description: "个人简介"
|
||||
youCanIncludeHashtags: "您可以包含一个哈希标签。"
|
||||
youCanIncludeHashtags: "你可以在个人简介中包含一个#标签。"
|
||||
metadata: "附加信息"
|
||||
metadataEdit: "附加信息编辑"
|
||||
metadataDescription: "最多可以在个人资料中以表格形式显示四条其他信息。"
|
||||
@@ -1587,6 +1812,7 @@ _notification:
|
||||
pollEnded: "问卷调查结果已生成。"
|
||||
unreadAntennaNote: "天线 {name}"
|
||||
emptyPushNotificationMessage: "推送通知已更新"
|
||||
achievementEarned: "获得成就"
|
||||
_types:
|
||||
all: "全部"
|
||||
follow: "关注中"
|
||||
|
@@ -240,7 +240,7 @@ removeAreYouSure: "確定要刪掉「{x}」嗎?"
|
||||
deleteAreYouSure: "確定要刪掉「{x}」嗎?"
|
||||
resetAreYouSure: "確定要重設嗎?"
|
||||
saved: "已儲存"
|
||||
messaging: "傳送訊息"
|
||||
messaging: "聊天"
|
||||
upload: "上傳"
|
||||
keepOriginalUploading: "保留原圖"
|
||||
keepOriginalUploadingDescription: "上傳圖片時保留原始圖片。關閉時,瀏覽器會在上傳時生成一張用於web發布的圖片。"
|
||||
@@ -331,10 +331,10 @@ registration: "註冊"
|
||||
enableRegistration: "開啟新使用者註冊"
|
||||
invite: "邀請"
|
||||
driveCapacityPerLocalAccount: "每個本地用戶的雲端空間大小"
|
||||
driveCapacityPerRemoteAccount: "每個非本地用戶的雲端容量"
|
||||
driveCapacityPerRemoteAccount: "每個非本地用戶的雲端空間大小"
|
||||
inMb: "以Mbps為單位"
|
||||
iconUrl: "圖像URL"
|
||||
bannerUrl: "橫幅圖像URL"
|
||||
iconUrl: "圖標URL"
|
||||
bannerUrl: "橫幅圖片URL"
|
||||
backgroundImageUrl: "背景圖片的來源網址 "
|
||||
basicInfo: "基本資訊"
|
||||
pinnedUsers: "置頂用戶"
|
||||
@@ -373,8 +373,8 @@ connectedTo: "您的帳戶已連接到以下社交帳戶"
|
||||
notesAndReplies: "貼文與回覆"
|
||||
withFiles: "附件"
|
||||
silence: "禁言"
|
||||
silenceConfirm: "確定要禁言此用戶嗎?"
|
||||
unsilence: "解除禁言"
|
||||
silenceConfirm: "確定要靜音此使用者嗎?"
|
||||
unsilence: "解除靜音"
|
||||
unsilenceConfirm: "確定要解除禁言嗎?"
|
||||
popularUsers: "熱門使用者"
|
||||
recentlyUpdatedUsers: "最近發文的使用者"
|
||||
@@ -383,13 +383,13 @@ recentlyDiscoveredUsers: "最近發現的使用者"
|
||||
exploreUsersCount: "有{count}個使用者"
|
||||
exploreFediverse: "探索聯邦世界"
|
||||
popularTags: "熱門標籤"
|
||||
userList: "清單"
|
||||
about: "資訊"
|
||||
userList: "使用者清單"
|
||||
about: "關於"
|
||||
aboutMisskey: "關於 Misskey"
|
||||
administrator: "管理員"
|
||||
token: "權杖"
|
||||
twoStepAuthentication: "兩階段驗證"
|
||||
moderator: "監察員"
|
||||
moderator: "審核員"
|
||||
moderation: "監察"
|
||||
nUsersMentioned: "提到了{n}"
|
||||
securityKey: "安全金鑰"
|
||||
@@ -421,7 +421,7 @@ invites: "邀請"
|
||||
groupName: "群組名稱"
|
||||
members: "成員"
|
||||
transfer: "轉讓"
|
||||
messagingWithUser: "傳送訊息給其他使用者"
|
||||
messagingWithUser: "與其他使用者聊天"
|
||||
messagingWithGroup: "發送訊息至群組"
|
||||
title: "標題"
|
||||
text: "文字"
|
||||
@@ -473,7 +473,7 @@ createAccount: "建立帳戶"
|
||||
existingAccount: "現有帳戶"
|
||||
regenerate: "再生"
|
||||
fontSize: "字體大小"
|
||||
noFollowRequests: "沒有要求跟隨您的申請"
|
||||
noFollowRequests: "沒有跟隨您的請求"
|
||||
openImageInNewTab: "於新分頁中開啟圖片"
|
||||
dashboard: "儀表板"
|
||||
local: "本地"
|
||||
@@ -530,8 +530,8 @@ installedDate: "安裝時間"
|
||||
lastUsedDate: "最後上線日期"
|
||||
state: "狀態"
|
||||
sort: "排序"
|
||||
ascendingOrder: "昇冪"
|
||||
descendingOrder: "降冪"
|
||||
ascendingOrder: "遞增"
|
||||
descendingOrder: "遞減"
|
||||
scratchpad: "暫存記憶體"
|
||||
scratchpadDescription: "AiScript控制台為AiScript提供了實驗環境。您可以在此編寫、執行和確認代碼與Misskey互動的结果。"
|
||||
output: "輸出"
|
||||
@@ -950,13 +950,13 @@ _achievements:
|
||||
title: "若干貼文"
|
||||
description: "發表了10則貼文"
|
||||
_notes100:
|
||||
title: "許多的貼文"
|
||||
title: "許多貼文"
|
||||
description: "發表了100則貼文"
|
||||
_notes500:
|
||||
title: "滿滿的貼文"
|
||||
description: "發表了500則貼文"
|
||||
_notes1000:
|
||||
title: "一堆貼文"
|
||||
title: "堆積如山的貼文"
|
||||
description: "發表了1000則貼文"
|
||||
_notes5000:
|
||||
title: "滔滔不絕的貼文"
|
||||
@@ -989,11 +989,192 @@ _achievements:
|
||||
title: "貼文宇宙"
|
||||
description: "發表了90000則貼文"
|
||||
_notes100000:
|
||||
title: "ALL YOUR NOTE ARE BELONG TO US"
|
||||
description: "發表了100,000則貼文"
|
||||
flavor: "有這麼多東西要寫嗎?"
|
||||
_login3:
|
||||
title: "初學者 I"
|
||||
title: "初學者Ⅰ"
|
||||
description: "總登入天數為3天"
|
||||
flavor: "從今天開始,我就是Misskist"
|
||||
_login7:
|
||||
title: "初學者ⅠⅠ"
|
||||
description: "總登入天數為7天"
|
||||
flavor: "您開始習慣了嗎?"
|
||||
_login15:
|
||||
title: "初學者ⅠⅠⅠ"
|
||||
description: "總登入天數為15天"
|
||||
_login30:
|
||||
title: "Misskist Ⅰ"
|
||||
description: "總登入天數為30天"
|
||||
_login60:
|
||||
title: "Misskist ⅠⅠ"
|
||||
description: "總登入天數為60天"
|
||||
_login100:
|
||||
title: "Misskist ⅠⅠⅠ"
|
||||
description: "總登入天數為100天"
|
||||
flavor: "辣個 Misskist 用戶"
|
||||
_login200:
|
||||
title: "普通Ⅰ"
|
||||
description: "總登入天數為200天"
|
||||
_login300:
|
||||
title: "普通IⅠ"
|
||||
description: "總登入天數為300天"
|
||||
_login400:
|
||||
title: "普通IIⅠ"
|
||||
description: "總登入天數為400天"
|
||||
_login500:
|
||||
title: "老兵Ⅰ"
|
||||
description: "總登入天數為500天"
|
||||
flavor: "諸君,我喜歡貼文"
|
||||
_login600:
|
||||
title: "老兵ⅠⅠ"
|
||||
description: "總登入天數為600天"
|
||||
_login700:
|
||||
title: "老兵ⅠⅠⅠ"
|
||||
description: "總登入天數為700天"
|
||||
_login800:
|
||||
title: "貼文大師Ⅰ"
|
||||
description: "總登入天數為800天"
|
||||
_login900:
|
||||
title: "貼文大師ⅠⅠ"
|
||||
description: "總登入天數為900天"
|
||||
_login1000:
|
||||
title: "貼文大師ⅠⅠⅠ"
|
||||
description: "總登入天數為1,000天"
|
||||
flavor: "感謝您使用Misskey!"
|
||||
_noteClipped1:
|
||||
title: "忍不住要收進摘錄裡"
|
||||
description: "第一次將貼文收進摘錄"
|
||||
_noteFavorited1:
|
||||
title: "觀星者"
|
||||
description: "第一次將貼文收藏至我的最愛"
|
||||
_myNoteFavorited1:
|
||||
title: "想要星星"
|
||||
description: "自己的貼文被他人收藏至「我的最愛」了"
|
||||
_profileFilled:
|
||||
title: "有備而來"
|
||||
description: "設定了個人檔案"
|
||||
_markedAsCat:
|
||||
title: "我是貓"
|
||||
description: "已將帳戶設定為貓"
|
||||
flavor: "還沒有名字。"
|
||||
_following1:
|
||||
title: "首次追隨"
|
||||
description: "首次追隨了"
|
||||
_following10:
|
||||
title: "跟著跟著"
|
||||
description: "跟隨超過10人了"
|
||||
_following50:
|
||||
title: "朋友很多"
|
||||
description: "跟隨超過50人了"
|
||||
_following100:
|
||||
title: "100位朋友"
|
||||
description: "跟隨超過100人了"
|
||||
_following300:
|
||||
title: "朋友過多"
|
||||
description: "跟隨超過300人了"
|
||||
_followers1:
|
||||
title: "第一個追隨者"
|
||||
description: "第一次被追隨"
|
||||
_followers10:
|
||||
title: "Follow me!"
|
||||
description: "跟隨者超過10人了"
|
||||
_followers50:
|
||||
title: "成群結隊"
|
||||
description: "跟隨者超過50人了"
|
||||
_followers100:
|
||||
title: "紅人"
|
||||
description: "跟隨者超過100人了"
|
||||
_followers300:
|
||||
title: "請排成一排"
|
||||
description: "跟隨者超過300人了"
|
||||
_followers500:
|
||||
title: "基地台"
|
||||
description: "超過500名追隨者了"
|
||||
_followers1000:
|
||||
title: "影響者"
|
||||
description: "超過1000名追隨者了"
|
||||
_collectAchievements30:
|
||||
title: "成就收藏家"
|
||||
description: "獲得30個以上的成就"
|
||||
_viewAchievements3min:
|
||||
title: "喜愛成就"
|
||||
description: "看成就列表要花3分鐘以上"
|
||||
_iLoveMisskey:
|
||||
title: "I Love Misskey"
|
||||
description: "發布「I ❤ #Misskey」"
|
||||
flavor: "感謝您使用Misskey! by 開發團隊"
|
||||
_foundTreasure:
|
||||
title: "尋寶"
|
||||
description: "發現了隱藏的寶藏"
|
||||
_client30min:
|
||||
title: "休息一下"
|
||||
description: "用戶端啟動已超過30分鐘"
|
||||
_noteDeletedWithin1min:
|
||||
title: "現在沒有了"
|
||||
description: "發文後1分鐘內刪文"
|
||||
_postedAtLateNight:
|
||||
title: "夜行性"
|
||||
description: "在深夜發佈貼文"
|
||||
flavor: "該去睡覺了。"
|
||||
_postedAt0min0sec:
|
||||
title: "報時"
|
||||
description: "在0分0秒發佈貼文"
|
||||
flavor: "啵.啵.啵.嗶ー"
|
||||
_selfQuote:
|
||||
title: "自我引用"
|
||||
description: "引用了自己的貼文"
|
||||
_htl20npm:
|
||||
title: "流動的TL"
|
||||
description: "在首頁時間軸的流速超過20npm"
|
||||
_viewInstanceChart:
|
||||
title: "分析師"
|
||||
description: "顯示了實例的圖表"
|
||||
_outputHelloWorldOnScratchpad:
|
||||
title: "Hello world!"
|
||||
description: "在暫存記憶體輸出了 hello world"
|
||||
_open3windows:
|
||||
title: "多重視窗"
|
||||
description: "開啟了3個以上的視窗"
|
||||
_driveFolderCircularReference:
|
||||
title: "循環引用"
|
||||
description: "試圖遞迴套入雲端硬碟資料夾"
|
||||
_reactWithoutRead:
|
||||
title: "有好好讀過嗎?"
|
||||
description: "對包含100字以上內容的貼文做出情感反應"
|
||||
_clickedClickHere:
|
||||
title: "點擊這裡"
|
||||
description: "已點擊這裡了"
|
||||
_justPlainLucky:
|
||||
title: "只是運氣好"
|
||||
description: "每10秒有0.01%的機率獲得"
|
||||
_setNameToSyuilo:
|
||||
title: "神的情結"
|
||||
description: "將名稱設定為 syuilo"
|
||||
_passedSinceAccountCreated1:
|
||||
title: "一周年"
|
||||
description: "自建立帳戶開始過了1年"
|
||||
_passedSinceAccountCreated2:
|
||||
title: "二周年"
|
||||
description: "自建立帳戶開始過了2年"
|
||||
_passedSinceAccountCreated3:
|
||||
title: "三周年"
|
||||
description: "自建立帳戶開始過了3年"
|
||||
_loggedInOnBirthday:
|
||||
title: "生日快樂"
|
||||
description: "在生日當天登入了"
|
||||
_loggedInOnNewYearsDay:
|
||||
title: "新年快樂"
|
||||
description: "在元旦當天登入了"
|
||||
flavor: "今年也請對敝實例多多指教"
|
||||
_cookieClicked:
|
||||
title: "點擊餅乾的遊戲"
|
||||
description: "點擊了餅乾"
|
||||
flavor: "是不是軟體有問題?"
|
||||
_brainDiver:
|
||||
title: "Brain Driver"
|
||||
description: "發佈了Brain Driver的連結"
|
||||
flavor: "Misskey-Misskey La-Tu-Ma"
|
||||
_role:
|
||||
new: "建立角色"
|
||||
edit: "編輯角色"
|
||||
@@ -1643,6 +1824,7 @@ _notification:
|
||||
pollEnded: "問卷調查已產生結果"
|
||||
unreadAntennaNote: "天線 {name}"
|
||||
emptyPushNotificationMessage: "推送通知已更新"
|
||||
achievementEarned: "獲得成就"
|
||||
_types:
|
||||
all: "全部 "
|
||||
follow: "追隨中"
|
||||
|
20
package.json
20
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "13.1.0-beta.2",
|
||||
"version": "13.3.0",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -38,8 +38,8 @@
|
||||
"cleanall": "pnpm clean-all"
|
||||
},
|
||||
"resolutions": {
|
||||
"chokidar": "^3.3.1",
|
||||
"lodash": "^4.17.21"
|
||||
"chokidar": "3.5.3",
|
||||
"lodash": "4.17.21"
|
||||
},
|
||||
"dependencies": {
|
||||
"execa": "5.1.1",
|
||||
@@ -49,19 +49,19 @@
|
||||
"gulp-replace": "1.1.4",
|
||||
"gulp-terser": "2.1.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"typescript": "4.9.4"
|
||||
"typescript": "4.9.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.48.1",
|
||||
"@typescript-eslint/parser": "5.48.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.50.0",
|
||||
"@typescript-eslint/parser": "5.50.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "12.3.0",
|
||||
"eslint": "^8.31.0",
|
||||
"start-server-and-test": "1.15.2"
|
||||
"cypress": "12.5.1",
|
||||
"eslint": "8.33.0",
|
||||
"start-server-and-test": "1.15.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tensorflow/tfjs-core": "^4.2.0"
|
||||
"@tensorflow/tfjs-core": "4.2.0"
|
||||
}
|
||||
}
|
||||
|
29
packages/backend/migration/1675404035646-cleanup.js
Normal file
29
packages/backend/migration/1675404035646-cleanup.js
Normal file
@@ -0,0 +1,29 @@
|
||||
export class cleanup1675404035646 {
|
||||
name = 'cleanup1675404035646'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableTwitterIntegration"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableGithubIntegration"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableDiscordIntegration"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "twitterConsumerKey"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "twitterConsumerSecret"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "githubClientId"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "githubClientSecret"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "discordClientId"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "discordClientSecret"`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "integrations"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ADD "integrations" jsonb NOT NULL DEFAULT '{}'`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "discordClientSecret" character varying(128)`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "discordClientId" character varying(128)`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "githubClientSecret" character varying(128)`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "githubClientId" character varying(128)`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "twitterConsumerSecret" character varying(128)`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "twitterConsumerKey" character varying(128)`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "enableDiscordIntegration" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "enableGithubIntegration" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "enableTwitterIntegration" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
}
|
@@ -19,27 +19,27 @@
|
||||
"test-and-coverage": "pnpm jest-and-coverage"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tensorflow/tfjs": "^4.1.0",
|
||||
"@tensorflow/tfjs-node": "4.1.0"
|
||||
"@tensorflow/tfjs": "4.2.0",
|
||||
"@tensorflow/tfjs-node": "4.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^4.10.2",
|
||||
"@bull-board/fastify": "^4.10.2",
|
||||
"@bull-board/ui": "^4.10.2",
|
||||
"@bull-board/api": "4.11.0",
|
||||
"@bull-board/fastify": "4.11.0",
|
||||
"@bull-board/ui": "4.11.0",
|
||||
"@discordapp/twemoji": "14.0.2",
|
||||
"@fastify/accepts": "4.1.0",
|
||||
"@fastify/cookie": "^8.3.0",
|
||||
"@fastify/cookie": "8.3.0",
|
||||
"@fastify/cors": "8.2.0",
|
||||
"@fastify/http-proxy": "^8.4.0",
|
||||
"@fastify/http-proxy": "8.4.0",
|
||||
"@fastify/multipart": "7.4.0",
|
||||
"@fastify/static": "6.6.1",
|
||||
"@fastify/view": "7.4.0",
|
||||
"@nestjs/common": "9.2.1",
|
||||
"@nestjs/core": "9.2.1",
|
||||
"@nestjs/testing": "9.2.1",
|
||||
"@fastify/static": "6.8.0",
|
||||
"@fastify/view": "7.4.1",
|
||||
"@nestjs/common": "9.3.1",
|
||||
"@nestjs/core": "9.3.1",
|
||||
"@nestjs/testing": "9.3.1",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sinonjs/fake-timers": "10.0.2",
|
||||
"accepts": "^1.3.8",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.12.0",
|
||||
"archiver": "5.3.1",
|
||||
"autwh": "0.1.0",
|
||||
@@ -58,38 +58,38 @@
|
||||
"date-fns": "2.29.3",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"escape-regexp": "0.0.1",
|
||||
"fastify": "4.11.0",
|
||||
"fastify": "4.12.0",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "18.1.0",
|
||||
"file-type": "18.2.0",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"form-data": "^4.0.0",
|
||||
"form-data": "4.0.0",
|
||||
"got": "12.5.3",
|
||||
"hpagent": "1.2.0",
|
||||
"ioredis": "4.28.5",
|
||||
"ip-cidr": "3.0.11",
|
||||
"ip-cidr": "3.1.0",
|
||||
"is-svg": "4.3.2",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "21.0.0",
|
||||
"jsdom": "21.1.0",
|
||||
"json5": "2.2.3",
|
||||
"json5-loader": "4.0.1",
|
||||
"jsonld": "8.1.0",
|
||||
"jsrsasign": "10.6.1",
|
||||
"mfm-js": "0.23.3",
|
||||
"mime-types": "2.1.35",
|
||||
"misskey-js": "0.0.14",
|
||||
"misskey-js": "0.0.15",
|
||||
"ms": "3.0.0-canary.1",
|
||||
"nested-property": "4.0.0",
|
||||
"nodemailer": "6.9.0",
|
||||
"node-fetch": "3.3.0",
|
||||
"nodemailer": "6.9.1",
|
||||
"nsfwjs": "2.4.2",
|
||||
"oauth": "^0.10.0",
|
||||
"oauth": "0.10.0",
|
||||
"os-utils": "0.0.14",
|
||||
"parse5": "7.1.2",
|
||||
"pg": "8.8.0",
|
||||
"pg": "8.9.0",
|
||||
"private-ip": "3.0.0",
|
||||
"probe-image-size": "7.2.3",
|
||||
"promise-limit": "2.7.0",
|
||||
"pug": "3.0.2",
|
||||
"punycode": "2.2.0",
|
||||
"punycode": "2.3.0",
|
||||
"pureimage": "0.3.15",
|
||||
"qrcode": "1.5.1",
|
||||
"random-seed": "0.3.0",
|
||||
@@ -102,25 +102,23 @@
|
||||
"rss-parser": "3.12.0",
|
||||
"rxjs": "7.8.0",
|
||||
"s-age": "1.1.2",
|
||||
"sanitize-html": "2.8.1",
|
||||
"seedrandom": "^3.0.5",
|
||||
"sanitize-html": "2.9.0",
|
||||
"seedrandom": "3.0.5",
|
||||
"semver": "7.3.8",
|
||||
"sharp": "0.31.3",
|
||||
"speakeasy": "2.0.0",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"summaly": "2.7.0",
|
||||
"syslog-pro": "git+https://github.com/misskey-dev/SyslogPro#0.2.9-misskey.2",
|
||||
"systeminformation": "5.17.3",
|
||||
"systeminformation": "5.17.8",
|
||||
"tinycolor2": "1.5.2",
|
||||
"tmp": "0.2.1",
|
||||
"tsc-alias": "1.8.2",
|
||||
"tsconfig-paths": "4.1.2",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typeorm": "0.3.11",
|
||||
"typescript": "4.9.4",
|
||||
"typescript": "4.9.5",
|
||||
"ulid": "2.3.0",
|
||||
"undici": "^5.15.0",
|
||||
"unzipper": "0.10.11",
|
||||
"uuid": "9.0.0",
|
||||
"vary": "1.1.2",
|
||||
@@ -130,25 +128,26 @@
|
||||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redocly/openapi-core": "1.0.0-beta.120",
|
||||
"@swc/cli": "^0.1.59",
|
||||
"@swc/core": "1.3.26",
|
||||
"@jest/globals": "29.4.1",
|
||||
"@redocly/openapi-core": "1.0.0-beta.123",
|
||||
"@swc/cli": "0.1.61",
|
||||
"@swc/core": "1.3.32",
|
||||
"@swc/jest": "0.2.24",
|
||||
"@types/accepts": "1.3.5",
|
||||
"@types/archiver": "5.3.1",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
"@types/bull": "4.10.0",
|
||||
"@types/cbor": "6.0.0",
|
||||
"@types/color-convert": "^2.0.0",
|
||||
"@types/content-disposition": "^0.5.5",
|
||||
"@types/color-convert": "2.0.0",
|
||||
"@types/content-disposition": "0.5.5",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/fluent-ffmpeg": "2.1.20",
|
||||
"@types/ioredis": "4.28.10",
|
||||
"@types/jest": "29.2.5",
|
||||
"@types/jest": "29.4.0",
|
||||
"@types/js-yaml": "4.0.5",
|
||||
"@types/jsdom": "20.0.1",
|
||||
"@types/jsonld": "1.5.8",
|
||||
"@types/jsrsasign": "10.5.4",
|
||||
"@types/jsrsasign": "10.5.5",
|
||||
"@types/mime-types": "2.1.1",
|
||||
"@types/node": "18.11.18",
|
||||
"@types/node-fetch": "3.0.3",
|
||||
@@ -167,7 +166,6 @@
|
||||
"@types/sharp": "0.31.1",
|
||||
"@types/sinonjs__fake-timers": "8.1.2",
|
||||
"@types/speakeasy": "2.0.7",
|
||||
"@types/syslog-pro": "^1.0.0",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
"@types/tmp": "0.2.3",
|
||||
"@types/unzipper": "0.10.5",
|
||||
@@ -176,14 +174,13 @@
|
||||
"@types/web-push": "3.3.2",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.48.1",
|
||||
"@typescript-eslint/parser": "5.48.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.50.0",
|
||||
"@typescript-eslint/parser": "5.50.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.31.0",
|
||||
"eslint-plugin-import": "2.27.4",
|
||||
"eslint": "8.33.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"execa": "6.1.0",
|
||||
"jest": "29.3.1",
|
||||
"jest-mock": "^29.3.1",
|
||||
"node-fetch": "3.3.0"
|
||||
"jest": "29.4.1",
|
||||
"jest-mock": "29.4.1"
|
||||
}
|
||||
}
|
||||
|
35
packages/backend/src/boot/common.ts
Normal file
35
packages/backend/src/boot/common.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ChartManagementService } from '@/core/chart/ChartManagementService.js';
|
||||
import { QueueProcessorService } from '@/queue/QueueProcessorService.js';
|
||||
import { NestLogger } from '@/NestLogger.js';
|
||||
import { QueueProcessorModule } from '@/queue/QueueProcessorModule.js';
|
||||
import { JanitorService } from '@/daemons/JanitorService.js';
|
||||
import { QueueStatsService } from '@/daemons/QueueStatsService.js';
|
||||
import { ServerStatsService } from '@/daemons/ServerStatsService.js';
|
||||
import { ServerService } from '@/server/ServerService.js';
|
||||
import { MainModule } from '@/MainModule.js';
|
||||
|
||||
export async function server() {
|
||||
const app = await NestFactory.createApplicationContext(MainModule, {
|
||||
logger: new NestLogger(),
|
||||
});
|
||||
app.enableShutdownHooks();
|
||||
|
||||
const serverService = app.get(ServerService);
|
||||
serverService.launch();
|
||||
|
||||
app.get(ChartManagementService).start();
|
||||
app.get(JanitorService).start();
|
||||
app.get(QueueStatsService).start();
|
||||
app.get(ServerStatsService).start();
|
||||
}
|
||||
|
||||
export async function jobQueue() {
|
||||
const jobQueue = await NestFactory.createApplicationContext(QueueProcessorModule, {
|
||||
logger: new NestLogger(),
|
||||
});
|
||||
jobQueue.enableShutdownHooks();
|
||||
|
||||
jobQueue.get(QueueProcessorService).start();
|
||||
jobQueue.get(ChartManagementService).start();
|
||||
}
|
@@ -6,21 +6,12 @@ import cluster from 'node:cluster';
|
||||
import chalk from 'chalk';
|
||||
import chalkTemplate from 'chalk-template';
|
||||
import semver from 'semver';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import Logger from '@/logger.js';
|
||||
import { loadConfig } from '@/config.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { lessThan } from '@/misc/prelude/array.js';
|
||||
import { showMachineInfo } from '@/misc/show-machine-info.js';
|
||||
import { DaemonModule } from '@/daemons/DaemonModule.js';
|
||||
import { JanitorService } from '@/daemons/JanitorService.js';
|
||||
import { QueueStatsService } from '@/daemons/QueueStatsService.js';
|
||||
import { ServerStatsService } from '@/daemons/ServerStatsService.js';
|
||||
import { NestLogger } from '@/NestLogger.js';
|
||||
import { ChartManagementService } from '@/core/chart/ChartManagementService.js';
|
||||
import { ServerService } from '@/server/ServerService.js';
|
||||
import { MainModule } from '@/MainModule.js';
|
||||
import { envOption } from '../env.js';
|
||||
import { envOption } from '@/env.js';
|
||||
import { jobQueue, server } from './common.js';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
@@ -73,14 +64,13 @@ export async function masterMain() {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const app = await NestFactory.createApplicationContext(MainModule, {
|
||||
logger: new NestLogger(),
|
||||
});
|
||||
app.enableShutdownHooks();
|
||||
|
||||
// start server
|
||||
const serverService = app.get(ServerService);
|
||||
serverService.launch();
|
||||
if (envOption.onlyServer) {
|
||||
await server();
|
||||
} else if (envOption.onlyQueue) {
|
||||
await jobQueue();
|
||||
} else {
|
||||
await server();
|
||||
}
|
||||
|
||||
bootLogger.succ('Misskey initialized');
|
||||
|
||||
@@ -89,11 +79,6 @@ export async function masterMain() {
|
||||
}
|
||||
|
||||
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true);
|
||||
|
||||
app.get(ChartManagementService).start();
|
||||
app.get(JanitorService).start();
|
||||
app.get(QueueStatsService).start();
|
||||
app.get(ServerStatsService).start();
|
||||
}
|
||||
|
||||
function showEnvironment(): void {
|
||||
|
@@ -1,23 +1,18 @@
|
||||
import cluster from 'node:cluster';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ChartManagementService } from '@/core/chart/ChartManagementService.js';
|
||||
import { QueueProcessorService } from '@/queue/QueueProcessorService.js';
|
||||
import { NestLogger } from '@/NestLogger.js';
|
||||
import { QueueProcessorModule } from '@/queue/QueueProcessorModule.js';
|
||||
import { envOption } from '@/env.js';
|
||||
import { jobQueue, server } from './common.js';
|
||||
|
||||
/**
|
||||
* Init worker process
|
||||
*/
|
||||
export async function workerMain() {
|
||||
const jobQueue = await NestFactory.createApplicationContext(QueueProcessorModule, {
|
||||
logger: new NestLogger(),
|
||||
});
|
||||
jobQueue.enableShutdownHooks();
|
||||
|
||||
// start job queue
|
||||
jobQueue.get(QueueProcessorService).start();
|
||||
|
||||
jobQueue.get(ChartManagementService).start();
|
||||
if (envOption.onlyServer) {
|
||||
await server();
|
||||
} else if (envOption.onlyQueue) {
|
||||
await jobQueue();
|
||||
} else {
|
||||
await jobQueue();
|
||||
}
|
||||
|
||||
if (cluster.isWorker) {
|
||||
// Send a 'ready' message to parent process
|
||||
|
@@ -65,11 +65,6 @@ export type Source = {
|
||||
deliverJobMaxAttempts?: number;
|
||||
inboxJobMaxAttempts?: number;
|
||||
|
||||
syslog: {
|
||||
host: string;
|
||||
port: number;
|
||||
};
|
||||
|
||||
mediaProxy?: string;
|
||||
proxyRemoteFiles?: boolean;
|
||||
|
||||
@@ -113,7 +108,7 @@ const path = process.env.NODE_ENV === 'test'
|
||||
|
||||
export function loadConfig() {
|
||||
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8'));
|
||||
const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json')
|
||||
const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json');
|
||||
const clientManifest = clientManifestExists ?
|
||||
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8'))
|
||||
: { 'src/init.ts': { file: 'src/init.ts' } };
|
||||
|
@@ -44,6 +44,7 @@ const ACHIEVEMENT_TYPES = [
|
||||
'loggedInOnNewYearsDay',
|
||||
'noteClipped1',
|
||||
'noteFavorited1',
|
||||
'myNoteFavorited1',
|
||||
'profileFilled',
|
||||
'markedAsCat',
|
||||
'following1',
|
||||
@@ -61,12 +62,14 @@ const ACHIEVEMENT_TYPES = [
|
||||
'collectAchievements30',
|
||||
'viewAchievements3min',
|
||||
'iLoveMisskey',
|
||||
'foundTreasure',
|
||||
'client30min',
|
||||
'noteDeletedWithin1min',
|
||||
'postedAtLateNight',
|
||||
'postedAt0min0sec',
|
||||
'selfQuote',
|
||||
'htl20npm',
|
||||
'viewInstanceChart',
|
||||
'outputHelloWorldOnScratchpad',
|
||||
'open3windows',
|
||||
'driveFolderCircularReference',
|
||||
@@ -94,7 +97,7 @@ export class AchievementService {
|
||||
@bindThis
|
||||
public async create(
|
||||
userId: User['id'],
|
||||
type: string,
|
||||
type: typeof ACHIEVEMENT_TYPES[number],
|
||||
): Promise<void> {
|
||||
if (!ACHIEVEMENT_TYPES.includes(type)) return;
|
||||
|
||||
|
@@ -77,10 +77,16 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'antennaCreated':
|
||||
this.antennas.push(body);
|
||||
this.antennas.push({
|
||||
...body,
|
||||
createdAt: new Date(body.createdAt),
|
||||
});
|
||||
break;
|
||||
case 'antennaUpdated':
|
||||
this.antennas[this.antennas.findIndex(a => a.id === body.id)] = body;
|
||||
this.antennas[this.antennas.findIndex(a => a.id === body.id)] = {
|
||||
...body,
|
||||
createdAt: new Date(body.createdAt),
|
||||
};
|
||||
break;
|
||||
case 'antennaDeleted':
|
||||
this.antennas = this.antennas.filter(a => a.id !== body.id);
|
||||
|
@@ -21,18 +21,13 @@ export class CaptchaService {
|
||||
response,
|
||||
});
|
||||
|
||||
const res = await this.httpRequestService.fetch(
|
||||
url,
|
||||
{
|
||||
method: 'POST',
|
||||
body: params,
|
||||
const res = await this.httpRequestService.send(url, {
|
||||
method: 'POST',
|
||||
body: params.toString(),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
{
|
||||
noOkError: true,
|
||||
}
|
||||
).catch(err => {
|
||||
throw `${err.message ?? err}`;
|
||||
});
|
||||
}, { throwErrorWhenResponseNotOk: false });
|
||||
|
||||
if (!res.ok) {
|
||||
throw `${res.status}`;
|
||||
|
@@ -62,7 +62,6 @@ import PerUserNotesChart from './chart/charts/per-user-notes.js';
|
||||
import PerUserPvChart from './chart/charts/per-user-pv.js';
|
||||
import DriveChart from './chart/charts/drive.js';
|
||||
import PerUserReactionsChart from './chart/charts/per-user-reactions.js';
|
||||
import HashtagChart from './chart/charts/hashtag.js';
|
||||
import PerUserFollowingChart from './chart/charts/per-user-following.js';
|
||||
import PerUserDriveChart from './chart/charts/per-user-drive.js';
|
||||
import ApRequestChart from './chart/charts/ap-request.js';
|
||||
@@ -187,7 +186,6 @@ const $PerUserNotesChart: Provider = { provide: 'PerUserNotesChart', useExisting
|
||||
const $PerUserPvChart: Provider = { provide: 'PerUserPvChart', useExisting: PerUserPvChart };
|
||||
const $DriveChart: Provider = { provide: 'DriveChart', useExisting: DriveChart };
|
||||
const $PerUserReactionsChart: Provider = { provide: 'PerUserReactionsChart', useExisting: PerUserReactionsChart };
|
||||
const $HashtagChart: Provider = { provide: 'HashtagChart', useExisting: HashtagChart };
|
||||
const $PerUserFollowingChart: Provider = { provide: 'PerUserFollowingChart', useExisting: PerUserFollowingChart };
|
||||
const $PerUserDriveChart: Provider = { provide: 'PerUserDriveChart', useExisting: PerUserDriveChart };
|
||||
const $ApRequestChart: Provider = { provide: 'ApRequestChart', useExisting: ApRequestChart };
|
||||
@@ -315,7 +313,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
PerUserPvChart,
|
||||
DriveChart,
|
||||
PerUserReactionsChart,
|
||||
HashtagChart,
|
||||
PerUserFollowingChart,
|
||||
PerUserDriveChart,
|
||||
ApRequestChart,
|
||||
@@ -437,7 +434,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$PerUserPvChart,
|
||||
$DriveChart,
|
||||
$PerUserReactionsChart,
|
||||
$HashtagChart,
|
||||
$PerUserFollowingChart,
|
||||
$PerUserDriveChart,
|
||||
$ApRequestChart,
|
||||
@@ -559,7 +555,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
PerUserPvChart,
|
||||
DriveChart,
|
||||
PerUserReactionsChart,
|
||||
HashtagChart,
|
||||
PerUserFollowingChart,
|
||||
PerUserDriveChart,
|
||||
ApRequestChart,
|
||||
@@ -680,7 +675,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$PerUserPvChart,
|
||||
$DriveChart,
|
||||
$PerUserReactionsChart,
|
||||
$HashtagChart,
|
||||
$PerUserFollowingChart,
|
||||
$PerUserDriveChart,
|
||||
$ApRequestChart,
|
||||
|
@@ -2,22 +2,39 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource, In, IsNull } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
||||
import type { Emoji } from '@/models/entities/Emoji.js';
|
||||
import type { EmojisRepository } from '@/models/index.js';
|
||||
import type { EmojisRepository, Note } from '@/models/index.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { ReactionService } from '@/core/ReactionService.js';
|
||||
import { query } from '@/misc/prelude/url.js';
|
||||
|
||||
@Injectable()
|
||||
export class CustomEmojiService {
|
||||
private cache: Cache<Emoji | null>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
private utilityService: UtilityService,
|
||||
private idService: IdService,
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private reactionService: ReactionService,
|
||||
) {
|
||||
this.cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -40,8 +57,135 @@ export class CustomEmojiService {
|
||||
type: data.driveFile.webpublicType ?? data.driveFile.type,
|
||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||
if (data.host == null) {
|
||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
||||
emoji: await this.emojiEntityService.pack(emoji.id),
|
||||
});
|
||||
}
|
||||
|
||||
return emoji;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null {
|
||||
// クエリに使うホスト
|
||||
let host = src === '.' ? null // .はローカルホスト (ここがマッチするのはリアクションのみ)
|
||||
: src === undefined ? noteUserHost // ノートなどでホスト省略表記の場合はローカルホスト (ここがリアクションにマッチすることはない)
|
||||
: this.utilityService.isSelfHost(src) ? null // 自ホスト指定
|
||||
: (src || noteUserHost); // 指定されたホスト || ノートなどの所有者のホスト (こっちがリアクションにマッチすることはない)
|
||||
|
||||
host = this.utilityService.toPunyNullable(host);
|
||||
|
||||
return host;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private parseEmojiStr(emojiName: string, noteUserHost: string | null) {
|
||||
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
|
||||
if (!match) return { name: null, host: null };
|
||||
|
||||
const name = match[1];
|
||||
|
||||
// ホスト正規化
|
||||
const host = this.utilityService.toPunyNullable(this.normalizeHost(match[2], noteUserHost));
|
||||
|
||||
return { name, host };
|
||||
}
|
||||
|
||||
/**
|
||||
* 添付用(リモート)カスタム絵文字URLを解決する
|
||||
* @param emojiName ノートやユーザープロフィールに添付された、またはリアクションのカスタム絵文字名 (:は含めない, リアクションでローカルホストの場合は@.を付ける (これはdecodeReactionで可能))
|
||||
* @param noteUserHost ノートやユーザープロフィールの所有者のホスト
|
||||
* @returns URL, nullは未マッチを意味する
|
||||
*/
|
||||
@bindThis
|
||||
public async populateEmoji(emojiName: string, noteUserHost: string | null): Promise<string | null> {
|
||||
const { name, host } = this.parseEmojiStr(emojiName, noteUserHost);
|
||||
if (name == null) return null;
|
||||
if (host == null) return null;
|
||||
|
||||
const queryOrNull = async () => (await this.emojisRepository.findOneBy({
|
||||
name,
|
||||
host: host ?? IsNull(),
|
||||
})) ?? null;
|
||||
|
||||
const emoji = await this.cache.fetch(`${name} ${host}`, queryOrNull);
|
||||
|
||||
if (emoji == null) return null;
|
||||
|
||||
const isLocal = emoji.host == null;
|
||||
const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
const url = isLocal
|
||||
? emojiUrl
|
||||
: this.config.proxyRemoteFiles
|
||||
? `${this.config.url}/proxy/${encodeURIComponent((new URL(emojiUrl)).pathname)}?${query({ url: emojiUrl })}`
|
||||
: emojiUrl;
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 複数の添付用(リモート)カスタム絵文字URLを解決する (キャシュ付き, 存在しないものは結果から除外される)
|
||||
*/
|
||||
@bindThis
|
||||
public async populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise<Record<string, string>> {
|
||||
const emojis = await Promise.all(emojiNames.map(x => this.populateEmoji(x, noteUserHost)));
|
||||
const res = {} as any;
|
||||
for (let i = 0; i < emojiNames.length; i++) {
|
||||
if (emojis[i] != null) {
|
||||
res[emojiNames[i]] = emojis[i];
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public aggregateNoteEmojis(notes: Note[]) {
|
||||
let emojis: { name: string | null; host: string | null; }[] = [];
|
||||
for (const note of notes) {
|
||||
emojis = emojis.concat(note.emojis
|
||||
.map(e => this.parseEmojiStr(e, note.userHost)));
|
||||
if (note.renote) {
|
||||
emojis = emojis.concat(note.renote.emojis
|
||||
.map(e => this.parseEmojiStr(e, note.renote!.userHost)));
|
||||
if (note.renote.user) {
|
||||
emojis = emojis.concat(note.renote.user.emojis
|
||||
.map(e => this.parseEmojiStr(e, note.renote!.userHost)));
|
||||
}
|
||||
}
|
||||
const customReactions = Object.keys(note.reactions).map(x => this.reactionService.decodeReaction(x)).filter(x => x.name != null) as typeof emojis;
|
||||
emojis = emojis.concat(customReactions);
|
||||
if (note.user) {
|
||||
emojis = emojis.concat(note.user.emojis
|
||||
.map(e => this.parseEmojiStr(e, note.userHost)));
|
||||
}
|
||||
}
|
||||
return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します
|
||||
*/
|
||||
@bindThis
|
||||
public async prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise<void> {
|
||||
const notCachedEmojis = emojis.filter(emoji => this.cache.get(`${emoji.name} ${emoji.host}`) == null);
|
||||
const emojisQuery: any[] = [];
|
||||
const hosts = new Set(notCachedEmojis.map(e => e.host));
|
||||
for (const host of hosts) {
|
||||
if (host == null) continue;
|
||||
emojisQuery.push({
|
||||
name: In(notCachedEmojis.filter(e => e.host === host).map(e => e.name)),
|
||||
host: host,
|
||||
});
|
||||
}
|
||||
const _emojis = emojisQuery.length > 0 ? await this.emojisRepository.find({
|
||||
where: emojisQuery,
|
||||
select: ['name', 'host', 'originalUrl', 'publicUrl'],
|
||||
}) : [];
|
||||
for (const emoji of _emojis) {
|
||||
this.cache.set(`${emoji.name} ${emoji.host}`, emoji);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,16 +4,15 @@ import * as util from 'node:util';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import IPCIDR from 'ip-cidr';
|
||||
import PrivateIp from 'private-ip';
|
||||
import got, * as Got from 'got';
|
||||
import chalk from 'chalk';
|
||||
import got, * as Got from 'got';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { HttpRequestService, UndiciFetcher } from '@/core/HttpRequestService.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { buildConnector } from 'undici';
|
||||
|
||||
const pipeline = util.promisify(stream.pipeline);
|
||||
import { bindThis } from '@/decorators.js';
|
||||
@@ -21,7 +20,6 @@ import { bindThis } from '@/decorators.js';
|
||||
@Injectable()
|
||||
export class DownloadService {
|
||||
private logger: Logger;
|
||||
private undiciFetcher: UndiciFetcher;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
@@ -31,24 +29,6 @@ export class DownloadService {
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('download');
|
||||
|
||||
this.undiciFetcher = new UndiciFetcher(this.httpRequestService.getStandardUndiciFetcherOption(
|
||||
{
|
||||
connect: process.env.NODE_ENV === 'development' ?
|
||||
this.httpRequestService.clientDefaults.connect
|
||||
:
|
||||
this.httpRequestService.getConnectorWithIpCheck(
|
||||
buildConnector({
|
||||
...this.httpRequestService.clientDefaults.connect,
|
||||
}),
|
||||
(ip) => !this.isPrivateIp(ip)
|
||||
),
|
||||
bodyTimeout: 30 * 1000,
|
||||
},
|
||||
{
|
||||
connect: this.httpRequestService.clientDefaults.connect,
|
||||
}
|
||||
), this.logger);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -59,14 +39,60 @@ export class DownloadService {
|
||||
const operationTimeout = 60 * 1000;
|
||||
const maxSize = this.config.maxFileSize ?? 262144000;
|
||||
|
||||
const response = await this.undiciFetcher.fetch(url);
|
||||
const req = got.stream(url, {
|
||||
headers: {
|
||||
'User-Agent': this.config.userAgent,
|
||||
},
|
||||
timeout: {
|
||||
lookup: timeout,
|
||||
connect: timeout,
|
||||
secureConnect: timeout,
|
||||
socket: timeout, // read timeout
|
||||
response: timeout,
|
||||
send: timeout,
|
||||
request: operationTimeout, // whole operation timeout
|
||||
},
|
||||
agent: {
|
||||
http: this.httpRequestService.httpAgent,
|
||||
https: this.httpRequestService.httpsAgent,
|
||||
},
|
||||
http2: false, // default
|
||||
retry: {
|
||||
limit: 0,
|
||||
},
|
||||
}).on('response', (res: Got.Response) => {
|
||||
if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !this.config.proxy && res.ip) {
|
||||
if (this.isPrivateIp(res.ip)) {
|
||||
this.logger.warn(`Blocked address: ${res.ip}`);
|
||||
req.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
if (response.body === null) {
|
||||
throw new StatusError('No body', 400, 'No body');
|
||||
const contentLength = res.headers['content-length'];
|
||||
if (contentLength != null) {
|
||||
const size = Number(contentLength);
|
||||
if (size > maxSize) {
|
||||
this.logger.warn(`maxSize exceeded (${size} > ${maxSize}) on response`);
|
||||
req.destroy();
|
||||
}
|
||||
}
|
||||
}).on('downloadProgress', (progress: Got.Progress) => {
|
||||
if (progress.transferred > maxSize) {
|
||||
this.logger.warn(`maxSize exceeded (${progress.transferred} > ${maxSize}) on downloadProgress`);
|
||||
req.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await pipeline(req, fs.createWriteStream(path));
|
||||
} catch (e) {
|
||||
if (e instanceof Got.HTTPError) {
|
||||
throw new StatusError(`${e.response.statusCode} ${e.response.statusMessage}`, e.response.statusCode, e.response.statusMessage);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
await pipeline(stream.Readable.fromWeb(response.body), fs.createWriteStream(path));
|
||||
|
||||
this.logger.succ(`Download finished: ${chalk.cyan(url)}`);
|
||||
}
|
||||
|
||||
|
@@ -2,6 +2,7 @@ import { URL } from 'node:url';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import fetch from 'node-fetch';
|
||||
import type { Instance } from '@/models/entities/Instance.js';
|
||||
import type { InstancesRepository } from '@/models/index.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
@@ -190,7 +191,9 @@ export class FetchInstanceMetadataService {
|
||||
|
||||
const faviconUrl = url + '/favicon.ico';
|
||||
|
||||
const favicon = await this.httpRequestService.fetch(faviconUrl, {}, { noOkError: true });
|
||||
const favicon = await this.httpRequestService.send(faviconUrl, {
|
||||
method: 'HEAD',
|
||||
}, { throwErrorWhenResponseNotOk: false });
|
||||
|
||||
if (favicon.ok) {
|
||||
return faviconUrl;
|
||||
|
@@ -4,7 +4,6 @@ import type { User } from '@/models/entities/User.js';
|
||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { Hashtag } from '@/models/entities/Hashtag.js';
|
||||
import HashtagChart from '@/core/chart/charts/hashtag.js';
|
||||
import type { HashtagsRepository, UsersRepository } from '@/models/index.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
@@ -20,7 +19,6 @@ export class HashtagService {
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
private hashtagChart: HashtagChart,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -143,9 +141,5 @@ export class HashtagService {
|
||||
} as Hashtag);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isUserAttached) {
|
||||
this.hashtagChart.update(tag, user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,257 +1,67 @@
|
||||
import * as http from 'node:http';
|
||||
import * as https from 'node:https';
|
||||
import CacheableLookup from 'cacheable-lookup';
|
||||
import fetch from 'node-fetch';
|
||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import * as undici from 'undici';
|
||||
import { LookupFunction } from 'node:net';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import type Logger from '@/logger.js';
|
||||
|
||||
// true to allow, false to deny
|
||||
export type IpChecker = (ip: string) => boolean;
|
||||
|
||||
/*
|
||||
* Child class to create and save Agent for fetch.
|
||||
* You should construct this when you want
|
||||
* to change timeout, size limit, socket connect function, etc.
|
||||
*/
|
||||
export class UndiciFetcher {
|
||||
/**
|
||||
* Get http non-proxy agent (undici)
|
||||
*/
|
||||
public nonProxiedAgent: undici.Agent;
|
||||
|
||||
/**
|
||||
* Get http proxy or non-proxy agent (undici)
|
||||
*/
|
||||
public agent: undici.ProxyAgent | undici.Agent;
|
||||
|
||||
private proxyBypassHosts: string[];
|
||||
private userAgent: string | undefined;
|
||||
|
||||
private logger: Logger | undefined;
|
||||
|
||||
constructor(
|
||||
args: {
|
||||
agentOptions: undici.Agent.Options;
|
||||
proxy?: {
|
||||
uri: string;
|
||||
options?: undici.Agent.Options; // Override of agentOptions
|
||||
},
|
||||
proxyBypassHosts?: string[];
|
||||
userAgent?: string;
|
||||
},
|
||||
logger?: Logger,
|
||||
) {
|
||||
this.logger = logger;
|
||||
this.logger?.debug('UndiciFetcher constructor', args);
|
||||
|
||||
this.proxyBypassHosts = args.proxyBypassHosts ?? [];
|
||||
this.userAgent = args.userAgent;
|
||||
|
||||
this.nonProxiedAgent = new undici.Agent({
|
||||
...args.agentOptions,
|
||||
connect: (process.env.NODE_ENV !== 'production' && typeof args.agentOptions.connect !== 'function')
|
||||
? (options, cb) => {
|
||||
// Custom connector for debug
|
||||
undici.buildConnector(args.agentOptions.connect as undici.buildConnector.BuildOptions)(options, (err, socket) => {
|
||||
this.logger?.debug('Socket connector called', socket);
|
||||
if (err) {
|
||||
this.logger?.debug(`Socket error`, err);
|
||||
cb(new Error(`Error while socket connecting\n${err}`), null);
|
||||
return;
|
||||
}
|
||||
this.logger?.debug(`Socket connected: port ${socket.localPort} => remote ${socket.remoteAddress}`);
|
||||
cb(null, socket);
|
||||
});
|
||||
} : args.agentOptions.connect,
|
||||
});
|
||||
|
||||
this.agent = args.proxy
|
||||
? new undici.ProxyAgent({
|
||||
...args.agentOptions,
|
||||
...args.proxy.options,
|
||||
|
||||
uri: args.proxy.uri,
|
||||
|
||||
connect: (process.env.NODE_ENV !== 'production' && typeof (args.proxy?.options?.connect ?? args.agentOptions.connect) !== 'function')
|
||||
? (options, cb) => {
|
||||
// Custom connector for debug
|
||||
undici.buildConnector((args.proxy?.options?.connect ?? args.agentOptions.connect) as undici.buildConnector.BuildOptions)(options, (err, socket) => {
|
||||
this.logger?.debug('Socket connector called (secure)', socket);
|
||||
if (err) {
|
||||
this.logger?.debug(`Socket error`, err);
|
||||
cb(new Error(`Error while socket connecting\n${err}`), null);
|
||||
return;
|
||||
}
|
||||
this.logger?.debug(`Socket connected (secure): port ${socket.localPort} => remote ${socket.remoteAddress}`);
|
||||
cb(null, socket);
|
||||
});
|
||||
} : (args.proxy?.options?.connect ?? args.agentOptions.connect),
|
||||
})
|
||||
: this.nonProxiedAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent by URL
|
||||
* @param url URL
|
||||
* @param bypassProxy Allways bypass proxy
|
||||
*/
|
||||
@bindThis
|
||||
public getAgentByUrl(url: URL, bypassProxy = false): undici.Agent | undici.ProxyAgent {
|
||||
if (bypassProxy || this.proxyBypassHosts.includes(url.hostname)) {
|
||||
return this.nonProxiedAgent;
|
||||
} else {
|
||||
return this.agent;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async fetch(
|
||||
url: string | URL,
|
||||
options: undici.RequestInit = {},
|
||||
privateOptions: { noOkError?: boolean; bypassProxy?: boolean; } = { noOkError: false, bypassProxy: false }
|
||||
): Promise<undici.Response> {
|
||||
const res = await undici.fetch(url, {
|
||||
dispatcher: this.getAgentByUrl(new URL(url), privateOptions.bypassProxy),
|
||||
...options,
|
||||
headers: {
|
||||
'User-Agent': this.userAgent ?? '',
|
||||
...(options.headers ?? {}),
|
||||
},
|
||||
}).catch((err) => {
|
||||
this.logger?.error(`fetch error to ${typeof url === 'string' ? url : url.href}`, err);
|
||||
throw new StatusError('Resource Unreachable', 500, 'Resource Unreachable');
|
||||
});
|
||||
if (!res.ok && !privateOptions.noOkError) {
|
||||
throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getJson<T extends unknown>(url: string, accept = 'application/json, */*', headers?: Record<string, string>): Promise<T> {
|
||||
const res = await this.fetch(
|
||||
url,
|
||||
{
|
||||
headers: Object.assign({
|
||||
Accept: accept,
|
||||
}, headers ?? {}),
|
||||
}
|
||||
);
|
||||
|
||||
return await res.json() as T;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getHtml(url: string, accept = 'text/html, */*', headers?: Record<string, string>): Promise<string> {
|
||||
const res = await this.fetch(
|
||||
url,
|
||||
{
|
||||
headers: Object.assign({
|
||||
Accept: accept,
|
||||
}, headers ?? {}),
|
||||
}
|
||||
);
|
||||
|
||||
return await res.text();
|
||||
}
|
||||
}
|
||||
import type { Response } from 'node-fetch';
|
||||
import type { URL } from 'node:url';
|
||||
|
||||
@Injectable()
|
||||
export class HttpRequestService {
|
||||
public defaultFetcher: UndiciFetcher;
|
||||
public fetch: UndiciFetcher['fetch'];
|
||||
public getHtml: UndiciFetcher['getHtml'];
|
||||
public defaultJsonFetcher: UndiciFetcher;
|
||||
public getJson: UndiciFetcher['getJson'];
|
||||
|
||||
//#region for old http/https, only used in S3Service
|
||||
// http non-proxy agent
|
||||
/**
|
||||
* Get http non-proxy agent
|
||||
*/
|
||||
private http: http.Agent;
|
||||
|
||||
// https non-proxy agent
|
||||
/**
|
||||
* Get https non-proxy agent
|
||||
*/
|
||||
private https: https.Agent;
|
||||
|
||||
// http proxy or non-proxy agent
|
||||
/**
|
||||
* Get http proxy or non-proxy agent
|
||||
*/
|
||||
public httpAgent: http.Agent;
|
||||
|
||||
// https proxy or non-proxy agent
|
||||
/**
|
||||
* Get https proxy or non-proxy agent
|
||||
*/
|
||||
public httpsAgent: https.Agent;
|
||||
//#endregion
|
||||
|
||||
public readonly dnsCache: CacheableLookup;
|
||||
public readonly clientDefaults: undici.Agent.Options;
|
||||
private maxSockets: number;
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('http-request');
|
||||
|
||||
this.dnsCache = new CacheableLookup({
|
||||
const cache = new CacheableLookup({
|
||||
maxTtl: 3600, // 1hours
|
||||
errorTtl: 30, // 30secs
|
||||
lookup: false, // nativeのdns.lookupにfallbackしない
|
||||
});
|
||||
|
||||
this.clientDefaults = {
|
||||
keepAliveTimeout: 30 * 1000,
|
||||
keepAliveMaxTimeout: 10 * 60 * 1000,
|
||||
keepAliveTimeoutThreshold: 1 * 1000,
|
||||
strictContentLength: true,
|
||||
headersTimeout: 10 * 1000,
|
||||
bodyTimeout: 10 * 1000,
|
||||
maxHeaderSize: 16364, // default
|
||||
maxResponseSize: 10 * 1024 * 1024,
|
||||
maxRedirections: 3,
|
||||
connect: {
|
||||
timeout: 10 * 1000, // コネクションが確立するまでのタイムアウト
|
||||
maxCachedSessions: 300, // TLSセッションのキャッシュ数 https://github.com/nodejs/undici/blob/v5.14.0/lib/core/connect.js#L80
|
||||
lookup: this.dnsCache.lookup as LookupFunction, // https://github.com/nodejs/undici/blob/v5.14.0/lib/core/connect.js#L98
|
||||
},
|
||||
}
|
||||
|
||||
this.maxSockets = Math.max(64, this.config.deliverJobConcurrency ?? 128);
|
||||
|
||||
this.defaultFetcher = new UndiciFetcher(this.getStandardUndiciFetcherOption(), this.logger);
|
||||
|
||||
this.fetch = this.defaultFetcher.fetch;
|
||||
this.getHtml = this.defaultFetcher.getHtml;
|
||||
|
||||
this.defaultJsonFetcher = new UndiciFetcher(this.getStandardUndiciFetcherOption({
|
||||
maxResponseSize: 1024 * 256,
|
||||
}), this.logger);
|
||||
|
||||
this.getJson = this.defaultJsonFetcher.getJson;
|
||||
|
||||
//#region for old http/https, only used in S3Service
|
||||
|
||||
this.http = new http.Agent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30 * 1000,
|
||||
lookup: this.dnsCache.lookup,
|
||||
lookup: cache.lookup,
|
||||
} as http.AgentOptions);
|
||||
|
||||
this.https = new https.Agent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30 * 1000,
|
||||
lookup: this.dnsCache.lookup,
|
||||
lookup: cache.lookup,
|
||||
} as https.AgentOptions);
|
||||
|
||||
|
||||
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
|
||||
|
||||
this.httpAgent = config.proxy
|
||||
? new HttpProxyAgent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30 * 1000,
|
||||
maxSockets: this.maxSockets,
|
||||
maxSockets,
|
||||
maxFreeSockets: 256,
|
||||
scheduling: 'lifo',
|
||||
proxy: config.proxy,
|
||||
@@ -262,42 +72,21 @@ export class HttpRequestService {
|
||||
? new HttpsProxyAgent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30 * 1000,
|
||||
maxSockets: this.maxSockets,
|
||||
maxSockets,
|
||||
maxFreeSockets: 256,
|
||||
scheduling: 'lifo',
|
||||
proxy: config.proxy,
|
||||
})
|
||||
: this.https;
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getStandardUndiciFetcherOption(opts: undici.Agent.Options = {}, proxyOpts: undici.Agent.Options = {}) {
|
||||
return {
|
||||
agentOptions: {
|
||||
...this.clientDefaults,
|
||||
...opts,
|
||||
},
|
||||
...(this.config.proxy ? {
|
||||
proxy: {
|
||||
uri: this.config.proxy,
|
||||
options: {
|
||||
connections: this.maxSockets,
|
||||
...proxyOpts,
|
||||
}
|
||||
}
|
||||
} : {}),
|
||||
userAgent: this.config.userAgent,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get http agent by URL
|
||||
* Get agent by URL
|
||||
* @param url URL
|
||||
* @param bypassProxy Allways bypass proxy
|
||||
*/
|
||||
@bindThis
|
||||
public getHttpAgentByUrl(url: URL, bypassProxy = false): http.Agent | https.Agent {
|
||||
public getAgentByUrl(url: URL, bypassProxy = false): http.Agent | https.Agent {
|
||||
if (bypassProxy || (this.config.proxyBypassHosts || []).includes(url.hostname)) {
|
||||
return url.protocol === 'http:' ? this.http : this.https;
|
||||
} else {
|
||||
@@ -305,37 +94,67 @@ export class HttpRequestService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* check ip
|
||||
*/
|
||||
@bindThis
|
||||
public getConnectorWithIpCheck(connector: undici.buildConnector.connector, checkIp: IpChecker): undici.buildConnector.connectorAsync {
|
||||
return (options, cb) => {
|
||||
connector(options, (err, socket) => {
|
||||
this.logger.debug('Socket connector (with ip checker) called', socket);
|
||||
if (err) {
|
||||
this.logger.error(`Socket error`, err)
|
||||
cb(new Error(`Error while socket connecting\n${err}`), null);
|
||||
return;
|
||||
}
|
||||
public async getJson<T = unknown>(url: string, accept = 'application/json, */*', headers?: Record<string, string>): Promise<T> {
|
||||
const res = await this.send(url, {
|
||||
method: 'GET',
|
||||
headers: Object.assign({
|
||||
'User-Agent': this.config.userAgent,
|
||||
Accept: accept,
|
||||
}, headers ?? {}),
|
||||
timeout: 5000,
|
||||
size: 1024 * 256,
|
||||
});
|
||||
|
||||
if (socket.remoteAddress == undefined) {
|
||||
this.logger.error(`Socket error: remoteAddress is undefined`);
|
||||
cb(new Error('remoteAddress is undefined (maybe socket destroyed)'), null);
|
||||
return;
|
||||
}
|
||||
return await res.json() as T;
|
||||
}
|
||||
|
||||
// allow
|
||||
if (checkIp(socket.remoteAddress)) {
|
||||
this.logger.debug(`Socket connected (ip ok): ${socket.localPort} => ${socket.remoteAddress}`);
|
||||
cb(null, socket);
|
||||
return;
|
||||
}
|
||||
@bindThis
|
||||
public async getHtml(url: string, accept = 'text/html, */*', headers?: Record<string, string>): Promise<string> {
|
||||
const res = await this.send(url, {
|
||||
method: 'GET',
|
||||
headers: Object.assign({
|
||||
'User-Agent': this.config.userAgent,
|
||||
Accept: accept,
|
||||
}, headers ?? {}),
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
this.logger.error('IP is not allowed', socket);
|
||||
cb(new StatusError('IP is not allowed', 403, 'IP is not allowed'), null);
|
||||
socket.destroy();
|
||||
});
|
||||
};
|
||||
return await res.text();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async send(url: string, args: {
|
||||
method?: string,
|
||||
body?: string,
|
||||
headers?: Record<string, string>,
|
||||
timeout?: number,
|
||||
size?: number,
|
||||
} = {}, extra: {
|
||||
throwErrorWhenResponseNotOk: boolean;
|
||||
} = {
|
||||
throwErrorWhenResponseNotOk: true,
|
||||
}): Promise<Response> {
|
||||
const timeout = args.timeout ?? 5000;
|
||||
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => {
|
||||
controller.abort();
|
||||
}, timeout);
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: args.method ?? 'GET',
|
||||
headers: args.headers,
|
||||
body: args.body,
|
||||
size: args.size ?? 10 * 1024 * 1024,
|
||||
agent: (url) => this.getAgentByUrl(url),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!res.ok && extra.throwErrorWhenResponseNotOk) {
|
||||
throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
@@ -9,6 +9,14 @@ export type IImage = {
|
||||
type: string;
|
||||
};
|
||||
|
||||
export type IImageStream = {
|
||||
data: Readable;
|
||||
ext: string | null;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export type IImageStreamable = IImage | IImageStream;
|
||||
|
||||
export const webpDefault: sharp.WebpOptions = {
|
||||
quality: 85,
|
||||
alphaQuality: 95,
|
||||
@@ -19,6 +27,7 @@ export const webpDefault: sharp.WebpOptions = {
|
||||
};
|
||||
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { Readable } from 'node:stream';
|
||||
|
||||
@Injectable()
|
||||
export class ImageProcessingService {
|
||||
@@ -64,7 +73,7 @@ export class ImageProcessingService {
|
||||
*/
|
||||
@bindThis
|
||||
public async convertToWebp(path: string, width: number, height: number, options: sharp.WebpOptions = webpDefault): Promise<IImage> {
|
||||
return this.convertSharpToWebp(await sharp(path), width, height, options);
|
||||
return this.convertSharpToWebp(sharp(path), width, height, options);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -85,6 +94,27 @@ export class ImageProcessingService {
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public convertToWebpStream(path: string, width: number, height: number, options: sharp.WebpOptions = webpDefault): IImageStream {
|
||||
return this.convertSharpToWebpStream(sharp(path), width, height, options);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public convertSharpToWebpStream(sharp: sharp.Sharp, width: number, height: number, options: sharp.WebpOptions = webpDefault): IImageStream {
|
||||
const data = sharp
|
||||
.resize(width, height, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.rotate()
|
||||
.webp(options)
|
||||
|
||||
return {
|
||||
data,
|
||||
ext: 'webp',
|
||||
type: 'image/webp',
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Convert to PNG
|
||||
* with resize, remove metadata, resolve orientation, stop animation
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as SyslogPro from 'syslog-pro';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import Logger from '@/logger.js';
|
||||
@@ -8,29 +7,14 @@ import type { KEYWORD } from 'color-convert/conversions';
|
||||
|
||||
@Injectable()
|
||||
export class LoggerService {
|
||||
private syslogClient;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
) {
|
||||
if (this.config.syslog) {
|
||||
this.syslogClient = new SyslogPro.RFC5424({
|
||||
applicationName: 'Misskey',
|
||||
timestamp: true,
|
||||
includeStructuredData: true,
|
||||
color: true,
|
||||
extendedColor: true,
|
||||
server: {
|
||||
target: config.syslog.host,
|
||||
port: config.syslog.port,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) {
|
||||
return new Logger(domain, color, store, this.syslogClient);
|
||||
return new Logger(domain, color, store);
|
||||
}
|
||||
}
|
||||
|
@@ -9,9 +9,9 @@ import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository, AntennaNotesRepository } from '@/models/index.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { NotificationService } from './NotificationService.js';
|
||||
import { AntennaService } from './AntennaService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { PushNotificationService } from './PushNotificationService.js';
|
||||
|
||||
@Injectable()
|
||||
@@ -107,12 +107,6 @@ export class NoteReadService {
|
||||
followingChannels: Set<Channel['id']>;
|
||||
},
|
||||
): Promise<void> {
|
||||
const following = info?.following ? info.following : new Set<string>((await this.followingsRepository.find({
|
||||
where: {
|
||||
followerId: userId,
|
||||
},
|
||||
select: ['followeeId'],
|
||||
})).map(x => x.followeeId));
|
||||
const followingChannels = info?.followingChannels ? info.followingChannels : new Set<string>((await this.channelFollowingsRepository.find({
|
||||
where: {
|
||||
followerId: userId,
|
||||
@@ -139,7 +133,7 @@ export class NoteReadService {
|
||||
|
||||
if (note.user != null) { // たぶんnullになることは無いはずだけど一応
|
||||
for (const antenna of myAntennas) {
|
||||
if (await this.antennaService.checkHitAntenna(antenna, note, note.user, undefined, Array.from(following))) {
|
||||
if (await this.antennaService.checkHitAntenna(antenna, note, note.user)) {
|
||||
readAntennaNotes.push(note);
|
||||
}
|
||||
}
|
||||
|
@@ -91,10 +91,12 @@ export class RoleService implements OnApplicationShutdown {
|
||||
case 'roleCreated': {
|
||||
const cached = this.rolesCache.get(null);
|
||||
if (cached) {
|
||||
body.createdAt = new Date(body.createdAt);
|
||||
body.updatedAt = new Date(body.updatedAt);
|
||||
body.lastUsedAt = new Date(body.lastUsedAt);
|
||||
cached.push(body);
|
||||
cached.push({
|
||||
...body,
|
||||
createdAt: new Date(body.createdAt),
|
||||
updatedAt: new Date(body.updatedAt),
|
||||
lastUsedAt: new Date(body.lastUsedAt),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -103,10 +105,12 @@ export class RoleService implements OnApplicationShutdown {
|
||||
if (cached) {
|
||||
const i = cached.findIndex(x => x.id === body.id);
|
||||
if (i > -1) {
|
||||
body.createdAt = new Date(body.createdAt);
|
||||
body.updatedAt = new Date(body.updatedAt);
|
||||
body.lastUsedAt = new Date(body.lastUsedAt);
|
||||
cached[i] = body;
|
||||
cached[i] = {
|
||||
...body,
|
||||
createdAt: new Date(body.createdAt),
|
||||
updatedAt: new Date(body.updatedAt),
|
||||
lastUsedAt: new Date(body.lastUsedAt),
|
||||
};
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -121,8 +125,10 @@ export class RoleService implements OnApplicationShutdown {
|
||||
case 'userRoleAssigned': {
|
||||
const cached = this.roleAssignmentByUserIdCache.get(body.userId);
|
||||
if (cached) {
|
||||
body.createdAt = new Date(body.createdAt);
|
||||
cached.push(body);
|
||||
cached.push({
|
||||
...body,
|
||||
createdAt: new Date(body.createdAt),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@@ -33,7 +33,7 @@ export class S3Service {
|
||||
? false
|
||||
: meta.objectStorageS3ForcePathStyle,
|
||||
httpOptions: {
|
||||
agent: this.httpRequestService.getHttpAgentByUrl(new URL(u), !meta.objectStorageUseProxy),
|
||||
agent: this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@@ -44,16 +44,25 @@ export class WebhookService implements OnApplicationShutdown {
|
||||
switch (type) {
|
||||
case 'webhookCreated':
|
||||
if (body.active) {
|
||||
this.webhooks.push(body);
|
||||
this.webhooks.push({
|
||||
...body,
|
||||
createdAt: new Date(body.createdAt),
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'webhookUpdated':
|
||||
if (body.active) {
|
||||
const i = this.webhooks.findIndex(a => a.id === body.id);
|
||||
if (i > -1) {
|
||||
this.webhooks[i] = body;
|
||||
this.webhooks[i] = {
|
||||
...body,
|
||||
createdAt: new Date(body.createdAt),
|
||||
};
|
||||
} else {
|
||||
this.webhooks.push(body);
|
||||
this.webhooks.push({
|
||||
...body,
|
||||
createdAt: new Date(body.createdAt),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.webhooks = this.webhooks.filter(a => a.id !== body.id);
|
||||
|
@@ -21,11 +21,11 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
|
||||
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, EmojisRepository, PollsRepository } from '@/models/index.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { LdSignatureService } from './LdSignatureService.js';
|
||||
import { ApMfmService } from './ApMfmService.js';
|
||||
import type { IActivity, IObject } from './type.js';
|
||||
import type { IIdentifier } from './models/identifier.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class ApRendererService {
|
||||
@@ -274,7 +274,7 @@ export class ApRendererService {
|
||||
} as any;
|
||||
|
||||
if (reaction.startsWith(':')) {
|
||||
const name = reaction.replace(/:/g, '');
|
||||
const name = reaction.replaceAll(':', '');
|
||||
const emoji = await this.emojisRepository.findOneBy({
|
||||
name,
|
||||
host: IsNull(),
|
||||
|
@@ -5,7 +5,7 @@ import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js';
|
||||
import { HttpRequestService, UndiciFetcher } from '@/core/HttpRequestService.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type Logger from '@/logger.js';
|
||||
@@ -30,7 +30,6 @@ type PrivateKey = {
|
||||
|
||||
@Injectable()
|
||||
export class ApRequestService {
|
||||
private undiciFetcher: UndiciFetcher;
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@@ -41,10 +40,8 @@ export class ApRequestService {
|
||||
private httpRequestService: HttpRequestService,
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる
|
||||
this.undiciFetcher = new UndiciFetcher(this.httpRequestService.getStandardUndiciFetcherOption({
|
||||
maxRedirections: 0,
|
||||
}), this.logger );
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -163,14 +160,11 @@ export class ApRequestService {
|
||||
},
|
||||
});
|
||||
|
||||
await this.undiciFetcher.fetch(
|
||||
url,
|
||||
{
|
||||
method: req.request.method,
|
||||
headers: req.request.headers,
|
||||
body,
|
||||
}
|
||||
);
|
||||
await this.httpRequestService.send(url, {
|
||||
method: req.request.method,
|
||||
headers: req.request.headers,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -192,13 +186,10 @@ export class ApRequestService {
|
||||
},
|
||||
});
|
||||
|
||||
const res = await this.httpRequestService.fetch(
|
||||
url,
|
||||
{
|
||||
method: req.request.method,
|
||||
headers: req.request.headers,
|
||||
}
|
||||
);
|
||||
const res = await this.httpRequestService.send(url, {
|
||||
method: req.request.method,
|
||||
headers: req.request.headers,
|
||||
});
|
||||
|
||||
return await res.json();
|
||||
}
|
||||
|
@@ -4,22 +4,21 @@ import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { HttpRequestService, UndiciFetcher } from '@/core/HttpRequestService.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { isCollectionOrOrderedCollection } from './type.js';
|
||||
import { ApDbResolverService } from './ApDbResolverService.js';
|
||||
import { ApRendererService } from './ApRendererService.js';
|
||||
import { ApRequestService } from './ApRequestService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import type { IObject, ICollection, IOrderedCollection } from './type.js';
|
||||
import type Logger from '@/logger.js';
|
||||
|
||||
export class Resolver {
|
||||
private history: Set<string>;
|
||||
private user?: ILocalUser;
|
||||
private undiciFetcher: UndiciFetcher;
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@@ -39,10 +38,8 @@ export class Resolver {
|
||||
private recursionLimit = 100,
|
||||
) {
|
||||
this.history = new Set();
|
||||
this.logger = this.loggerService?.getLogger('ap-resolve'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる
|
||||
this.undiciFetcher = new UndiciFetcher(this.httpRequestService.getStandardUndiciFetcherOption({
|
||||
maxRedirections: 0,
|
||||
}), this.logger);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
this.logger = this.loggerService?.getLogger('ap-resolve'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -106,7 +103,7 @@ export class Resolver {
|
||||
|
||||
const object = (this.user
|
||||
? await this.apRequestService.signedGet(value, this.user) as IObject
|
||||
: await this.undiciFetcher.getJson<IObject>(value, 'application/activity+json, application/ld+json'));
|
||||
: await this.httpRequestService.getJson(value, 'application/activity+json, application/ld+json')) as IObject;
|
||||
|
||||
if (object == null || (
|
||||
Array.isArray(object['@context']) ?
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import * as crypto from 'node:crypto';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import jsonld from 'jsonld';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CONTEXTS } from './misc/contexts.js';
|
||||
@@ -9,7 +10,7 @@ import { CONTEXTS } from './misc/contexts.js';
|
||||
class LdSignature {
|
||||
public debug = false;
|
||||
public preLoad = true;
|
||||
public loderTimeout = 10 * 1000;
|
||||
public loderTimeout = 5000;
|
||||
|
||||
constructor(
|
||||
private httpRequestService: HttpRequestService,
|
||||
@@ -84,7 +85,9 @@ class LdSignature {
|
||||
@bindThis
|
||||
public async normalize(data: any) {
|
||||
const customLoader = this.getLoader();
|
||||
return 42;
|
||||
return await jsonld.normalize(data, {
|
||||
documentLoader: customLoader,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -115,19 +118,12 @@ class LdSignature {
|
||||
|
||||
@bindThis
|
||||
private async fetchDocument(url: string) {
|
||||
const json = await this.httpRequestService.fetch(
|
||||
url,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/ld+json, application/json',
|
||||
},
|
||||
// TODO
|
||||
//timeout: this.loderTimeout,
|
||||
const json = await this.httpRequestService.send(url, {
|
||||
headers: {
|
||||
Accept: 'application/ld+json, application/json',
|
||||
},
|
||||
{
|
||||
noOkError: true,
|
||||
}
|
||||
).then(res => {
|
||||
timeout: this.loderTimeout,
|
||||
}, { throwErrorWhenResponseNotOk: false }).then(res => {
|
||||
if (!res.ok) {
|
||||
throw `${res.status} ${res.statusText}`;
|
||||
} else {
|
||||
|
@@ -29,6 +29,7 @@ import { UserNotePining } from '@/models/entities/UserNotePining.js';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
import type { UtilityService } from '@/core/UtilityService.js';
|
||||
import type { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
|
||||
import { extractApHashtags } from './tag.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
@@ -43,37 +44,6 @@ import type { IActor, IObject, IApPropertyValue } from '../type.js';
|
||||
const nameLength = 128;
|
||||
const summaryLength = 2048;
|
||||
|
||||
const services: {
|
||||
[x: string]: (id: string, username: string) => any
|
||||
} = {
|
||||
'misskey:authentication:twitter': (userId, screenName) => ({ userId, screenName }),
|
||||
'misskey:authentication:github': (id, login) => ({ id, login }),
|
||||
'misskey:authentication:discord': (id, name) => $discord(id, name),
|
||||
};
|
||||
|
||||
const $discord = (id: string, name: string) => {
|
||||
if (typeof name !== 'string') {
|
||||
name = 'unknown#0000';
|
||||
}
|
||||
const [username, discriminator] = name.split('#');
|
||||
return { id, username, discriminator };
|
||||
};
|
||||
|
||||
function addService(target: { [x: string]: any }, source: IApPropertyValue) {
|
||||
const service = services[source.name];
|
||||
|
||||
if (typeof source.value !== 'string') {
|
||||
source.value = 'unknown';
|
||||
}
|
||||
|
||||
const [id, username] = source.value.split('@');
|
||||
|
||||
if (service) {
|
||||
target[source.name.split(':')[2]] = service(id, username);
|
||||
}
|
||||
}
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class ApPersonService implements OnModuleInit {
|
||||
private utilityService: UtilityService;
|
||||
@@ -540,22 +510,16 @@ export class ApPersonService implements OnModuleInit {
|
||||
name: string,
|
||||
value: string
|
||||
}[] = [];
|
||||
const services: { [x: string]: any } = {};
|
||||
|
||||
if (Array.isArray(attachments)) {
|
||||
for (const attachment of attachments.filter(isPropertyValue)) {
|
||||
if (isPropertyValue(attachment.identifier)) {
|
||||
addService(services, attachment.identifier);
|
||||
} else {
|
||||
fields.push({
|
||||
name: attachment.name,
|
||||
value: this.mfmService.fromHtml(attachment.value),
|
||||
});
|
||||
}
|
||||
fields.push({
|
||||
name: attachment.name,
|
||||
value: this.mfmService.fromHtml(attachment.value),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { fields, services };
|
||||
return { fields };
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -566,22 +530,22 @@ export class ApPersonService implements OnModuleInit {
|
||||
|
||||
this.logger.info(`Updating the featured: ${user.uri}`);
|
||||
|
||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||
const _resolver = resolver ?? this.apResolverService.createResolver();
|
||||
|
||||
// Resolve to (Ordered)Collection Object
|
||||
const collection = await resolver.resolveCollection(user.featured);
|
||||
const collection = await _resolver.resolveCollection(user.featured);
|
||||
if (!isCollectionOrOrderedCollection(collection)) throw new Error('Object is not Collection or OrderedCollection');
|
||||
|
||||
// Resolve to Object(may be Note) arrays
|
||||
const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems;
|
||||
const items = await Promise.all(toArray(unresolvedItems).map(x => resolver.resolve(x)));
|
||||
const items = await Promise.all(toArray(unresolvedItems).map(x => _resolver.resolve(x)));
|
||||
|
||||
// Resolve and regist Notes
|
||||
const limit = promiseLimit<Note | null>(2);
|
||||
const featuredNotes = await Promise.all(items
|
||||
.filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも
|
||||
.slice(0, 5)
|
||||
.map(item => limit(() => this.apNoteService.resolveNote(item, resolver))));
|
||||
.map(item => limit(() => this.apNoteService.resolveNote(item, _resolver))));
|
||||
|
||||
await this.db.transaction(async transactionalEntityManager => {
|
||||
await transactionalEntityManager.delete(UserNotePining, { userId: user.id });
|
||||
|
@@ -10,7 +10,6 @@ import PerUserNotesChart from './charts/per-user-notes.js';
|
||||
import PerUserPvChart from './charts/per-user-pv.js';
|
||||
import DriveChart from './charts/drive.js';
|
||||
import PerUserReactionsChart from './charts/per-user-reactions.js';
|
||||
import HashtagChart from './charts/hashtag.js';
|
||||
import PerUserFollowingChart from './charts/per-user-following.js';
|
||||
import PerUserDriveChart from './charts/per-user-drive.js';
|
||||
import ApRequestChart from './charts/ap-request.js';
|
||||
@@ -31,7 +30,6 @@ export class ChartManagementService implements OnApplicationShutdown {
|
||||
private perUserPvChart: PerUserPvChart,
|
||||
private driveChart: DriveChart,
|
||||
private perUserReactionsChart: PerUserReactionsChart,
|
||||
private hashtagChart: HashtagChart,
|
||||
private perUserFollowingChart: PerUserFollowingChart,
|
||||
private perUserDriveChart: PerUserDriveChart,
|
||||
private apRequestChart: ApRequestChart,
|
||||
@@ -46,7 +44,6 @@ export class ChartManagementService implements OnApplicationShutdown {
|
||||
this.perUserPvChart,
|
||||
this.driveChart,
|
||||
this.perUserReactionsChart,
|
||||
this.hashtagChart,
|
||||
this.perUserFollowingChart,
|
||||
this.perUserDriveChart,
|
||||
this.apRequestChart,
|
||||
|
@@ -1,10 +0,0 @@
|
||||
import Chart from '../../core.js';
|
||||
|
||||
export const name = 'hashtag';
|
||||
|
||||
export const schema = {
|
||||
'local.users': { uniqueIncrement: true },
|
||||
'remote.users': { uniqueIncrement: true },
|
||||
} as const;
|
||||
|
||||
export const entity = Chart.schemaToEntity(name, schema, true);
|
@@ -1,45 +0,0 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import Chart from '../core.js';
|
||||
import { ChartLoggerService } from '../ChartLoggerService.js';
|
||||
import { name, schema } from './entities/hashtag.js';
|
||||
import type { KVs } from '../core.js';
|
||||
|
||||
/**
|
||||
* ハッシュタグに関するチャート
|
||||
*/
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class HashtagChart extends Chart<typeof schema> {
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
private appLockService: AppLockService,
|
||||
private userEntityService: UserEntityService,
|
||||
private chartLoggerService: ChartLoggerService,
|
||||
) {
|
||||
super(db, (k) => appLockService.getChartInsertLock(k), chartLoggerService.logger, name, schema, true);
|
||||
}
|
||||
|
||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||
return {};
|
||||
}
|
||||
|
||||
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
|
||||
return {};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async update(hashtag: string, user: { id: User['id'], host: User['host'] }): Promise<void> {
|
||||
await this.commit({
|
||||
'local.users': this.userEntityService.isLocalUser(user) ? [user.id] : [],
|
||||
'remote.users': this.userEntityService.isLocalUser(user) ? [] : [user.id],
|
||||
}, hashtag);
|
||||
}
|
||||
}
|
@@ -11,9 +11,9 @@ import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { Repository, DataSource } from 'typeorm';
|
||||
|
||||
const columnPrefix = '___' as const;
|
||||
const uniqueTempColumnPrefix = 'unique_temp___' as const;
|
||||
const columnDot = '_' as const;
|
||||
const COLUMN_PREFIX = '___' as const;
|
||||
const UNIQUE_TEMP_COLUMN_PREFIX = 'unique_temp___' as const;
|
||||
const COLUMN_DELIMITER = '_' as const;
|
||||
|
||||
type Schema = Record<string, {
|
||||
uniqueIncrement?: boolean;
|
||||
@@ -26,14 +26,14 @@ type Schema = Record<string, {
|
||||
accumulate?: boolean;
|
||||
}>;
|
||||
|
||||
type KeyToColumnName<T extends string> = T extends `${infer R1}.${infer R2}` ? `${R1}${typeof columnDot}${KeyToColumnName<R2>}` : T;
|
||||
type KeyToColumnName<T extends string> = T extends `${infer R1}.${infer R2}` ? `${R1}${typeof COLUMN_DELIMITER}${KeyToColumnName<R2>}` : T;
|
||||
|
||||
type Columns<S extends Schema> = {
|
||||
[K in keyof S as `${typeof columnPrefix}${KeyToColumnName<string & K>}`]: number;
|
||||
[K in keyof S as `${typeof COLUMN_PREFIX}${KeyToColumnName<string & K>}`]: number;
|
||||
};
|
||||
|
||||
type TempColumnsForUnique<S extends Schema> = {
|
||||
[K in keyof S as `${typeof uniqueTempColumnPrefix}${KeyToColumnName<string & K>}`]: S[K]['uniqueIncrement'] extends true ? string[] : never;
|
||||
[K in keyof S as `${typeof UNIQUE_TEMP_COLUMN_PREFIX}${KeyToColumnName<string & K>}`]: S[K]['uniqueIncrement'] extends true ? string[] : never;
|
||||
};
|
||||
|
||||
type RawRecord<S extends Schema> = {
|
||||
@@ -138,20 +138,20 @@ export default abstract class Chart<T extends Schema> {
|
||||
private static convertSchemaToColumnDefinitions(schema: Schema): Record<string, { type: string; array?: boolean; default?: any; }> {
|
||||
const columns = {} as Record<string, { type: string; array?: boolean; default?: any; }>;
|
||||
for (const [k, v] of Object.entries(schema)) {
|
||||
const name = k.replaceAll('.', columnDot);
|
||||
const name = k.replaceAll('.', COLUMN_DELIMITER);
|
||||
const type = v.range === 'big' ? 'bigint' : v.range === 'small' ? 'smallint' : 'integer';
|
||||
if (v.uniqueIncrement) {
|
||||
columns[uniqueTempColumnPrefix + name] = {
|
||||
columns[UNIQUE_TEMP_COLUMN_PREFIX + name] = {
|
||||
type: 'varchar',
|
||||
array: true,
|
||||
default: '{}',
|
||||
};
|
||||
columns[columnPrefix + name] = {
|
||||
columns[COLUMN_PREFIX + name] = {
|
||||
type,
|
||||
default: 0,
|
||||
};
|
||||
} else {
|
||||
columns[columnPrefix + name] = {
|
||||
columns[COLUMN_PREFIX + name] = {
|
||||
type,
|
||||
default: 0,
|
||||
};
|
||||
@@ -253,8 +253,8 @@ export default abstract class Chart<T extends Schema> {
|
||||
@bindThis
|
||||
private convertRawRecord(x: RawRecord<T>): KVs<T> {
|
||||
const kvs = {} as Record<string, number>;
|
||||
for (const k of Object.keys(x).filter((k) => k.startsWith(columnPrefix)) as (keyof Columns<T>)[]) {
|
||||
kvs[(k as string).substr(columnPrefix.length).split(columnDot).join('.')] = x[k] as unknown as number;
|
||||
for (const k of Object.keys(x).filter((k) => k.startsWith(COLUMN_PREFIX)) as (keyof Columns<T>)[]) {
|
||||
kvs[(k as string).substr(COLUMN_PREFIX.length).split(COLUMN_DELIMITER).join('.')] = x[k] as unknown as number;
|
||||
}
|
||||
return kvs as KVs<T>;
|
||||
}
|
||||
@@ -357,8 +357,8 @@ export default abstract class Chart<T extends Schema> {
|
||||
|
||||
const columns = {} as Record<string, number | unknown[]>;
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
const name = k.replaceAll('.', columnDot);
|
||||
columns[columnPrefix + name] = v;
|
||||
const name = k.replaceAll('.', COLUMN_DELIMITER);
|
||||
columns[COLUMN_PREFIX + name] = v;
|
||||
}
|
||||
|
||||
// 新規ログ挿入
|
||||
@@ -419,13 +419,13 @@ export default abstract class Chart<T extends Schema> {
|
||||
const queryForDay: Record<keyof RawRecord<T>, number | (() => string)> = {} as any;
|
||||
for (const [k, v] of Object.entries(finalDiffs)) {
|
||||
if (typeof v === 'number') {
|
||||
const name = columnPrefix + k.replaceAll('.', columnDot) as string & keyof Columns<T>;
|
||||
const name = COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as string & keyof Columns<T>;
|
||||
if (v > 0) queryForHour[name] = () => `"${name}" + ${v}`;
|
||||
if (v < 0) queryForHour[name] = () => `"${name}" - ${Math.abs(v)}`;
|
||||
if (v > 0) queryForDay[name] = () => `"${name}" + ${v}`;
|
||||
if (v < 0) queryForDay[name] = () => `"${name}" - ${Math.abs(v)}`;
|
||||
} else if (Array.isArray(v) && v.length > 0) { // ユニークインクリメント
|
||||
const tempColumnName = uniqueTempColumnPrefix + k.replaceAll('.', columnDot) as string & keyof TempColumnsForUnique<T>;
|
||||
const tempColumnName = UNIQUE_TEMP_COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as string & keyof TempColumnsForUnique<T>;
|
||||
// TODO: item をSQLエスケープ
|
||||
const itemsForHour = v.filter(item => !(logHour[tempColumnName] as unknown as string[]).includes(item)).map(item => `"${item}"`);
|
||||
const itemsForDay = v.filter(item => !(logDay[tempColumnName] as unknown as string[]).includes(item)).map(item => `"${item}"`);
|
||||
@@ -437,8 +437,8 @@ export default abstract class Chart<T extends Schema> {
|
||||
// bake unique count
|
||||
for (const [k, v] of Object.entries(finalDiffs)) {
|
||||
if (this.schema[k].uniqueIncrement) {
|
||||
const name = columnPrefix + k.replaceAll('.', columnDot) as keyof Columns<T>;
|
||||
const tempColumnName = uniqueTempColumnPrefix + k.replaceAll('.', columnDot) as keyof TempColumnsForUnique<T>;
|
||||
const name = COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof Columns<T>;
|
||||
const tempColumnName = UNIQUE_TEMP_COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof TempColumnsForUnique<T>;
|
||||
queryForHour[name] = new Set([...(v as string[]), ...(logHour[tempColumnName] as unknown as string[])]).size;
|
||||
queryForDay[name] = new Set([...(v as string[]), ...(logDay[tempColumnName] as unknown as string[])]).size;
|
||||
}
|
||||
@@ -449,15 +449,15 @@ export default abstract class Chart<T extends Schema> {
|
||||
for (const [k, v] of Object.entries(this.schema)) {
|
||||
const intersection = v.intersection;
|
||||
if (intersection) {
|
||||
const name = columnPrefix + k.replaceAll('.', columnDot) as keyof Columns<T>;
|
||||
const name = COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof Columns<T>;
|
||||
const firstKey = intersection[0];
|
||||
const firstTempColumnName = uniqueTempColumnPrefix + firstKey.replaceAll('.', columnDot) as keyof TempColumnsForUnique<T>;
|
||||
const firstTempColumnName = UNIQUE_TEMP_COLUMN_PREFIX + firstKey.replaceAll('.', COLUMN_DELIMITER) as keyof TempColumnsForUnique<T>;
|
||||
const firstValues = finalDiffs[firstKey] as string[] | undefined;
|
||||
const currentValuesForHour = new Set([...(firstValues ?? []), ...(logHour[firstTempColumnName] as unknown as string[])]);
|
||||
const currentValuesForDay = new Set([...(firstValues ?? []), ...(logDay[firstTempColumnName] as unknown as string[])]);
|
||||
for (let i = 1; i < intersection.length; i++) {
|
||||
const targetKey = intersection[i];
|
||||
const targetTempColumnName = uniqueTempColumnPrefix + targetKey.replaceAll('.', columnDot) as keyof TempColumnsForUnique<T>;
|
||||
const targetTempColumnName = UNIQUE_TEMP_COLUMN_PREFIX + targetKey.replaceAll('.', COLUMN_DELIMITER) as keyof TempColumnsForUnique<T>;
|
||||
const targetValues = finalDiffs[targetKey] as string[] | undefined;
|
||||
const targetValuesForHour = new Set([...(targetValues ?? []), ...(logHour[targetTempColumnName] as unknown as string[])]);
|
||||
const targetValuesForDay = new Set([...(targetValues ?? []), ...(logDay[targetTempColumnName] as unknown as string[])]);
|
||||
@@ -510,7 +510,7 @@ export default abstract class Chart<T extends Schema> {
|
||||
|
||||
const columns = {} as Record<keyof Columns<T>, number>;
|
||||
for (const [k, v] of Object.entries(data) as ([keyof typeof data, number])[]) {
|
||||
const name = columnPrefix + (k as string).replaceAll('.', columnDot) as keyof Columns<T>;
|
||||
const name = COLUMN_PREFIX + (k as string).replaceAll('.', COLUMN_DELIMITER) as keyof Columns<T>;
|
||||
columns[name] = v;
|
||||
}
|
||||
|
||||
@@ -556,7 +556,7 @@ export default abstract class Chart<T extends Schema> {
|
||||
const columns = {} as Record<keyof TempColumnsForUnique<T>, []>;
|
||||
for (const [k, v] of Object.entries(this.schema)) {
|
||||
if (v.uniqueIncrement) {
|
||||
const name = uniqueTempColumnPrefix + k.replaceAll('.', columnDot) as keyof TempColumnsForUnique<T>;
|
||||
const name = UNIQUE_TEMP_COLUMN_PREFIX + k.replaceAll('.', COLUMN_DELIMITER) as keyof TempColumnsForUnique<T>;
|
||||
columns[name] = [];
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,6 @@ import { entity as PerUserNotesChart } from './charts/entities/per-user-notes.js
|
||||
import { entity as PerUserPvChart } from './charts/entities/per-user-pv.js';
|
||||
import { entity as DriveChart } from './charts/entities/drive.js';
|
||||
import { entity as PerUserReactionsChart } from './charts/entities/per-user-reactions.js';
|
||||
import { entity as HashtagChart } from './charts/entities/hashtag.js';
|
||||
import { entity as PerUserFollowingChart } from './charts/entities/per-user-following.js';
|
||||
import { entity as PerUserDriveChart } from './charts/entities/per-user-drive.js';
|
||||
import { entity as ApRequestChart } from './charts/entities/ap-request.js';
|
||||
@@ -27,7 +26,6 @@ export const entities = [
|
||||
PerUserPvChart.hour, PerUserPvChart.day,
|
||||
DriveChart.hour, DriveChart.day,
|
||||
PerUserReactionsChart.hour, PerUserReactionsChart.day,
|
||||
HashtagChart.hour, HashtagChart.day,
|
||||
PerUserFollowingChart.hour, PerUserFollowingChart.day,
|
||||
PerUserDriveChart.hour, PerUserDriveChart.day,
|
||||
ApRequestChart.hour, ApRequestChart.day,
|
||||
|
@@ -22,8 +22,10 @@ export class EmojiEntityService {
|
||||
@bindThis
|
||||
public async pack(
|
||||
src: Emoji['id'] | Emoji,
|
||||
opts: { omitHost?: boolean; omitId?: boolean; } = {},
|
||||
opts: { omitHost?: boolean; omitId?: boolean; withUrl?: boolean; } = { omitHost: true, omitId: true, withUrl: true },
|
||||
): Promise<Packed<'Emoji'>> {
|
||||
opts = { omitHost: true, omitId: true, withUrl: true, ...opts }
|
||||
|
||||
const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
@@ -32,13 +34,15 @@ export class EmojiEntityService {
|
||||
name: emoji.name,
|
||||
category: emoji.category,
|
||||
host: opts.omitHost ? undefined : emoji.host,
|
||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
url: opts.withUrl ? (emoji.publicUrl || emoji.originalUrl) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packMany(
|
||||
emojis: any[],
|
||||
opts: { omitHost?: boolean; omitId?: boolean; } = {},
|
||||
opts: { omitHost?: boolean; omitId?: boolean; withUrl?: boolean; } = {},
|
||||
) {
|
||||
return Promise.all(emojis.map(x => this.pack(x, opts)));
|
||||
}
|
||||
|
@@ -282,7 +282,9 @@ export class NoteEntityService implements OnModuleInit {
|
||||
: await this.channelsRepository.findOneBy({ id: note.channelId })
|
||||
: null;
|
||||
|
||||
const reactionEmojiNames = Object.keys(note.reactions).filter(x => x.startsWith(':')).map(x => this.reactionService.decodeReaction(x).reaction).map(x => x.replace(/:/g, ''));
|
||||
const reactionEmojiNames = Object.keys(note.reactions)
|
||||
.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
|
||||
.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
|
||||
|
||||
const packed: Packed<'Note'> = await awaitAll({
|
||||
id: note.id,
|
||||
@@ -299,6 +301,8 @@ export class NoteEntityService implements OnModuleInit {
|
||||
renoteCount: note.renoteCount,
|
||||
repliesCount: note.repliesCount,
|
||||
reactions: this.reactionService.convertLegacyReactions(note.reactions),
|
||||
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
|
||||
emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined,
|
||||
tags: note.tags.length > 0 ? note.tags : undefined,
|
||||
fileIds: note.fileIds,
|
||||
files: this.driveFileEntityService.packMany(note.fileIds),
|
||||
@@ -384,6 +388,8 @@ export class NoteEntityService implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
await this.customEmojiService.prefetchEmojis(this.customEmojiService.aggregateNoteEmojis(notes));
|
||||
|
||||
return await Promise.all(notes.map(n => this.pack(n, me, {
|
||||
...options,
|
||||
_hint_: {
|
||||
|
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { AccessTokensRepository, NoteReactionsRepository, NotificationsRepository } from '@/models/index.js';
|
||||
import type { AccessTokensRepository, NoteReactionsRepository, NotificationsRepository, User } from '@/models/index.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { Notification } from '@/models/entities/Notification.js';
|
||||
import type { NoteReaction } from '@/models/entities/NoteReaction.js';
|
||||
@@ -146,6 +146,8 @@ export class NotificationEntityService implements OnModuleInit {
|
||||
myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) ?? null);
|
||||
}
|
||||
|
||||
await this.customEmojiService.prefetchEmojis(this.customEmojiService.aggregateNoteEmojis(notes));
|
||||
|
||||
return await Promise.all(notifications.map(x => this.pack(x, {
|
||||
_hintForEachNotes_: {
|
||||
myReactions: myReactionsMap,
|
||||
|
@@ -413,6 +413,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
faviconUrl: instance.faviconUrl,
|
||||
themeColor: instance.themeColor,
|
||||
} : undefined) : undefined,
|
||||
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
|
||||
onlineStatus: this.getOnlineStatus(user),
|
||||
|
||||
...(opts.detail ? {
|
||||
@@ -488,7 +489,6 @@ export class UserEntityService implements OnModuleInit {
|
||||
hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id),
|
||||
hasUnreadNotification: this.getHasUnreadNotification(user.id),
|
||||
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
||||
integrations: profile!.integrations,
|
||||
mutedWords: profile!.mutedWords,
|
||||
mutedInstances: profile!.mutedInstances,
|
||||
mutingNotificationTypes: profile!.mutingNotificationTypes,
|
||||
@@ -496,10 +496,10 @@ export class UserEntityService implements OnModuleInit {
|
||||
showTimelineReplies: user.showTimelineReplies ?? falsy,
|
||||
achievements: profile!.achievements,
|
||||
loggedInDays: profile!.loggedInDates.length,
|
||||
policies: this.roleService.getUserPolicies(user.id),
|
||||
} : {}),
|
||||
|
||||
...(opts.includeSecrets ? {
|
||||
policies: this.roleService.getUserPolicies(user.id),
|
||||
email: profile!.email,
|
||||
emailVerified: profile!.emailVerified,
|
||||
securityKeysList: profile!.twoFactorEnabled
|
||||
|
@@ -17,15 +17,13 @@ export default class Logger {
|
||||
private context: Context;
|
||||
private parentLogger: Logger | null = null;
|
||||
private store: boolean;
|
||||
private syslogClient: any | null = null;
|
||||
|
||||
constructor(context: string, color?: KEYWORD, store = true, syslogClient = null) {
|
||||
constructor(context: string, color?: KEYWORD, store = true) {
|
||||
this.context = {
|
||||
name: context,
|
||||
color: color,
|
||||
};
|
||||
this.store = store;
|
||||
this.syslogClient = syslogClient;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -68,20 +66,7 @@ export default class Logger {
|
||||
if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log;
|
||||
|
||||
console.log(important ? chalk.bold(log) : log);
|
||||
|
||||
if (store) {
|
||||
if (this.syslogClient) {
|
||||
const send =
|
||||
level === 'error' ? this.syslogClient.error :
|
||||
level === 'warning' ? this.syslogClient.warning :
|
||||
level === 'success' ? this.syslogClient.info :
|
||||
level === 'debug' ? this.syslogClient.info :
|
||||
level === 'info' ? this.syslogClient.info :
|
||||
null as never;
|
||||
|
||||
send.bind(this.syslogClient)(message).catch(() => {});
|
||||
}
|
||||
}
|
||||
if (level === 'error' && data) console.log(data);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
11
packages/backend/src/misc/dev-null.ts
Normal file
11
packages/backend/src/misc/dev-null.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Writable, WritableOptions } from "node:stream";
|
||||
|
||||
export class DevNull extends Writable implements NodeJS.WritableStream {
|
||||
constructor(opts?: WritableOptions) {
|
||||
super(opts);
|
||||
}
|
||||
|
||||
_write (chunk: any, encoding: BufferEncoding, cb: (err?: Error | null) => void) {
|
||||
setImmediate(cb);
|
||||
}
|
||||
}
|
@@ -4,7 +4,7 @@ import { unique } from '@/misc/prelude/array.js';
|
||||
export function extractCustomEmojisFromMfm(nodes: mfm.MfmNode[]): string[] {
|
||||
const emojiNodes = mfm.extract(nodes, (node) => {
|
||||
return (node.type === 'emojiCode' && node.props.name.length <= 100);
|
||||
});
|
||||
}) as mfm.MfmEmojiCode[];
|
||||
|
||||
return unique(emojiNodes.map(x => x.props.name));
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ import * as mfm from 'mfm-js';
|
||||
import { unique } from '@/misc/prelude/array.js';
|
||||
|
||||
export function extractHashtags(nodes: mfm.MfmNode[]): string[] {
|
||||
const hashtagNodes = mfm.extract(nodes, (node) => node.type === 'hashtag');
|
||||
const hashtagNodes = mfm.extract(nodes, (node) => node.type === 'hashtag') as mfm.MfmHashtag[];
|
||||
const hashtags = unique(hashtagNodes.map(x => x.props.hashtag));
|
||||
|
||||
return hashtags;
|
||||
|
@@ -1,9 +1,14 @@
|
||||
import IPCIDR from 'ip-cidr';
|
||||
|
||||
export function getIpHash(ip: string) {
|
||||
// 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 prefix = IPCIDR.createAddress(ip).mask(64);
|
||||
return 'ip-' + BigInt('0b' + prefix).toString(36);
|
||||
try {
|
||||
// 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 prefix = IPCIDR.createAddress(ip).mask(64);
|
||||
return 'ip-' + BigInt('0b' + prefix).toString(36);
|
||||
} catch (e) {
|
||||
const prefix = IPCIDR.createAddress(ip.replace(/:[0-9]+$/, '')).mask(64);
|
||||
return 'ip-' + BigInt('0b' + prefix).toString(36);
|
||||
}
|
||||
}
|
||||
|
@@ -1,14 +1,14 @@
|
||||
export function nyaize(text: string): string {
|
||||
return text
|
||||
// ja-JP
|
||||
.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ')
|
||||
.replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ')
|
||||
// en-US
|
||||
.replace(/(?<=n)a/gi, x => x === 'A' ? 'YA' : 'ya')
|
||||
.replace(/(?<=morn)ing/gi, x => x === 'ING' ? 'YAN' : 'yan')
|
||||
.replace(/(?<=every)one/gi, x => x === 'ONE' ? 'NYAN' : 'nyan')
|
||||
// ko-KR
|
||||
.replace(/[나-낳]/g, match => String.fromCharCode(
|
||||
match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0)
|
||||
match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0),
|
||||
))
|
||||
.replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥')
|
||||
.replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥');
|
||||
|
@@ -132,11 +132,27 @@ type NullOrUndefined<p extends Schema, T> =
|
||||
// https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
|
||||
// Get intersection from union
|
||||
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
|
||||
type PartialIntersection<T> = Partial<UnionToIntersection<T>>;
|
||||
|
||||
// https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552
|
||||
// To get union, we use `Foo extends any ? Hoge<Foo> : never`
|
||||
type UnionSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? SchemaType<X> : never;
|
||||
type ArrayUnion<T> = T extends any ? Array<T> : never;
|
||||
type UnionObjectSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? ObjectSchemaType<X> : never;
|
||||
type ArrayUnion<T> = T extends any ? Array<T> : never;
|
||||
|
||||
type ObjectSchemaTypeDef<p extends Schema> =
|
||||
p['ref'] extends keyof typeof refs ? Packed<p['ref']> :
|
||||
p['properties'] extends NonNullable<Obj> ?
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ?
|
||||
ObjType<p['properties'], NonNullable<p['required']>[number]> & UnionObjectSchemaType<p['anyOf']> & PartialIntersection<UnionObjectSchemaType<p['anyOf']>>
|
||||
:
|
||||
ObjType<p['properties'], NonNullable<p['required']>[number]>
|
||||
:
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? UnionObjectSchemaType<p['anyOf']> & PartialIntersection<UnionObjectSchemaType<p['anyOf']>> :
|
||||
p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
|
||||
any
|
||||
|
||||
type ObjectSchemaType<p extends Schema> = NullOrUndefined<p, ObjectSchemaTypeDef<p>>;
|
||||
|
||||
export type SchemaTypeDef<p extends Schema> =
|
||||
p['type'] extends 'null' ? null :
|
||||
@@ -149,13 +165,7 @@ export type SchemaTypeDef<p extends Schema> =
|
||||
string
|
||||
) :
|
||||
p['type'] extends 'boolean' ? boolean :
|
||||
p['type'] extends 'object' ? (
|
||||
p['ref'] extends keyof typeof refs ? Packed<p['ref']> :
|
||||
p['properties'] extends NonNullable<Obj> ? ObjType<p['properties'], NonNullable<p['required']>[number]> :
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & Partial<UnionToIntersection<UnionSchemaType<p['anyOf']>>> :
|
||||
p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
|
||||
any
|
||||
) :
|
||||
p['type'] extends 'object' ? ObjectSchemaTypeDef<p> :
|
||||
p['type'] extends 'array' ? (
|
||||
p['items'] extends OfSchema ? (
|
||||
p['items']['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<NonNullable<p['items']['anyOf']>>[] :
|
||||
@@ -166,6 +176,7 @@ export type SchemaTypeDef<p extends Schema> =
|
||||
p['items'] extends NonNullable<Schema> ? SchemaTypeDef<p['items']>[] :
|
||||
any[]
|
||||
) :
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & PartialIntersection<UnionSchemaType<p['anyOf']>> :
|
||||
p['oneOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['oneOf']> :
|
||||
any;
|
||||
|
||||
|
@@ -279,57 +279,6 @@ export class Meta {
|
||||
})
|
||||
public swPrivateKey: string | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public enableTwitterIntegration: boolean;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
nullable: true,
|
||||
})
|
||||
public twitterConsumerKey: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
nullable: true,
|
||||
})
|
||||
public twitterConsumerSecret: string | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public enableGithubIntegration: boolean;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
nullable: true,
|
||||
})
|
||||
public githubClientId: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
nullable: true,
|
||||
})
|
||||
public githubClientSecret: string | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public enableDiscordIntegration: boolean;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
nullable: true,
|
||||
})
|
||||
public discordClientId: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
nullable: true,
|
||||
})
|
||||
public discordClientSecret: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
nullable: true,
|
||||
|
@@ -184,11 +184,6 @@ export class UserProfile {
|
||||
@JoinColumn()
|
||||
public pinnedPage: Page | null;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: {},
|
||||
})
|
||||
public integrations: Record<string, any>;
|
||||
|
||||
@Index()
|
||||
@Column('boolean', {
|
||||
default: false, select: false,
|
||||
|
@@ -29,5 +29,9 @@ export const packedEmojiSchema = {
|
||||
optional: true, nullable: true,
|
||||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
@@ -323,10 +323,6 @@ export const packedMeDetailedOnlySchema = {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
integrations: {
|
||||
type: 'object',
|
||||
nullable: true, optional: false,
|
||||
},
|
||||
mutedWords: {
|
||||
type: 'array',
|
||||
nullable: false, optional: false,
|
||||
|
@@ -57,8 +57,15 @@ export class AggregateRetentionProcessorService {
|
||||
usersCount: targetUserIds.length,
|
||||
});
|
||||
|
||||
// 今日活動したユーザーを全て取得
|
||||
const activeUsers = await this.usersRepository.findBy({
|
||||
host: IsNull(),
|
||||
lastActiveDate: MoreThan(new Date(Date.now() - (1000 * 60 * 60 * 24))),
|
||||
});
|
||||
const activeUsersIds = activeUsers.map(u => u.id);
|
||||
|
||||
for (const record of pastRecords) {
|
||||
const retention = record.userIds.filter(id => targetUserIds.includes(id)).length;
|
||||
const retention = record.userIds.filter(id => activeUsersIds.includes(id)).length;
|
||||
|
||||
const data = deepClone(record.data);
|
||||
data[dateKey] = retention;
|
||||
|
@@ -12,7 +12,6 @@ import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js';
|
||||
import PerUserPvChart from '@/core/chart/charts/per-user-pv.js';
|
||||
import DriveChart from '@/core/chart/charts/drive.js';
|
||||
import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js';
|
||||
import HashtagChart from '@/core/chart/charts/hashtag.js';
|
||||
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
|
||||
import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js';
|
||||
import ApRequestChart from '@/core/chart/charts/ap-request.js';
|
||||
@@ -37,7 +36,6 @@ export class CleanChartsProcessorService {
|
||||
private perUserPvChart: PerUserPvChart,
|
||||
private driveChart: DriveChart,
|
||||
private perUserReactionsChart: PerUserReactionsChart,
|
||||
private hashtagChart: HashtagChart,
|
||||
private perUserFollowingChart: PerUserFollowingChart,
|
||||
private perUserDriveChart: PerUserDriveChart,
|
||||
private apRequestChart: ApRequestChart,
|
||||
@@ -61,7 +59,6 @@ export class CleanChartsProcessorService {
|
||||
this.perUserPvChart.clean(),
|
||||
this.driveChart.clean(),
|
||||
this.perUserReactionsChart.clean(),
|
||||
this.hashtagChart.clean(),
|
||||
this.perUserFollowingChart.clean(),
|
||||
this.perUserDriveChart.clean(),
|
||||
this.apRequestChart.clean(),
|
||||
|
@@ -11,13 +11,12 @@ import InstanceChart from '@/core/chart/charts/instance.js';
|
||||
import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js';
|
||||
import DriveChart from '@/core/chart/charts/drive.js';
|
||||
import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js';
|
||||
import HashtagChart from '@/core/chart/charts/hashtag.js';
|
||||
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
|
||||
import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js';
|
||||
import ApRequestChart from '@/core/chart/charts/ap-request.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class ResyncChartsProcessorService {
|
||||
@@ -35,7 +34,6 @@ export class ResyncChartsProcessorService {
|
||||
private perUserNotesChart: PerUserNotesChart,
|
||||
private driveChart: DriveChart,
|
||||
private perUserReactionsChart: PerUserReactionsChart,
|
||||
private hashtagChart: HashtagChart,
|
||||
private perUserFollowingChart: PerUserFollowingChart,
|
||||
private perUserDriveChart: PerUserDriveChart,
|
||||
private apRequestChart: ApRequestChart,
|
||||
|
@@ -12,7 +12,6 @@ import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js';
|
||||
import PerUserPvChart from '@/core/chart/charts/per-user-pv.js';
|
||||
import DriveChart from '@/core/chart/charts/drive.js';
|
||||
import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js';
|
||||
import HashtagChart from '@/core/chart/charts/hashtag.js';
|
||||
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
|
||||
import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js';
|
||||
import ApRequestChart from '@/core/chart/charts/ap-request.js';
|
||||
@@ -37,7 +36,6 @@ export class TickChartsProcessorService {
|
||||
private perUserPvChart: PerUserPvChart,
|
||||
private driveChart: DriveChart,
|
||||
private perUserReactionsChart: PerUserReactionsChart,
|
||||
private hashtagChart: HashtagChart,
|
||||
private perUserFollowingChart: PerUserFollowingChart,
|
||||
private perUserDriveChart: PerUserDriveChart,
|
||||
private apRequestChart: ApRequestChart,
|
||||
@@ -61,7 +59,6 @@ export class TickChartsProcessorService {
|
||||
this.perUserPvChart.tick(false),
|
||||
this.driveChart.tick(false),
|
||||
this.perUserReactionsChart.tick(false),
|
||||
this.hashtagChart.tick(false),
|
||||
this.perUserFollowingChart.tick(false),
|
||||
this.perUserDriveChart.tick(false),
|
||||
this.apRequestChart.tick(false),
|
||||
|
@@ -6,10 +6,10 @@ import type { Config } from '@/config.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type Bull from 'bull';
|
||||
import type { WebhookDeliverJobData } from '../types.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class WebhookDeliverProcessorService {
|
||||
@@ -33,26 +33,23 @@ export class WebhookDeliverProcessorService {
|
||||
try {
|
||||
this.logger.debug(`delivering ${job.data.webhookId}`);
|
||||
|
||||
const res = await this.httpRequestService.fetch(
|
||||
job.data.to,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'User-Agent': 'Misskey-Hooks',
|
||||
'X-Misskey-Host': this.config.host,
|
||||
'X-Misskey-Hook-Id': job.data.webhookId,
|
||||
'X-Misskey-Hook-Secret': job.data.secret,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
hookId: job.data.webhookId,
|
||||
userId: job.data.userId,
|
||||
eventId: job.data.eventId,
|
||||
createdAt: job.data.createdAt,
|
||||
type: job.data.type,
|
||||
body: job.data.content,
|
||||
}),
|
||||
}
|
||||
);
|
||||
const res = await this.httpRequestService.send(job.data.to, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'User-Agent': 'Misskey-Hooks',
|
||||
'X-Misskey-Host': this.config.host,
|
||||
'X-Misskey-Hook-Id': job.data.webhookId,
|
||||
'X-Misskey-Hook-Secret': job.data.secret,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
hookId: job.data.webhookId,
|
||||
userId: job.data.userId,
|
||||
eventId: job.data.eventId,
|
||||
createdAt: job.data.createdAt,
|
||||
type: job.data.type,
|
||||
body: job.data.content,
|
||||
}),
|
||||
});
|
||||
|
||||
this.webhooksRepository.update({ id: job.data.webhookId }, {
|
||||
latestSentAt: new Date(),
|
||||
|
@@ -5,14 +5,14 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import fastifyStatic from '@fastify/static';
|
||||
import rename from 'rename';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { DriveFilesRepository } from '@/models/index.js';
|
||||
import type { DriveFile, DriveFilesRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { DownloadService } from '@/core/DownloadService.js';
|
||||
import { ImageProcessingService } from '@/core/ImageProcessingService.js';
|
||||
import { IImageStreamable, ImageProcessingService, webpDefault } from '@/core/ImageProcessingService.js';
|
||||
import { VideoProcessingService } from '@/core/VideoProcessingService.js';
|
||||
import { InternalStorageService } from '@/core/InternalStorageService.js';
|
||||
import { contentDisposition } from '@/misc/content-disposition.js';
|
||||
@@ -20,6 +20,8 @@ import { FileInfoService } from '@/core/FileInfoService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
@@ -57,7 +59,7 @@ export class FileServerService {
|
||||
reply.header('Cache-Control', 'max-age=300');
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@bindThis
|
||||
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||
fastify.addHook('onRequest', (request, reply, done) => {
|
||||
@@ -70,23 +72,309 @@ export class FileServerService {
|
||||
serve: false,
|
||||
});
|
||||
|
||||
fastify.get('/app-default.jpg', (request, reply) => {
|
||||
fastify.get('/files/app-default.jpg', (request, reply) => {
|
||||
const file = fs.createReadStream(`${_dirname}/assets/dummy.png`);
|
||||
reply.header('Content-Type', 'image/jpeg');
|
||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||
return reply.send(file);
|
||||
});
|
||||
|
||||
fastify.get<{ Params: { key: string; } }>('/:key', async (request, reply) => await this.sendDriveFile(request, reply));
|
||||
fastify.get<{ Params: { key: string; } }>('/:key/*', async (request, reply) => await this.sendDriveFile(request, reply));
|
||||
fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => {
|
||||
return await this.sendDriveFile(request, reply)
|
||||
.catch(err => this.errorHandler(request, reply, err));
|
||||
});
|
||||
fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => {
|
||||
return await this.sendDriveFile(request, reply)
|
||||
.catch(err => this.errorHandler(request, reply, err));
|
||||
});
|
||||
|
||||
fastify.get<{
|
||||
Params: { url: string; };
|
||||
Querystring: { url?: string; };
|
||||
}>('/proxy/:url*', async (request, reply) => {
|
||||
return await this.proxyHandler(request, reply)
|
||||
.catch(err => this.errorHandler(request, reply, err));
|
||||
});
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async errorHandler(request: FastifyRequest<{ Params?: { [x: string]: any }; Querystring?: { [x: string]: any }; }>, reply: FastifyReply, err?: any) {
|
||||
this.logger.error(`${err}`);
|
||||
|
||||
reply.header('Cache-Control', 'max-age=300');
|
||||
|
||||
if (request.query && 'fallback' in request.query) {
|
||||
return reply.sendFile('/dummy.png', assets);
|
||||
}
|
||||
|
||||
if (err instanceof StatusError && (err.statusCode === 302 || err.isClientError)) {
|
||||
reply.code(err.statusCode);
|
||||
return;
|
||||
}
|
||||
|
||||
reply.code(500);
|
||||
return;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async sendDriveFile(request: FastifyRequest<{ Params: { key: string; } }>, reply: FastifyReply) {
|
||||
const key = request.params.key;
|
||||
const file = await this.getFileFromKey(key).then();
|
||||
|
||||
if (file === '404') {
|
||||
reply.code(404);
|
||||
reply.header('Cache-Control', 'max-age=86400');
|
||||
return reply.sendFile('/dummy.png', assets);
|
||||
}
|
||||
|
||||
if (file === '204') {
|
||||
reply.code(204);
|
||||
reply.header('Cache-Control', 'max-age=86400');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (file.state === 'remote') {
|
||||
const convertFile = async () => {
|
||||
if (file.fileRole === 'thumbnail') {
|
||||
if (['image/jpeg', 'image/webp', 'image/avif', 'image/png', 'image/svg+xml'].includes(file.mime)) {
|
||||
return this.imageProcessingService.convertToWebpStream(
|
||||
file.path,
|
||||
498,
|
||||
280
|
||||
);
|
||||
} else if (file.mime.startsWith('video/')) {
|
||||
return await this.videoProcessingService.generateVideoThumbnail(file.path);
|
||||
}
|
||||
}
|
||||
|
||||
if (file.fileRole === 'webpublic') {
|
||||
if (['image/svg+xml'].includes(file.mime)) {
|
||||
return this.imageProcessingService.convertToWebpStream(
|
||||
file.path,
|
||||
2048,
|
||||
2048,
|
||||
{ ...webpDefault, lossless: true }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: fs.createReadStream(file.path),
|
||||
ext: file.ext,
|
||||
type: file.mime,
|
||||
};
|
||||
};
|
||||
|
||||
const image = await convertFile();
|
||||
|
||||
if ('pipe' in image.data && typeof image.data.pipe === 'function') {
|
||||
// image.dataがstreamなら、stream終了後にcleanup
|
||||
image.data.on('end', file.cleanup);
|
||||
image.data.on('close', file.cleanup);
|
||||
} else {
|
||||
// image.dataがstreamでないなら直ちにcleanup
|
||||
file.cleanup();
|
||||
}
|
||||
|
||||
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
|
||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||
return image.data;
|
||||
}
|
||||
|
||||
if (file.fileRole !== 'original') {
|
||||
const filename = rename(file.file.name, {
|
||||
suffix: file.fileRole === 'thumbnail' ? '-thumb' : '-web',
|
||||
extname: file.ext ? `.${file.ext}` : undefined,
|
||||
}).toString();
|
||||
|
||||
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream');
|
||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||
reply.header('Content-Disposition', contentDisposition('inline', filename));
|
||||
return fs.createReadStream(file.path);
|
||||
} else {
|
||||
const stream = fs.createReadStream(file.path);
|
||||
stream.on('error', this.commonReadableHandlerGenerator(reply));
|
||||
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream');
|
||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||
reply.header('Content-Disposition', contentDisposition('inline', file.file.name));
|
||||
return stream;
|
||||
}
|
||||
} catch (e) {
|
||||
if ('cleanup' in file) file.cleanup();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async proxyHandler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) {
|
||||
const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
|
||||
|
||||
if (typeof url !== 'string') {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create temp file
|
||||
const file = await this.getStreamAndTypeFromUrl(url);
|
||||
if (file === '404') {
|
||||
reply.code(404);
|
||||
reply.header('Cache-Control', 'max-age=86400');
|
||||
return reply.sendFile('/dummy.png', assets);
|
||||
}
|
||||
|
||||
if (file === '204') {
|
||||
reply.code(204);
|
||||
reply.header('Cache-Control', 'max-age=86400');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const isConvertibleImage = isMimeImage(file.mime, 'sharp-convertible-image');
|
||||
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image');
|
||||
|
||||
let image: IImageStreamable | null = null;
|
||||
if ('emoji' in request.query && isConvertibleImage) {
|
||||
if (!isAnimationConvertibleImage && !('static' in request.query)) {
|
||||
image = {
|
||||
data: fs.createReadStream(file.path),
|
||||
ext: file.ext,
|
||||
type: file.mime,
|
||||
};
|
||||
} else {
|
||||
const data = sharp(file.path, { animated: !('static' in request.query) })
|
||||
.resize({
|
||||
height: 128,
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.webp(webpDefault);
|
||||
|
||||
image = {
|
||||
data,
|
||||
ext: 'webp',
|
||||
type: 'image/webp',
|
||||
};
|
||||
}
|
||||
} else if ('static' in request.query && isConvertibleImage) {
|
||||
image = this.imageProcessingService.convertToWebpStream(file.path, 498, 280);
|
||||
} else if ('preview' in request.query && isConvertibleImage) {
|
||||
image = this.imageProcessingService.convertToWebpStream(file.path, 200, 200);
|
||||
} else if ('badge' in request.query) {
|
||||
if (!isConvertibleImage) {
|
||||
// 画像でないなら404でお茶を濁す
|
||||
throw new StatusError('Unexpected mime', 404);
|
||||
}
|
||||
|
||||
const mask = sharp(file.path)
|
||||
.resize(96, 96, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: false,
|
||||
})
|
||||
.greyscale()
|
||||
.normalise()
|
||||
.linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast
|
||||
.flatten({ background: '#000' })
|
||||
.toColorspace('b-w');
|
||||
|
||||
const stats = await mask.clone().stats();
|
||||
|
||||
if (stats.entropy < 0.1) {
|
||||
// エントロピーがあまりない場合は404にする
|
||||
throw new StatusError('Skip to provide badge', 404);
|
||||
}
|
||||
|
||||
const data = sharp({
|
||||
create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
|
||||
})
|
||||
.pipelineColorspace('b-w')
|
||||
.boolean(await mask.png().toBuffer(), 'eor');
|
||||
|
||||
image = {
|
||||
data: await data.png().toBuffer(),
|
||||
ext: 'png',
|
||||
type: 'image/png',
|
||||
};
|
||||
} else if (file.mime === 'image/svg+xml') {
|
||||
image = this.imageProcessingService.convertToWebpStream(file.path, 2048, 2048);
|
||||
} else if (!file.mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(file.mime)) {
|
||||
throw new StatusError('Rejected type', 403, 'Rejected type');
|
||||
}
|
||||
|
||||
if (!image) {
|
||||
image = {
|
||||
data: fs.createReadStream(file.path),
|
||||
ext: file.ext,
|
||||
type: file.mime,
|
||||
};
|
||||
}
|
||||
|
||||
if ('cleanup' in file) {
|
||||
if ('pipe' in image.data && typeof image.data.pipe === 'function') {
|
||||
// image.dataがstreamなら、stream終了後にcleanup
|
||||
image.data.on('end', file.cleanup);
|
||||
image.data.on('close', file.cleanup);
|
||||
} else {
|
||||
// image.dataがstreamでないなら直ちにcleanup
|
||||
file.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
reply.header('Content-Type', image.type);
|
||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||
return image.data;
|
||||
} catch (e) {
|
||||
if ('cleanup' in file) file.cleanup();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async getStreamAndTypeFromUrl(url: string): Promise<
|
||||
{ state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: DriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; }
|
||||
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; }
|
||||
| '404'
|
||||
| '204'
|
||||
> {
|
||||
if (url.startsWith(`${this.config.url}/files/`)) {
|
||||
const key = url.replace(`${this.config.url}/files/`, '').split('/').shift();
|
||||
if (!key) throw new StatusError('Invalid File Key', 400, 'Invalid File Key');
|
||||
|
||||
return await this.getFileFromKey(key);
|
||||
}
|
||||
|
||||
return await this.downloadAndDetectTypeFromUrl(url);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async downloadAndDetectTypeFromUrl(url: string): Promise<
|
||||
{ state: 'remote' ; mime: string; ext: string | null; path: string; cleanup: () => void; }
|
||||
> {
|
||||
const [path, cleanup] = await createTemp();
|
||||
try {
|
||||
await this.downloadService.downloadUrl(url, path);
|
||||
|
||||
const { mime, ext } = await this.fileInfoService.detectType(path);
|
||||
|
||||
return {
|
||||
state: 'remote',
|
||||
mime, ext,
|
||||
path, cleanup,
|
||||
}
|
||||
} catch (e) {
|
||||
cleanup();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async getFileFromKey(key: string): Promise<
|
||||
{ state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; }
|
||||
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; }
|
||||
| '404'
|
||||
| '204'
|
||||
> {
|
||||
// Fetch drive file
|
||||
const file = await this.driveFilesRepository.createQueryBuilder('file')
|
||||
.where('file.accessKey = :accessKey', { accessKey: key })
|
||||
@@ -94,89 +382,41 @@ export class FileServerService {
|
||||
.orWhere('file.webpublicAccessKey = :webpublicAccessKey', { webpublicAccessKey: key })
|
||||
.getOne();
|
||||
|
||||
if (file == null) {
|
||||
reply.code(404);
|
||||
reply.header('Cache-Control', 'max-age=86400');
|
||||
return reply.sendFile('/dummy.png', assets);
|
||||
}
|
||||
if (file == null) return '404';
|
||||
|
||||
const isThumbnail = file.thumbnailAccessKey === key;
|
||||
const isWebpublic = file.webpublicAccessKey === key;
|
||||
|
||||
if (!file.storedInternal) {
|
||||
if (file.isLink && file.uri) { // 期限切れリモートファイル
|
||||
const [path, cleanup] = await createTemp();
|
||||
|
||||
try {
|
||||
await this.downloadService.downloadUrl(file.uri, path);
|
||||
|
||||
const { mime, ext } = await this.fileInfoService.detectType(path);
|
||||
|
||||
const convertFile = async () => {
|
||||
if (isThumbnail) {
|
||||
if (['image/jpeg', 'image/webp', 'image/avif', 'image/png', 'image/svg+xml'].includes(mime)) {
|
||||
return await this.imageProcessingService.convertToWebp(path, 498, 280);
|
||||
} else if (mime.startsWith('video/')) {
|
||||
return await this.videoProcessingService.generateVideoThumbnail(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (isWebpublic) {
|
||||
if (['image/svg+xml'].includes(mime)) {
|
||||
return await this.imageProcessingService.convertToPng(path, 2048, 2048);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: fs.readFileSync(path),
|
||||
ext,
|
||||
type: mime,
|
||||
};
|
||||
};
|
||||
|
||||
const image = await convertFile();
|
||||
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
|
||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||
return image.data;
|
||||
} catch (err) {
|
||||
this.logger.error(`${err}`);
|
||||
|
||||
if (err instanceof StatusError && err.isClientError) {
|
||||
reply.code(err.statusCode);
|
||||
reply.header('Cache-Control', 'max-age=86400');
|
||||
} else {
|
||||
reply.code(500);
|
||||
reply.header('Cache-Control', 'max-age=300');
|
||||
}
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
return;
|
||||
if (!(file.isLink && file.uri)) return '204';
|
||||
const result = await this.downloadAndDetectTypeFromUrl(file.uri);
|
||||
return {
|
||||
...result,
|
||||
fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
|
||||
file,
|
||||
}
|
||||
|
||||
reply.code(204);
|
||||
reply.header('Cache-Control', 'max-age=86400');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isThumbnail || isWebpublic) {
|
||||
const { mime, ext } = await this.fileInfoService.detectType(this.internalStorageService.resolvePath(key));
|
||||
const filename = rename(file.name, {
|
||||
suffix: isThumbnail ? '-thumb' : '-web',
|
||||
extname: ext ? `.${ext}` : undefined,
|
||||
}).toString();
|
||||
const path = this.internalStorageService.resolvePath(key);
|
||||
|
||||
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(mime) ? mime : 'application/octet-stream');
|
||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||
reply.header('Content-Disposition', contentDisposition('inline', filename));
|
||||
return this.internalStorageService.read(key);
|
||||
} else {
|
||||
const readable = this.internalStorageService.read(file.accessKey!);
|
||||
readable.on('error', this.commonReadableHandlerGenerator(reply));
|
||||
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.type) ? file.type : 'application/octet-stream');
|
||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||
reply.header('Content-Disposition', contentDisposition('inline', file.name));
|
||||
return readable;
|
||||
if (isThumbnail || isWebpublic) {
|
||||
const { mime, ext } = await this.fileInfoService.detectType(path);
|
||||
return {
|
||||
state: 'stored_internal',
|
||||
fileRole: isThumbnail ? 'thumbnail' : 'webpublic',
|
||||
file,
|
||||
mime, ext,
|
||||
path,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
state: 'stored_internal',
|
||||
fileRole: 'original',
|
||||
file,
|
||||
mime: file.type,
|
||||
ext: null,
|
||||
path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,177 +0,0 @@
|
||||
import * as fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import sharp from 'sharp';
|
||||
import fastifyStatic from '@fastify/static';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import { DownloadService } from '@/core/DownloadService.js';
|
||||
import { ImageProcessingService, webpDefault } from '@/core/ImageProcessingService.js';
|
||||
import type { IImage } from '@/core/ImageProcessingService.js';
|
||||
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { FileInfoService } from '@/core/FileInfoService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { FastifyInstance, FastifyPluginOptions, FastifyReply, FastifyRequest } from 'fastify';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
|
||||
const assets = `${_dirname}/../../src/server/assets/`;
|
||||
|
||||
@Injectable()
|
||||
export class MediaProxyServerService {
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
private fileInfoService: FileInfoService,
|
||||
private downloadService: DownloadService,
|
||||
private imageProcessingService: ImageProcessingService,
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('server', 'gray', false);
|
||||
|
||||
//this.createServer = this.createServer.bind(this);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||
fastify.addHook('onRequest', (request, reply, done) => {
|
||||
reply.header('Content-Security-Policy', 'default-src \'none\'; img-src \'self\'; media-src \'self\'; style-src \'unsafe-inline\'');
|
||||
done();
|
||||
});
|
||||
|
||||
fastify.register(fastifyStatic, {
|
||||
root: _dirname,
|
||||
serve: false,
|
||||
});
|
||||
|
||||
fastify.get<{
|
||||
Params: { url: string; };
|
||||
Querystring: { url?: string; };
|
||||
}>('/:url*', async (request, reply) => await this.handler(request, reply));
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async handler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) {
|
||||
const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
|
||||
|
||||
if (typeof url !== 'string') {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create temp file
|
||||
const [path, cleanup] = await createTemp();
|
||||
|
||||
try {
|
||||
await this.downloadService.downloadUrl(url, path);
|
||||
|
||||
const { mime, ext } = await this.fileInfoService.detectType(path);
|
||||
const isConvertibleImage = isMimeImage(mime, 'sharp-convertible-image');
|
||||
const isAnimationConvertibleImage = isMimeImage(mime, 'sharp-animation-convertible-image');
|
||||
|
||||
let image: IImage;
|
||||
if ('emoji' in request.query && isConvertibleImage) {
|
||||
if (!isAnimationConvertibleImage && !('static' in request.query)) {
|
||||
image = {
|
||||
data: fs.readFileSync(path),
|
||||
ext,
|
||||
type: mime,
|
||||
};
|
||||
} else {
|
||||
const data = await sharp(path, { animated: !('static' in request.query) })
|
||||
.resize({
|
||||
height: 128,
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.webp(webpDefault)
|
||||
.toBuffer();
|
||||
|
||||
image = {
|
||||
data,
|
||||
ext: 'webp',
|
||||
type: 'image/webp',
|
||||
};
|
||||
}
|
||||
} else if ('static' in request.query && isConvertibleImage) {
|
||||
image = await this.imageProcessingService.convertToWebp(path, 498, 280);
|
||||
} else if ('preview' in request.query && isConvertibleImage) {
|
||||
image = await this.imageProcessingService.convertToWebp(path, 200, 200);
|
||||
} else if ('badge' in request.query) {
|
||||
if (!isConvertibleImage) {
|
||||
// 画像でないなら404でお茶を濁す
|
||||
throw new StatusError('Unexpected mime', 404);
|
||||
}
|
||||
|
||||
const mask = sharp(path)
|
||||
.resize(96, 96, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: false,
|
||||
})
|
||||
.greyscale()
|
||||
.normalise()
|
||||
.linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast
|
||||
.flatten({ background: '#000' })
|
||||
.toColorspace('b-w');
|
||||
|
||||
const stats = await mask.clone().stats();
|
||||
|
||||
if (stats.entropy < 0.1) {
|
||||
// エントロピーがあまりない場合は404にする
|
||||
throw new StatusError('Skip to provide badge', 404);
|
||||
}
|
||||
|
||||
const data = sharp({
|
||||
create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
|
||||
})
|
||||
.pipelineColorspace('b-w')
|
||||
.boolean(await mask.png().toBuffer(), 'eor');
|
||||
|
||||
image = {
|
||||
data: await data.png().toBuffer(),
|
||||
ext: 'png',
|
||||
type: 'image/png',
|
||||
};
|
||||
} else if (mime === 'image/svg+xml') {
|
||||
image = await this.imageProcessingService.convertToWebp(path, 2048, 2048, webpDefault);
|
||||
} else if (!mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(mime)) {
|
||||
throw new StatusError('Rejected type', 403, 'Rejected type');
|
||||
} else {
|
||||
image = {
|
||||
data: fs.readFileSync(path),
|
||||
ext,
|
||||
type: mime,
|
||||
};
|
||||
}
|
||||
|
||||
reply.header('Content-Type', image.type);
|
||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||
return image.data;
|
||||
} catch (err) {
|
||||
this.logger.error(`${err}`);
|
||||
|
||||
if ('fallback' in request.query) {
|
||||
return reply.sendFile('/dummy.png', assets);
|
||||
}
|
||||
|
||||
if (err instanceof StatusError && (err.statusCode === 302 || err.isClientError)) {
|
||||
reply.code(err.statusCode);
|
||||
} else {
|
||||
reply.code(500);
|
||||
}
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
}
|
@@ -111,9 +111,6 @@ export class NodeinfoServerService {
|
||||
enableHcaptcha: meta.enableHcaptcha,
|
||||
enableRecaptcha: meta.enableRecaptcha,
|
||||
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
|
||||
enableTwitterIntegration: meta.enableTwitterIntegration,
|
||||
enableGithubIntegration: meta.enableGithubIntegration,
|
||||
enableDiscordIntegration: meta.enableDiscordIntegration,
|
||||
enableEmail: meta.enableEmail,
|
||||
enableServiceWorker: meta.enableServiceWorker,
|
||||
proxyAccountName: proxyAccount ? proxyAccount.username : null,
|
||||
|
@@ -3,14 +3,10 @@ import { EndpointsModule } from '@/server/api/EndpointsModule.js';
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import { ApiCallService } from './api/ApiCallService.js';
|
||||
import { FileServerService } from './FileServerService.js';
|
||||
import { MediaProxyServerService } from './MediaProxyServerService.js';
|
||||
import { NodeinfoServerService } from './NodeinfoServerService.js';
|
||||
import { ServerService } from './ServerService.js';
|
||||
import { WellKnownServerService } from './WellKnownServerService.js';
|
||||
import { GetterService } from './api/GetterService.js';
|
||||
import { DiscordServerService } from './api/integration/DiscordServerService.js';
|
||||
import { GithubServerService } from './api/integration/GithubServerService.js';
|
||||
import { TwitterServerService } from './api/integration/TwitterServerService.js';
|
||||
import { ChannelsService } from './api/stream/ChannelsService.js';
|
||||
import { ActivityPubServerService } from './ActivityPubServerService.js';
|
||||
import { ApiLoggerService } from './api/ApiLoggerService.js';
|
||||
@@ -51,14 +47,10 @@ import { UserListChannelService } from './api/stream/channels/user-list.js';
|
||||
UrlPreviewService,
|
||||
ActivityPubServerService,
|
||||
FileServerService,
|
||||
MediaProxyServerService,
|
||||
NodeinfoServerService,
|
||||
ServerService,
|
||||
WellKnownServerService,
|
||||
GetterService,
|
||||
DiscordServerService,
|
||||
GithubServerService,
|
||||
TwitterServerService,
|
||||
ChannelsService,
|
||||
ApiCallService,
|
||||
ApiLoggerService,
|
||||
|
@@ -20,7 +20,6 @@ import { NodeinfoServerService } from './NodeinfoServerService.js';
|
||||
import { ApiServerService } from './api/ApiServerService.js';
|
||||
import { StreamingApiServerService } from './api/StreamingApiServerService.js';
|
||||
import { WellKnownServerService } from './WellKnownServerService.js';
|
||||
import { MediaProxyServerService } from './MediaProxyServerService.js';
|
||||
import { FileServerService } from './FileServerService.js';
|
||||
import { ClientServerService } from './web/ClientServerService.js';
|
||||
|
||||
@@ -48,7 +47,6 @@ export class ServerService {
|
||||
private wellKnownServerService: WellKnownServerService,
|
||||
private nodeinfoServerService: NodeinfoServerService,
|
||||
private fileServerService: FileServerService,
|
||||
private mediaProxyServerService: MediaProxyServerService,
|
||||
private clientServerService: ClientServerService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private loggerService: LoggerService,
|
||||
@@ -73,8 +71,7 @@ export class ServerService {
|
||||
}
|
||||
|
||||
fastify.register(this.apiServerService.createServer, { prefix: '/api' });
|
||||
fastify.register(this.fileServerService.createServer, { prefix: '/files' });
|
||||
fastify.register(this.mediaProxyServerService.createServer, { prefix: '/proxy' });
|
||||
fastify.register(this.fileServerService.createServer);
|
||||
fastify.register(this.activityPubServerService.createServer);
|
||||
fastify.register(this.nodeinfoServerService.createServer);
|
||||
fastify.register(this.wellKnownServerService.createServer);
|
||||
|
@@ -12,9 +12,6 @@ import endpoints, { IEndpoint } from './endpoints.js';
|
||||
import { ApiCallService } from './ApiCallService.js';
|
||||
import { SignupApiService } from './SignupApiService.js';
|
||||
import { SigninApiService } from './SigninApiService.js';
|
||||
import { GithubServerService } from './integration/GithubServerService.js';
|
||||
import { DiscordServerService } from './integration/DiscordServerService.js';
|
||||
import { TwitterServerService } from './integration/TwitterServerService.js';
|
||||
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
|
||||
|
||||
@Injectable()
|
||||
@@ -38,9 +35,6 @@ export class ApiServerService {
|
||||
private apiCallService: ApiCallService,
|
||||
private signupApiService: SignupApiService,
|
||||
private signinApiService: SigninApiService,
|
||||
private githubServerService: GithubServerService,
|
||||
private discordServerService: DiscordServerService,
|
||||
private twitterServerService: TwitterServerService,
|
||||
) {
|
||||
//this.createServer = this.createServer.bind(this);
|
||||
}
|
||||
@@ -133,10 +127,6 @@ export class ApiServerService {
|
||||
|
||||
fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiService.signupPending(request, reply));
|
||||
|
||||
fastify.register(this.discordServerService.create);
|
||||
fastify.register(this.githubServerService.create);
|
||||
fastify.register(this.twitterServerService.create);
|
||||
|
||||
fastify.get('/v1/instance/peers', async (request, reply) => {
|
||||
const instances = await this.instancesRepository.find({
|
||||
select: ['host'],
|
||||
|
@@ -97,7 +97,6 @@ import * as ep___charts_activeUsers from './endpoints/charts/active-users.js';
|
||||
import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
|
||||
import * as ep___charts_drive from './endpoints/charts/drive.js';
|
||||
import * as ep___charts_federation from './endpoints/charts/federation.js';
|
||||
import * as ep___charts_hashtag from './endpoints/charts/hashtag.js';
|
||||
import * as ep___charts_instance from './endpoints/charts/instance.js';
|
||||
import * as ep___charts_notes from './endpoints/charts/notes.js';
|
||||
import * as ep___charts_user_drive from './endpoints/charts/user/drive.js';
|
||||
@@ -433,7 +432,6 @@ const $charts_activeUsers: Provider = { provide: 'ep:charts/active-users', useCl
|
||||
const $charts_apRequest: Provider = { provide: 'ep:charts/ap-request', useClass: ep___charts_apRequest.default };
|
||||
const $charts_drive: Provider = { provide: 'ep:charts/drive', useClass: ep___charts_drive.default };
|
||||
const $charts_federation: Provider = { provide: 'ep:charts/federation', useClass: ep___charts_federation.default };
|
||||
const $charts_hashtag: Provider = { provide: 'ep:charts/hashtag', useClass: ep___charts_hashtag.default };
|
||||
const $charts_instance: Provider = { provide: 'ep:charts/instance', useClass: ep___charts_instance.default };
|
||||
const $charts_notes: Provider = { provide: 'ep:charts/notes', useClass: ep___charts_notes.default };
|
||||
const $charts_user_drive: Provider = { provide: 'ep:charts/user/drive', useClass: ep___charts_user_drive.default };
|
||||
@@ -773,7 +771,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$charts_apRequest,
|
||||
$charts_drive,
|
||||
$charts_federation,
|
||||
$charts_hashtag,
|
||||
$charts_instance,
|
||||
$charts_notes,
|
||||
$charts_user_drive,
|
||||
@@ -1107,7 +1104,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$charts_apRequest,
|
||||
$charts_drive,
|
||||
$charts_federation,
|
||||
$charts_hashtag,
|
||||
$charts_instance,
|
||||
$charts_notes,
|
||||
$charts_user_drive,
|
||||
|
@@ -96,7 +96,6 @@ import * as ep___charts_activeUsers from './endpoints/charts/active-users.js';
|
||||
import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
|
||||
import * as ep___charts_drive from './endpoints/charts/drive.js';
|
||||
import * as ep___charts_federation from './endpoints/charts/federation.js';
|
||||
import * as ep___charts_hashtag from './endpoints/charts/hashtag.js';
|
||||
import * as ep___charts_instance from './endpoints/charts/instance.js';
|
||||
import * as ep___charts_notes from './endpoints/charts/notes.js';
|
||||
import * as ep___charts_user_drive from './endpoints/charts/user/drive.js';
|
||||
@@ -430,7 +429,6 @@ const eps = [
|
||||
['charts/ap-request', ep___charts_apRequest],
|
||||
['charts/drive', ep___charts_drive],
|
||||
['charts/federation', ep___charts_federation],
|
||||
['charts/hashtag', ep___charts_hashtag],
|
||||
['charts/instance', ep___charts_instance],
|
||||
['charts/notes', ep___charts_notes],
|
||||
['charts/user/drive', ep___charts_user_drive],
|
||||
|
@@ -3,6 +3,8 @@ import { DataSource, In } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { EmojisRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@@ -35,6 +37,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const emojis = await this.emojisRepository.findBy({
|
||||
@@ -49,6 +54,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}
|
||||
|
||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||
emojis: await this.emojiEntityService.packMany(ps.ids),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -2,12 +2,10 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import rndstr from 'rndstr';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { DriveFilesRepository, EmojisRepository } from '@/models/index.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { DriveFilesRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -39,43 +37,26 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
private customEmojiService: CustomEmojiService,
|
||||
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
private idService: IdService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||
const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||
|
||||
if (file == null) throw new ApiError(meta.errors.noSuchFile);
|
||||
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
|
||||
|
||||
const name = file.name.split('.')[0].match(/^[a-z0-9_]+$/) ? file.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`;
|
||||
const name = driveFile.name.split('.')[0].match(/^[a-z0-9_]+$/) ? driveFile.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`;
|
||||
|
||||
const emoji = await this.emojisRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
updatedAt: new Date(),
|
||||
name: name,
|
||||
const emoji = await this.customEmojiService.add({
|
||||
driveFile,
|
||||
name,
|
||||
category: null,
|
||||
host: null,
|
||||
aliases: [],
|
||||
originalUrl: file.url,
|
||||
publicUrl: file.webpublicUrl ?? file.url,
|
||||
type: file.webpublicType ?? file.type,
|
||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
||||
emoji: await this.emojiEntityService.pack(emoji.id),
|
||||
host: null,
|
||||
});
|
||||
|
||||
this.moderationLogService.insertModerationLog(me, 'addEmoji', {
|
||||
|
@@ -4,6 +4,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { EmojisRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@@ -35,6 +37,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
private moderationLogService: ModerationLogService,
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const emojis = await this.emojisRepository.findBy({
|
||||
@@ -43,13 +47,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
for (const emoji of emojis) {
|
||||
await this.emojisRepository.delete(emoji.id);
|
||||
|
||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
this.moderationLogService.insertModerationLog(me, 'deleteEmoji', {
|
||||
emoji: emoji,
|
||||
});
|
||||
}
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
||||
emojis: await this.emojiEntityService.packMany(emojis),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,8 @@ import type { EmojisRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@@ -42,6 +44,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
private moderationLogService: ModerationLogService,
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const emoji = await this.emojisRepository.findOneBy({ id: ps.id });
|
||||
@@ -52,6 +56,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
||||
emojis: [ await this.emojiEntityService.pack(emoji) ],
|
||||
});
|
||||
|
||||
this.moderationLogService.insertModerationLog(me, 'deleteEmoji', {
|
||||
emoji: emoji,
|
||||
});
|
||||
|
@@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.take(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return this.emojiEntityService.packMany(emojis);
|
||||
return this.emojiEntityService.packMany(emojis, { omitHost: false, omitId: false, withUrl: false });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -98,7 +98,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
emojis = await q.take(ps.limit).getMany();
|
||||
}
|
||||
|
||||
return this.emojiEntityService.packMany(emojis);
|
||||
return this.emojiEntityService.packMany(emojis, { omitHost: false, omitId: false, withUrl: false });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,8 @@ import { DataSource, In } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { EmojisRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@@ -35,6 +37,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const emojis = await this.emojisRepository.findBy({
|
||||
@@ -49,6 +54,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}
|
||||
|
||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||
emojis: await this.emojiEntityService.packMany(ps.ids),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,8 @@ import { DataSource, In } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { EmojisRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@@ -35,6 +37,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.emojisRepository.update({
|
||||
@@ -45,6 +50,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
});
|
||||
|
||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||
emojis: await this.emojiEntityService.packMany(ps.ids),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,8 @@ import { DataSource, In } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { EmojisRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@@ -37,6 +39,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.emojisRepository.update({
|
||||
@@ -47,6 +52,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
});
|
||||
|
||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||
emojis: await this.emojiEntityService.packMany(ps.ids),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { EmojisRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@@ -48,6 +50,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const emoji = await this.emojisRepository.findOneBy({ id: ps.id });
|
||||
@@ -62,6 +67,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
});
|
||||
|
||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
const updated = await this.emojiEntityService.pack(emoji.id);
|
||||
|
||||
if (emoji.name === ps.name) {
|
||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||
emojis: [ updated ],
|
||||
});
|
||||
} else {
|
||||
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
||||
emojis: [ await this.emojiEntityService.pack(emoji) ],
|
||||
});
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
||||
emoji: updated,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -138,18 +138,6 @@ export const meta = {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableTwitterIntegration: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableGithubIntegration: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableDiscordIntegration: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableServiceWorker: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
@@ -223,30 +211,6 @@ export const meta = {
|
||||
optional: true, nullable: true,
|
||||
format: 'id',
|
||||
},
|
||||
twitterConsumerKey: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
twitterConsumerSecret: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
githubClientId: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
githubClientSecret: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
discordClientId: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
discordClientSecret: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
summaryProxy: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
@@ -389,9 +353,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
defaultLightTheme: instance.defaultLightTheme,
|
||||
defaultDarkTheme: instance.defaultDarkTheme,
|
||||
enableEmail: instance.enableEmail,
|
||||
enableTwitterIntegration: instance.enableTwitterIntegration,
|
||||
enableGithubIntegration: instance.enableGithubIntegration,
|
||||
enableDiscordIntegration: instance.enableDiscordIntegration,
|
||||
enableServiceWorker: instance.enableServiceWorker,
|
||||
translatorAvailable: instance.deeplAuthKey != null,
|
||||
pinnedPages: instance.pinnedPages,
|
||||
@@ -409,12 +370,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically,
|
||||
enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos,
|
||||
proxyAccountId: instance.proxyAccountId,
|
||||
twitterConsumerKey: instance.twitterConsumerKey,
|
||||
twitterConsumerSecret: instance.twitterConsumerSecret,
|
||||
githubClientId: instance.githubClientId,
|
||||
githubClientSecret: instance.githubClientSecret,
|
||||
discordClientId: instance.discordClientId,
|
||||
discordClientSecret: instance.discordClientSecret,
|
||||
summalyProxy: instance.summalyProxy,
|
||||
email: instance.email,
|
||||
smtpSecure: instance.smtpSecure,
|
||||
|
@@ -65,11 +65,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
};
|
||||
}
|
||||
|
||||
const maskedKeys = ['accessToken', 'accessTokenSecret', 'refreshToken'];
|
||||
Object.keys(profile.integrations).forEach(integration => {
|
||||
maskedKeys.forEach(key => profile.integrations[integration][key] = '<MASKED>');
|
||||
});
|
||||
|
||||
const signins = await this.signinsRepository.findBy({ userId: user.id });
|
||||
|
||||
const roles = await this.roleService.getUserRoles(user.id);
|
||||
@@ -84,7 +79,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
carefulBot: profile.carefulBot,
|
||||
injectFeaturedNote: profile.injectFeaturedNote,
|
||||
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
|
||||
integrations: profile.integrations,
|
||||
mutedWords: profile.mutedWords,
|
||||
mutedInstances: profile.mutedInstances,
|
||||
mutingNotificationTypes: profile.mutingNotificationTypes,
|
||||
|
@@ -68,15 +68,6 @@ export const paramDef = {
|
||||
summalyProxy: { type: 'string', nullable: true },
|
||||
deeplAuthKey: { type: 'string', nullable: true },
|
||||
deeplIsPro: { type: 'boolean' },
|
||||
enableTwitterIntegration: { type: 'boolean' },
|
||||
twitterConsumerKey: { type: 'string', nullable: true },
|
||||
twitterConsumerSecret: { type: 'string', nullable: true },
|
||||
enableGithubIntegration: { type: 'boolean' },
|
||||
githubClientId: { type: 'string', nullable: true },
|
||||
githubClientSecret: { type: 'string', nullable: true },
|
||||
enableDiscordIntegration: { type: 'boolean' },
|
||||
discordClientId: { type: 'string', nullable: true },
|
||||
discordClientSecret: { type: 'string', nullable: true },
|
||||
enableEmail: { type: 'boolean' },
|
||||
email: { type: 'string', nullable: true },
|
||||
smtpSecure: { type: 'boolean' },
|
||||
@@ -270,42 +261,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
set.summalyProxy = ps.summalyProxy;
|
||||
}
|
||||
|
||||
if (ps.enableTwitterIntegration !== undefined) {
|
||||
set.enableTwitterIntegration = ps.enableTwitterIntegration;
|
||||
}
|
||||
|
||||
if (ps.twitterConsumerKey !== undefined) {
|
||||
set.twitterConsumerKey = ps.twitterConsumerKey;
|
||||
}
|
||||
|
||||
if (ps.twitterConsumerSecret !== undefined) {
|
||||
set.twitterConsumerSecret = ps.twitterConsumerSecret;
|
||||
}
|
||||
|
||||
if (ps.enableGithubIntegration !== undefined) {
|
||||
set.enableGithubIntegration = ps.enableGithubIntegration;
|
||||
}
|
||||
|
||||
if (ps.githubClientId !== undefined) {
|
||||
set.githubClientId = ps.githubClientId;
|
||||
}
|
||||
|
||||
if (ps.githubClientSecret !== undefined) {
|
||||
set.githubClientSecret = ps.githubClientSecret;
|
||||
}
|
||||
|
||||
if (ps.enableDiscordIntegration !== undefined) {
|
||||
set.enableDiscordIntegration = ps.enableDiscordIntegration;
|
||||
}
|
||||
|
||||
if (ps.discordClientId !== undefined) {
|
||||
set.discordClientId = ps.discordClientId;
|
||||
}
|
||||
|
||||
if (ps.discordClientSecret !== undefined) {
|
||||
set.discordClientSecret = ps.discordClientSecret;
|
||||
}
|
||||
|
||||
if (ps.enableEmail !== undefined) {
|
||||
set.enableEmail = ps.enableEmail;
|
||||
}
|
||||
|
@@ -1,37 +0,0 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { getJsonSchema } from '@/core/chart/core.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import HashtagChart from '@/core/chart/charts/hashtag.js';
|
||||
import { schema } from '@/core/chart/charts/entities/hashtag.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['charts', 'hashtags'],
|
||||
|
||||
res: getJsonSchema(schema),
|
||||
|
||||
allowGet: true,
|
||||
cacheSec: 60 * 60,
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
span: { type: 'string', enum: ['day', 'hour'] },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 500, default: 30 },
|
||||
offset: { type: 'integer', nullable: true, default: null },
|
||||
tag: { type: 'string' },
|
||||
},
|
||||
required: ['span', 'tag'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
private hashtagChart: HashtagChart,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
return await this.hashtagChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.tag);
|
||||
});
|
||||
}
|
||||
}
|
@@ -10,6 +10,8 @@ export const meta = {
|
||||
tags: ['meta'],
|
||||
|
||||
requireCredential: false,
|
||||
allowGet: true,
|
||||
cacheSec: 3600,
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
@@ -83,6 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
emojis: await this.emojiEntityService.packMany(emojis, {
|
||||
omitId: true,
|
||||
omitHost: true,
|
||||
withUrl: true,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user