Compare commits
170 Commits
13.1.0-bet
...
13.2.6
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 | ||
![]() |
69bb377cb1 | ||
![]() |
80bfa02831 | ||
![]() |
8631740ca4 | ||
![]() |
4b75c68753 | ||
![]() |
3bf775c9a8 | ||
![]() |
8dc0e0abbb | ||
![]() |
2b377a3dc5 | ||
![]() |
9d367882fb | ||
![]() |
890564e1da | ||
![]() |
002f98987d | ||
![]() |
43956f3ffb | ||
![]() |
f2a9194c79 | ||
![]() |
4cd70df7f4 | ||
![]() |
21e4c3dfe9 | ||
![]() |
d56fc41865 | ||
![]() |
fcabc99303 | ||
![]() |
fccd9c32e8 | ||
![]() |
58a3a0b7d4 | ||
![]() |
a2a1636c10 | ||
![]() |
46ec0303b7 | ||
![]() |
3b1669fb6b | ||
![]() |
09591fa4ae | ||
![]() |
85ce00adc0 | ||
![]() |
f25518af91 | ||
![]() |
b796aacf7f | ||
![]() |
ff24811676 | ||
![]() |
4c8a1867f0 | ||
![]() |
bce48dfee9 | ||
![]() |
c20311b8a7 | ||
![]() |
fb14ac50b8 | ||
![]() |
84d984bd31 | ||
![]() |
1bc856c451 |
2
.github/workflows/docker-develop.yml
vendored
2
.github/workflows/docker-develop.yml
vendored
@@ -31,3 +31,5 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: misskey/misskey:develop
|
tags: misskey/misskey:develop
|
||||||
labels: 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
|
# https://github.com/cypress-io/cypress/issues/4351#issuecomment-559489091
|
||||||
- name: ALSA Env
|
- 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
|
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
|
- name: Cypress run
|
||||||
uses: cypress-io/github-action@v4
|
uses: cypress-io/github-action@v5
|
||||||
with:
|
with:
|
||||||
install: false
|
install: false
|
||||||
start: pnpm start:test
|
start: pnpm start:test
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -32,6 +32,7 @@ coverage
|
|||||||
!/.config/example.yml
|
!/.config/example.yml
|
||||||
!/.config/docker_example.yml
|
!/.config/docker_example.yml
|
||||||
!/.config/docker_example.env
|
!/.config/docker_example.env
|
||||||
|
docker-compose.yml
|
||||||
|
|
||||||
# misskey
|
# misskey
|
||||||
/build
|
/build
|
||||||
|
@@ -1 +1 @@
|
|||||||
v18.12.1
|
v18.13.0
|
||||||
|
95
CHANGELOG.md
95
CHANGELOG.md
@@ -9,7 +9,97 @@
|
|||||||
You should also include the user name that made the change.
|
You should also include the user name that made the change.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## 13.x.x (unreleased)
|
## 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
|
### Improvements
|
||||||
- 実績機能
|
- 実績機能
|
||||||
@@ -23,6 +113,8 @@ You should also include the user name that made the change.
|
|||||||
|
|
||||||
### Bugfixes
|
### Bugfixes
|
||||||
- playを削除する手段がなかったのを修正
|
- playを削除する手段がなかったのを修正
|
||||||
|
- The … button on notes does nothing when not logged in
|
||||||
|
- twitterと連携するときに autwh is not a function になるのを修正
|
||||||
|
|
||||||
## 13.0.0 (2023/01/16)
|
## 13.0.0 (2023/01/16)
|
||||||
|
|
||||||
@@ -44,6 +136,7 @@ You should also include the user name that made the change.
|
|||||||
- Node.js 18.x or later is required
|
- Node.js 18.x or later is required
|
||||||
- PostgreSQL 15.x is required
|
- PostgreSQL 15.x is required
|
||||||
- Misskey not using 15 specific features at 13.0.0, but may do so in the future.
|
- 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のサポートが削除されました
|
||||||
- 代わりに今後任意の検索プロバイダを設定できる仕組みを構想しています。その仕組みを使えば今まで通りElasticsearchも利用できます
|
- 代わりに今後任意の検索プロバイダを設定できる仕組みを構想しています。その仕組みを使えば今まで通りElasticsearchも利用できます
|
||||||
- Yarnからpnpmに移行されました
|
- Yarnからpnpmに移行されました
|
||||||
|
19
Dockerfile
19
Dockerfile
@@ -2,8 +2,12 @@ ARG NODE_VERSION=18.13.0-bullseye
|
|||||||
|
|
||||||
FROM node:${NODE_VERSION} AS builder
|
FROM node:${NODE_VERSION} AS builder
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
&& apt-get install -y --no-install-recommends \
|
--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
|
build-essential
|
||||||
|
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
@@ -16,7 +20,8 @@ COPY ["packages/backend/package.json", "./packages/backend/"]
|
|||||||
COPY ["packages/frontend/package.json", "./packages/frontend/"]
|
COPY ["packages/frontend/package.json", "./packages/frontend/"]
|
||||||
COPY ["packages/sw/package.json", "./packages/sw/"]
|
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 . ./
|
COPY . ./
|
||||||
|
|
||||||
@@ -30,11 +35,13 @@ FROM node:${NODE_VERSION}-slim AS runner
|
|||||||
ARG UID="991"
|
ARG UID="991"
|
||||||
ARG GID="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 \
|
&& apt-get install -y --no-install-recommends \
|
||||||
ffmpeg tini \
|
ffmpeg tini \
|
||||||
&& apt-get -y clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
|
||||||
&& corepack enable \
|
&& corepack enable \
|
||||||
&& groupadd -g "${GID}" misskey \
|
&& groupadd -g "${GID}" misskey \
|
||||||
&& useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey
|
&& useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey
|
||||||
|
@@ -20,7 +20,7 @@ gulp.task('copy:frontend:fonts', () =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
gulp.task('copy:frontend:tabler-icons', () =>
|
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 => {
|
gulp.task('copy:frontend:locales', cb => {
|
||||||
|
@@ -108,6 +108,7 @@ clickToShow: "اضغط للعرض"
|
|||||||
sensitive: "محتوى حساس"
|
sensitive: "محتوى حساس"
|
||||||
add: "إضافة"
|
add: "إضافة"
|
||||||
reaction: "التفاعلات"
|
reaction: "التفاعلات"
|
||||||
|
reactions: "التفاعلات"
|
||||||
reactionSetting: "التفاعلات المراد عرضها في منتقي التفاعلات."
|
reactionSetting: "التفاعلات المراد عرضها في منتقي التفاعلات."
|
||||||
reactionSettingDescription2: "اسحب لترتيب ، انقر للحذف ، استخدم \"+\" للإضافة."
|
reactionSettingDescription2: "اسحب لترتيب ، انقر للحذف ، استخدم \"+\" للإضافة."
|
||||||
rememberNoteVisibility: "تذكر إعدادت مدى رؤية الملاحظات"
|
rememberNoteVisibility: "تذكر إعدادت مدى رؤية الملاحظات"
|
||||||
|
@@ -107,6 +107,7 @@ clickToShow: "দেখার জন্য ক্লিক করুন"
|
|||||||
sensitive: "সংবেদনশীল বিষয়বস্তু"
|
sensitive: "সংবেদনশীল বিষয়বস্তু"
|
||||||
add: "যুক্ত করুন"
|
add: "যুক্ত করুন"
|
||||||
reaction: "প্রতিক্রিয়া"
|
reaction: "প্রতিক্রিয়া"
|
||||||
|
reactions: "প্রতিক্রিয়া"
|
||||||
reactionSetting: "রিঅ্যাকশন পিকারে যেসকল প্রতিক্রিয়া দেখানো হবে"
|
reactionSetting: "রিঅ্যাকশন পিকারে যেসকল প্রতিক্রিয়া দেখানো হবে"
|
||||||
reactionSettingDescription2: "পুনরায় সাজাতে টেনে আনুন, মুছতে ক্লিক করুন, যোগ করতে + টিপুন।"
|
reactionSettingDescription2: "পুনরায় সাজাতে টেনে আনুন, মুছতে ক্লিক করুন, যোগ করতে + টিপুন।"
|
||||||
rememberNoteVisibility: "নোটের দৃশ্যমান্যতার সেটিংস মনে রাখুন"
|
rememberNoteVisibility: "নোটের দৃশ্যমান্যতার সেটিংস মনে রাখুন"
|
||||||
|
@@ -108,6 +108,7 @@ clickToShow: "Fes clic per mostrar"
|
|||||||
sensitive: "NSFW"
|
sensitive: "NSFW"
|
||||||
add: "Afegir"
|
add: "Afegir"
|
||||||
reaction: "Reaccions"
|
reaction: "Reaccions"
|
||||||
|
reactions: "Reaccions"
|
||||||
reactionSetting: "Reaccions a mostrar al selector de reaccions"
|
reactionSetting: "Reaccions a mostrar al selector de reaccions"
|
||||||
reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, prem \"+\" per afegir."
|
reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, prem \"+\" per afegir."
|
||||||
rememberNoteVisibility: "Recorda la configuració de visibilitat de les notes"
|
rememberNoteVisibility: "Recorda la configuració de visibilitat de les notes"
|
||||||
|
@@ -105,6 +105,7 @@ clickToShow: "Klikněte pro zobrazení"
|
|||||||
sensitive: "NSFW"
|
sensitive: "NSFW"
|
||||||
add: "Přidat"
|
add: "Přidat"
|
||||||
reaction: "Reakce"
|
reaction: "Reakce"
|
||||||
|
reactions: "Reakce"
|
||||||
reactionSettingDescription2: "Přetažením změníte pořadí, kliknutím smažete, zmáčkněte \"+\" k přidání"
|
reactionSettingDescription2: "Přetažením změníte pořadí, kliknutím smažete, zmáčkněte \"+\" k přidání"
|
||||||
rememberNoteVisibility: "Zapamatovat nastavení zobrazení poznámky"
|
rememberNoteVisibility: "Zapamatovat nastavení zobrazení poznámky"
|
||||||
attachCancel: "Odstranit přílohu"
|
attachCancel: "Odstranit přílohu"
|
||||||
|
@@ -110,6 +110,7 @@ clickToShow: "Zum Anzeigen anklicken"
|
|||||||
sensitive: "NSFW"
|
sensitive: "NSFW"
|
||||||
add: "Hinzufügen"
|
add: "Hinzufügen"
|
||||||
reaction: "Reaktionen"
|
reaction: "Reaktionen"
|
||||||
|
reactions: "Reaktionen"
|
||||||
reactionSetting: "In der Reaktionsauswahl anzuzeigende Reaktionen"
|
reactionSetting: "In der Reaktionsauswahl anzuzeigende Reaktionen"
|
||||||
reactionSettingDescription2: "Ziehe um Anzuordnen, klicke um zu löschen, drücke „+“ um hinzuzufügen"
|
reactionSettingDescription2: "Ziehe um Anzuordnen, klicke um zu löschen, drücke „+“ um hinzuzufügen"
|
||||||
rememberNoteVisibility: "Notizsichtbarkeit merken"
|
rememberNoteVisibility: "Notizsichtbarkeit merken"
|
||||||
@@ -937,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."
|
cannotPerformTemporaryDescription: "Diese Aktion ist wegen des Überschreitenes des Ausführungslimits temporär nicht verfügbar. Bitte versuche es nach einiger Zeit erneut."
|
||||||
preset: "Vorlage"
|
preset: "Vorlage"
|
||||||
selectFromPresets: "Aus Vorlagen wählen"
|
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:
|
_role:
|
||||||
new: "Rolle erstellen"
|
new: "Rolle erstellen"
|
||||||
edit: "Rolle bearbeiten"
|
edit: "Rolle bearbeiten"
|
||||||
@@ -1586,6 +1824,7 @@ _notification:
|
|||||||
pollEnded: "Umfrageergebnisse sind verfügbar"
|
pollEnded: "Umfrageergebnisse sind verfügbar"
|
||||||
unreadAntennaNote: "Antenne {name}"
|
unreadAntennaNote: "Antenne {name}"
|
||||||
emptyPushNotificationMessage: "Push-Benachrichtigungen wurden aktualisiert"
|
emptyPushNotificationMessage: "Push-Benachrichtigungen wurden aktualisiert"
|
||||||
|
achievementEarned: "Errungenschaft freigeschaltet"
|
||||||
_types:
|
_types:
|
||||||
all: "Alle"
|
all: "Alle"
|
||||||
follow: "Neue Follower"
|
follow: "Neue Follower"
|
||||||
|
@@ -103,6 +103,7 @@ you: "Εσύ"
|
|||||||
clickToShow: "Κάντε κλικ για εμφάνιση"
|
clickToShow: "Κάντε κλικ για εμφάνιση"
|
||||||
add: "Προσθέστε"
|
add: "Προσθέστε"
|
||||||
reaction: "Αντιδράσεις"
|
reaction: "Αντιδράσεις"
|
||||||
|
reactions: "Αντιδράσεις"
|
||||||
reactionSetting: "Αντιδράσεις για εμφάνιση στην επιλογή αντίδρασης"
|
reactionSetting: "Αντιδράσεις για εμφάνιση στην επιλογή αντίδρασης"
|
||||||
reactionSettingDescription2: "Σύρετε για να αλλάξετε τη σειρά, κάντε κλικ για να διαγράψετε, πατήστε \"+\" για να προσθέσετε."
|
reactionSettingDescription2: "Σύρετε για να αλλάξετε τη σειρά, κάντε κλικ για να διαγράψετε, πατήστε \"+\" για να προσθέσετε."
|
||||||
rememberNoteVisibility: "Θυμήσου τις ρυθμίσεις ορατότητας σημειώματος"
|
rememberNoteVisibility: "Θυμήσου τις ρυθμίσεις ορατότητας σημειώματος"
|
||||||
|
@@ -110,6 +110,7 @@ clickToShow: "Click to show"
|
|||||||
sensitive: "NSFW"
|
sensitive: "NSFW"
|
||||||
add: "Add"
|
add: "Add"
|
||||||
reaction: "Reactions"
|
reaction: "Reactions"
|
||||||
|
reactions: "Reactions"
|
||||||
reactionSetting: "Reactions to show in the reaction picker"
|
reactionSetting: "Reactions to show in the reaction picker"
|
||||||
reactionSettingDescription2: "Drag to reorder, click to delete, press \"+\" to add."
|
reactionSettingDescription2: "Drag to reorder, click to delete, press \"+\" to add."
|
||||||
rememberNoteVisibility: "Remember note visibility settings"
|
rememberNoteVisibility: "Remember note visibility settings"
|
||||||
@@ -937,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."
|
cannotPerformTemporaryDescription: "This action cannot be performed temporarily due to exceeding the execution limit. Please wait for a while and then try again."
|
||||||
preset: "Preset"
|
preset: "Preset"
|
||||||
selectFromPresets: "Choose from presets"
|
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:
|
_role:
|
||||||
new: "New role"
|
new: "New role"
|
||||||
edit: "Edit role"
|
edit: "Edit role"
|
||||||
@@ -1586,6 +1824,7 @@ _notification:
|
|||||||
pollEnded: "Poll results have become available"
|
pollEnded: "Poll results have become available"
|
||||||
unreadAntennaNote: "Antenna {name}"
|
unreadAntennaNote: "Antenna {name}"
|
||||||
emptyPushNotificationMessage: "Push notifications have been updated"
|
emptyPushNotificationMessage: "Push notifications have been updated"
|
||||||
|
achievementEarned: "Achievement unlocked"
|
||||||
_types:
|
_types:
|
||||||
all: "All"
|
all: "All"
|
||||||
follow: "New followers"
|
follow: "New followers"
|
||||||
|
@@ -110,6 +110,7 @@ clickToShow: "Click para ver"
|
|||||||
sensitive: "Marcado como sensible"
|
sensitive: "Marcado como sensible"
|
||||||
add: "Agregar"
|
add: "Agregar"
|
||||||
reaction: "Reacción"
|
reaction: "Reacción"
|
||||||
|
reactions: "Reacción"
|
||||||
reactionSetting: "Reacciones para mostrar en el menú de reacciones"
|
reactionSetting: "Reacciones para mostrar en el menú de reacciones"
|
||||||
reactionSettingDescription2: "Arrastre para reordenar, click para borrar, apriete la tecla + para añadir."
|
reactionSettingDescription2: "Arrastre para reordenar, click para borrar, apriete la tecla + para añadir."
|
||||||
rememberNoteVisibility: "Recordar visibilidad"
|
rememberNoteVisibility: "Recordar visibilidad"
|
||||||
|
@@ -110,6 +110,7 @@ clickToShow: "Cliquer pour afficher"
|
|||||||
sensitive: "Contenu sensible"
|
sensitive: "Contenu sensible"
|
||||||
add: "Ajouter"
|
add: "Ajouter"
|
||||||
reaction: "Réactions"
|
reaction: "Réactions"
|
||||||
|
reactions: "Réactions"
|
||||||
reactionSetting: "Réactions à afficher dans le sélecteur de réactions"
|
reactionSetting: "Réactions à afficher dans le sélecteur de réactions"
|
||||||
reactionSettingDescription2: "Déplacer pour réorganiser, cliquer pour effacer, utiliser « + » pour ajouter."
|
reactionSettingDescription2: "Déplacer pour réorganiser, cliquer pour effacer, utiliser « + » pour ajouter."
|
||||||
rememberNoteVisibility: "Activer l'option \" se souvenir de la visibilité des notes \" vous permet de réutiliser automatiquement la visibilité utilisée lors de la publication de votre note précédente."
|
rememberNoteVisibility: "Activer l'option \" se souvenir de la visibilité des notes \" vous permet de réutiliser automatiquement la visibilité utilisée lors de la publication de votre note précédente."
|
||||||
|
@@ -13,6 +13,7 @@ fetchingAsApObject: "Mengambil data dari Fediverse..."
|
|||||||
ok: "OK"
|
ok: "OK"
|
||||||
gotIt: "Saya mengerti"
|
gotIt: "Saya mengerti"
|
||||||
cancel: "Batalkan"
|
cancel: "Batalkan"
|
||||||
|
noThankYou: "Tidak sekarang."
|
||||||
enterUsername: "Masukkan nama pengguna"
|
enterUsername: "Masukkan nama pengguna"
|
||||||
renotedBy: "direnote oleh {user}"
|
renotedBy: "direnote oleh {user}"
|
||||||
noNotes: "Tidak ada catatan"
|
noNotes: "Tidak ada catatan"
|
||||||
@@ -109,6 +110,7 @@ clickToShow: "Klik untuk melihat"
|
|||||||
sensitive: "Konten sensitif"
|
sensitive: "Konten sensitif"
|
||||||
add: "Tambahkan"
|
add: "Tambahkan"
|
||||||
reaction: "Reaksi"
|
reaction: "Reaksi"
|
||||||
|
reactions: "Reaksi"
|
||||||
reactionSetting: "Reaksi untuk dimunculkan di bilah reaksi"
|
reactionSetting: "Reaksi untuk dimunculkan di bilah reaksi"
|
||||||
reactionSettingDescription2: "Geser untuk memindah urutkan, klik untuk menghapus, tekan \"+\" untuk menambahkan"
|
reactionSettingDescription2: "Geser untuk memindah urutkan, klik untuk menghapus, tekan \"+\" untuk menambahkan"
|
||||||
rememberNoteVisibility: "Ingat pengaturan visibilitas catatan"
|
rememberNoteVisibility: "Ingat pengaturan visibilitas catatan"
|
||||||
@@ -205,6 +207,7 @@ done: "Selesai"
|
|||||||
processing: "Memproses"
|
processing: "Memproses"
|
||||||
preview: "Pratinjau"
|
preview: "Pratinjau"
|
||||||
default: "Bawaan"
|
default: "Bawaan"
|
||||||
|
defaultValueIs: "Bawaan: {value}"
|
||||||
noCustomEmojis: "Tidak ada emoji kustom"
|
noCustomEmojis: "Tidak ada emoji kustom"
|
||||||
noJobs: "Tidak ada kerja"
|
noJobs: "Tidak ada kerja"
|
||||||
federating: "memfederasi"
|
federating: "memfederasi"
|
||||||
@@ -348,6 +351,8 @@ recaptcha: "reCAPTCHA"
|
|||||||
enableRecaptcha: "Nyalakan reCAPTCHA"
|
enableRecaptcha: "Nyalakan reCAPTCHA"
|
||||||
recaptchaSiteKey: "Site key"
|
recaptchaSiteKey: "Site key"
|
||||||
recaptchaSecretKey: "Secret Key"
|
recaptchaSecretKey: "Secret Key"
|
||||||
|
turnstile: "Turnstile"
|
||||||
|
enableTurnstile: "Nyalakan Turnstile"
|
||||||
turnstileSiteKey: "Site key"
|
turnstileSiteKey: "Site key"
|
||||||
turnstileSecretKey: "Secret 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."
|
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."
|
||||||
@@ -453,6 +458,7 @@ uiLanguage: "Bahasa antarmuka pengguna"
|
|||||||
groupInvited: "Telah diundang ke grup"
|
groupInvited: "Telah diundang ke grup"
|
||||||
aboutX: "Tentang {x}"
|
aboutX: "Tentang {x}"
|
||||||
emojiStyle: "Gaya emoji"
|
emojiStyle: "Gaya emoji"
|
||||||
|
native: "Native"
|
||||||
disableDrawer: "Jangan gunakan menu bergaya laci"
|
disableDrawer: "Jangan gunakan menu bergaya laci"
|
||||||
youHaveNoGroups: "Kamu tidak memiliki grup"
|
youHaveNoGroups: "Kamu tidak memiliki grup"
|
||||||
joinOrCreateGroup: "Bergabunglah dengan grup atau kamu dapat membuat grupmu sendiri."
|
joinOrCreateGroup: "Bergabunglah dengan grup atau kamu dapat membuat grupmu sendiri."
|
||||||
@@ -856,10 +862,21 @@ rateLimitExceeded: "Batas sudah terlampaui"
|
|||||||
cropImage: "potong gambar"
|
cropImage: "potong gambar"
|
||||||
cropImageAsk: "Ingin memotong gambar?"
|
cropImageAsk: "Ingin memotong gambar?"
|
||||||
file: "Berkas"
|
file: "Berkas"
|
||||||
|
recentNHours: "{n} jam terakhir"
|
||||||
|
recentNDays: "{n} hari terakhir"
|
||||||
noEmailServerWarning: "Mail Server tidak disetel."
|
noEmailServerWarning: "Mail Server tidak disetel."
|
||||||
|
thereIsUnresolvedAbuseReportWarning: "Ada laporan yang belum diselesaikan."
|
||||||
recommended: "Disarankan"
|
recommended: "Disarankan"
|
||||||
check: "Cek"
|
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"
|
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?"
|
logoutConfirm: "Anda yakin ingin keluar?"
|
||||||
lastActiveDate: "Terakhir digunakan"
|
lastActiveDate: "Terakhir digunakan"
|
||||||
statusbar: "Bilah status"
|
statusbar: "Bilah status"
|
||||||
@@ -869,20 +886,189 @@ colored: "Diwarnai"
|
|||||||
refreshInterval: "Jeda pembaharuan"
|
refreshInterval: "Jeda pembaharuan"
|
||||||
label: "Label"
|
label: "Label"
|
||||||
type: "Tipe"
|
type: "Tipe"
|
||||||
|
speed: "Kecepatan"
|
||||||
|
slow: "Lambat"
|
||||||
|
fast: "Cepat"
|
||||||
|
sensitiveMediaDetection: "Deteksi media NSFW"
|
||||||
localOnly: "Hanya lokal"
|
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"
|
shuffle: "Acak"
|
||||||
account: "Akun"
|
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"
|
like: "Suka"
|
||||||
unlike: "Tidak Suka"
|
unlike: "Tidak Suka"
|
||||||
numberOfLikes: "Jumlah yang disukai"
|
numberOfLikes: "Jumlah yang disukai"
|
||||||
show: "Tampilkan"
|
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"
|
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:
|
_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: "Prioritas"
|
||||||
_priority:
|
_priority:
|
||||||
low: "Rendah"
|
low: "Rendah"
|
||||||
middle: "Sedang"
|
middle: "Sedang"
|
||||||
high: "Tinggi"
|
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:
|
_emailUnavailable:
|
||||||
used: "Alamat surel ini telah digunakan"
|
used: "Alamat surel ini telah digunakan"
|
||||||
format: "Format tidak valid."
|
format: "Format tidak valid."
|
||||||
@@ -1166,6 +1352,7 @@ _tutorial:
|
|||||||
step7_1: "Yay, Selamat! Kamu sudah menyelesaikan tutorial dasar Misskey."
|
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_2: "Jika kamu ingin mempelajari lebih lanjut tentang Misskey, cobalah berkunjung ke bagian {help}."
|
||||||
step7_3: "Semoga berhasil dan bersenang-senanglah! 🚀"
|
step7_3: "Semoga berhasil dan bersenang-senanglah! 🚀"
|
||||||
|
step8_3: "Kamu dapat mengganti pengaturan ini nanti."
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "Kamu telah mendaftarkan perangkat otentikasi dua faktor."
|
alreadyRegistered: "Kamu telah mendaftarkan perangkat otentikasi dua faktor."
|
||||||
registerDevice: "Daftarkan perangkat baru"
|
registerDevice: "Daftarkan perangkat baru"
|
||||||
@@ -1240,10 +1427,13 @@ _widgets:
|
|||||||
trends: "Tren"
|
trends: "Tren"
|
||||||
clock: "Jam"
|
clock: "Jam"
|
||||||
rss: "Pembaca RSS"
|
rss: "Pembaca RSS"
|
||||||
|
rssTicker: "RSS-Ticker"
|
||||||
activity: "Aktivitas"
|
activity: "Aktivitas"
|
||||||
photos: "Foto"
|
photos: "Foto"
|
||||||
digitalClock: "Jam digital"
|
digitalClock: "Jam digital"
|
||||||
|
unixClock: "Jam UNIX"
|
||||||
federation: "Federasi"
|
federation: "Federasi"
|
||||||
|
instanceCloud: "Instansi awan"
|
||||||
postForm: "Buat catatan"
|
postForm: "Buat catatan"
|
||||||
slideshow: "Slideshow"
|
slideshow: "Slideshow"
|
||||||
button: "Tombol"
|
button: "Tombol"
|
||||||
@@ -1253,8 +1443,10 @@ _widgets:
|
|||||||
aiscript: "Konsol AiScript"
|
aiscript: "Konsol AiScript"
|
||||||
aiscriptApp: "Aplikasi AiScript"
|
aiscriptApp: "Aplikasi AiScript"
|
||||||
aichan: "Ai"
|
aichan: "Ai"
|
||||||
|
userList: "Daftar pengguna"
|
||||||
_userList:
|
_userList:
|
||||||
chooseList: "Pilih daftar"
|
chooseList: "Pilih daftar"
|
||||||
|
clicker: "Pengeklik"
|
||||||
_cw:
|
_cw:
|
||||||
hide: "Sembunyikan"
|
hide: "Sembunyikan"
|
||||||
show: "Lihat konten"
|
show: "Lihat konten"
|
||||||
@@ -1318,6 +1510,7 @@ _profile:
|
|||||||
changeBanner: "Ubah header"
|
changeBanner: "Ubah header"
|
||||||
_exportOrImport:
|
_exportOrImport:
|
||||||
allNotes: "Semua catatan"
|
allNotes: "Semua catatan"
|
||||||
|
favoritedNotes: "Catatan favorit"
|
||||||
followingList: "Ikuti"
|
followingList: "Ikuti"
|
||||||
muteList: "Bisukan"
|
muteList: "Bisukan"
|
||||||
blockingList: "Blokir"
|
blockingList: "Blokir"
|
||||||
@@ -1436,7 +1629,9 @@ _notification:
|
|||||||
yourFollowRequestAccepted: "Permintaan mengikuti kamu telah diterima"
|
yourFollowRequestAccepted: "Permintaan mengikuti kamu telah diterima"
|
||||||
youWereInvitedToGroup: "Telah diundang ke grup"
|
youWereInvitedToGroup: "Telah diundang ke grup"
|
||||||
pollEnded: "Hasil Kuesioner telah keluar"
|
pollEnded: "Hasil Kuesioner telah keluar"
|
||||||
|
unreadAntennaNote: "Antena {name}"
|
||||||
emptyPushNotificationMessage: "Pembaruan notifikasi dorong"
|
emptyPushNotificationMessage: "Pembaruan notifikasi dorong"
|
||||||
|
achievementEarned: "Pencapaian didapatkan"
|
||||||
_types:
|
_types:
|
||||||
all: "Semua"
|
all: "Semua"
|
||||||
follow: "Ikuti"
|
follow: "Ikuti"
|
||||||
@@ -1458,6 +1653,7 @@ _deck:
|
|||||||
alwaysShowMainColumn: "Selalu tampilkan kolom utama"
|
alwaysShowMainColumn: "Selalu tampilkan kolom utama"
|
||||||
columnAlign: "Luruskan kolom"
|
columnAlign: "Luruskan kolom"
|
||||||
addColumn: "Tambahkan kolom"
|
addColumn: "Tambahkan kolom"
|
||||||
|
configureColumn: "Atur kolom"
|
||||||
swapLeft: "Pindah ke kiri"
|
swapLeft: "Pindah ke kiri"
|
||||||
swapRight: "Pindah ke kanan"
|
swapRight: "Pindah ke kanan"
|
||||||
swapUp: "Pindah ke atas"
|
swapUp: "Pindah ke atas"
|
||||||
@@ -1465,6 +1661,11 @@ _deck:
|
|||||||
stackLeft: "Tumpukkan di kolom kiri"
|
stackLeft: "Tumpukkan di kolom kiri"
|
||||||
popRight: "Keluarkan di kanan"
|
popRight: "Keluarkan di kanan"
|
||||||
profile: "Profil"
|
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:
|
_columns:
|
||||||
main: "Utama"
|
main: "Utama"
|
||||||
widgets: "Widget"
|
widgets: "Widget"
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
_lang_: "Italiano"
|
_lang_: "Italiano"
|
||||||
headlineMisskey: "Rete collegata tramite note"
|
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>."
|
poweredByMisskeyDescription: "{name} è uno dei servizi (chiamati istanze) che utilizzano la piattaforma open source <b>Misskey</b>."
|
||||||
monthAndDay: "{day}/{month}"
|
monthAndDay: "{day}/{month}"
|
||||||
search: "Cerca"
|
search: "Cerca"
|
||||||
@@ -95,7 +95,7 @@ follow: "Segui"
|
|||||||
followRequest: "Richiesta di follow"
|
followRequest: "Richiesta di follow"
|
||||||
followRequests: "Richieste di follow"
|
followRequests: "Richieste di follow"
|
||||||
unfollow: "Smetti di seguire"
|
unfollow: "Smetti di seguire"
|
||||||
followRequestPending: "La richiesta di follow deve essere approvata"
|
followRequestPending: "Richiesta in approvazione"
|
||||||
enterEmoji: "Inserisci emoji"
|
enterEmoji: "Inserisci emoji"
|
||||||
renote: "Rinota"
|
renote: "Rinota"
|
||||||
unrenote: "Annulla rinota"
|
unrenote: "Annulla rinota"
|
||||||
@@ -110,6 +110,7 @@ clickToShow: "Clicca per visualizzare"
|
|||||||
sensitive: "Contenuto sensibile"
|
sensitive: "Contenuto sensibile"
|
||||||
add: "Aggiungi"
|
add: "Aggiungi"
|
||||||
reaction: "Reazioni"
|
reaction: "Reazioni"
|
||||||
|
reactions: "Reazioni"
|
||||||
reactionSetting: "Reazioni visualizzate sul pannello"
|
reactionSetting: "Reazioni visualizzate sul pannello"
|
||||||
reactionSettingDescription2: "Trascina per riorganizzare, clicca per cancellare, usa il pulsante \"+\" per aggiungere."
|
reactionSettingDescription2: "Trascina per riorganizzare, clicca per cancellare, usa il pulsante \"+\" per aggiungere."
|
||||||
rememberNoteVisibility: "Ricordare le impostazioni di visibilità delle note"
|
rememberNoteVisibility: "Ricordare le impostazioni di visibilità delle note"
|
||||||
@@ -937,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."
|
cannotPerformTemporaryDescription: "L'attività non può essere svolta, poiché si è raggiunto il limite di esecuzioni possibili. Per favore, riprova più tardi."
|
||||||
preset: "Preimpostato"
|
preset: "Preimpostato"
|
||||||
selectFromPresets: "Seleziona 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:
|
_role:
|
||||||
new: "Nuovo ruolo"
|
new: "Nuovo ruolo"
|
||||||
edit: "Modifica ruolo"
|
edit: "Modifica ruolo"
|
||||||
@@ -1269,8 +1507,8 @@ _sfx:
|
|||||||
channel: "Notifiche di canale"
|
channel: "Notifiche di canale"
|
||||||
_ago:
|
_ago:
|
||||||
future: "Futuro"
|
future: "Futuro"
|
||||||
justNow: "Ora"
|
justNow: "Adesso"
|
||||||
secondsAgo: "{n}s fa"
|
secondsAgo: "{n} sec fa"
|
||||||
minutesAgo: "{n} min fa"
|
minutesAgo: "{n} min fa"
|
||||||
hoursAgo: "{n} ore fa"
|
hoursAgo: "{n} ore fa"
|
||||||
daysAgo: "{n} gg fa"
|
daysAgo: "{n} gg fa"
|
||||||
@@ -1292,7 +1530,7 @@ _tutorial:
|
|||||||
step3_1: "Hai finito di impostare il tuo profilo?"
|
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_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_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_1: "Hai pubblicato qualcosa?"
|
||||||
step4_2: "Se puoi visualizzare la tua nota sulla timeline, ce l'hai fatta!"
|
step4_2: "Se puoi visualizzare la tua nota sulla timeline, ce l'hai fatta!"
|
||||||
step5_1: "Adesso, cerca di seguire altre persone per vivacizzare la tua timeline. "
|
step5_1: "Adesso, cerca di seguire altre persone per vivacizzare la tua timeline. "
|
||||||
@@ -1586,6 +1824,7 @@ _notification:
|
|||||||
pollEnded: "Risultati del sondaggio."
|
pollEnded: "Risultati del sondaggio."
|
||||||
unreadAntennaNote: "Antenna {name}"
|
unreadAntennaNote: "Antenna {name}"
|
||||||
emptyPushNotificationMessage: "Le notifiche push sono state aggiornate."
|
emptyPushNotificationMessage: "Le notifiche push sono state aggiornate."
|
||||||
|
achievementEarned: "Obiettivo raggiunto"
|
||||||
_types:
|
_types:
|
||||||
all: "Tutto"
|
all: "Tutto"
|
||||||
follow: "Novità follower"
|
follow: "Novità follower"
|
||||||
|
@@ -1049,6 +1049,9 @@ _achievements:
|
|||||||
_noteFavorited1:
|
_noteFavorited1:
|
||||||
title: "星をみるひと"
|
title: "星をみるひと"
|
||||||
description: "初めてノートをお気に入りに登録した"
|
description: "初めてノートをお気に入りに登録した"
|
||||||
|
_myNoteFavorited1:
|
||||||
|
title: "星が欲しい"
|
||||||
|
description: "自分のノートが他の人からお気に入りに登録された"
|
||||||
_profileFilled:
|
_profileFilled:
|
||||||
title: "準備万端"
|
title: "準備万端"
|
||||||
description: "プロフィール設定を行った"
|
description: "プロフィール設定を行った"
|
||||||
@@ -1095,10 +1098,16 @@ _achievements:
|
|||||||
_collectAchievements30:
|
_collectAchievements30:
|
||||||
title: "実績コレクター"
|
title: "実績コレクター"
|
||||||
description: "実績を30個以上獲得した"
|
description: "実績を30個以上獲得した"
|
||||||
|
_viewAchievements3min:
|
||||||
|
title: "実績好き"
|
||||||
|
description: "実績一覧を3分以上眺め続けた"
|
||||||
_iLoveMisskey:
|
_iLoveMisskey:
|
||||||
title: "I Love Misskey"
|
title: "I Love Misskey"
|
||||||
description: "\"I ❤ #Misskey\"を投稿した"
|
description: "\"I ❤ #Misskey\"を投稿した"
|
||||||
flavor: "Misskeyを使ってくださりありがとうございます! by 開発チーム"
|
flavor: "Misskeyを使ってくださりありがとうございます! by 開発チーム"
|
||||||
|
_foundTreasure:
|
||||||
|
title: "宝探し"
|
||||||
|
description: "隠されたお宝を発見した"
|
||||||
_client30min:
|
_client30min:
|
||||||
title: "ひとやすみ"
|
title: "ひとやすみ"
|
||||||
description: "クライアントを起動してから30分以上経過した"
|
description: "クライアントを起動してから30分以上経過した"
|
||||||
@@ -1119,6 +1128,15 @@ _achievements:
|
|||||||
_htl20npm:
|
_htl20npm:
|
||||||
title: "流れるTL"
|
title: "流れるTL"
|
||||||
description: "ホームタイムラインの流速が20npmを越す"
|
description: "ホームタイムラインの流速が20npmを越す"
|
||||||
|
_viewInstanceChart:
|
||||||
|
title: "アナリスト"
|
||||||
|
description: "インスタンスのチャートを表示した"
|
||||||
|
_outputHelloWorldOnScratchpad:
|
||||||
|
title: "Hello, world!"
|
||||||
|
description: "スクラッチパッドで hello world を出力した"
|
||||||
|
_open3windows:
|
||||||
|
title: "マルチウィンドウ"
|
||||||
|
description: "ウィンドウを3つ以上開いた状態にした"
|
||||||
_driveFolderCircularReference:
|
_driveFolderCircularReference:
|
||||||
title: "循環参照"
|
title: "循環参照"
|
||||||
description: "ドライブのフォルダを再帰的な入れ子にしようとした"
|
description: "ドライブのフォルダを再帰的な入れ子にしようとした"
|
||||||
@@ -1146,6 +1164,10 @@ _achievements:
|
|||||||
_loggedInOnBirthday:
|
_loggedInOnBirthday:
|
||||||
title: "ハッピーバースデー"
|
title: "ハッピーバースデー"
|
||||||
description: "誕生日にログインした"
|
description: "誕生日にログインした"
|
||||||
|
_loggedInOnNewYearsDay:
|
||||||
|
title: "あけましておめでとうございます"
|
||||||
|
description: "元日にログインした"
|
||||||
|
flavor: "今年も弊インスタンスをよろしくお願いします"
|
||||||
_cookieClicked:
|
_cookieClicked:
|
||||||
title: "クッキーをクリックするゲーム"
|
title: "クッキーをクリックするゲーム"
|
||||||
description: "クッキーをクリックした"
|
description: "クッキーをクリックした"
|
||||||
|
@@ -8,9 +8,9 @@ search: "探す"
|
|||||||
notifications: "通知"
|
notifications: "通知"
|
||||||
username: "ユーザー名"
|
username: "ユーザー名"
|
||||||
password: "パスワード"
|
password: "パスワード"
|
||||||
forgotPassword: "パスワード忘れてん"
|
forgotPassword: "パスワード忘れてもうた"
|
||||||
fetchingAsApObject: "今ちと連合に照会しとるで"
|
fetchingAsApObject: "今ちと連合に照会しとるで"
|
||||||
ok: "OKや"
|
ok: "ええで"
|
||||||
gotIt: "ほい"
|
gotIt: "ほい"
|
||||||
cancel: "やめとく"
|
cancel: "やめとく"
|
||||||
noThankYou: "やめとく"
|
noThankYou: "やめとく"
|
||||||
@@ -110,6 +110,7 @@ clickToShow: "押したら見えるで"
|
|||||||
sensitive: "ちょっとアカンやつやで"
|
sensitive: "ちょっとアカンやつやで"
|
||||||
add: "増やす"
|
add: "増やす"
|
||||||
reaction: "リアクション"
|
reaction: "リアクション"
|
||||||
|
reactions: "リアクション"
|
||||||
reactionSetting: "Reaction that will be displayed in Picker. "
|
reactionSetting: "Reaction that will be displayed in Picker. "
|
||||||
reactionSettingDescription2: "ドラッグで並び替え、クリックで削除、+を押して追加やで。"
|
reactionSettingDescription2: "ドラッグで並び替え、クリックで削除、+を押して追加やで。"
|
||||||
rememberNoteVisibility: "公開範囲覚えといて"
|
rememberNoteVisibility: "公開範囲覚えといて"
|
||||||
@@ -607,7 +608,7 @@ wordMute: "ワードミュート"
|
|||||||
regexpError: "正規表現エラー"
|
regexpError: "正規表現エラー"
|
||||||
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが出てきたで:"
|
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが出てきたで:"
|
||||||
instanceMute: "インスタンスミュート"
|
instanceMute: "インスタンスミュート"
|
||||||
userSaysSomething: "{name}が何か言ったようやで"
|
userSaysSomething: "{name}が何か言うとるわ"
|
||||||
makeActive: "使うで"
|
makeActive: "使うで"
|
||||||
display: "表示"
|
display: "表示"
|
||||||
copy: "コピー"
|
copy: "コピー"
|
||||||
|
@@ -110,6 +110,7 @@ clickToShow: "클릭하여 보기"
|
|||||||
sensitive: "열람주의"
|
sensitive: "열람주의"
|
||||||
add: "추가"
|
add: "추가"
|
||||||
reaction: "리액션"
|
reaction: "리액션"
|
||||||
|
reactions: "리액션"
|
||||||
reactionSetting: "선택기에 표시할 리액션"
|
reactionSetting: "선택기에 표시할 리액션"
|
||||||
reactionSettingDescription2: "끌어서 순서 변경, 클릭해서 삭제, +를 눌러서 추가할 수 있습니다."
|
reactionSettingDescription2: "끌어서 순서 변경, 클릭해서 삭제, +를 눌러서 추가할 수 있습니다."
|
||||||
rememberNoteVisibility: "공개 범위를 기억하기"
|
rememberNoteVisibility: "공개 범위를 기억하기"
|
||||||
@@ -937,6 +938,243 @@ cannotPerformTemporary: "일시적으로 사용할 수 없음"
|
|||||||
cannotPerformTemporaryDescription: "조작 횟수 제한을 초과하여 일시적으로 사용이 불가합니다. 잠시 후 다시 시도해 주세요."
|
cannotPerformTemporaryDescription: "조작 횟수 제한을 초과하여 일시적으로 사용이 불가합니다. 잠시 후 다시 시도해 주세요."
|
||||||
preset: "프리셋"
|
preset: "프리셋"
|
||||||
selectFromPresets: "프리셋에서 선택"
|
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: "오늘부터 여러분도 미스키스트에요!"
|
||||||
|
_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: "제군, 나는 노트가 좋다"
|
||||||
|
_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: "주소록 한 권으론 부족해"
|
||||||
|
description: "100명의 사용자를 팔로우했습니다"
|
||||||
|
_following300:
|
||||||
|
title: "친구가 넘쳐나"
|
||||||
|
description: "300명의 사용자를 팔로우했습니다"
|
||||||
|
_followers1:
|
||||||
|
title: "첫 팔로워"
|
||||||
|
description: "사용자가 처음으로 팔로잉했습니다"
|
||||||
|
_followers10:
|
||||||
|
title: "팔로우 미!"
|
||||||
|
description: "10명의 사용자가 팔로우했습니다"
|
||||||
|
_followers50:
|
||||||
|
title: "이곳저곳"
|
||||||
|
description: "50명의 사용자가 팔로우했습니다"
|
||||||
|
_followers100:
|
||||||
|
title: "인기왕"
|
||||||
|
description: "100명의 사용자가 팔로우했습니다"
|
||||||
|
_followers300:
|
||||||
|
title: "줄 좀 서봐요"
|
||||||
|
description: "100명의 사용자가 팔로우했습니다"
|
||||||
|
_followers500:
|
||||||
|
title: "기지국"
|
||||||
|
description: "500명의 사용자가 팔로우했습니다"
|
||||||
|
_followers1000:
|
||||||
|
title: "유명인사"
|
||||||
|
description: "1,000명의 사용자가 팔로우했습니다"
|
||||||
|
_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: "0분 0초 정각에 노트를 작성했습니다"
|
||||||
|
flavor: "째깍 째깍 째깍 땡!"
|
||||||
|
_selfQuote:
|
||||||
|
title: "혼잣말"
|
||||||
|
description: "자기 노트를 인용했습니다"
|
||||||
|
_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초 안에 반응했습니다"
|
||||||
|
_clickedClickHere:
|
||||||
|
title: "여길 눌러보세요"
|
||||||
|
description: "여길을 눌러봤습니다"
|
||||||
|
_justPlainLucky:
|
||||||
|
title: "그냥 운이 좋았어"
|
||||||
|
description: "매 10초마다 0.01%의 확률로 달성됩니다"
|
||||||
|
_setNameToSyuilo:
|
||||||
|
title: "신 콤플렉스"
|
||||||
|
description: "이름을 syuilo로 설정했습니다"
|
||||||
|
_passedSinceAccountCreated1:
|
||||||
|
title: "1주년"
|
||||||
|
description: "계정을 생성하고 1년이 지났습니다"
|
||||||
|
_passedSinceAccountCreated2:
|
||||||
|
title: "2주년"
|
||||||
|
description: "계정을 생성하고 2년이 지났습니다"
|
||||||
|
_passedSinceAccountCreated3:
|
||||||
|
title: "3주년"
|
||||||
|
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:
|
_role:
|
||||||
new: "새 역할 생성"
|
new: "새 역할 생성"
|
||||||
edit: "역할 수정"
|
edit: "역할 수정"
|
||||||
@@ -1586,6 +1824,7 @@ _notification:
|
|||||||
pollEnded: "투표 결과가 발표되었습니다"
|
pollEnded: "투표 결과가 발표되었습니다"
|
||||||
unreadAntennaNote: "안테나 {name}"
|
unreadAntennaNote: "안테나 {name}"
|
||||||
emptyPushNotificationMessage: "푸시 알림이 갱신되었습니다"
|
emptyPushNotificationMessage: "푸시 알림이 갱신되었습니다"
|
||||||
|
achievementEarned: "도전 과제를 달성했습니다"
|
||||||
_types:
|
_types:
|
||||||
all: "전부"
|
all: "전부"
|
||||||
follow: "팔로잉"
|
follow: "팔로잉"
|
||||||
|
@@ -109,6 +109,7 @@ clickToShow: "Klik om te bekijken"
|
|||||||
sensitive: "NSFW"
|
sensitive: "NSFW"
|
||||||
add: "Toevoegen"
|
add: "Toevoegen"
|
||||||
reaction: "Reacties"
|
reaction: "Reacties"
|
||||||
|
reactions: "Reacties"
|
||||||
reactionSetting: "Reacties die in de reactie-selector worden getoond"
|
reactionSetting: "Reacties die in de reactie-selector worden getoond"
|
||||||
reactionSettingDescription2: "Sleep om opnieuw te ordenen, Klik om te verwijderen, Druk op \"+\" om toe te voegen"
|
reactionSettingDescription2: "Sleep om opnieuw te ordenen, Klik om te verwijderen, Druk op \"+\" om toe te voegen"
|
||||||
rememberNoteVisibility: "Vergeet niet de notitie zichtbaarheidsinstellingen"
|
rememberNoteVisibility: "Vergeet niet de notitie zichtbaarheidsinstellingen"
|
||||||
|
@@ -110,6 +110,7 @@ clickToShow: "Kliknij, aby wyświetlić"
|
|||||||
sensitive: "NSFW"
|
sensitive: "NSFW"
|
||||||
add: "Dodaj"
|
add: "Dodaj"
|
||||||
reaction: "Reakcja"
|
reaction: "Reakcja"
|
||||||
|
reactions: "Reakcja"
|
||||||
reactionSetting: "Reakcje do pokazania w wyborniku reakcji"
|
reactionSetting: "Reakcje do pokazania w wyborniku reakcji"
|
||||||
reactionSettingDescription2: "Przeciągnij aby zmienić kolejność, naciśnij aby usunąć, naciśnij „+” aby dodać"
|
reactionSettingDescription2: "Przeciągnij aby zmienić kolejność, naciśnij aby usunąć, naciśnij „+” aby dodać"
|
||||||
rememberNoteVisibility: "Zapamiętuj ustawienia widoczności wpisu"
|
rememberNoteVisibility: "Zapamiętuj ustawienia widoczności wpisu"
|
||||||
|
@@ -107,6 +107,7 @@ clickToShow: "Clique para ver"
|
|||||||
sensitive: "Conteúdo sensível"
|
sensitive: "Conteúdo sensível"
|
||||||
add: "Adicionar"
|
add: "Adicionar"
|
||||||
reaction: "Reações"
|
reaction: "Reações"
|
||||||
|
reactions: "Reações"
|
||||||
reactionSetting: "Quais reações a mostrar no selecionador de reações"
|
reactionSetting: "Quais reações a mostrar no selecionador de reações"
|
||||||
reactionSettingDescription2: "Arraste para reordenar, clique para excluir, pressione + para adicionar."
|
reactionSettingDescription2: "Arraste para reordenar, clique para excluir, pressione + para adicionar."
|
||||||
rememberNoteVisibility: "Lembrar das configurações de visibilidade de notas"
|
rememberNoteVisibility: "Lembrar das configurações de visibilidade de notas"
|
||||||
|
@@ -107,6 +107,7 @@ clickToShow: "Click pentru a afișa"
|
|||||||
sensitive: "NSFW"
|
sensitive: "NSFW"
|
||||||
add: "Adaugă"
|
add: "Adaugă"
|
||||||
reaction: "Reacție"
|
reaction: "Reacție"
|
||||||
|
reactions: "Reacție"
|
||||||
reactionSetting: "Reacții care să apară in selectorul de reacții"
|
reactionSetting: "Reacții care să apară in selectorul de reacții"
|
||||||
reactionSettingDescription2: "Trage pentru a rearanja, apasă pe \"+\" pentru a adăuga."
|
reactionSettingDescription2: "Trage pentru a rearanja, apasă pe \"+\" pentru a adăuga."
|
||||||
rememberNoteVisibility: "Amintește setarea de vizibilitate a notelor"
|
rememberNoteVisibility: "Amintește setarea de vizibilitate a notelor"
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
_lang_: "Русский"
|
_lang_: "Русский"
|
||||||
headlineMisskey: "Сеть, сплетённая из заметок"
|
headlineMisskey: "Сеть, сплетённая из заметок"
|
||||||
introMisskey: "Добро пожаловать! Misskey — это децентрализованный сервис микроблогов с открытым исходным кодом.\nПишите «заметки» — делитесь со всеми происходящим вокруг или рассказывайте о себе 📡\nСтавьте «реакции» — выражайте свои чувства и эмоции от заметок других 👍\nОткройте для себя новый мир 🚀"
|
introMisskey: "Добро пожаловать! Misskey — это децентрализованный сервис микроблогов с открытым исходным кодом.\nПишите «заметки» — делитесь со всеми происходящим вокруг или рассказывайте о себе 📡\nСтавьте «реакции» — выражайте свои чувства и эмоции от заметок других 👍\nОткройте для себя новый мир 🚀"
|
||||||
|
poweredByMisskeyDescription: "{name} – один из инстансов (также называемый экземпляром Misskey), использующий платформу с открытым исходным кодом <b>Misskey</b>."
|
||||||
monthAndDay: "{day}.{month}"
|
monthAndDay: "{day}.{month}"
|
||||||
search: "Поиск"
|
search: "Поиск"
|
||||||
notifications: "Уведомления"
|
notifications: "Уведомления"
|
||||||
@@ -12,6 +13,7 @@ fetchingAsApObject: "Приём с других сайтов"
|
|||||||
ok: "Окей"
|
ok: "Окей"
|
||||||
gotIt: "Ясно!"
|
gotIt: "Ясно!"
|
||||||
cancel: "Отмена"
|
cancel: "Отмена"
|
||||||
|
noThankYou: "Нет, спасибо"
|
||||||
enterUsername: "Введите имя пользователя"
|
enterUsername: "Введите имя пользователя"
|
||||||
renotedBy: "{user} делится"
|
renotedBy: "{user} делится"
|
||||||
noNotes: "Нет ни одной заметки"
|
noNotes: "Нет ни одной заметки"
|
||||||
@@ -20,7 +22,7 @@ instance: "Инстанс"
|
|||||||
settings: "Настройки"
|
settings: "Настройки"
|
||||||
basicSettings: "Основные настройки"
|
basicSettings: "Основные настройки"
|
||||||
otherSettings: "Прочие настройки"
|
otherSettings: "Прочие настройки"
|
||||||
openInWindow: "Открывать в плавающих окнах"
|
openInWindow: "Открыть в плавающем окне"
|
||||||
profile: "Профиль"
|
profile: "Профиль"
|
||||||
timeline: "Лента"
|
timeline: "Лента"
|
||||||
noAccountDescription: "Пользователь ничего не написал про себя"
|
noAccountDescription: "Пользователь ничего не написал про себя"
|
||||||
@@ -47,6 +49,7 @@ deleteAndEdit: "Удалить и отредактировать"
|
|||||||
deleteAndEditConfirm: "Удалить эту заметку и создать отредактированную? Все реакции, ссылки и ответы на существующую будут будут потеряны."
|
deleteAndEditConfirm: "Удалить эту заметку и создать отредактированную? Все реакции, ссылки и ответы на существующую будут будут потеряны."
|
||||||
addToList: "Добавить в список"
|
addToList: "Добавить в список"
|
||||||
sendMessage: "Отправить сообщение"
|
sendMessage: "Отправить сообщение"
|
||||||
|
copyRSS: "Скопировать RSS"
|
||||||
copyUsername: "Скопировать имя пользователя"
|
copyUsername: "Скопировать имя пользователя"
|
||||||
searchUser: "Поиск людей"
|
searchUser: "Поиск людей"
|
||||||
reply: "Ответить"
|
reply: "Ответить"
|
||||||
@@ -107,6 +110,7 @@ clickToShow: "Нажмите для просмотра"
|
|||||||
sensitive: "Содержимое не для всех"
|
sensitive: "Содержимое не для всех"
|
||||||
add: "Добавить"
|
add: "Добавить"
|
||||||
reaction: "Реакции"
|
reaction: "Реакции"
|
||||||
|
reactions: "Реакции"
|
||||||
reactionSetting: "Реакции, отображаемые в палитре"
|
reactionSetting: "Реакции, отображаемые в палитре"
|
||||||
reactionSettingDescription2: "Расставляйте перетаскиванием, удаляйте нажатием, добавляйте кнопкой «+»."
|
reactionSettingDescription2: "Расставляйте перетаскиванием, удаляйте нажатием, добавляйте кнопкой «+»."
|
||||||
rememberNoteVisibility: "Запоминать видимость заметок"
|
rememberNoteVisibility: "Запоминать видимость заметок"
|
||||||
@@ -269,7 +273,7 @@ light: "Светлый"
|
|||||||
dark: "Тёмный"
|
dark: "Тёмный"
|
||||||
lightThemes: "Светлые темы"
|
lightThemes: "Светлые темы"
|
||||||
darkThemes: "Тёмные темы"
|
darkThemes: "Тёмные темы"
|
||||||
syncDeviceDarkMode: "Синхронизировать с темным режимом устройства"
|
syncDeviceDarkMode: "Синхронизировать с тёмной темой системы"
|
||||||
drive: "Диск"
|
drive: "Диск"
|
||||||
fileName: "Имя файла"
|
fileName: "Имя файла"
|
||||||
selectFile: "Выберите файл"
|
selectFile: "Выберите файл"
|
||||||
@@ -451,6 +455,8 @@ language: "Язык"
|
|||||||
uiLanguage: "Язык интерфейса"
|
uiLanguage: "Язык интерфейса"
|
||||||
groupInvited: "Приглашение в группу"
|
groupInvited: "Приглашение в группу"
|
||||||
aboutX: "Описание {x}"
|
aboutX: "Описание {x}"
|
||||||
|
emojiStyle: "Стиль эмодзи"
|
||||||
|
native: "Системные"
|
||||||
disableDrawer: "Не использовать выдвижные меню"
|
disableDrawer: "Не использовать выдвижные меню"
|
||||||
youHaveNoGroups: "У вас нет ни одной группы"
|
youHaveNoGroups: "У вас нет ни одной группы"
|
||||||
joinOrCreateGroup: "Получайте приглашения в группы или создавайте свои собственные"
|
joinOrCreateGroup: "Получайте приглашения в группы или создавайте свои собственные"
|
||||||
@@ -598,6 +604,7 @@ smtpSecureInfo: "Выключите при использовании STARTTLS."
|
|||||||
testEmail: "Проверка доставки электронной почты"
|
testEmail: "Проверка доставки электронной почты"
|
||||||
wordMute: "Скрытие слов"
|
wordMute: "Скрытие слов"
|
||||||
regexpError: "Ошибка в регулярном выражении"
|
regexpError: "Ошибка в регулярном выражении"
|
||||||
|
regexpErrorDescription: "В списке {tab} скрытых слов, в строке {line} обнаружена синтаксическая ошибка:"
|
||||||
instanceMute: "Глушение инстансов"
|
instanceMute: "Глушение инстансов"
|
||||||
userSaysSomething: "{name} что-то сообщает"
|
userSaysSomething: "{name} что-то сообщает"
|
||||||
makeActive: "Активировать"
|
makeActive: "Активировать"
|
||||||
@@ -708,6 +715,7 @@ accentColor: "Акцент"
|
|||||||
textColor: "Текст"
|
textColor: "Текст"
|
||||||
saveAs: "Сохранить под названием…"
|
saveAs: "Сохранить под названием…"
|
||||||
advanced: "Для продвинутых"
|
advanced: "Для продвинутых"
|
||||||
|
advancedSettings: "Расширенные настройки "
|
||||||
value: "Значения"
|
value: "Значения"
|
||||||
createdAt: "Создано"
|
createdAt: "Создано"
|
||||||
updatedAt: "Обновлено"
|
updatedAt: "Обновлено"
|
||||||
@@ -798,7 +806,7 @@ translate: "Перевод"
|
|||||||
translatedFrom: "Перевод. Язык оригинала — {x}"
|
translatedFrom: "Перевод. Язык оригинала — {x}"
|
||||||
accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи"
|
accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи"
|
||||||
usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере. Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания (_). Имена пользователей не могут быть изменены позже."
|
usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере. Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания (_). Имена пользователей не могут быть изменены позже."
|
||||||
aiChanMode: "ИИ режим"
|
aiChanMode: "Режим Ай"
|
||||||
keepCw: "Сохраняйте Предупреждения о содержимом"
|
keepCw: "Сохраняйте Предупреждения о содержимом"
|
||||||
pubSub: "Учётные записи Pub/Sub"
|
pubSub: "Учётные записи Pub/Sub"
|
||||||
lastCommunication: "Последнее сообщение"
|
lastCommunication: "Последнее сообщение"
|
||||||
@@ -815,8 +823,8 @@ manageAccounts: "Управление аккаунтом"
|
|||||||
makeReactionsPublic: "Опубликовать список реакций"
|
makeReactionsPublic: "Опубликовать список реакций"
|
||||||
makeReactionsPublicDescription: "Список сделанных вами реакций доступен для просмотра всем желающим."
|
makeReactionsPublicDescription: "Список сделанных вами реакций доступен для просмотра всем желающим."
|
||||||
classic: "Классика"
|
classic: "Классика"
|
||||||
muteThread: "Заглушить цепочку"
|
muteThread: "Скрыть цепочку"
|
||||||
unmuteThread: "Отменить глушение цепочки"
|
unmuteThread: "Отменить сокрытие цепочки"
|
||||||
ffVisibility: "Видимость подписок и подписчиков"
|
ffVisibility: "Видимость подписок и подписчиков"
|
||||||
ffVisibilityDescription: "Здесь можно настроить, кто будет видеть ваши подписки и подписчиков."
|
ffVisibilityDescription: "Здесь можно настроить, кто будет видеть ваши подписки и подписчиков."
|
||||||
continueThread: "Показать следующие ответы"
|
continueThread: "Показать следующие ответы"
|
||||||
@@ -839,40 +847,385 @@ numberOfColumn: "Количество столбцов"
|
|||||||
searchByGoogle: "Поиск"
|
searchByGoogle: "Поиск"
|
||||||
instanceDefaultLightTheme: "Светлая тема по умолчанию"
|
instanceDefaultLightTheme: "Светлая тема по умолчанию"
|
||||||
instanceDefaultDarkTheme: "Темная тема по умолчанию"
|
instanceDefaultDarkTheme: "Темная тема по умолчанию"
|
||||||
|
instanceDefaultThemeDescription: "Описание темы по умолчанию для инстанса"
|
||||||
mutePeriod: "Продолжительность скрытия"
|
mutePeriod: "Продолжительность скрытия"
|
||||||
indefinitely: "вечно"
|
indefinitely: "вечно"
|
||||||
tenMinutes: "10 минут"
|
tenMinutes: "10 минут"
|
||||||
oneHour: "1 час"
|
oneHour: "1 час"
|
||||||
oneDay: "1 день"
|
oneDay: "1 день"
|
||||||
oneWeek: "1 неделя"
|
oneWeek: "1 неделя"
|
||||||
|
reflectMayTakeTime: "Изменения могут занять время для отображения"
|
||||||
|
failedToFetchAccountInformation: "Не удалось получить информацию об аккаунте"
|
||||||
cropImage: "Кадрирование"
|
cropImage: "Кадрирование"
|
||||||
cropImageAsk: "Нужно ли кадрировать изображение?"
|
cropImageAsk: "Нужно ли кадрировать изображение?"
|
||||||
file: "Файлы"
|
file: "Файлы"
|
||||||
recentNHours: "Последние {n} ч"
|
recentNHours: "Последние {n} ч"
|
||||||
recentNDays: "Последние {n} сут"
|
recentNDays: "Последние {n} сут"
|
||||||
|
noEmailServerWarning: "Почтовый сервер не установлен "
|
||||||
|
thereIsUnresolvedAbuseReportWarning: "Остались нерешённые жалобы"
|
||||||
recommended: "Рекомендуем"
|
recommended: "Рекомендуем"
|
||||||
check: "Проверить"
|
check: "Проверить"
|
||||||
driveCapOverrideLabel: "Изменение лимита дискового пространства для этого пользователя"
|
driveCapOverrideLabel: "Изменение лимита дискового пространства для этого пользователя"
|
||||||
|
driveCapOverrideCaption: "Укажите меньше или равное нулю для отмены"
|
||||||
|
requireAdminForView: "Для просмотра необходимо иметь аккаунт администратора"
|
||||||
|
isSystemAccount: "Данная учётная запись создана автоматически и управляется системой"
|
||||||
|
typeToConfirm: "Введите {x} для продолжения"
|
||||||
deleteAccount: "Удаление учётной записи"
|
deleteAccount: "Удаление учётной записи"
|
||||||
|
document: "Документ"
|
||||||
|
numberOfPageCache: "Количество сохранённых страниц в кэше"
|
||||||
|
numberOfPageCacheDescription: "Описание количества страниц в кэше"
|
||||||
|
logoutConfirm: "Вы хотите выйти из аккаунта?"
|
||||||
|
lastActiveDate: "Последняя дата использования"
|
||||||
|
statusbar: "Статусбар"
|
||||||
|
pleaseSelect: "Пожалуйста, выберите"
|
||||||
reverse: "Переворот"
|
reverse: "Переворот"
|
||||||
colored: "Выделена цветом"
|
colored: "Выделена цветом"
|
||||||
|
refreshInterval: "Интервал перезагрузки"
|
||||||
label: "Метка"
|
label: "Метка"
|
||||||
|
type: "Тип"
|
||||||
|
speed: "Скорость"
|
||||||
|
sensitiveMediaDetection: "Определение содержимого деликатного характера"
|
||||||
localOnly: "Локально"
|
localOnly: "Локально"
|
||||||
|
remoteOnly: "Только удалённо"
|
||||||
|
failedToUpload: "Сбой выгрузки"
|
||||||
|
cannotUploadBecauseInappropriate: "Файл не может быть загружен, так как было установлено, что он может содержать неприемлемое содержимое."
|
||||||
|
cannotUploadBecauseNoFreeSpace: "Файл не может быть загружен, так как не осталось места на диске"
|
||||||
beta: "Бета"
|
beta: "Бета"
|
||||||
enableAutoSensitive: "Автоматическое определение NSFW"
|
enableAutoSensitive: "Автоматическое определение NSFW"
|
||||||
enableAutoSensitiveDescription: "Если доступно, используйте машинное обучение для автоматической установки флага NSFW на носителе. Даже если эта функция отключена, она может быть установлена автоматически в зависимости от инстанта."
|
enableAutoSensitiveDescription: "Если доступно, используйте машинное обучение для автоматической установки флага NSFW на носителе. Даже если эта функция отключена, она может быть установлена автоматически в зависимости от инстанта."
|
||||||
|
activeEmailValidationDescription: "Если включено, будет проводиться более строгая проверка адреса электронной почты, в том числе на то, что он действительный и не временный. Если же отключено, то проверяется только корректность написания адреса."
|
||||||
|
navbar: "Панель навигации"
|
||||||
|
shuffle: "Перемешать"
|
||||||
account: "Учётные записи"
|
account: "Учётные записи"
|
||||||
|
move: "Переместить"
|
||||||
|
pushNotification: "Push-уведомления"
|
||||||
|
subscribePushNotification: "Включить push-уведомления"
|
||||||
|
unsubscribePushNotification: "Выключить push-уведомления"
|
||||||
|
pushNotificationAlreadySubscribed: "Push-уведомления уже включены"
|
||||||
|
pushNotificationNotSupported: "Push-уведмления не поддерживаются инстансом или браузером"
|
||||||
|
sendPushNotificationReadMessage: "Удалять push-уведомления когда сообщение или прочитано"
|
||||||
|
sendPushNotificationReadMessageCaption: "На мгновение появится уведомление \"{emptyPushNotificationMessage}\". Расход заряда батареи может увеличиться "
|
||||||
windowMaximize: "Развернуть"
|
windowMaximize: "Развернуть"
|
||||||
windowRestore: "Восстановить"
|
windowRestore: "Восстановить"
|
||||||
|
caption: "Подпись (Automatic Translation)"
|
||||||
|
loggedInAsBot: "Вы под аккаунтом бота!"
|
||||||
|
tools: "Инструменты"
|
||||||
|
cannotLoad: "Не удалось загрузить"
|
||||||
|
numberOfProfileView: "Количество профилей для просмотра"
|
||||||
like: "Нравится!"
|
like: "Нравится!"
|
||||||
|
unlike: "Отменить «нравится»"
|
||||||
|
numberOfLikes: "Количество лайков"
|
||||||
show: "Отображение"
|
show: "Отображение"
|
||||||
|
neverShow: "Больше не показывать"
|
||||||
|
remindMeLater: "Напомнить позже"
|
||||||
|
didYouLikeMisskey: "Вам нравится Misskey?"
|
||||||
|
pleaseDonate: "Сайт {host} работает на Misskey. Это бесплатное программное обеспечение, и ваши пожертвования очень бы помогли продолжать его разработку!"
|
||||||
|
roles: "Роли"
|
||||||
|
role: "Роль"
|
||||||
|
normalUser: "Обычный пользователь"
|
||||||
|
undefined: "неопределён"
|
||||||
|
assign: "Назначить"
|
||||||
|
unassign: "Отменить назначение"
|
||||||
color: "Цвет"
|
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:
|
_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: "Приоритет"
|
||||||
_priority:
|
_priority:
|
||||||
low: "Низкий"
|
low: "Низкий"
|
||||||
middle: "Средне"
|
middle: "Средне"
|
||||||
high: "Высокий"
|
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:
|
_sensitiveMediaDetection:
|
||||||
description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно."
|
description: "Машинное обучение может быть использовано для автоматического обнаружения чувствительных медиа для модерации. Нагрузка на сервер увеличивается незначительно."
|
||||||
setSensitiveFlagAutomatically: "Установить флаг NSFW"
|
setSensitiveFlagAutomatically: "Установить флаг NSFW"
|
||||||
@@ -919,6 +1272,24 @@ _plugin:
|
|||||||
install: "Установка расширений"
|
install: "Установка расширений"
|
||||||
installWarn: "Пожалуйста, не устанавливайте расширения, которым не доверяете."
|
installWarn: "Пожалуйста, не устанавливайте расширения, которым не доверяете."
|
||||||
manage: "Управление расширениями"
|
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:
|
_registry:
|
||||||
scope: "Область"
|
scope: "Область"
|
||||||
key: "Ключ"
|
key: "Ключ"
|
||||||
@@ -1002,6 +1373,8 @@ _mfm:
|
|||||||
sparkleDescription: "Добавляет эффект искрящихся частиц."
|
sparkleDescription: "Добавляет эффект искрящихся частиц."
|
||||||
rotate: "Повернуть"
|
rotate: "Повернуть"
|
||||||
rotateDescription: "Поворачивает на заданный угол."
|
rotateDescription: "Поворачивает на заданный угол."
|
||||||
|
plain: "Буквально"
|
||||||
|
plainDescription: "MFM внутри отключается, и текст отображается как есть"
|
||||||
_instanceTicker:
|
_instanceTicker:
|
||||||
none: "Не показывать"
|
none: "Не показывать"
|
||||||
remote: "Только для других сайтов"
|
remote: "Только для других сайтов"
|
||||||
@@ -1031,12 +1404,14 @@ _wordMute:
|
|||||||
muteWordsDescription2: "Здесь можно использовать регулярные выражения — просто заключите их между двумя дробными чертами (/)."
|
muteWordsDescription2: "Здесь можно использовать регулярные выражения — просто заключите их между двумя дробными чертами (/)."
|
||||||
softDescription: "Соответствующие условиям заметки будут спрятаны из вашей ленты."
|
softDescription: "Соответствующие условиям заметки будут спрятаны из вашей ленты."
|
||||||
hardDescription: "Соответстующие условиям заметки вообще не будут попадать в вашу ленту. Даже если вы поменяете условия, отсеенные таким образом заметки уже не появятся."
|
hardDescription: "Соответстующие условиям заметки вообще не будут попадать в вашу ленту. Даже если вы поменяете условия, отсеенные таким образом заметки уже не появятся."
|
||||||
soft: "Мягкий"
|
soft: "Мягко"
|
||||||
hard: "Жёсткий"
|
hard: "Жёстко"
|
||||||
mutedNotes: "Скрытые заметки"
|
mutedNotes: "Скрытые заметки"
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
|
instanceMuteDescription: "Заметки и репосты с указанных здесь инстансов, а также ответы пользователям оттуда же не будут отображаться."
|
||||||
|
instanceMuteDescription2: "Пишите каждый инстанс на отдельной строке"
|
||||||
title: "Скрывает заметки с заданных инстансов."
|
title: "Скрывает заметки с заданных инстансов."
|
||||||
heading: "Список заглушенных инстансов"
|
heading: "Список скрытых инстансов"
|
||||||
_theme:
|
_theme:
|
||||||
explore: "Обзор"
|
explore: "Обзор"
|
||||||
install: "Установить тему"
|
install: "Установить тему"
|
||||||
@@ -1157,12 +1532,16 @@ _tutorial:
|
|||||||
step7_1: "На этом вводный урок по использованию Misskey закончен. Спасибо, что прошли его до конца!"
|
step7_1: "На этом вводный урок по использованию Misskey закончен. Спасибо, что прошли его до конца!"
|
||||||
step7_2: "Хотите изучить Misskey глубже — добро пожаловать в раздел «{help}»."
|
step7_2: "Хотите изучить Misskey глубже — добро пожаловать в раздел «{help}»."
|
||||||
step7_3: "Приятно вам провести время с Misskey🚀"
|
step7_3: "Приятно вам провести время с Misskey🚀"
|
||||||
|
step8_1: "Ах, да, не хотите ли включить push-уведомления?"
|
||||||
|
step8_2: "С push-уведомлениями вы будете в курсе репостов, ответов, реакций и всего такого, даже когда закрыли Misskey."
|
||||||
|
step8_3: "Эту настройку вы всегда сможете поменять"
|
||||||
_2fa:
|
_2fa:
|
||||||
alreadyRegistered: "Двухфакторная аутентификация уже настроена."
|
alreadyRegistered: "Двухфакторная аутентификация уже настроена."
|
||||||
registerDevice: "Зарегистрируйте ваше устройство"
|
registerDevice: "Зарегистрируйте ваше устройство"
|
||||||
registerKey: "Зарегистрировать ключ"
|
registerKey: "Зарегистрировать ключ"
|
||||||
step1: "Прежде всего, установите на устройство приложение для аутентификации, например, {a} или {b}."
|
step1: "Прежде всего, установите на устройство приложение для аутентификации, например, {a} или {b}."
|
||||||
step2: "Далее отсканируйте отображаемый QR-код при помощи приложения."
|
step2: "Далее отсканируйте отображаемый QR-код при помощи приложения."
|
||||||
|
step2Url: "Если пользуетесь приложением на компьютере, можете ввести в него эту строку (URL):"
|
||||||
step3: "И наконец, введите код, который покажет приложение."
|
step3: "И наконец, введите код, который покажет приложение."
|
||||||
step4: "Теперь при каждом входе на сайт вам нужно будет вводить код из приложения аналогичным образом."
|
step4: "Теперь при каждом входе на сайт вам нужно будет вводить код из приложения аналогичным образом."
|
||||||
securityKeyInfo: "Вы можете настроить вход с помощью аппаратного ключа безопасности, поддерживающего FIDO2, или отпечатка пальца или PIN-кода на устройстве."
|
securityKeyInfo: "Вы можете настроить вход с помощью аппаратного ключа безопасности, поддерживающего FIDO2, или отпечатка пальца или PIN-кода на устройстве."
|
||||||
@@ -1179,7 +1558,7 @@ _permissions:
|
|||||||
"write:following": "Изменять спискок подписок"
|
"write:following": "Изменять спискок подписок"
|
||||||
"read:messaging": "Смотреть сообщения"
|
"read:messaging": "Смотреть сообщения"
|
||||||
"write:messaging": "Писать и удалять сообщения"
|
"write:messaging": "Писать и удалять сообщения"
|
||||||
"read:mutes": "Смотреть спискок скрытых пользователей"
|
"read:mutes": "Смотреть список скрытых пользователей"
|
||||||
"write:mutes": "Изменять список скрытых пользователей"
|
"write:mutes": "Изменять список скрытых пользователей"
|
||||||
"write:notes": "Писать и удалять заметки"
|
"write:notes": "Писать и удалять заметки"
|
||||||
"read:notifications": "Смотреть уведомления"
|
"read:notifications": "Смотреть уведомления"
|
||||||
@@ -1230,10 +1609,13 @@ _widgets:
|
|||||||
trends: "Актуальное"
|
trends: "Актуальное"
|
||||||
clock: "Часы"
|
clock: "Часы"
|
||||||
rss: "Просмотр RSS"
|
rss: "Просмотр RSS"
|
||||||
|
rssTicker: "Бегущая строка RSS"
|
||||||
activity: "Активность"
|
activity: "Активность"
|
||||||
photos: "Фото"
|
photos: "Фото"
|
||||||
digitalClock: "Цифровые часы"
|
digitalClock: "Цифровые часы"
|
||||||
|
unixClock: "Часы UNIX"
|
||||||
federation: "Федерация"
|
federation: "Федерация"
|
||||||
|
instanceCloud: "Облако инстансов"
|
||||||
postForm: "Форма отправки"
|
postForm: "Форма отправки"
|
||||||
slideshow: "Показ слайдов"
|
slideshow: "Показ слайдов"
|
||||||
button: "Кнопка"
|
button: "Кнопка"
|
||||||
@@ -1241,9 +1623,12 @@ _widgets:
|
|||||||
jobQueue: "Очередь заданий"
|
jobQueue: "Очередь заданий"
|
||||||
serverMetric: "Показатели сервера"
|
serverMetric: "Показатели сервера"
|
||||||
aiscript: "Консоль AiScript"
|
aiscript: "Консоль AiScript"
|
||||||
|
aiscriptApp: "Приложение на AiScript"
|
||||||
aichan: "Ай"
|
aichan: "Ай"
|
||||||
|
userList: "Список аккаунтов"
|
||||||
_userList:
|
_userList:
|
||||||
chooseList: "Выберите список"
|
chooseList: "Выберите список"
|
||||||
|
clicker: "Счётчик щелчков"
|
||||||
_cw:
|
_cw:
|
||||||
hide: "Спрятать"
|
hide: "Спрятать"
|
||||||
show: "Показать еще"
|
show: "Показать еще"
|
||||||
@@ -1306,12 +1691,13 @@ _profile:
|
|||||||
changeAvatar: "Поменять аватар"
|
changeAvatar: "Поменять аватар"
|
||||||
changeBanner: "Поменять изображение в шапке"
|
changeBanner: "Поменять изображение в шапке"
|
||||||
_exportOrImport:
|
_exportOrImport:
|
||||||
allNotes: "Все записи\n"
|
allNotes: "Все заметки\n"
|
||||||
|
favoritedNotes: "Избранное"
|
||||||
followingList: "Подписки"
|
followingList: "Подписки"
|
||||||
muteList: "Скрытые"
|
muteList: "Скрытые"
|
||||||
blockingList: "Заблокированные"
|
blockingList: "Заблокированные"
|
||||||
userLists: "Списки"
|
userLists: "Списки"
|
||||||
excludeMutingUsers: "За исключением заглушенных пользователей"
|
excludeMutingUsers: "За исключением скрытых пользователей"
|
||||||
excludeInactiveUsers: "Без неактивных учётных записей"
|
excludeInactiveUsers: "Без неактивных учётных записей"
|
||||||
_charts:
|
_charts:
|
||||||
federation: "Федерация"
|
federation: "Федерация"
|
||||||
@@ -1415,6 +1801,9 @@ _notification:
|
|||||||
youReceivedFollowRequest: "У вас новый запрос на подписку."
|
youReceivedFollowRequest: "У вас новый запрос на подписку."
|
||||||
yourFollowRequestAccepted: "Ваш запрос на подписку одобрен."
|
yourFollowRequestAccepted: "Ваш запрос на подписку одобрен."
|
||||||
youWereInvitedToGroup: "Вы приглашены в группу."
|
youWereInvitedToGroup: "Вы приглашены в группу."
|
||||||
|
pollEnded: "Подведены окончательные итоги опроса"
|
||||||
|
emptyPushNotificationMessage: "Обновлены push-уведомления"
|
||||||
|
achievementEarned: "Получено достижение"
|
||||||
_types:
|
_types:
|
||||||
all: "Все"
|
all: "Все"
|
||||||
follow: "Подписки"
|
follow: "Подписки"
|
||||||
@@ -1423,11 +1812,13 @@ _notification:
|
|||||||
renote: "Репосты"
|
renote: "Репосты"
|
||||||
quote: "Цитаты"
|
quote: "Цитаты"
|
||||||
reaction: "Реакции"
|
reaction: "Реакции"
|
||||||
|
pollEnded: "Окончания опросов"
|
||||||
receiveFollowRequest: "Получен запрос на подписку"
|
receiveFollowRequest: "Получен запрос на подписку"
|
||||||
followRequestAccepted: "Запрос на подписку одобрен"
|
followRequestAccepted: "Запрос на подписку одобрен"
|
||||||
groupInvited: "Приглашение в группы"
|
groupInvited: "Приглашение в группы"
|
||||||
app: "Уведомления из приложений"
|
app: "Уведомления из приложений"
|
||||||
_actions:
|
_actions:
|
||||||
|
followBack: "отвечает взаимной подпиской"
|
||||||
reply: "Ответить"
|
reply: "Ответить"
|
||||||
renote: "Репост"
|
renote: "Репост"
|
||||||
_deck:
|
_deck:
|
||||||
@@ -1441,7 +1832,12 @@ _deck:
|
|||||||
swapDown: "Переставить ниже"
|
swapDown: "Переставить ниже"
|
||||||
stackLeft: "В столбик влево"
|
stackLeft: "В столбик влево"
|
||||||
popRight: "Из столбика вправо"
|
popRight: "Из столбика вправо"
|
||||||
profile: "Профиль"
|
profile: "Расстановка"
|
||||||
|
newProfile: "Новая расстановка"
|
||||||
|
deleteProfile: "Удаление расстановки"
|
||||||
|
introduction: "Создайте идеальный интерфейс расставляя колонки как угодно"
|
||||||
|
introduction2: "Чтобы добавлять колонки в любом месте, жмите «+» справа экрана."
|
||||||
|
widgetsIntroduction: "Чтобы добавлять виджеты, выбирайте «Редактировать виджеты» в меню колонки."
|
||||||
_columns:
|
_columns:
|
||||||
main: "Основная"
|
main: "Основная"
|
||||||
widgets: "Виджеты"
|
widgets: "Виджеты"
|
||||||
|
@@ -110,6 +110,7 @@ clickToShow: "Kliknutím zobrazíte"
|
|||||||
sensitive: "NSFW"
|
sensitive: "NSFW"
|
||||||
add: "Pridať"
|
add: "Pridať"
|
||||||
reaction: "Reakcie"
|
reaction: "Reakcie"
|
||||||
|
reactions: "Reakcie"
|
||||||
reactionSetting: "Reakcie zobrazené vo výbere reakcií"
|
reactionSetting: "Reakcie zobrazené vo výbere reakcií"
|
||||||
reactionSettingDescription2: "Ťahaním preusporiadate, kliknutím odstránite, Stlačením \"+\" pridáte"
|
reactionSettingDescription2: "Ťahaním preusporiadate, kliknutím odstránite, Stlačením \"+\" pridáte"
|
||||||
rememberNoteVisibility: "Zapamätať nastavenia viditeľnosti poznámky"
|
rememberNoteVisibility: "Zapamätať nastavenia viditeľnosti poznámky"
|
||||||
|
@@ -110,6 +110,7 @@ clickToShow: "Klicka för att visa"
|
|||||||
sensitive: "Känsligt innehåll"
|
sensitive: "Känsligt innehåll"
|
||||||
add: "Lägg till"
|
add: "Lägg till"
|
||||||
reaction: "Reaktioner"
|
reaction: "Reaktioner"
|
||||||
|
reactions: "Reaktioner"
|
||||||
reactionSetting: "Reaktioner som ska visas i reaktionsväljaren"
|
reactionSetting: "Reaktioner som ska visas i reaktionsväljaren"
|
||||||
reactionSettingDescription2: "Dra för att omordna, klicka för att radera, tryck \"+\" för att lägga till."
|
reactionSettingDescription2: "Dra för att omordna, klicka för att radera, tryck \"+\" för att lägga till."
|
||||||
rememberNoteVisibility: "Komihåg notvisningsinställningar"
|
rememberNoteVisibility: "Komihåg notvisningsinställningar"
|
||||||
|
@@ -110,6 +110,7 @@ clickToShow: "คลิกเพื่อแสดง"
|
|||||||
sensitive: "เนื้อหาที่ละเอียดอ่อน NSFW"
|
sensitive: "เนื้อหาที่ละเอียดอ่อน NSFW"
|
||||||
add: "เพิ่ม"
|
add: "เพิ่ม"
|
||||||
reaction: "รีแอคชั่น"
|
reaction: "รีแอคชั่น"
|
||||||
|
reactions: "รีแอคชั่น"
|
||||||
reactionSetting: "รีแอคชั่นไปยังแสดงผลในตัวเลือกการรีแอคชั่น"
|
reactionSetting: "รีแอคชั่นไปยังแสดงผลในตัวเลือกการรีแอคชั่น"
|
||||||
reactionSettingDescription2: "กดลากเพื่อจัดลำดับใหม่ กดคลิกเพื่อลบ กด \"+\" เพื่อเพิ่ม"
|
reactionSettingDescription2: "กดลากเพื่อจัดลำดับใหม่ กดคลิกเพื่อลบ กด \"+\" เพื่อเพิ่ม"
|
||||||
rememberNoteVisibility: "จดจำการตั้งค่าการมองเห็นตัวโน้ต"
|
rememberNoteVisibility: "จดจำการตั้งค่าการมองเห็นตัวโน้ต"
|
||||||
@@ -932,6 +933,248 @@ assign: "กำหนด"
|
|||||||
unassign: "ยังไม่มอบหมาย"
|
unassign: "ยังไม่มอบหมาย"
|
||||||
color: "สี"
|
color: "สี"
|
||||||
manageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง"
|
manageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง"
|
||||||
|
youCannotCreateAnymore: "คุณถึงขีดจํากัดการสร้างแล้วนะ"
|
||||||
|
cannotPerformTemporary: "ไม่สามารถใช้การได้ชั่วคราว"
|
||||||
|
cannotPerformTemporaryDescription: "การดําเนินการนี้ไม่สามารถดําเนินการได้ชั่วคราว เนื่องจากเกินขีดจํากัดการดําเนินการ กรุณารอสักครู่แล้วลองใหม่อีกครั้งนะค่ะ"
|
||||||
|
preset: "พรีเซ็ต"
|
||||||
|
selectFromPresets: "เลือกจากการพรีเซ็ต"
|
||||||
|
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:
|
_role:
|
||||||
new: "บทบาทใหม่"
|
new: "บทบาทใหม่"
|
||||||
edit: "แก้ไขบทบาท"
|
edit: "แก้ไขบทบาท"
|
||||||
@@ -948,6 +1191,7 @@ _role:
|
|||||||
isPublic: "บทบาทสาธารณะ"
|
isPublic: "บทบาทสาธารณะ"
|
||||||
descriptionOfIsPublic: "ทุกคนสามารถดูได้ว่าผู้ใช้งานนั้นได้รับมอบหมายบทบาทด้วยหรือไม่ \n\nบทบาทจะแสดงในโปรไฟล์ของผู้ใช้ด้วย"
|
descriptionOfIsPublic: "ทุกคนสามารถดูได้ว่าผู้ใช้งานนั้นได้รับมอบหมายบทบาทด้วยหรือไม่ \n\nบทบาทจะแสดงในโปรไฟล์ของผู้ใช้ด้วย"
|
||||||
options: "ตัวเลือกบทบาท"
|
options: "ตัวเลือกบทบาท"
|
||||||
|
policies: "นโยบาย"
|
||||||
baseRole: "บทบาทพื้นฐาน"
|
baseRole: "บทบาทพื้นฐาน"
|
||||||
useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น"
|
useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น"
|
||||||
chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด"
|
chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด"
|
||||||
@@ -965,7 +1209,17 @@ _role:
|
|||||||
canInvite: "สร้างรหัสเชิญอินสแตนซ์"
|
canInvite: "สร้างรหัสเชิญอินสแตนซ์"
|
||||||
canManageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง"
|
canManageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง"
|
||||||
driveCapacity: "ความจุของไดรฟ์"
|
driveCapacity: "ความจุของไดรฟ์"
|
||||||
|
pinMax: "จํานวนสูงสุดของโน้ตที่ปักหมุดไว้"
|
||||||
antennaMax: "จำนวนสูงสุดของเสาอากาศ"
|
antennaMax: "จำนวนสูงสุดของเสาอากาศ"
|
||||||
|
wordMuteMax: "จำนวนอักขระสูงสุดที่อนุญาตในการปิดเสียงคำ"
|
||||||
|
webhookMax: "จำนวนเว็บฮุคสูงสุด"
|
||||||
|
clipMax: "จำนวนคลิปสูงสุด"
|
||||||
|
noteEachClipsMax: "จำนวนโน้ตสูงสุดภายในคลิป"
|
||||||
|
userListMax: "จำนวนรายชื่อผู้ใช้สูงสุด"
|
||||||
|
userEachUserListsMax: "จำนวนผู้ใช้สูงสุดภายในรายการผู้ใช้"
|
||||||
|
rateLimitFactor: "ขีดจำกัดอัตรา"
|
||||||
|
descriptionOfRateLimitFactor: "ขีดจํากัดอัตราที่ต่ำกว่ามีข้อจํากัดน้อยกว่าข้อจํากัดที่สูงกว่า"
|
||||||
|
canHideAds: "ซ่อนโฆษณา"
|
||||||
_condition:
|
_condition:
|
||||||
isLocal: "ผู้ใช้ภายใน"
|
isLocal: "ผู้ใช้ภายใน"
|
||||||
isRemote: "ผู้ใช้ระยะไกล"
|
isRemote: "ผู้ใช้ระยะไกล"
|
||||||
@@ -1570,6 +1824,7 @@ _notification:
|
|||||||
pollEnded: "โพลสำรวจความคิดเห็นผลลัพธ์มีพร้อมใช้งาน"
|
pollEnded: "โพลสำรวจความคิดเห็นผลลัพธ์มีพร้อมใช้งาน"
|
||||||
unreadAntennaNote: "เสาอากาศ {name}"
|
unreadAntennaNote: "เสาอากาศ {name}"
|
||||||
emptyPushNotificationMessage: "การแจ้งเตือนแบบพุชได้รับการอัพเดทแล้ว"
|
emptyPushNotificationMessage: "การแจ้งเตือนแบบพุชได้รับการอัพเดทแล้ว"
|
||||||
|
achievementEarned: "รับความสำเร็จ"
|
||||||
_types:
|
_types:
|
||||||
all: "ทั้งหมด"
|
all: "ทั้งหมด"
|
||||||
follow: "กำลังติดตาม"
|
follow: "กำลังติดตาม"
|
||||||
|
@@ -109,6 +109,7 @@ clickToShow: "Натисніть для перегляду"
|
|||||||
sensitive: "NSFW"
|
sensitive: "NSFW"
|
||||||
add: "Додати"
|
add: "Додати"
|
||||||
reaction: "Реакції"
|
reaction: "Реакції"
|
||||||
|
reactions: "Реакції"
|
||||||
reactionSetting: "Налаштування реакцій"
|
reactionSetting: "Налаштування реакцій"
|
||||||
reactionSettingDescription2: "Перемістити щоб змінити порядок, Клацнути мишою щоб видалити, Натиснути \"+\" щоб додати."
|
reactionSettingDescription2: "Перемістити щоб змінити порядок, Клацнути мишою щоб видалити, Натиснути \"+\" щоб додати."
|
||||||
rememberNoteVisibility: "Пам’ятати параметри видимісті"
|
rememberNoteVisibility: "Пам’ятати параметри видимісті"
|
||||||
@@ -687,7 +688,7 @@ pageLikesCount: "Кількість отриманих вподобань сто
|
|||||||
pageLikedCount: "Кількість вподобаних сторінок"
|
pageLikedCount: "Кількість вподобаних сторінок"
|
||||||
contact: "Контакт"
|
contact: "Контакт"
|
||||||
useSystemFont: "Використовувати стандартний шрифт системи"
|
useSystemFont: "Використовувати стандартний шрифт системи"
|
||||||
clips: "Добірка"
|
clips: "Добірки"
|
||||||
experimentalFeatures: "Експериментальні функції"
|
experimentalFeatures: "Експериментальні функції"
|
||||||
developer: "Розробник"
|
developer: "Розробник"
|
||||||
makeExplorable: "Зробіть обліковий запис видимим у розділі \"Огляд\""
|
makeExplorable: "Зробіть обліковий запис видимим у розділі \"Огляд\""
|
||||||
@@ -898,6 +899,212 @@ unlike: "Не вподобати"
|
|||||||
numberOfLikes: "Вподобання"
|
numberOfLikes: "Вподобання"
|
||||||
show: "Відображення"
|
show: "Відображення"
|
||||||
color: "Колір"
|
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: "Аналітик"
|
||||||
|
_clickedClickHere:
|
||||||
|
title: "Натисніть тут"
|
||||||
|
description: "Натиснуто тут"
|
||||||
|
_setNameToSyuilo:
|
||||||
|
title: "Комплекс бога"
|
||||||
|
description: "Встановлено ім'я \"syuilo\""
|
||||||
|
_passedSinceAccountCreated1:
|
||||||
|
title: "Перша річниця"
|
||||||
|
_passedSinceAccountCreated2:
|
||||||
|
title: "Друга річниця"
|
||||||
|
_passedSinceAccountCreated3:
|
||||||
|
title: "Третя річниця"
|
||||||
|
description: "Минуло 3 роки з моменту створення акаунта"
|
||||||
|
_loggedInOnBirthday:
|
||||||
|
title: "З Днем народження!"
|
||||||
|
_loggedInOnNewYearsDay:
|
||||||
|
description: "Увійшли в перший день року"
|
||||||
|
_brainDiver:
|
||||||
|
title: "Brain Diver"
|
||||||
|
description: "Відправити посилання на \"Brain Diver\""
|
||||||
|
flavor: "Misskey-Misskey La-Tu-Ma"
|
||||||
_role:
|
_role:
|
||||||
priority: "Пріоритет"
|
priority: "Пріоритет"
|
||||||
_priority:
|
_priority:
|
||||||
@@ -1170,7 +1377,7 @@ _tutorial:
|
|||||||
step3_1: "Ви успішно налаштували свій обліковий запис?"
|
step3_1: "Ви успішно налаштували свій обліковий запис?"
|
||||||
step3_2: "Наступним кроком є написання нотатки. Це можна зробити, натиснувши зображення олівця на екрані."
|
step3_2: "Наступним кроком є написання нотатки. Це можна зробити, натиснувши зображення олівця на екрані."
|
||||||
step3_3: "Після написання вмісту ви можете опублікувати його, натиснувши кнопку у верхньому правому куті форми."
|
step3_3: "Після написання вмісту ви можете опублікувати його, натиснувши кнопку у верхньому правому куті форми."
|
||||||
step3_4: "Не знаєте що написати? Спробуйте \"налаштовую свій msky\"!"
|
step3_4: "Не знаєте що написати? Спробуйте \"Привіт, Misskey!\""
|
||||||
step4_1: "Ви розмістили свій перший запис?"
|
step4_1: "Ви розмістили свій перший запис?"
|
||||||
step4_2: "Ура! Ваш перший запис відображається на вашій стрічці подій."
|
step4_2: "Ура! Ваш перший запис відображається на вашій стрічці подій."
|
||||||
step5_1: "Настав час оживити вашу стрічку подій підписавшись на інших користувачів."
|
step5_1: "Настав час оживити вашу стрічку подій підписавшись на інших користувачів."
|
||||||
@@ -1434,6 +1641,7 @@ _notification:
|
|||||||
youReceivedFollowRequest: "Ви отримали запит на підписку"
|
youReceivedFollowRequest: "Ви отримали запит на підписку"
|
||||||
yourFollowRequestAccepted: "Запит на підписку прийнято"
|
yourFollowRequestAccepted: "Запит на підписку прийнято"
|
||||||
youWereInvitedToGroup: "Запрошення до групи"
|
youWereInvitedToGroup: "Запрошення до групи"
|
||||||
|
achievementEarned: "Досягнення відкрито"
|
||||||
_types:
|
_types:
|
||||||
all: "Все"
|
all: "Все"
|
||||||
follow: "Підписки"
|
follow: "Підписки"
|
||||||
|
@@ -107,6 +107,7 @@ clickToShow: "Nhấn để xem"
|
|||||||
sensitive: "Nhạy cảm"
|
sensitive: "Nhạy cảm"
|
||||||
add: "Thêm"
|
add: "Thêm"
|
||||||
reaction: "Biểu cảm"
|
reaction: "Biểu cảm"
|
||||||
|
reactions: "Biểu cảm"
|
||||||
reactionSetting: "Chọn những biểu cảm hiển thị"
|
reactionSetting: "Chọn những biểu cảm hiển thị"
|
||||||
reactionSettingDescription2: "Kéo để sắp xếp, nhấn để xóa, nhấn \"+\" để thêm."
|
reactionSettingDescription2: "Kéo để sắp xếp, nhấn để xóa, nhấn \"+\" để thêm."
|
||||||
rememberNoteVisibility: "Lưu kiểu tút mặc định"
|
rememberNoteVisibility: "Lưu kiểu tút mặc định"
|
||||||
|
@@ -110,6 +110,7 @@ clickToShow: "点击以显示"
|
|||||||
sensitive: "敏感内容"
|
sensitive: "敏感内容"
|
||||||
add: "添加"
|
add: "添加"
|
||||||
reaction: "回应"
|
reaction: "回应"
|
||||||
|
reactions: "回应"
|
||||||
reactionSetting: "在选择器中显示的回应"
|
reactionSetting: "在选择器中显示的回应"
|
||||||
reactionSettingDescription2: "拖动重新排序,单击删除,点击 + 添加。"
|
reactionSettingDescription2: "拖动重新排序,单击删除,点击 + 添加。"
|
||||||
rememberNoteVisibility: "保存上次设置的可见性"
|
rememberNoteVisibility: "保存上次设置的可见性"
|
||||||
@@ -937,6 +938,225 @@ cannotPerformTemporary: "暂时不可用"
|
|||||||
cannotPerformTemporaryDescription: "因操作过于频繁,暂时不可用,请稍后再试。"
|
cannotPerformTemporaryDescription: "因操作过于频繁,暂时不可用,请稍后再试。"
|
||||||
preset: "預設值"
|
preset: "預設值"
|
||||||
selectFromPresets: "從預設值中選擇"
|
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: "引用了自己的帖子"
|
||||||
|
_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:
|
_role:
|
||||||
new: "创建角色"
|
new: "创建角色"
|
||||||
edit: "编辑角色"
|
edit: "编辑角色"
|
||||||
@@ -1455,7 +1675,7 @@ _profile:
|
|||||||
name: "昵称"
|
name: "昵称"
|
||||||
username: "用户名"
|
username: "用户名"
|
||||||
description: "个人简介"
|
description: "个人简介"
|
||||||
youCanIncludeHashtags: "您可以包含一个哈希标签。"
|
youCanIncludeHashtags: "你可以在个人简介中包含一个#标签。"
|
||||||
metadata: "附加信息"
|
metadata: "附加信息"
|
||||||
metadataEdit: "附加信息编辑"
|
metadataEdit: "附加信息编辑"
|
||||||
metadataDescription: "最多可以在个人资料中以表格形式显示四条其他信息。"
|
metadataDescription: "最多可以在个人资料中以表格形式显示四条其他信息。"
|
||||||
@@ -1586,6 +1806,7 @@ _notification:
|
|||||||
pollEnded: "问卷调查结果已生成。"
|
pollEnded: "问卷调查结果已生成。"
|
||||||
unreadAntennaNote: "天线 {name}"
|
unreadAntennaNote: "天线 {name}"
|
||||||
emptyPushNotificationMessage: "推送通知已更新"
|
emptyPushNotificationMessage: "推送通知已更新"
|
||||||
|
achievementEarned: "获得成就"
|
||||||
_types:
|
_types:
|
||||||
all: "全部"
|
all: "全部"
|
||||||
follow: "关注中"
|
follow: "关注中"
|
||||||
|
@@ -110,6 +110,7 @@ clickToShow: "按一下以顯示"
|
|||||||
sensitive: "敏感內容"
|
sensitive: "敏感內容"
|
||||||
add: "新增"
|
add: "新增"
|
||||||
reaction: "情感"
|
reaction: "情感"
|
||||||
|
reactions: "情感"
|
||||||
reactionSetting: "在選擇器中顯示反應"
|
reactionSetting: "在選擇器中顯示反應"
|
||||||
reactionSettingDescription2: "拖動以重新列序,點擊以刪除,按下 + 添加。"
|
reactionSettingDescription2: "拖動以重新列序,點擊以刪除,按下 + 添加。"
|
||||||
rememberNoteVisibility: "記住貼文可見性"
|
rememberNoteVisibility: "記住貼文可見性"
|
||||||
@@ -239,7 +240,7 @@ removeAreYouSure: "確定要刪掉「{x}」嗎?"
|
|||||||
deleteAreYouSure: "確定要刪掉「{x}」嗎?"
|
deleteAreYouSure: "確定要刪掉「{x}」嗎?"
|
||||||
resetAreYouSure: "確定要重設嗎?"
|
resetAreYouSure: "確定要重設嗎?"
|
||||||
saved: "已儲存"
|
saved: "已儲存"
|
||||||
messaging: "傳送訊息"
|
messaging: "聊天"
|
||||||
upload: "上傳"
|
upload: "上傳"
|
||||||
keepOriginalUploading: "保留原圖"
|
keepOriginalUploading: "保留原圖"
|
||||||
keepOriginalUploadingDescription: "上傳圖片時保留原始圖片。關閉時,瀏覽器會在上傳時生成一張用於web發布的圖片。"
|
keepOriginalUploadingDescription: "上傳圖片時保留原始圖片。關閉時,瀏覽器會在上傳時生成一張用於web發布的圖片。"
|
||||||
@@ -330,10 +331,10 @@ registration: "註冊"
|
|||||||
enableRegistration: "開啟新使用者註冊"
|
enableRegistration: "開啟新使用者註冊"
|
||||||
invite: "邀請"
|
invite: "邀請"
|
||||||
driveCapacityPerLocalAccount: "每個本地用戶的雲端空間大小"
|
driveCapacityPerLocalAccount: "每個本地用戶的雲端空間大小"
|
||||||
driveCapacityPerRemoteAccount: "每個非本地用戶的雲端容量"
|
driveCapacityPerRemoteAccount: "每個非本地用戶的雲端空間大小"
|
||||||
inMb: "以Mbps為單位"
|
inMb: "以Mbps為單位"
|
||||||
iconUrl: "圖像URL"
|
iconUrl: "圖標URL"
|
||||||
bannerUrl: "橫幅圖像URL"
|
bannerUrl: "橫幅圖片URL"
|
||||||
backgroundImageUrl: "背景圖片的來源網址 "
|
backgroundImageUrl: "背景圖片的來源網址 "
|
||||||
basicInfo: "基本資訊"
|
basicInfo: "基本資訊"
|
||||||
pinnedUsers: "置頂用戶"
|
pinnedUsers: "置頂用戶"
|
||||||
@@ -372,8 +373,8 @@ connectedTo: "您的帳戶已連接到以下社交帳戶"
|
|||||||
notesAndReplies: "貼文與回覆"
|
notesAndReplies: "貼文與回覆"
|
||||||
withFiles: "附件"
|
withFiles: "附件"
|
||||||
silence: "禁言"
|
silence: "禁言"
|
||||||
silenceConfirm: "確定要禁言此用戶嗎?"
|
silenceConfirm: "確定要靜音此使用者嗎?"
|
||||||
unsilence: "解除禁言"
|
unsilence: "解除靜音"
|
||||||
unsilenceConfirm: "確定要解除禁言嗎?"
|
unsilenceConfirm: "確定要解除禁言嗎?"
|
||||||
popularUsers: "熱門使用者"
|
popularUsers: "熱門使用者"
|
||||||
recentlyUpdatedUsers: "最近發文的使用者"
|
recentlyUpdatedUsers: "最近發文的使用者"
|
||||||
@@ -382,13 +383,13 @@ recentlyDiscoveredUsers: "最近發現的使用者"
|
|||||||
exploreUsersCount: "有{count}個使用者"
|
exploreUsersCount: "有{count}個使用者"
|
||||||
exploreFediverse: "探索聯邦世界"
|
exploreFediverse: "探索聯邦世界"
|
||||||
popularTags: "熱門標籤"
|
popularTags: "熱門標籤"
|
||||||
userList: "清單"
|
userList: "使用者清單"
|
||||||
about: "資訊"
|
about: "關於"
|
||||||
aboutMisskey: "關於 Misskey"
|
aboutMisskey: "關於 Misskey"
|
||||||
administrator: "管理員"
|
administrator: "管理員"
|
||||||
token: "權杖"
|
token: "權杖"
|
||||||
twoStepAuthentication: "兩階段驗證"
|
twoStepAuthentication: "兩階段驗證"
|
||||||
moderator: "監察員"
|
moderator: "審核員"
|
||||||
moderation: "監察"
|
moderation: "監察"
|
||||||
nUsersMentioned: "提到了{n}"
|
nUsersMentioned: "提到了{n}"
|
||||||
securityKey: "安全金鑰"
|
securityKey: "安全金鑰"
|
||||||
@@ -420,7 +421,7 @@ invites: "邀請"
|
|||||||
groupName: "群組名稱"
|
groupName: "群組名稱"
|
||||||
members: "成員"
|
members: "成員"
|
||||||
transfer: "轉讓"
|
transfer: "轉讓"
|
||||||
messagingWithUser: "傳送訊息給其他使用者"
|
messagingWithUser: "與其他使用者聊天"
|
||||||
messagingWithGroup: "發送訊息至群組"
|
messagingWithGroup: "發送訊息至群組"
|
||||||
title: "標題"
|
title: "標題"
|
||||||
text: "文字"
|
text: "文字"
|
||||||
@@ -472,7 +473,7 @@ createAccount: "建立帳戶"
|
|||||||
existingAccount: "現有帳戶"
|
existingAccount: "現有帳戶"
|
||||||
regenerate: "再生"
|
regenerate: "再生"
|
||||||
fontSize: "字體大小"
|
fontSize: "字體大小"
|
||||||
noFollowRequests: "沒有要求跟隨您的申請"
|
noFollowRequests: "沒有跟隨您的請求"
|
||||||
openImageInNewTab: "於新分頁中開啟圖片"
|
openImageInNewTab: "於新分頁中開啟圖片"
|
||||||
dashboard: "儀表板"
|
dashboard: "儀表板"
|
||||||
local: "本地"
|
local: "本地"
|
||||||
@@ -529,8 +530,8 @@ installedDate: "安裝時間"
|
|||||||
lastUsedDate: "最後上線日期"
|
lastUsedDate: "最後上線日期"
|
||||||
state: "狀態"
|
state: "狀態"
|
||||||
sort: "排序"
|
sort: "排序"
|
||||||
ascendingOrder: "昇冪"
|
ascendingOrder: "遞增"
|
||||||
descendingOrder: "降冪"
|
descendingOrder: "遞減"
|
||||||
scratchpad: "暫存記憶體"
|
scratchpad: "暫存記憶體"
|
||||||
scratchpadDescription: "AiScript控制台為AiScript提供了實驗環境。您可以在此編寫、執行和確認代碼與Misskey互動的结果。"
|
scratchpadDescription: "AiScript控制台為AiScript提供了實驗環境。您可以在此編寫、執行和確認代碼與Misskey互動的结果。"
|
||||||
output: "輸出"
|
output: "輸出"
|
||||||
@@ -937,6 +938,243 @@ cannotPerformTemporary: "暫時無法進行"
|
|||||||
cannotPerformTemporaryDescription: "由於超過操作次數限制,暫時無法進行。請過一段時間之後再嘗試。"
|
cannotPerformTemporaryDescription: "由於超過操作次數限制,暫時無法進行。請過一段時間之後再嘗試。"
|
||||||
preset: "預設值"
|
preset: "預設值"
|
||||||
selectFromPresets: "從預設值中選擇"
|
selectFromPresets: "從預設值中選擇"
|
||||||
|
achievements: "成就"
|
||||||
|
_achievements:
|
||||||
|
earnedAt: "獲得日期"
|
||||||
|
_types:
|
||||||
|
_notes1:
|
||||||
|
title: "just setting up my msky"
|
||||||
|
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: "發表了10000則貼文"
|
||||||
|
_notes20000:
|
||||||
|
title: "需要更多的貼文"
|
||||||
|
description: "發表了20000則貼文"
|
||||||
|
_notes30000:
|
||||||
|
title: "貼文貼文貼文"
|
||||||
|
description: "發表了30000則貼文"
|
||||||
|
_notes40000:
|
||||||
|
title: "貼文工廠"
|
||||||
|
description: "發表了40000則貼文"
|
||||||
|
_notes50000:
|
||||||
|
title: "貼文星球"
|
||||||
|
description: "發表了50000則貼文"
|
||||||
|
_notes60000:
|
||||||
|
title: "貼文類星體"
|
||||||
|
description: "發表了60000則貼文"
|
||||||
|
_notes70000:
|
||||||
|
title: "貼文黑洞"
|
||||||
|
description: "發表了70000則貼文"
|
||||||
|
_notes80000:
|
||||||
|
title: "貼文銀河"
|
||||||
|
description: "發表了80000則貼文"
|
||||||
|
_notes90000:
|
||||||
|
title: "貼文宇宙"
|
||||||
|
description: "發表了90000則貼文"
|
||||||
|
_notes100000:
|
||||||
|
title: "ALL YOUR NOTE ARE BELONG TO US"
|
||||||
|
description: "發表了100,000則貼文"
|
||||||
|
flavor: "有這麼多東西要寫嗎?"
|
||||||
|
_login3:
|
||||||
|
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:
|
_role:
|
||||||
new: "建立角色"
|
new: "建立角色"
|
||||||
edit: "編輯角色"
|
edit: "編輯角色"
|
||||||
@@ -1586,6 +1824,7 @@ _notification:
|
|||||||
pollEnded: "問卷調查已產生結果"
|
pollEnded: "問卷調查已產生結果"
|
||||||
unreadAntennaNote: "天線 {name}"
|
unreadAntennaNote: "天線 {name}"
|
||||||
emptyPushNotificationMessage: "推送通知已更新"
|
emptyPushNotificationMessage: "推送通知已更新"
|
||||||
|
achievementEarned: "獲得成就"
|
||||||
_types:
|
_types:
|
||||||
all: "全部 "
|
all: "全部 "
|
||||||
follow: "追隨中"
|
follow: "追隨中"
|
||||||
|
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "13.0.0",
|
"version": "13.2.6",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"cleanall": "pnpm clean-all"
|
"cleanall": "pnpm clean-all"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"chokidar": "^3.3.1",
|
"chokidar": "^3.5.3",
|
||||||
"lodash": "^4.17.21"
|
"lodash": "^4.17.21"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -54,12 +54,12 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/gulp": "4.0.10",
|
"@types/gulp": "4.0.10",
|
||||||
"@types/gulp-rename": "2.0.1",
|
"@types/gulp-rename": "2.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "5.48.1",
|
"@typescript-eslint/eslint-plugin": "5.49.0",
|
||||||
"@typescript-eslint/parser": "5.48.1",
|
"@typescript-eslint/parser": "5.49.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "12.3.0",
|
"cypress": "12.4.0",
|
||||||
"eslint": "^8.31.0",
|
"eslint": "^8.32.0",
|
||||||
"start-server-and-test": "1.15.2"
|
"start-server-and-test": "1.15.3"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@tensorflow/tfjs-core": "^4.2.0"
|
"@tensorflow/tfjs-core": "^4.2.0"
|
||||||
|
@@ -19,21 +19,21 @@
|
|||||||
"test-and-coverage": "pnpm jest-and-coverage"
|
"test-and-coverage": "pnpm jest-and-coverage"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@tensorflow/tfjs": "^4.1.0",
|
"@tensorflow/tfjs": "^4.2.0",
|
||||||
"@tensorflow/tfjs-node": "4.1.0"
|
"@tensorflow/tfjs-node": "4.2.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bull-board/api": "^4.10.2",
|
"@bull-board/api": "^4.11.0",
|
||||||
"@bull-board/fastify": "^4.10.2",
|
"@bull-board/fastify": "^4.11.0",
|
||||||
"@bull-board/ui": "^4.10.2",
|
"@bull-board/ui": "^4.11.0",
|
||||||
"@discordapp/twemoji": "14.0.2",
|
"@discordapp/twemoji": "14.0.2",
|
||||||
"@fastify/accepts": "4.1.0",
|
"@fastify/accepts": "4.1.0",
|
||||||
"@fastify/cookie": "^8.3.0",
|
"@fastify/cookie": "^8.3.0",
|
||||||
"@fastify/cors": "8.2.0",
|
"@fastify/cors": "8.2.0",
|
||||||
"@fastify/http-proxy": "^8.4.0",
|
"@fastify/http-proxy": "^8.4.0",
|
||||||
"@fastify/multipart": "7.4.0",
|
"@fastify/multipart": "7.4.0",
|
||||||
"@fastify/static": "6.6.1",
|
"@fastify/static": "6.7.0",
|
||||||
"@fastify/view": "7.4.0",
|
"@fastify/view": "7.4.1",
|
||||||
"@nestjs/common": "9.2.1",
|
"@nestjs/common": "9.2.1",
|
||||||
"@nestjs/core": "9.2.1",
|
"@nestjs/core": "9.2.1",
|
||||||
"@nestjs/testing": "9.2.1",
|
"@nestjs/testing": "9.2.1",
|
||||||
@@ -58,20 +58,19 @@
|
|||||||
"date-fns": "2.29.3",
|
"date-fns": "2.29.3",
|
||||||
"deep-email-validator": "0.1.21",
|
"deep-email-validator": "0.1.21",
|
||||||
"escape-regexp": "0.0.1",
|
"escape-regexp": "0.0.1",
|
||||||
"fastify": "4.11.0",
|
"fastify": "4.12.0",
|
||||||
"feed": "4.2.2",
|
"feed": "4.2.2",
|
||||||
"file-type": "18.1.0",
|
"file-type": "18.2.0",
|
||||||
"fluent-ffmpeg": "2.1.2",
|
"fluent-ffmpeg": "2.1.2",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"got": "12.5.3",
|
"got": "^12.5.3",
|
||||||
"hpagent": "1.2.0",
|
"hpagent": "1.2.0",
|
||||||
"ioredis": "4.28.5",
|
"ioredis": "4.28.5",
|
||||||
"ip-cidr": "3.0.11",
|
"ip-cidr": "3.0.11",
|
||||||
"is-svg": "4.3.2",
|
"is-svg": "4.3.2",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"jsdom": "21.0.0",
|
"jsdom": "21.1.0",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"json5-loader": "4.0.1",
|
|
||||||
"jsonld": "8.1.0",
|
"jsonld": "8.1.0",
|
||||||
"jsrsasign": "10.6.1",
|
"jsrsasign": "10.6.1",
|
||||||
"mfm-js": "0.23.3",
|
"mfm-js": "0.23.3",
|
||||||
@@ -89,7 +88,7 @@
|
|||||||
"probe-image-size": "7.2.3",
|
"probe-image-size": "7.2.3",
|
||||||
"promise-limit": "2.7.0",
|
"promise-limit": "2.7.0",
|
||||||
"pug": "3.0.2",
|
"pug": "3.0.2",
|
||||||
"punycode": "2.2.0",
|
"punycode": "2.3.0",
|
||||||
"pureimage": "0.3.15",
|
"pureimage": "0.3.15",
|
||||||
"qrcode": "1.5.1",
|
"qrcode": "1.5.1",
|
||||||
"random-seed": "0.3.0",
|
"random-seed": "0.3.0",
|
||||||
@@ -111,7 +110,7 @@
|
|||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
"summaly": "2.7.0",
|
"summaly": "2.7.0",
|
||||||
"syslog-pro": "git+https://github.com/misskey-dev/SyslogPro#0.2.9-misskey.2",
|
"syslog-pro": "git+https://github.com/misskey-dev/SyslogPro#0.2.9-misskey.2",
|
||||||
"systeminformation": "5.17.3",
|
"systeminformation": "5.17.4",
|
||||||
"tinycolor2": "1.5.2",
|
"tinycolor2": "1.5.2",
|
||||||
"tmp": "0.2.1",
|
"tmp": "0.2.1",
|
||||||
"tsc-alias": "1.8.2",
|
"tsc-alias": "1.8.2",
|
||||||
@@ -120,19 +119,19 @@
|
|||||||
"typeorm": "0.3.11",
|
"typeorm": "0.3.11",
|
||||||
"typescript": "4.9.4",
|
"typescript": "4.9.4",
|
||||||
"ulid": "2.3.0",
|
"ulid": "2.3.0",
|
||||||
"undici": "^5.15.0",
|
|
||||||
"unzipper": "0.10.11",
|
"unzipper": "0.10.11",
|
||||||
"uuid": "9.0.0",
|
"uuid": "9.0.0",
|
||||||
"vary": "1.1.2",
|
"vary": "1.1.2",
|
||||||
"web-push": "3.5.0",
|
"web-push": "3.5.0",
|
||||||
"websocket": "1.0.34",
|
"websocket": "1.0.34",
|
||||||
"ws": "8.12.0",
|
"ws": "8.12.0",
|
||||||
"xev": "3.0.2"
|
"xev": "3.0.2",
|
||||||
|
"node-fetch": "3.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@redocly/openapi-core": "1.0.0-beta.120",
|
"@redocly/openapi-core": "1.0.0-beta.120",
|
||||||
"@swc/cli": "^0.1.59",
|
"@swc/cli": "^0.1.59",
|
||||||
"@swc/core": "1.3.26",
|
"@swc/core": "1.3.29",
|
||||||
"@swc/jest": "0.2.24",
|
"@swc/jest": "0.2.24",
|
||||||
"@types/accepts": "1.3.5",
|
"@types/accepts": "1.3.5",
|
||||||
"@types/archiver": "5.3.1",
|
"@types/archiver": "5.3.1",
|
||||||
@@ -144,11 +143,11 @@
|
|||||||
"@types/escape-regexp": "0.0.1",
|
"@types/escape-regexp": "0.0.1",
|
||||||
"@types/fluent-ffmpeg": "2.1.20",
|
"@types/fluent-ffmpeg": "2.1.20",
|
||||||
"@types/ioredis": "4.28.10",
|
"@types/ioredis": "4.28.10",
|
||||||
"@types/jest": "29.2.5",
|
"@types/jest": "29.4.0",
|
||||||
"@types/js-yaml": "4.0.5",
|
"@types/js-yaml": "4.0.5",
|
||||||
"@types/jsdom": "20.0.1",
|
"@types/jsdom": "20.0.1",
|
||||||
"@types/jsonld": "1.5.8",
|
"@types/jsonld": "1.5.8",
|
||||||
"@types/jsrsasign": "10.5.4",
|
"@types/jsrsasign": "10.5.5",
|
||||||
"@types/mime-types": "2.1.1",
|
"@types/mime-types": "2.1.1",
|
||||||
"@types/node": "18.11.18",
|
"@types/node": "18.11.18",
|
||||||
"@types/node-fetch": "3.0.3",
|
"@types/node-fetch": "3.0.3",
|
||||||
@@ -176,14 +175,13 @@
|
|||||||
"@types/web-push": "3.3.2",
|
"@types/web-push": "3.3.2",
|
||||||
"@types/websocket": "1.0.5",
|
"@types/websocket": "1.0.5",
|
||||||
"@types/ws": "8.5.4",
|
"@types/ws": "8.5.4",
|
||||||
"@typescript-eslint/eslint-plugin": "5.48.1",
|
"@typescript-eslint/eslint-plugin": "5.49.0",
|
||||||
"@typescript-eslint/parser": "5.48.1",
|
"@typescript-eslint/parser": "5.49.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "8.31.0",
|
"eslint": "8.32.0",
|
||||||
"eslint-plugin-import": "2.27.4",
|
"eslint-plugin-import": "2.27.5",
|
||||||
"execa": "6.1.0",
|
"execa": "6.1.0",
|
||||||
"jest": "29.3.1",
|
"jest": "29.4.1",
|
||||||
"jest-mock": "^29.3.1",
|
"jest-mock": "^29.4.1"
|
||||||
"node-fetch": "3.3.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 chalk from 'chalk';
|
||||||
import chalkTemplate from 'chalk-template';
|
import chalkTemplate from 'chalk-template';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
import { NestFactory } from '@nestjs/core';
|
|
||||||
import Logger from '@/logger.js';
|
import Logger from '@/logger.js';
|
||||||
import { loadConfig } from '@/config.js';
|
import { loadConfig } from '@/config.js';
|
||||||
import type { Config } 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 { showMachineInfo } from '@/misc/show-machine-info.js';
|
||||||
import { DaemonModule } from '@/daemons/DaemonModule.js';
|
import { envOption } from '@/env.js';
|
||||||
import { JanitorService } from '@/daemons/JanitorService.js';
|
import { jobQueue, server } from './common.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';
|
|
||||||
|
|
||||||
const _filename = fileURLToPath(import.meta.url);
|
const _filename = fileURLToPath(import.meta.url);
|
||||||
const _dirname = dirname(_filename);
|
const _dirname = dirname(_filename);
|
||||||
@@ -73,14 +64,13 @@ export async function masterMain() {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = await NestFactory.createApplicationContext(MainModule, {
|
if (envOption.onlyServer) {
|
||||||
logger: new NestLogger(),
|
await server();
|
||||||
});
|
} else if (envOption.onlyQueue) {
|
||||||
app.enableShutdownHooks();
|
await jobQueue();
|
||||||
|
} else {
|
||||||
// start server
|
await server();
|
||||||
const serverService = app.get(ServerService);
|
}
|
||||||
serverService.launch();
|
|
||||||
|
|
||||||
bootLogger.succ('Misskey initialized');
|
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);
|
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 {
|
function showEnvironment(): void {
|
||||||
|
@@ -1,23 +1,18 @@
|
|||||||
import cluster from 'node:cluster';
|
import cluster from 'node:cluster';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { envOption } from '@/env.js';
|
||||||
import { ChartManagementService } from '@/core/chart/ChartManagementService.js';
|
import { jobQueue, server } from './common.js';
|
||||||
import { QueueProcessorService } from '@/queue/QueueProcessorService.js';
|
|
||||||
import { NestLogger } from '@/NestLogger.js';
|
|
||||||
import { QueueProcessorModule } from '@/queue/QueueProcessorModule.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init worker process
|
* Init worker process
|
||||||
*/
|
*/
|
||||||
export async function workerMain() {
|
export async function workerMain() {
|
||||||
const jobQueue = await NestFactory.createApplicationContext(QueueProcessorModule, {
|
if (envOption.onlyServer) {
|
||||||
logger: new NestLogger(),
|
await server();
|
||||||
});
|
} else if (envOption.onlyQueue) {
|
||||||
jobQueue.enableShutdownHooks();
|
await jobQueue();
|
||||||
|
} else {
|
||||||
// start job queue
|
await jobQueue();
|
||||||
jobQueue.get(QueueProcessorService).start();
|
}
|
||||||
|
|
||||||
jobQueue.get(ChartManagementService).start();
|
|
||||||
|
|
||||||
if (cluster.isWorker) {
|
if (cluster.isWorker) {
|
||||||
// Send a 'ready' message to parent process
|
// Send a 'ready' message to parent process
|
||||||
|
@@ -41,8 +41,10 @@ const ACHIEVEMENT_TYPES = [
|
|||||||
'passedSinceAccountCreated2',
|
'passedSinceAccountCreated2',
|
||||||
'passedSinceAccountCreated3',
|
'passedSinceAccountCreated3',
|
||||||
'loggedInOnBirthday',
|
'loggedInOnBirthday',
|
||||||
|
'loggedInOnNewYearsDay',
|
||||||
'noteClipped1',
|
'noteClipped1',
|
||||||
'noteFavorited1',
|
'noteFavorited1',
|
||||||
|
'myNoteFavorited1',
|
||||||
'profileFilled',
|
'profileFilled',
|
||||||
'markedAsCat',
|
'markedAsCat',
|
||||||
'following1',
|
'following1',
|
||||||
@@ -58,13 +60,18 @@ const ACHIEVEMENT_TYPES = [
|
|||||||
'followers500',
|
'followers500',
|
||||||
'followers1000',
|
'followers1000',
|
||||||
'collectAchievements30',
|
'collectAchievements30',
|
||||||
|
'viewAchievements3min',
|
||||||
'iLoveMisskey',
|
'iLoveMisskey',
|
||||||
|
'foundTreasure',
|
||||||
'client30min',
|
'client30min',
|
||||||
'noteDeletedWithin1min',
|
'noteDeletedWithin1min',
|
||||||
'postedAtLateNight',
|
'postedAtLateNight',
|
||||||
'postedAt0min0sec',
|
'postedAt0min0sec',
|
||||||
'selfQuote',
|
'selfQuote',
|
||||||
'htl20npm',
|
'htl20npm',
|
||||||
|
'viewInstanceChart',
|
||||||
|
'outputHelloWorldOnScratchpad',
|
||||||
|
'open3windows',
|
||||||
'driveFolderCircularReference',
|
'driveFolderCircularReference',
|
||||||
'reactWithoutRead',
|
'reactWithoutRead',
|
||||||
'clickedClickHere',
|
'clickedClickHere',
|
||||||
@@ -90,7 +97,7 @@ export class AchievementService {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public async create(
|
public async create(
|
||||||
userId: User['id'],
|
userId: User['id'],
|
||||||
type: string,
|
type: typeof ACHIEVEMENT_TYPES[number],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!ACHIEVEMENT_TYPES.includes(type)) return;
|
if (!ACHIEVEMENT_TYPES.includes(type)) return;
|
||||||
|
|
||||||
|
@@ -77,10 +77,16 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'antennaCreated':
|
case 'antennaCreated':
|
||||||
this.antennas.push(body);
|
this.antennas.push({
|
||||||
|
...body,
|
||||||
|
createdAt: new Date(body.createdAt),
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case 'antennaUpdated':
|
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;
|
break;
|
||||||
case 'antennaDeleted':
|
case 'antennaDeleted':
|
||||||
this.antennas = this.antennas.filter(a => a.id !== body.id);
|
this.antennas = this.antennas.filter(a => a.id !== body.id);
|
||||||
|
@@ -21,18 +21,13 @@ export class CaptchaService {
|
|||||||
response,
|
response,
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await this.httpRequestService.fetch(
|
const res = await this.httpRequestService.send(url, {
|
||||||
url,
|
method: 'POST',
|
||||||
{
|
body: params.toString(),
|
||||||
method: 'POST',
|
headers: {
|
||||||
body: params,
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
{
|
}, { throwErrorWhenResponseNotOk: false });
|
||||||
noOkError: true,
|
|
||||||
}
|
|
||||||
).catch(err => {
|
|
||||||
throw `${err.message ?? err}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw `${res.status}`;
|
throw `${res.status}`;
|
||||||
|
@@ -2,22 +2,39 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { DataSource, In, IsNull } from 'typeorm';
|
import { DataSource, In, IsNull } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { IdService } from '@/core/IdService.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 { DriveFile } from '@/models/entities/DriveFile.js';
|
||||||
import type { Emoji } from '@/models/entities/Emoji.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 { 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()
|
@Injectable()
|
||||||
export class CustomEmojiService {
|
export class CustomEmojiService {
|
||||||
|
private cache: Cache<Emoji | null>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.config)
|
||||||
|
private config: Config,
|
||||||
|
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
@Inject(DI.emojisRepository)
|
@Inject(DI.emojisRepository)
|
||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
private utilityService: UtilityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
|
private reactionService: ReactionService,
|
||||||
) {
|
) {
|
||||||
|
this.cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -40,8 +57,135 @@ export class CustomEmojiService {
|
|||||||
type: data.driveFile.webpublicType ?? data.driveFile.type,
|
type: data.driveFile.webpublicType ?? data.driveFile.type,
|
||||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
}).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;
|
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 { Inject, Injectable } from '@nestjs/common';
|
||||||
import IPCIDR from 'ip-cidr';
|
import IPCIDR from 'ip-cidr';
|
||||||
import PrivateIp from 'private-ip';
|
import PrivateIp from 'private-ip';
|
||||||
import got, * as Got from 'got';
|
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
import got, * as Got from 'got';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.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 { createTemp } from '@/misc/create-temp.js';
|
||||||
import { StatusError } from '@/misc/status-error.js';
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { buildConnector } from 'undici';
|
|
||||||
|
|
||||||
const pipeline = util.promisify(stream.pipeline);
|
const pipeline = util.promisify(stream.pipeline);
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
@@ -21,7 +20,6 @@ import { bindThis } from '@/decorators.js';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class DownloadService {
|
export class DownloadService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
private undiciFetcher: UndiciFetcher;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
@@ -31,24 +29,6 @@ export class DownloadService {
|
|||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('download');
|
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
|
@bindThis
|
||||||
@@ -59,14 +39,60 @@ export class DownloadService {
|
|||||||
const operationTimeout = 60 * 1000;
|
const operationTimeout = 60 * 1000;
|
||||||
const maxSize = this.config.maxFileSize ?? 262144000;
|
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) {
|
const contentLength = res.headers['content-length'];
|
||||||
throw new StatusError('No body', 400, 'No body');
|
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)}`);
|
this.logger.succ(`Download finished: ${chalk.cyan(url)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,6 +2,7 @@ import { URL } from 'node:url';
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { JSDOM } from 'jsdom';
|
import { JSDOM } from 'jsdom';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
import type { Instance } from '@/models/entities/Instance.js';
|
import type { Instance } from '@/models/entities/Instance.js';
|
||||||
import type { InstancesRepository } from '@/models/index.js';
|
import type { InstancesRepository } from '@/models/index.js';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
import { AppLockService } from '@/core/AppLockService.js';
|
||||||
@@ -190,7 +191,9 @@ export class FetchInstanceMetadataService {
|
|||||||
|
|
||||||
const faviconUrl = url + '/favicon.ico';
|
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) {
|
if (favicon.ok) {
|
||||||
return faviconUrl;
|
return faviconUrl;
|
||||||
|
@@ -1,257 +1,67 @@
|
|||||||
import * as http from 'node:http';
|
import * as http from 'node:http';
|
||||||
import * as https from 'node:https';
|
import * as https from 'node:https';
|
||||||
import CacheableLookup from 'cacheable-lookup';
|
import CacheableLookup from 'cacheable-lookup';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { StatusError } from '@/misc/status-error.js';
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import * as undici from 'undici';
|
import type { Response } from 'node-fetch';
|
||||||
import { LookupFunction } from 'node:net';
|
import type { URL } from 'node:url';
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HttpRequestService {
|
export class HttpRequestService {
|
||||||
public defaultFetcher: UndiciFetcher;
|
/**
|
||||||
public fetch: UndiciFetcher['fetch'];
|
* Get http non-proxy agent
|
||||||
public getHtml: UndiciFetcher['getHtml'];
|
*/
|
||||||
public defaultJsonFetcher: UndiciFetcher;
|
|
||||||
public getJson: UndiciFetcher['getJson'];
|
|
||||||
|
|
||||||
//#region for old http/https, only used in S3Service
|
|
||||||
// http non-proxy agent
|
|
||||||
private http: http.Agent;
|
private http: http.Agent;
|
||||||
|
|
||||||
// https non-proxy agent
|
/**
|
||||||
|
* Get https non-proxy agent
|
||||||
|
*/
|
||||||
private https: https.Agent;
|
private https: https.Agent;
|
||||||
|
|
||||||
// http proxy or non-proxy agent
|
/**
|
||||||
|
* Get http proxy or non-proxy agent
|
||||||
|
*/
|
||||||
public httpAgent: http.Agent;
|
public httpAgent: http.Agent;
|
||||||
|
|
||||||
// https proxy or non-proxy agent
|
/**
|
||||||
|
* Get https proxy or non-proxy agent
|
||||||
|
*/
|
||||||
public httpsAgent: https.Agent;
|
public httpsAgent: https.Agent;
|
||||||
//#endregion
|
|
||||||
|
|
||||||
public readonly dnsCache: CacheableLookup;
|
|
||||||
public readonly clientDefaults: undici.Agent.Options;
|
|
||||||
private maxSockets: number;
|
|
||||||
|
|
||||||
private logger: Logger;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
private loggerService: LoggerService,
|
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('http-request');
|
const cache = new CacheableLookup({
|
||||||
|
|
||||||
this.dnsCache = new CacheableLookup({
|
|
||||||
maxTtl: 3600, // 1hours
|
maxTtl: 3600, // 1hours
|
||||||
errorTtl: 30, // 30secs
|
errorTtl: 30, // 30secs
|
||||||
lookup: false, // nativeのdns.lookupにfallbackしない
|
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({
|
this.http = new http.Agent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAliveMsecs: 30 * 1000,
|
||||||
lookup: this.dnsCache.lookup,
|
lookup: cache.lookup,
|
||||||
} as http.AgentOptions);
|
} as http.AgentOptions);
|
||||||
|
|
||||||
this.https = new https.Agent({
|
this.https = new https.Agent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAliveMsecs: 30 * 1000,
|
||||||
lookup: this.dnsCache.lookup,
|
lookup: cache.lookup,
|
||||||
} as https.AgentOptions);
|
} as https.AgentOptions);
|
||||||
|
|
||||||
|
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
|
||||||
|
|
||||||
this.httpAgent = config.proxy
|
this.httpAgent = config.proxy
|
||||||
? new HttpProxyAgent({
|
? new HttpProxyAgent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAliveMsecs: 30 * 1000,
|
||||||
maxSockets: this.maxSockets,
|
maxSockets,
|
||||||
maxFreeSockets: 256,
|
maxFreeSockets: 256,
|
||||||
scheduling: 'lifo',
|
scheduling: 'lifo',
|
||||||
proxy: config.proxy,
|
proxy: config.proxy,
|
||||||
@@ -262,42 +72,21 @@ export class HttpRequestService {
|
|||||||
? new HttpsProxyAgent({
|
? new HttpsProxyAgent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAliveMsecs: 30 * 1000,
|
||||||
maxSockets: this.maxSockets,
|
maxSockets,
|
||||||
maxFreeSockets: 256,
|
maxFreeSockets: 256,
|
||||||
scheduling: 'lifo',
|
scheduling: 'lifo',
|
||||||
proxy: config.proxy,
|
proxy: config.proxy,
|
||||||
})
|
})
|
||||||
: this.https;
|
: 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 url URL
|
||||||
* @param bypassProxy Allways bypass proxy
|
* @param bypassProxy Allways bypass proxy
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@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)) {
|
if (bypassProxy || (this.config.proxyBypassHosts || []).includes(url.hostname)) {
|
||||||
return url.protocol === 'http:' ? this.http : this.https;
|
return url.protocol === 'http:' ? this.http : this.https;
|
||||||
} else {
|
} else {
|
||||||
@@ -305,37 +94,67 @@ export class HttpRequestService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* check ip
|
|
||||||
*/
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public getConnectorWithIpCheck(connector: undici.buildConnector.connector, checkIp: IpChecker): undici.buildConnector.connectorAsync {
|
public async getJson(url: string, accept = 'application/json, */*', headers?: Record<string, string>): Promise<unknown> {
|
||||||
return (options, cb) => {
|
const res = await this.send(url, {
|
||||||
connector(options, (err, socket) => {
|
method: 'GET',
|
||||||
this.logger.debug('Socket connector (with ip checker) called', socket);
|
headers: Object.assign({
|
||||||
if (err) {
|
'User-Agent': this.config.userAgent,
|
||||||
this.logger.error(`Socket error`, err)
|
Accept: accept,
|
||||||
cb(new Error(`Error while socket connecting\n${err}`), null);
|
}, headers ?? {}),
|
||||||
return;
|
timeout: 5000,
|
||||||
}
|
size: 1024 * 256,
|
||||||
|
});
|
||||||
|
|
||||||
if (socket.remoteAddress == undefined) {
|
return await res.json();
|
||||||
this.logger.error(`Socket error: remoteAddress is undefined`);
|
}
|
||||||
cb(new Error('remoteAddress is undefined (maybe socket destroyed)'), null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// allow
|
@bindThis
|
||||||
if (checkIp(socket.remoteAddress)) {
|
public async getHtml(url: string, accept = 'text/html, */*', headers?: Record<string, string>): Promise<string> {
|
||||||
this.logger.debug(`Socket connected (ip ok): ${socket.localPort} => ${socket.remoteAddress}`);
|
const res = await this.send(url, {
|
||||||
cb(null, socket);
|
method: 'GET',
|
||||||
return;
|
headers: Object.assign({
|
||||||
}
|
'User-Agent': this.config.userAgent,
|
||||||
|
Accept: accept,
|
||||||
|
}, headers ?? {}),
|
||||||
|
timeout: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
this.logger.error('IP is not allowed', socket);
|
return await res.text();
|
||||||
cb(new StatusError('IP is not allowed', 403, 'IP is not allowed'), null);
|
}
|
||||||
socket.destroy();
|
|
||||||
});
|
@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;
|
type: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type IImageStream = {
|
||||||
|
data: Readable;
|
||||||
|
ext: string | null;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IImageStreamable = IImage | IImageStream;
|
||||||
|
|
||||||
export const webpDefault: sharp.WebpOptions = {
|
export const webpDefault: sharp.WebpOptions = {
|
||||||
quality: 85,
|
quality: 85,
|
||||||
alphaQuality: 95,
|
alphaQuality: 95,
|
||||||
@@ -19,6 +27,7 @@ export const webpDefault: sharp.WebpOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { Readable } from 'node:stream';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ImageProcessingService {
|
export class ImageProcessingService {
|
||||||
@@ -64,7 +73,7 @@ export class ImageProcessingService {
|
|||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async convertToWebp(path: string, width: number, height: number, options: sharp.WebpOptions = webpDefault): Promise<IImage> {
|
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
|
@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
|
* Convert to PNG
|
||||||
* with resize, remove metadata, resolve orientation, stop animation
|
* with resize, remove metadata, resolve orientation, stop animation
|
||||||
|
@@ -91,10 +91,12 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
case 'roleCreated': {
|
case 'roleCreated': {
|
||||||
const cached = this.rolesCache.get(null);
|
const cached = this.rolesCache.get(null);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
body.createdAt = new Date(body.createdAt);
|
cached.push({
|
||||||
body.updatedAt = new Date(body.updatedAt);
|
...body,
|
||||||
body.lastUsedAt = new Date(body.lastUsedAt);
|
createdAt: new Date(body.createdAt),
|
||||||
cached.push(body);
|
updatedAt: new Date(body.updatedAt),
|
||||||
|
lastUsedAt: new Date(body.lastUsedAt),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -103,10 +105,12 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
if (cached) {
|
if (cached) {
|
||||||
const i = cached.findIndex(x => x.id === body.id);
|
const i = cached.findIndex(x => x.id === body.id);
|
||||||
if (i > -1) {
|
if (i > -1) {
|
||||||
body.createdAt = new Date(body.createdAt);
|
cached[i] = {
|
||||||
body.updatedAt = new Date(body.updatedAt);
|
...body,
|
||||||
body.lastUsedAt = new Date(body.lastUsedAt);
|
createdAt: new Date(body.createdAt),
|
||||||
cached[i] = body;
|
updatedAt: new Date(body.updatedAt),
|
||||||
|
lastUsedAt: new Date(body.lastUsedAt),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -121,8 +125,10 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
case 'userRoleAssigned': {
|
case 'userRoleAssigned': {
|
||||||
const cached = this.roleAssignmentByUserIdCache.get(body.userId);
|
const cached = this.roleAssignmentByUserIdCache.get(body.userId);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
body.createdAt = new Date(body.createdAt);
|
cached.push({
|
||||||
cached.push(body);
|
...body,
|
||||||
|
createdAt: new Date(body.createdAt),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@@ -33,7 +33,7 @@ export class S3Service {
|
|||||||
? false
|
? false
|
||||||
: meta.objectStorageS3ForcePathStyle,
|
: meta.objectStorageS3ForcePathStyle,
|
||||||
httpOptions: {
|
httpOptions: {
|
||||||
agent: this.httpRequestService.getHttpAgentByUrl(new URL(u), !meta.objectStorageUseProxy),
|
agent: this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -21,11 +21,11 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
|
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
|
||||||
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, EmojisRepository, PollsRepository } from '@/models/index.js';
|
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, EmojisRepository, PollsRepository } from '@/models/index.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
import { LdSignatureService } from './LdSignatureService.js';
|
import { LdSignatureService } from './LdSignatureService.js';
|
||||||
import { ApMfmService } from './ApMfmService.js';
|
import { ApMfmService } from './ApMfmService.js';
|
||||||
import type { IActivity, IObject } from './type.js';
|
import type { IActivity, IObject } from './type.js';
|
||||||
import type { IIdentifier } from './models/identifier.js';
|
import type { IIdentifier } from './models/identifier.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApRendererService {
|
export class ApRendererService {
|
||||||
|
@@ -5,7 +5,7 @@ import { DI } from '@/di-symbols.js';
|
|||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.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 { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
@@ -30,7 +30,6 @@ type PrivateKey = {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApRequestService {
|
export class ApRequestService {
|
||||||
private undiciFetcher: UndiciFetcher;
|
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -41,10 +40,8 @@ export class ApRequestService {
|
|||||||
private httpRequestService: HttpRequestService,
|
private httpRequestService: HttpRequestService,
|
||||||
private loggerService: LoggerService,
|
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.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
|
@bindThis
|
||||||
@@ -163,14 +160,11 @@ export class ApRequestService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.undiciFetcher.fetch(
|
await this.httpRequestService.send(url, {
|
||||||
url,
|
method: req.request.method,
|
||||||
{
|
headers: req.request.headers,
|
||||||
method: req.request.method,
|
body,
|
||||||
headers: req.request.headers,
|
});
|
||||||
body,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -192,13 +186,10 @@ export class ApRequestService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await this.httpRequestService.fetch(
|
const res = await this.httpRequestService.send(url, {
|
||||||
url,
|
method: req.request.method,
|
||||||
{
|
headers: req.request.headers,
|
||||||
method: req.request.method,
|
});
|
||||||
headers: req.request.headers,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return await res.json();
|
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 { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { MetaService } from '@/core/MetaService.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 { DI } from '@/di-symbols.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
|
import type Logger from '@/logger.js';
|
||||||
import { isCollectionOrOrderedCollection } from './type.js';
|
import { isCollectionOrOrderedCollection } from './type.js';
|
||||||
import { ApDbResolverService } from './ApDbResolverService.js';
|
import { ApDbResolverService } from './ApDbResolverService.js';
|
||||||
import { ApRendererService } from './ApRendererService.js';
|
import { ApRendererService } from './ApRendererService.js';
|
||||||
import { ApRequestService } from './ApRequestService.js';
|
import { ApRequestService } from './ApRequestService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
|
||||||
import type { IObject, ICollection, IOrderedCollection } from './type.js';
|
import type { IObject, ICollection, IOrderedCollection } from './type.js';
|
||||||
import type Logger from '@/logger.js';
|
|
||||||
|
|
||||||
export class Resolver {
|
export class Resolver {
|
||||||
private history: Set<string>;
|
private history: Set<string>;
|
||||||
private user?: ILocalUser;
|
private user?: ILocalUser;
|
||||||
private undiciFetcher: UndiciFetcher;
|
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -39,10 +38,8 @@ export class Resolver {
|
|||||||
private recursionLimit = 100,
|
private recursionLimit = 100,
|
||||||
) {
|
) {
|
||||||
this.history = new Set();
|
this.history = new Set();
|
||||||
this.logger = this.loggerService?.getLogger('ap-resolve'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
this.undiciFetcher = new UndiciFetcher(this.httpRequestService.getStandardUndiciFetcherOption({
|
this.logger = this.loggerService?.getLogger('ap-resolve'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる
|
||||||
maxRedirections: 0,
|
|
||||||
}), this.logger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -106,7 +103,7 @@ export class Resolver {
|
|||||||
|
|
||||||
const object = (this.user
|
const object = (this.user
|
||||||
? await this.apRequestService.signedGet(value, this.user) as IObject
|
? 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 || (
|
if (object == null || (
|
||||||
Array.isArray(object['@context']) ?
|
Array.isArray(object['@context']) ?
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import * as crypto from 'node:crypto';
|
import * as crypto from 'node:crypto';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import jsonld from 'jsonld';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { CONTEXTS } from './misc/contexts.js';
|
import { CONTEXTS } from './misc/contexts.js';
|
||||||
@@ -9,7 +10,7 @@ import { CONTEXTS } from './misc/contexts.js';
|
|||||||
class LdSignature {
|
class LdSignature {
|
||||||
public debug = false;
|
public debug = false;
|
||||||
public preLoad = true;
|
public preLoad = true;
|
||||||
public loderTimeout = 10 * 1000;
|
public loderTimeout = 5000;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private httpRequestService: HttpRequestService,
|
private httpRequestService: HttpRequestService,
|
||||||
@@ -84,7 +85,9 @@ class LdSignature {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public async normalize(data: any) {
|
public async normalize(data: any) {
|
||||||
const customLoader = this.getLoader();
|
const customLoader = this.getLoader();
|
||||||
return 42;
|
return await jsonld.normalize(data, {
|
||||||
|
documentLoader: customLoader,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -115,19 +118,12 @@ class LdSignature {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async fetchDocument(url: string) {
|
private async fetchDocument(url: string) {
|
||||||
const json = await this.httpRequestService.fetch(
|
const json = await this.httpRequestService.send(url, {
|
||||||
url,
|
headers: {
|
||||||
{
|
Accept: 'application/ld+json, application/json',
|
||||||
headers: {
|
|
||||||
Accept: 'application/ld+json, application/json',
|
|
||||||
},
|
|
||||||
// TODO
|
|
||||||
//timeout: this.loderTimeout,
|
|
||||||
},
|
},
|
||||||
{
|
timeout: this.loderTimeout,
|
||||||
noOkError: true,
|
}, { throwErrorWhenResponseNotOk: false }).then(res => {
|
||||||
}
|
|
||||||
).then(res => {
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw `${res.status} ${res.statusText}`;
|
throw `${res.status} ${res.statusText}`;
|
||||||
} else {
|
} else {
|
||||||
|
@@ -566,22 +566,22 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
|
|
||||||
this.logger.info(`Updating the featured: ${user.uri}`);
|
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
|
// 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');
|
if (!isCollectionOrOrderedCollection(collection)) throw new Error('Object is not Collection or OrderedCollection');
|
||||||
|
|
||||||
// Resolve to Object(may be Note) arrays
|
// Resolve to Object(may be Note) arrays
|
||||||
const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems;
|
const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems;
|
||||||
const items = await Promise.all(toArray(unresolvedItems).map(x => resolver.resolve(x)));
|
const items = await Promise.all(toArray(unresolvedItems).map(x => _resolver.resolve(x)));
|
||||||
|
|
||||||
// Resolve and regist Notes
|
// Resolve and regist Notes
|
||||||
const limit = promiseLimit<Note | null>(2);
|
const limit = promiseLimit<Note | null>(2);
|
||||||
const featuredNotes = await Promise.all(items
|
const featuredNotes = await Promise.all(items
|
||||||
.filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも
|
.filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも
|
||||||
.slice(0, 5)
|
.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 this.db.transaction(async transactionalEntityManager => {
|
||||||
await transactionalEntityManager.delete(UserNotePining, { userId: user.id });
|
await transactionalEntityManager.delete(UserNotePining, { userId: user.id });
|
||||||
|
@@ -22,8 +22,10 @@ export class EmojiEntityService {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public async pack(
|
public async pack(
|
||||||
src: Emoji['id'] | Emoji,
|
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'>> {
|
): Promise<Packed<'Emoji'>> {
|
||||||
|
opts = { omitHost: true, omitId: true, withUrl: true, ...opts }
|
||||||
|
|
||||||
const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src });
|
const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -32,13 +34,15 @@ export class EmojiEntityService {
|
|||||||
name: emoji.name,
|
name: emoji.name,
|
||||||
category: emoji.category,
|
category: emoji.category,
|
||||||
host: opts.omitHost ? undefined : emoji.host,
|
host: opts.omitHost ? undefined : emoji.host,
|
||||||
|
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||||
|
url: opts.withUrl ? (emoji.publicUrl || emoji.originalUrl) : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public packMany(
|
public packMany(
|
||||||
emojis: any[],
|
emojis: any[],
|
||||||
opts: { omitHost?: boolean; omitId?: boolean; } = {},
|
opts: { omitHost?: boolean; omitId?: boolean; withUrl?: boolean; } = {},
|
||||||
) {
|
) {
|
||||||
return Promise.all(emojis.map(x => this.pack(x, opts)));
|
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 })
|
: await this.channelsRepository.findOneBy({ id: note.channelId })
|
||||||
: null;
|
: 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({
|
const packed: Packed<'Note'> = await awaitAll({
|
||||||
id: note.id,
|
id: note.id,
|
||||||
@@ -299,6 +301,8 @@ export class NoteEntityService implements OnModuleInit {
|
|||||||
renoteCount: note.renoteCount,
|
renoteCount: note.renoteCount,
|
||||||
repliesCount: note.repliesCount,
|
repliesCount: note.repliesCount,
|
||||||
reactions: this.reactionService.convertLegacyReactions(note.reactions),
|
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,
|
tags: note.tags.length > 0 ? note.tags : undefined,
|
||||||
fileIds: note.fileIds,
|
fileIds: note.fileIds,
|
||||||
files: this.driveFileEntityService.packMany(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, {
|
return await Promise.all(notes.map(n => this.pack(n, me, {
|
||||||
...options,
|
...options,
|
||||||
_hint_: {
|
_hint_: {
|
||||||
|
@@ -146,6 +146,8 @@ export class NotificationEntityService implements OnModuleInit {
|
|||||||
myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) ?? null);
|
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, {
|
return await Promise.all(notifications.map(x => this.pack(x, {
|
||||||
_hintForEachNotes_: {
|
_hintForEachNotes_: {
|
||||||
myReactions: myReactionsMap,
|
myReactions: myReactionsMap,
|
||||||
|
@@ -413,6 +413,7 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
faviconUrl: instance.faviconUrl,
|
faviconUrl: instance.faviconUrl,
|
||||||
themeColor: instance.themeColor,
|
themeColor: instance.themeColor,
|
||||||
} : undefined) : undefined,
|
} : undefined) : undefined,
|
||||||
|
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
|
||||||
onlineStatus: this.getOnlineStatus(user),
|
onlineStatus: this.getOnlineStatus(user),
|
||||||
|
|
||||||
...(opts.detail ? {
|
...(opts.detail ? {
|
||||||
@@ -496,10 +497,10 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
showTimelineReplies: user.showTimelineReplies ?? falsy,
|
showTimelineReplies: user.showTimelineReplies ?? falsy,
|
||||||
achievements: profile!.achievements,
|
achievements: profile!.achievements,
|
||||||
loggedInDays: profile!.loggedInDates.length,
|
loggedInDays: profile!.loggedInDates.length,
|
||||||
|
policies: this.roleService.getUserPolicies(user.id),
|
||||||
} : {}),
|
} : {}),
|
||||||
|
|
||||||
...(opts.includeSecrets ? {
|
...(opts.includeSecrets ? {
|
||||||
policies: this.roleService.getUserPolicies(user.id),
|
|
||||||
email: profile!.email,
|
email: profile!.email,
|
||||||
emailVerified: profile!.emailVerified,
|
emailVerified: profile!.emailVerified,
|
||||||
securityKeysList: profile!.twoFactorEnabled
|
securityKeysList: profile!.twoFactorEnabled
|
||||||
|
@@ -68,6 +68,7 @@ export default class Logger {
|
|||||||
if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log;
|
if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log;
|
||||||
|
|
||||||
console.log(important ? chalk.bold(log) : log);
|
console.log(important ? chalk.bold(log) : log);
|
||||||
|
if (level === 'error' && data) console.log(data);
|
||||||
|
|
||||||
if (store) {
|
if (store) {
|
||||||
if (this.syslogClient) {
|
if (this.syslogClient) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,9 +1,14 @@
|
|||||||
import IPCIDR from 'ip-cidr';
|
import IPCIDR from 'ip-cidr';
|
||||||
|
|
||||||
export function getIpHash(ip: string) {
|
export function getIpHash(ip: string) {
|
||||||
// because a single person may control many IPv6 addresses,
|
try {
|
||||||
// only a /64 subnet prefix of any IP will be taken into account.
|
// because a single person may control many IPv6 addresses,
|
||||||
// (this means for IPv4 the entire address is used)
|
// only a /64 subnet prefix of any IP will be taken into account.
|
||||||
const prefix = IPCIDR.createAddress(ip).mask(64);
|
// (this means for IPv4 the entire address is used)
|
||||||
return 'ip-' + BigInt('0b' + prefix).toString(36);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -29,5 +29,9 @@ export const packedEmojiSchema = {
|
|||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
description: 'The local host is represented with `null`.',
|
description: 'The local host is represented with `null`.',
|
||||||
},
|
},
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@@ -57,8 +57,15 @@ export class AggregateRetentionProcessorService {
|
|||||||
usersCount: targetUserIds.length,
|
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) {
|
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);
|
const data = deepClone(record.data);
|
||||||
data[dateKey] = retention;
|
data[dateKey] = retention;
|
||||||
|
@@ -6,10 +6,10 @@ import type { Config } from '@/config.js';
|
|||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { StatusError } from '@/misc/status-error.js';
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type Bull from 'bull';
|
import type Bull from 'bull';
|
||||||
import type { WebhookDeliverJobData } from '../types.js';
|
import type { WebhookDeliverJobData } from '../types.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WebhookDeliverProcessorService {
|
export class WebhookDeliverProcessorService {
|
||||||
@@ -33,26 +33,23 @@ export class WebhookDeliverProcessorService {
|
|||||||
try {
|
try {
|
||||||
this.logger.debug(`delivering ${job.data.webhookId}`);
|
this.logger.debug(`delivering ${job.data.webhookId}`);
|
||||||
|
|
||||||
const res = await this.httpRequestService.fetch(
|
const res = await this.httpRequestService.send(job.data.to, {
|
||||||
job.data.to,
|
method: 'POST',
|
||||||
{
|
headers: {
|
||||||
method: 'POST',
|
'User-Agent': 'Misskey-Hooks',
|
||||||
headers: {
|
'X-Misskey-Host': this.config.host,
|
||||||
'User-Agent': 'Misskey-Hooks',
|
'X-Misskey-Hook-Id': job.data.webhookId,
|
||||||
'X-Misskey-Host': this.config.host,
|
'X-Misskey-Hook-Secret': job.data.secret,
|
||||||
'X-Misskey-Hook-Id': job.data.webhookId,
|
},
|
||||||
'X-Misskey-Hook-Secret': job.data.secret,
|
body: JSON.stringify({
|
||||||
},
|
hookId: job.data.webhookId,
|
||||||
body: JSON.stringify({
|
userId: job.data.userId,
|
||||||
hookId: job.data.webhookId,
|
eventId: job.data.eventId,
|
||||||
userId: job.data.userId,
|
createdAt: job.data.createdAt,
|
||||||
eventId: job.data.eventId,
|
type: job.data.type,
|
||||||
createdAt: job.data.createdAt,
|
body: job.data.content,
|
||||||
type: job.data.type,
|
}),
|
||||||
body: job.data.content,
|
});
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.webhooksRepository.update({ id: job.data.webhookId }, {
|
this.webhooksRepository.update({ id: job.data.webhookId }, {
|
||||||
latestSentAt: new Date(),
|
latestSentAt: new Date(),
|
||||||
|
@@ -5,14 +5,14 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import fastifyStatic from '@fastify/static';
|
import fastifyStatic from '@fastify/static';
|
||||||
import rename from 'rename';
|
import rename from 'rename';
|
||||||
import type { Config } from '@/config.js';
|
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 { DI } from '@/di-symbols.js';
|
||||||
import { createTemp } from '@/misc/create-temp.js';
|
import { createTemp } from '@/misc/create-temp.js';
|
||||||
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
||||||
import { StatusError } from '@/misc/status-error.js';
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { DownloadService } from '@/core/DownloadService.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 { VideoProcessingService } from '@/core/VideoProcessingService.js';
|
||||||
import { InternalStorageService } from '@/core/InternalStorageService.js';
|
import { InternalStorageService } from '@/core/InternalStorageService.js';
|
||||||
import { contentDisposition } from '@/misc/content-disposition.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 { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
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 _filename = fileURLToPath(import.meta.url);
|
||||||
const _dirname = dirname(_filename);
|
const _dirname = dirname(_filename);
|
||||||
@@ -57,7 +59,7 @@ export class FileServerService {
|
|||||||
reply.header('Cache-Control', 'max-age=300');
|
reply.header('Cache-Control', 'max-age=300');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||||
fastify.addHook('onRequest', (request, reply, done) => {
|
fastify.addHook('onRequest', (request, reply, done) => {
|
||||||
@@ -70,23 +72,309 @@ export class FileServerService {
|
|||||||
serve: false,
|
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`);
|
const file = fs.createReadStream(`${_dirname}/assets/dummy.png`);
|
||||||
reply.header('Content-Type', 'image/jpeg');
|
reply.header('Content-Type', 'image/jpeg');
|
||||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||||
return reply.send(file);
|
return reply.send(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { key: string; } }>('/:key', async (request, reply) => await this.sendDriveFile(request, reply));
|
fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => {
|
||||||
fastify.get<{ Params: { key: string; } }>('/:key/*', async (request, reply) => await this.sendDriveFile(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();
|
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
|
@bindThis
|
||||||
private async sendDriveFile(request: FastifyRequest<{ Params: { key: string; } }>, reply: FastifyReply) {
|
private async sendDriveFile(request: FastifyRequest<{ Params: { key: string; } }>, reply: FastifyReply) {
|
||||||
const key = request.params.key;
|
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
|
// Fetch drive file
|
||||||
const file = await this.driveFilesRepository.createQueryBuilder('file')
|
const file = await this.driveFilesRepository.createQueryBuilder('file')
|
||||||
.where('file.accessKey = :accessKey', { accessKey: key })
|
.where('file.accessKey = :accessKey', { accessKey: key })
|
||||||
@@ -94,89 +382,41 @@ export class FileServerService {
|
|||||||
.orWhere('file.webpublicAccessKey = :webpublicAccessKey', { webpublicAccessKey: key })
|
.orWhere('file.webpublicAccessKey = :webpublicAccessKey', { webpublicAccessKey: key })
|
||||||
.getOne();
|
.getOne();
|
||||||
|
|
||||||
if (file == null) {
|
if (file == null) return '404';
|
||||||
reply.code(404);
|
|
||||||
reply.header('Cache-Control', 'max-age=86400');
|
|
||||||
return reply.sendFile('/dummy.png', assets);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isThumbnail = file.thumbnailAccessKey === key;
|
const isThumbnail = file.thumbnailAccessKey === key;
|
||||||
const isWebpublic = file.webpublicAccessKey === key;
|
const isWebpublic = file.webpublicAccessKey === key;
|
||||||
|
|
||||||
if (!file.storedInternal) {
|
if (!file.storedInternal) {
|
||||||
if (file.isLink && file.uri) { // 期限切れリモートファイル
|
if (!(file.isLink && file.uri)) return '204';
|
||||||
const [path, cleanup] = await createTemp();
|
const result = await this.downloadAndDetectTypeFromUrl(file.uri);
|
||||||
|
return {
|
||||||
try {
|
...result,
|
||||||
await this.downloadService.downloadUrl(file.uri, path);
|
fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
|
||||||
|
file,
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reply.code(204);
|
|
||||||
reply.header('Cache-Control', 'max-age=86400');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isThumbnail || isWebpublic) {
|
const path = this.internalStorageService.resolvePath(key);
|
||||||
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();
|
|
||||||
|
|
||||||
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(mime) ? mime : 'application/octet-stream');
|
if (isThumbnail || isWebpublic) {
|
||||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
const { mime, ext } = await this.fileInfoService.detectType(path);
|
||||||
reply.header('Content-Disposition', contentDisposition('inline', filename));
|
return {
|
||||||
return this.internalStorageService.read(key);
|
state: 'stored_internal',
|
||||||
} else {
|
fileRole: isThumbnail ? 'thumbnail' : 'webpublic',
|
||||||
const readable = this.internalStorageService.read(file.accessKey!);
|
file,
|
||||||
readable.on('error', this.commonReadableHandlerGenerator(reply));
|
mime, ext,
|
||||||
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.type) ? file.type : 'application/octet-stream');
|
path,
|
||||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
};
|
||||||
reply.header('Content-Disposition', contentDisposition('inline', file.name));
|
}
|
||||||
return readable;
|
|
||||||
|
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -3,7 +3,6 @@ import { EndpointsModule } from '@/server/api/EndpointsModule.js';
|
|||||||
import { CoreModule } from '@/core/CoreModule.js';
|
import { CoreModule } from '@/core/CoreModule.js';
|
||||||
import { ApiCallService } from './api/ApiCallService.js';
|
import { ApiCallService } from './api/ApiCallService.js';
|
||||||
import { FileServerService } from './FileServerService.js';
|
import { FileServerService } from './FileServerService.js';
|
||||||
import { MediaProxyServerService } from './MediaProxyServerService.js';
|
|
||||||
import { NodeinfoServerService } from './NodeinfoServerService.js';
|
import { NodeinfoServerService } from './NodeinfoServerService.js';
|
||||||
import { ServerService } from './ServerService.js';
|
import { ServerService } from './ServerService.js';
|
||||||
import { WellKnownServerService } from './WellKnownServerService.js';
|
import { WellKnownServerService } from './WellKnownServerService.js';
|
||||||
@@ -51,7 +50,6 @@ import { UserListChannelService } from './api/stream/channels/user-list.js';
|
|||||||
UrlPreviewService,
|
UrlPreviewService,
|
||||||
ActivityPubServerService,
|
ActivityPubServerService,
|
||||||
FileServerService,
|
FileServerService,
|
||||||
MediaProxyServerService,
|
|
||||||
NodeinfoServerService,
|
NodeinfoServerService,
|
||||||
ServerService,
|
ServerService,
|
||||||
WellKnownServerService,
|
WellKnownServerService,
|
||||||
|
@@ -20,7 +20,6 @@ import { NodeinfoServerService } from './NodeinfoServerService.js';
|
|||||||
import { ApiServerService } from './api/ApiServerService.js';
|
import { ApiServerService } from './api/ApiServerService.js';
|
||||||
import { StreamingApiServerService } from './api/StreamingApiServerService.js';
|
import { StreamingApiServerService } from './api/StreamingApiServerService.js';
|
||||||
import { WellKnownServerService } from './WellKnownServerService.js';
|
import { WellKnownServerService } from './WellKnownServerService.js';
|
||||||
import { MediaProxyServerService } from './MediaProxyServerService.js';
|
|
||||||
import { FileServerService } from './FileServerService.js';
|
import { FileServerService } from './FileServerService.js';
|
||||||
import { ClientServerService } from './web/ClientServerService.js';
|
import { ClientServerService } from './web/ClientServerService.js';
|
||||||
|
|
||||||
@@ -48,7 +47,6 @@ export class ServerService {
|
|||||||
private wellKnownServerService: WellKnownServerService,
|
private wellKnownServerService: WellKnownServerService,
|
||||||
private nodeinfoServerService: NodeinfoServerService,
|
private nodeinfoServerService: NodeinfoServerService,
|
||||||
private fileServerService: FileServerService,
|
private fileServerService: FileServerService,
|
||||||
private mediaProxyServerService: MediaProxyServerService,
|
|
||||||
private clientServerService: ClientServerService,
|
private clientServerService: ClientServerService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
@@ -73,8 +71,7 @@ export class ServerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fastify.register(this.apiServerService.createServer, { prefix: '/api' });
|
fastify.register(this.apiServerService.createServer, { prefix: '/api' });
|
||||||
fastify.register(this.fileServerService.createServer, { prefix: '/files' });
|
fastify.register(this.fileServerService.createServer);
|
||||||
fastify.register(this.mediaProxyServerService.createServer, { prefix: '/proxy' });
|
|
||||||
fastify.register(this.activityPubServerService.createServer);
|
fastify.register(this.activityPubServerService.createServer);
|
||||||
fastify.register(this.nodeinfoServerService.createServer);
|
fastify.register(this.nodeinfoServerService.createServer);
|
||||||
fastify.register(this.wellKnownServerService.createServer);
|
fastify.register(this.wellKnownServerService.createServer);
|
||||||
|
@@ -3,6 +3,8 @@ import { DataSource, In } from 'typeorm';
|
|||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { EmojisRepository } from '@/models/index.js';
|
import type { EmojisRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
@@ -35,6 +37,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
|
|
||||||
@Inject(DI.emojisRepository)
|
@Inject(DI.emojisRepository)
|
||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const emojis = await this.emojisRepository.findBy({
|
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']);
|
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 rndstr from 'rndstr';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { DriveFilesRepository, EmojisRepository } from '@/models/index.js';
|
import type { DriveFilesRepository } from '@/models/index.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
|
||||||
import { DI } from '@/di-symbols.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 { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
@@ -39,43 +37,26 @@ export const paramDef = {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.db)
|
|
||||||
private db: DataSource,
|
|
||||||
|
|
||||||
@Inject(DI.driveFilesRepository)
|
@Inject(DI.driveFilesRepository)
|
||||||
private driveFilesRepository: DriveFilesRepository,
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
|
|
||||||
@Inject(DI.emojisRepository)
|
private customEmojiService: CustomEmojiService,
|
||||||
private emojisRepository: EmojisRepository,
|
|
||||||
|
|
||||||
private emojiEntityService: EmojiEntityService,
|
|
||||||
private idService: IdService,
|
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
private moderationLogService: ModerationLogService,
|
private moderationLogService: ModerationLogService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
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({
|
const emoji = await this.customEmojiService.add({
|
||||||
id: this.idService.genId(),
|
driveFile,
|
||||||
updatedAt: new Date(),
|
name,
|
||||||
name: name,
|
|
||||||
category: null,
|
category: null,
|
||||||
host: null,
|
|
||||||
aliases: [],
|
aliases: [],
|
||||||
originalUrl: file.url,
|
host: null,
|
||||||
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),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.moderationLogService.insertModerationLog(me, 'addEmoji', {
|
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 type { EmojisRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
@@ -35,6 +37,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
private moderationLogService: ModerationLogService,
|
private moderationLogService: ModerationLogService,
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const emojis = await this.emojisRepository.findBy({
|
const emojis = await this.emojisRepository.findBy({
|
||||||
@@ -43,13 +47,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
|
|
||||||
for (const emoji of emojis) {
|
for (const emoji of emojis) {
|
||||||
await this.emojisRepository.delete(emoji.id);
|
await this.emojisRepository.delete(emoji.id);
|
||||||
|
|
||||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||||
|
|
||||||
this.moderationLogService.insertModerationLog(me, 'deleteEmoji', {
|
this.moderationLogService.insertModerationLog(me, 'deleteEmoji', {
|
||||||
emoji: emoji,
|
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 { DI } from '@/di-symbols.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
@@ -42,6 +44,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
private moderationLogService: ModerationLogService,
|
private moderationLogService: ModerationLogService,
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const emoji = await this.emojisRepository.findOneBy({ id: ps.id });
|
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']);
|
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||||
|
|
||||||
|
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
||||||
|
emojis: [ await this.emojiEntityService.pack(emoji) ],
|
||||||
|
});
|
||||||
|
|
||||||
this.moderationLogService.insertModerationLog(me, 'deleteEmoji', {
|
this.moderationLogService.insertModerationLog(me, 'deleteEmoji', {
|
||||||
emoji: emoji,
|
emoji: emoji,
|
||||||
});
|
});
|
||||||
|
@@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
.take(ps.limit)
|
.take(ps.limit)
|
||||||
.getMany();
|
.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();
|
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 { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { EmojisRepository } from '@/models/index.js';
|
import type { EmojisRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
@@ -35,6 +37,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
|
|
||||||
@Inject(DI.emojisRepository)
|
@Inject(DI.emojisRepository)
|
||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const emojis = await this.emojisRepository.findBy({
|
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']);
|
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 { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { EmojisRepository } from '@/models/index.js';
|
import type { EmojisRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
@@ -35,6 +37,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
|
|
||||||
@Inject(DI.emojisRepository)
|
@Inject(DI.emojisRepository)
|
||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
await this.emojisRepository.update({
|
await this.emojisRepository.update({
|
||||||
@@ -45,6 +50,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
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 { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { EmojisRepository } from '@/models/index.js';
|
import type { EmojisRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
@@ -37,6 +39,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
|
|
||||||
@Inject(DI.emojisRepository)
|
@Inject(DI.emojisRepository)
|
||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
await this.emojisRepository.update({
|
await this.emojisRepository.update({
|
||||||
@@ -47,6 +52,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
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 type { EmojisRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
@@ -48,6 +50,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
|
|
||||||
@Inject(DI.emojisRepository)
|
@Inject(DI.emojisRepository)
|
||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const emoji = await this.emojisRepository.findOneBy({ id: ps.id });
|
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']);
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,6 +10,8 @@ export const meta = {
|
|||||||
tags: ['meta'],
|
tags: ['meta'],
|
||||||
|
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
allowGet: true,
|
||||||
|
cacheSec: 3600,
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@@ -83,6 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
emojis: await this.emojiEntityService.packMany(emojis, {
|
emojis: await this.emojiEntityService.packMany(emojis, {
|
||||||
omitId: true,
|
omitId: true,
|
||||||
omitHost: true,
|
omitHost: true,
|
||||||
|
withUrl: true,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@@ -33,16 +33,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
private httpRequestService: HttpRequestService,
|
private httpRequestService: HttpRequestService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const res = await this.httpRequestService.fetch(
|
const res = await this.httpRequestService.send(ps.url, {
|
||||||
ps.url,
|
method: 'GET',
|
||||||
{
|
headers: {
|
||||||
method: 'GET',
|
Accept: 'application/rss+xml, */*',
|
||||||
headers: {
|
},
|
||||||
Accept: 'application/rss+xml, */*',
|
timeout: 5000,
|
||||||
},
|
});
|
||||||
// timeout: 5000,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
|
|
||||||
|
@@ -6,6 +6,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
|||||||
import { GetterService } from '@/server/api/GetterService.js';
|
import { GetterService } from '@/server/api/GetterService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
import { AchievementService } from '@/core/AchievementService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notes', 'favorites'],
|
tags: ['notes', 'favorites'],
|
||||||
@@ -51,6 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private getterService: GetterService,
|
private getterService: GetterService,
|
||||||
|
private achievementService: AchievementService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
// Get favoritee
|
// Get favoritee
|
||||||
@@ -76,6 +78,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (note.userHost == null) {
|
||||||
|
this.achievementService.create(note.userId, 'myNoteFavorited1');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,8 +7,8 @@ import { DI } from '@/di-symbols.js';
|
|||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { ApiError } from '../../error.js';
|
|
||||||
import { GetterService } from '@/server/api/GetterService.js';
|
import { GetterService } from '@/server/api/GetterService.js';
|
||||||
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notes'],
|
tags: ['notes'],
|
||||||
@@ -83,20 +83,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
|
|
||||||
const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
|
const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
|
||||||
|
|
||||||
const res = await this.httpRequestService.fetch(
|
const res = await this.httpRequestService.send(endpoint, {
|
||||||
endpoint,
|
method: 'POST',
|
||||||
{
|
headers: {
|
||||||
method: 'POST',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
headers: {
|
Accept: 'application/json, */*',
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
Accept: 'application/json, */*',
|
|
||||||
},
|
|
||||||
body: params.toString(),
|
|
||||||
},
|
},
|
||||||
{
|
body: params.toString(),
|
||||||
noOkError: false,
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const json = (await res.json()) as {
|
const json = (await res.json()) as {
|
||||||
translations: {
|
translations: {
|
||||||
|
@@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId });
|
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId });
|
||||||
|
|
||||||
if (me == null || (me.id !== ps.userId && !profile.publicReactions)) {
|
if ((me == null || me.id !== ps.userId) && !profile.publicReactions) {
|
||||||
throw new ApiError(meta.errors.reactionsNotPublic);
|
throw new ApiError(meta.errors.reactionsNotPublic);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import Redis from 'ioredis';
|
import Redis from 'ioredis';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
import autwh from 'autwh';
|
import * as autwh from 'autwh';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
@@ -30,7 +30,7 @@ export interface InternalStreamTypes {
|
|||||||
remoteUserUpdated: Serialized<{ id: User['id']; }>;
|
remoteUserUpdated: Serialized<{ id: User['id']; }>;
|
||||||
follow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>;
|
follow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>;
|
||||||
unfollow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>;
|
unfollow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>;
|
||||||
policiesUpdated: Serialized<Role['options']>;
|
policiesUpdated: Serialized<Role['policies']>;
|
||||||
roleCreated: Serialized<Role>;
|
roleCreated: Serialized<Role>;
|
||||||
roleDeleted: Serialized<Role>;
|
roleDeleted: Serialized<Role>;
|
||||||
roleUpdated: Serialized<Role>;
|
roleUpdated: Serialized<Role>;
|
||||||
@@ -49,6 +49,16 @@ export interface BroadcastTypes {
|
|||||||
emojiAdded: {
|
emojiAdded: {
|
||||||
emoji: Packed<'Emoji'>;
|
emoji: Packed<'Emoji'>;
|
||||||
};
|
};
|
||||||
|
emojiUpdated: {
|
||||||
|
emojis: Packed<'Emoji'>[];
|
||||||
|
};
|
||||||
|
emojiDeleted: {
|
||||||
|
emojis: {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
[other: string]: any;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserStreamTypes {
|
export interface UserStreamTypes {
|
||||||
|
@@ -22,18 +22,13 @@
|
|||||||
renderError('SOMETHING_HAPPENED_IN_PROMISE', e);
|
renderError('SOMETHING_HAPPENED_IN_PROMISE', e);
|
||||||
};
|
};
|
||||||
|
|
||||||
const v = localStorage.getItem('v') || VERSION;
|
|
||||||
|
|
||||||
let forceError = localStorage.getItem('forceError');
|
let forceError = localStorage.getItem('forceError');
|
||||||
if (forceError != null) {
|
if (forceError != null) {
|
||||||
renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.')
|
renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.')
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region Detect language & fetch translations
|
//#region Detect language & fetch translations
|
||||||
const localeVersion = localStorage.getItem('localeVersion');
|
if (!localStorage.hasOwnProperty('locale')) {
|
||||||
const localeOutdated = (localeVersion == null || localeVersion !== v);
|
|
||||||
|
|
||||||
if (!localStorage.hasOwnProperty('locale') || localeOutdated) {
|
|
||||||
const supportedLangs = LANGS;
|
const supportedLangs = LANGS;
|
||||||
let lang = localStorage.getItem('lang');
|
let lang = localStorage.getItem('lang');
|
||||||
if (lang == null || !supportedLangs.includes(lang)) {
|
if (lang == null || !supportedLangs.includes(lang)) {
|
||||||
@@ -47,13 +42,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await window.fetch(`/assets/locales/${lang}.${v}.json`);
|
const metaRes = await window.fetch('/api/meta', {
|
||||||
if (res.status === 200) {
|
method: 'POST',
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
credentials: 'omit',
|
||||||
|
cache: 'no-cache',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (metaRes.status !== 200) {
|
||||||
|
renderError('META_FETCH');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const meta = await metaRes.json();
|
||||||
|
const v = meta.version;
|
||||||
|
if (v == null) {
|
||||||
|
renderError('META_FETCH_V');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`);
|
||||||
|
if (localRes.status === 200) {
|
||||||
localStorage.setItem('lang', lang);
|
localStorage.setItem('lang', lang);
|
||||||
localStorage.setItem('locale', await res.text());
|
localStorage.setItem('locale', await localRes.text());
|
||||||
localStorage.setItem('localeVersion', v);
|
localStorage.setItem('localeVersion', v);
|
||||||
} else {
|
} else {
|
||||||
await checkUpdate();
|
|
||||||
renderError('LOCALE_FETCH');
|
renderError('LOCALE_FETCH');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -64,7 +77,6 @@
|
|||||||
function importAppScript() {
|
function importAppScript() {
|
||||||
import(`/vite/${CLIENT_ENTRY}`)
|
import(`/vite/${CLIENT_ENTRY}`)
|
||||||
.catch(async e => {
|
.catch(async e => {
|
||||||
await checkUpdate();
|
|
||||||
console.error(e);
|
console.error(e);
|
||||||
renderError('APP_IMPORT', e);
|
renderError('APP_IMPORT', e);
|
||||||
});
|
});
|
||||||
@@ -142,7 +154,7 @@
|
|||||||
<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
|
<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<h1>An error has occurred!</h1>
|
<h1>An error has occurred!</h1>
|
||||||
<button class="button-big" onclick="location.reload(true);">
|
<button class="button-big" onclick="location.reload();">
|
||||||
<span class="button-label-big">Refresh</span>
|
<span class="button-label-big">Refresh</span>
|
||||||
</button>
|
</button>
|
||||||
<p class="dont-worry">Don't worry, it's (probably) not your fault.</p>
|
<p class="dont-worry">Don't worry, it's (probably) not your fault.</p>
|
||||||
@@ -291,48 +303,4 @@
|
|||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-inner-declarations
|
|
||||||
async function checkUpdate() {
|
|
||||||
try {
|
|
||||||
const res = await window.fetch('/api/meta', {
|
|
||||||
method: 'POST',
|
|
||||||
cache: 'no-cache',
|
|
||||||
body: '{}',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const meta = await res.json();
|
|
||||||
|
|
||||||
if (meta.version == null) {
|
|
||||||
throw new Error('failed to fetch instance metadata');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (meta.version != v) {
|
|
||||||
localStorage.setItem('v', meta.version);
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
renderError('UPDATE_CHECK', e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-inner-declarations
|
|
||||||
function refresh() {
|
|
||||||
// Clear cache (service worker)
|
|
||||||
try {
|
|
||||||
navigator.serviceWorker.controller.postMessage('clear');
|
|
||||||
navigator.serviceWorker.getRegistrations().then(registrations => {
|
|
||||||
registrations.forEach(registration => registration.unregister());
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
|
@@ -35,7 +35,7 @@ html
|
|||||||
link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg')
|
link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg')
|
||||||
link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
|
link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
|
||||||
link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
|
link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
|
||||||
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.css')
|
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css')
|
||||||
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
|
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
|
||||||
|
|
||||||
if !config.clientManifestExists
|
if !config.clientManifestExists
|
||||||
|
@@ -8,20 +8,20 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordapp/twemoji": "14.0.2",
|
"@discordapp/twemoji": "14.0.2",
|
||||||
"@rollup/plugin-alias": "4.0.2",
|
"@rollup/plugin-alias": "4.0.3",
|
||||||
"@rollup/plugin-json": "6.0.0",
|
"@rollup/plugin-json": "6.0.0",
|
||||||
"@rollup/pluginutils": "5.0.2",
|
"@rollup/pluginutils": "5.0.2",
|
||||||
"@syuilo/aiscript": "0.12.2",
|
"@syuilo/aiscript": "0.12.4",
|
||||||
"@tabler/icons": "^1.118.0",
|
"@tabler/icons-webfont": "^2.1.2",
|
||||||
"@vitejs/plugin-vue": "4.0.0",
|
"@vitejs/plugin-vue": "4.0.0",
|
||||||
"@vue/compiler-sfc": "3.2.45",
|
"@vue/compiler-sfc": "3.2.45",
|
||||||
"autobind-decorator": "2.4.0",
|
"autobind-decorator": "2.4.0",
|
||||||
"autosize": "5.0.2",
|
"autosize": "5.0.2",
|
||||||
"blurhash": "2.0.4",
|
"blurhash": "2.0.4",
|
||||||
"broadcast-channel": "4.20.1",
|
"broadcast-channel": "4.20.2",
|
||||||
"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
|
"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
|
||||||
"canvas-confetti": "^1.6.0",
|
"canvas-confetti": "^1.6.0",
|
||||||
"chart.js": "4.1.2",
|
"chart.js": "4.2.0",
|
||||||
"chartjs-adapter-date-fns": "3.0.0",
|
"chartjs-adapter-date-fns": "3.0.0",
|
||||||
"chartjs-chart-matrix": "^1.3.0",
|
"chartjs-chart-matrix": "^1.3.0",
|
||||||
"chartjs-plugin-gradient": "0.6.1",
|
"chartjs-plugin-gradient": "0.6.1",
|
||||||
@@ -41,10 +41,10 @@
|
|||||||
"misskey-js": "0.0.14",
|
"misskey-js": "0.0.14",
|
||||||
"photoswipe": "5.3.4",
|
"photoswipe": "5.3.4",
|
||||||
"prismjs": "1.29.0",
|
"prismjs": "1.29.0",
|
||||||
"punycode": "2.2.0",
|
"punycode": "2.3.0",
|
||||||
"querystring": "0.2.1",
|
"querystring": "0.2.1",
|
||||||
"rndstr": "1.0.0",
|
"rndstr": "1.0.0",
|
||||||
"rollup": "3.10.0",
|
"rollup": "3.11.0",
|
||||||
"s-age": "1.1.2",
|
"s-age": "1.1.2",
|
||||||
"sanitize-html": "^2.8.1",
|
"sanitize-html": "^2.8.1",
|
||||||
"sass": "1.57.1",
|
"sass": "1.57.1",
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
"syuilo-password-strength": "0.0.1",
|
"syuilo-password-strength": "0.0.1",
|
||||||
"textarea-caret": "3.1.0",
|
"textarea-caret": "3.1.0",
|
||||||
"three": "0.148.0",
|
"three": "0.149.0",
|
||||||
"throttle-debounce": "5.0.0",
|
"throttle-debounce": "5.0.0",
|
||||||
"tinycolor2": "1.5.2",
|
"tinycolor2": "1.5.2",
|
||||||
"tsc-alias": "1.8.2",
|
"tsc-alias": "1.8.2",
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/escape-regexp": "0.0.1",
|
"@types/escape-regexp": "0.0.1",
|
||||||
"@types/glob": "8.0.0",
|
"@types/glob": "8.0.1",
|
||||||
"@types/gulp": "4.0.10",
|
"@types/gulp": "4.0.10",
|
||||||
"@types/gulp-rename": "2.0.1",
|
"@types/gulp-rename": "2.0.1",
|
||||||
"@types/matter-js": "0.18.2",
|
"@types/matter-js": "0.18.2",
|
||||||
@@ -82,15 +82,15 @@
|
|||||||
"@types/uuid": "9.0.0",
|
"@types/uuid": "9.0.0",
|
||||||
"@types/websocket": "1.0.5",
|
"@types/websocket": "1.0.5",
|
||||||
"@types/ws": "8.5.4",
|
"@types/ws": "8.5.4",
|
||||||
"@typescript-eslint/eslint-plugin": "5.48.1",
|
"@typescript-eslint/eslint-plugin": "5.49.0",
|
||||||
"@typescript-eslint/parser": "5.48.1",
|
"@typescript-eslint/parser": "5.49.0",
|
||||||
"@vue/runtime-core": "3.2.45",
|
"@vue/runtime-core": "3.2.45",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "12.3.0",
|
"cypress": "12.4.0",
|
||||||
"eslint": "8.31.0",
|
"eslint": "8.32.0",
|
||||||
"eslint-plugin-import": "2.27.4",
|
"eslint-plugin-import": "2.27.5",
|
||||||
"eslint-plugin-vue": "9.9.0",
|
"eslint-plugin-vue": "9.9.0",
|
||||||
"start-server-and-test": "1.15.2",
|
"start-server-and-test": "1.15.3",
|
||||||
"vue-eslint-parser": "^9.1.0",
|
"vue-eslint-parser": "^9.1.0",
|
||||||
"vue-tsc": "^1.0.24"
|
"vue-tsc": "^1.0.24"
|
||||||
}
|
}
|
||||||
|
@@ -16,8 +16,8 @@
|
|||||||
<time v-tooltip="new Date(achievement.unlockedAt).toLocaleString()">{{ new Date(achievement.unlockedAt).getFullYear() }}/{{ new Date(achievement.unlockedAt).getMonth() + 1 }}/{{ new Date(achievement.unlockedAt).getDate() }}</time>
|
<time v-tooltip="new Date(achievement.unlockedAt).toLocaleString()">{{ new Date(achievement.unlockedAt).getFullYear() }}/{{ new Date(achievement.unlockedAt).getMonth() + 1 }}/{{ new Date(achievement.unlockedAt).getDate() }}</time>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.description">{{ i18n.ts._achievements._types['_' + achievement.name].description }}</div>
|
<div :class="$style.description">{{ withDescription ? i18n.ts._achievements._types['_' + achievement.name].description : '???' }}</div>
|
||||||
<div v-if="i18n.ts._achievements._types['_' + achievement.name].flavor" :class="$style.flavor">{{ i18n.ts._achievements._types['_' + achievement.name].flavor }}</div>
|
<div v-if="i18n.ts._achievements._types['_' + achievement.name].flavor && withDescription" :class="$style.flavor">{{ i18n.ts._achievements._types['_' + achievement.name].flavor }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="withLocked">
|
<template v-if="withLocked">
|
||||||
@@ -49,8 +49,10 @@ import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/scrip
|
|||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
user: misskey.entities.User;
|
user: misskey.entities.User;
|
||||||
withLocked: boolean;
|
withLocked: boolean;
|
||||||
|
withDescription: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
withLocked: true,
|
withLocked: true,
|
||||||
|
withDescription: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
let achievements = $ref();
|
let achievements = $ref();
|
||||||
|
@@ -17,7 +17,7 @@
|
|||||||
</ol>
|
</ol>
|
||||||
<ol v-else-if="emojis.length > 0" ref="suggests" :class="$style.list">
|
<ol v-else-if="emojis.length > 0" ref="suggests" :class="$style.list">
|
||||||
<li v-for="emoji in emojis" :key="emoji.emoji" :class="$style.item" tabindex="-1" @click="complete(type, emoji.emoji)" @keydown="onKeydown">
|
<li v-for="emoji in emojis" :key="emoji.emoji" :class="$style.item" tabindex="-1" @click="complete(type, emoji.emoji)" @keydown="onKeydown">
|
||||||
<MkEmoji :emoji="emoji.emoji" :class="$style.emoji"/>
|
<MkCustomEmoji :name="emoji.emoji" :class="$style.emoji"/>
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<span v-if="q" :class="$style.emojiName" v-html="sanitizeHtml(emoji.name.replace(q, `<b>${q}</b>`))"></span>
|
<span v-if="q" :class="$style.emojiName" v-html="sanitizeHtml(emoji.name.replace(q, `<b>${q}</b>`))"></span>
|
||||||
<span v-else v-text="emoji.name"></span>
|
<span v-else v-text="emoji.name"></span>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { markRaw, ref, shallowRef, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
import { markRaw, ref, shallowRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
||||||
import sanitizeHtml from 'sanitize-html';
|
import sanitizeHtml from 'sanitize-html';
|
||||||
import contains from '@/scripts/contains';
|
import contains from '@/scripts/contains';
|
||||||
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base';
|
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base';
|
||||||
@@ -61,59 +61,62 @@ type EmojiDef = {
|
|||||||
|
|
||||||
const lib = emojilist.filter(x => x.category !== 'flags');
|
const lib = emojilist.filter(x => x.category !== 'flags');
|
||||||
|
|
||||||
const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
|
const emojiDb = computed(() => {
|
||||||
|
//#region Unicode Emoji
|
||||||
|
const char2path = defaultStore.reactiveState.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
|
||||||
|
|
||||||
const emjdb: EmojiDef[] = lib.map(x => ({
|
const unicodeEmojiDB: EmojiDef[] = lib.map(x => ({
|
||||||
emoji: x.char,
|
emoji: x.char,
|
||||||
name: x.name,
|
|
||||||
url: char2path(x.char),
|
|
||||||
}));
|
|
||||||
|
|
||||||
for (const x of lib) {
|
|
||||||
if (x.keywords) {
|
|
||||||
for (const k of x.keywords) {
|
|
||||||
emjdb.push({
|
|
||||||
emoji: x.char,
|
|
||||||
name: k,
|
|
||||||
aliasOf: x.name,
|
|
||||||
url: char2path(x.char),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emjdb.sort((a, b) => a.name.length - b.name.length);
|
|
||||||
|
|
||||||
//#region Construct Emoji DB
|
|
||||||
const emojiDefinitions: EmojiDef[] = [];
|
|
||||||
|
|
||||||
for (const x of customEmojis) {
|
|
||||||
emojiDefinitions.push({
|
|
||||||
name: x.name,
|
name: x.name,
|
||||||
emoji: `:${x.name}:`,
|
url: char2path(x.char),
|
||||||
isCustomEmoji: true,
|
}));
|
||||||
});
|
|
||||||
|
|
||||||
if (x.aliases) {
|
for (const x of lib) {
|
||||||
for (const alias of x.aliases) {
|
if (x.keywords) {
|
||||||
emojiDefinitions.push({
|
for (const k of x.keywords) {
|
||||||
name: alias,
|
unicodeEmojiDB.push({
|
||||||
aliasOf: x.name,
|
emoji: x.char,
|
||||||
emoji: `:${x.name}:`,
|
name: k,
|
||||||
isCustomEmoji: true,
|
aliasOf: x.name,
|
||||||
});
|
url: char2path(x.char),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
emojiDefinitions.sort((a, b) => a.name.length - b.name.length);
|
unicodeEmojiDB.sort((a, b) => a.name.length - b.name.length);
|
||||||
|
//#endregion
|
||||||
|
|
||||||
const emojiDb = markRaw(emojiDefinitions.concat(emjdb));
|
//#region Custom Emoji
|
||||||
//#endregion
|
const customEmojiDB: EmojiDef[] = [];
|
||||||
|
|
||||||
|
for (const x of customEmojis.value) {
|
||||||
|
customEmojiDB.push({
|
||||||
|
name: x.name,
|
||||||
|
emoji: `:${x.name}:`,
|
||||||
|
isCustomEmoji: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (x.aliases) {
|
||||||
|
for (const alias of x.aliases) {
|
||||||
|
customEmojiDB.push({
|
||||||
|
name: alias,
|
||||||
|
aliasOf: x.name,
|
||||||
|
emoji: `:${x.name}:`,
|
||||||
|
isCustomEmoji: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customEmojiDB.sort((a, b) => a.name.length - b.name.length);
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
return markRaw([...customEmojiDB, ...unicodeEmojiDB]);
|
||||||
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emojiDb,
|
emojiDb,
|
||||||
emojiDefinitions,
|
|
||||||
emojilist,
|
emojilist,
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -230,27 +233,27 @@ function exec() {
|
|||||||
} else if (props.type === 'emoji') {
|
} else if (props.type === 'emoji') {
|
||||||
if (!props.q || props.q === '') {
|
if (!props.q || props.q === '') {
|
||||||
// 最近使った絵文字をサジェスト
|
// 最近使った絵文字をサジェスト
|
||||||
emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
|
emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const matched: EmojiDef[] = [];
|
const matched: EmojiDef[] = [];
|
||||||
const max = 30;
|
const max = 30;
|
||||||
|
|
||||||
emojiDb.some(x => {
|
emojiDb.value.some(x => {
|
||||||
if (x.name.startsWith(props.q ?? '') && !x.aliasOf && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
|
if (x.name.startsWith(props.q ?? '') && !x.aliasOf && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
|
||||||
return matched.length === max;
|
return matched.length === max;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (matched.length < max) {
|
if (matched.length < max) {
|
||||||
emojiDb.some(x => {
|
emojiDb.value.some(x => {
|
||||||
if (x.name.startsWith(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
|
if (x.name.startsWith(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
|
||||||
return matched.length === max;
|
return matched.length === max;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matched.length < max) {
|
if (matched.length < max) {
|
||||||
emojiDb.some(x => {
|
emojiDb.value.some(x => {
|
||||||
if (x.name.includes(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
|
if (x.name.includes(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
|
||||||
return matched.length === max;
|
return matched.length === max;
|
||||||
});
|
});
|
||||||
|
@@ -11,17 +11,18 @@
|
|||||||
class="_button item"
|
class="_button item"
|
||||||
@click="emit('chosen', emoji, $event)"
|
@click="emit('chosen', emoji, $event)"
|
||||||
>
|
>
|
||||||
<MkEmoji class="emoji" :emoji="emoji" :normal="true"/>
|
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
|
||||||
|
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref, computed, Ref } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
emojis: string[];
|
emojis: string[] | Ref<string[]>;
|
||||||
initialShown?: boolean;
|
initialShown?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -29,5 +30,7 @@ const emit = defineEmits<{
|
|||||||
(ev: 'chosen', v: string, event: MouseEvent): void;
|
(ev: 'chosen', v: string, event: MouseEvent): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const emojis = computed(() => Array.isArray(props.emojis) ? props.emojis : props.emojis.value);
|
||||||
|
|
||||||
const shown = ref(!!props.initialShown);
|
const shown = ref(!!props.initialShown);
|
||||||
</script>
|
</script>
|
||||||
|
@@ -12,7 +12,7 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click="chosen(emoji, $event)"
|
@click="chosen(emoji, $event)"
|
||||||
>
|
>
|
||||||
<MkEmoji class="emoji" :emoji="`:${emoji.name}:`"/>
|
<MkCustomEmoji class="emoji" :name="emoji.name"/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="searchResultUnicode.length > 0" class="body">
|
<div v-if="searchResultUnicode.length > 0" class="body">
|
||||||
@@ -39,7 +39,8 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click="chosen(emoji, $event)"
|
@click="chosen(emoji, $event)"
|
||||||
>
|
>
|
||||||
<MkEmoji class="emoji" :emoji="emoji" :normal="true"/>
|
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
|
||||||
|
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -53,18 +54,27 @@
|
|||||||
class="_button item"
|
class="_button item"
|
||||||
@click="chosen(emoji, $event)"
|
@click="chosen(emoji, $event)"
|
||||||
>
|
>
|
||||||
<MkEmoji class="emoji" :emoji="emoji" :normal="true"/>
|
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
|
||||||
|
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div v-once class="group">
|
<div v-once class="group">
|
||||||
<header class="_acrylic">{{ i18n.ts.customEmojis }}</header>
|
<header class="_acrylic">{{ i18n.ts.customEmojis }}</header>
|
||||||
<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.ts.other }}</XSection>
|
<XSection
|
||||||
|
v-for="category in customEmojiCategories"
|
||||||
|
:key="`custom:${category}`"
|
||||||
|
:initial-shown="false"
|
||||||
|
:emojis="computed(() => customEmojis.filter(e => category === null ? (e.category === 'null' || !e.category) : e.category === category).map(e => `:${e.name}:`))"
|
||||||
|
@chosen="chosen"
|
||||||
|
>
|
||||||
|
{{ category || i18n.ts.other }}
|
||||||
|
</XSection>
|
||||||
</div>
|
</div>
|
||||||
<div v-once class="group">
|
<div v-once class="group">
|
||||||
<header class="_acrylic">{{ i18n.ts.emoji }}</header>
|
<header class="_acrylic">{{ i18n.ts.emoji }}</header>
|
||||||
<XSection v-for="category in categories" :key="category" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection>
|
<XSection v-for="category in categories" :key="category" :emojis="emojiCharByCategory.get(category) ?? []" @chosen="chosen">{{ category }}</XSection>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
@@ -80,7 +90,7 @@
|
|||||||
import { ref, shallowRef, computed, watch, onMounted } from 'vue';
|
import { ref, shallowRef, computed, watch, onMounted } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import XSection from '@/components/MkEmojiPicker.section.vue';
|
import XSection from '@/components/MkEmojiPicker.section.vue';
|
||||||
import { emojilist, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist';
|
import { emojilist, emojiCharByCategory, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist';
|
||||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { isTouchUsing } from '@/scripts/touch';
|
import { isTouchUsing } from '@/scripts/touch';
|
||||||
@@ -88,7 +98,7 @@ import { deviceKind } from '@/scripts/device-kind';
|
|||||||
import { instance } from '@/instance';
|
import { instance } from '@/instance';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import { getCustomEmojiCategories, customEmojis } from '@/custom-emojis';
|
import { customEmojiCategories, customEmojis } from '@/custom-emojis';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
showPinned?: boolean;
|
showPinned?: boolean;
|
||||||
@@ -104,7 +114,6 @@ const emit = defineEmits<{
|
|||||||
(ev: 'chosen', v: string): void;
|
(ev: 'chosen', v: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const customEmojiCategories = getCustomEmojiCategories();
|
|
||||||
const searchEl = shallowRef<HTMLInputElement>();
|
const searchEl = shallowRef<HTMLInputElement>();
|
||||||
const emojisEl = shallowRef<HTMLDivElement>();
|
const emojisEl = shallowRef<HTMLDivElement>();
|
||||||
|
|
||||||
@@ -138,7 +147,7 @@ watch(q, () => {
|
|||||||
|
|
||||||
const searchCustom = () => {
|
const searchCustom = () => {
|
||||||
const max = 8;
|
const max = 8;
|
||||||
const emojis = customEmojis;
|
const emojis = customEmojis.value;
|
||||||
const matches = new Set<Misskey.entities.CustomEmoji>();
|
const matches = new Set<Misskey.entities.CustomEmoji>();
|
||||||
|
|
||||||
const exactMatch = emojis.find(emoji => emoji.name === newQ);
|
const exactMatch = emojis.find(emoji => emoji.name === newQ);
|
||||||
@@ -323,7 +332,7 @@ function done(query?: string): boolean | void {
|
|||||||
if (query == null || typeof query !== 'string') return;
|
if (query == null || typeof query !== 'string') return;
|
||||||
|
|
||||||
const q2 = query.replace(/:/g, '');
|
const q2 = query.replace(/:/g, '');
|
||||||
const exactMatchCustom = customEmojis.find(emoji => emoji.name === q2);
|
const exactMatchCustom = customEmojis.value.find(emoji => emoji.name === q2);
|
||||||
if (exactMatchCustom) {
|
if (exactMatchCustom) {
|
||||||
chosen(exactMatchCustom);
|
chosen(exactMatchCustom);
|
||||||
return true;
|
return true;
|
||||||
|
@@ -88,6 +88,8 @@ const onInput = (ev: KeyboardEvent) => {
|
|||||||
emit('change', ev);
|
emit('change', ev);
|
||||||
};
|
};
|
||||||
const onKeydown = (ev: KeyboardEvent) => {
|
const onKeydown = (ev: KeyboardEvent) => {
|
||||||
|
if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
|
||||||
|
|
||||||
emit('keydown', ev);
|
emit('keydown', ev);
|
||||||
|
|
||||||
if (ev.code === 'Enter') {
|
if (ev.code === 'Enter') {
|
||||||
|
@@ -335,8 +335,7 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
margin-right: 5px;
|
margin-right: 8px;
|
||||||
width: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.caret {
|
.caret {
|
||||||
|
@@ -48,12 +48,12 @@
|
|||||||
<div :class="$style.text">
|
<div :class="$style.text">
|
||||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||||
<MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
<MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||||
<Mfm v-if="appearNote.text" v-once :text="appearNote.text" :author="appearNote.user" :i="$i"/>
|
<Mfm v-if="appearNote.text" v-once :text="appearNote.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/>
|
||||||
<div v-if="translating || translation" :class="$style.translation">
|
<div v-if="translating || translation" :class="$style.translation">
|
||||||
<MkLoading v-if="translating" mini/>
|
<MkLoading v-if="translating" mini/>
|
||||||
<div v-else :class="$style.translated">
|
<div v-else :class="$style.translated">
|
||||||
<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||||
<Mfm :text="translation.text" :author="appearNote.user" :i="$i"/>
|
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -65,13 +65,13 @@
|
|||||||
<div class="text">
|
<div class="text">
|
||||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||||
<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i"/>
|
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/>
|
||||||
<a v-if="appearNote.renote != null" class="rp">RN:</a>
|
<a v-if="appearNote.renote != null" class="rp">RN:</a>
|
||||||
<div v-if="translating || translation" class="translation">
|
<div v-if="translating || translation" class="translation">
|
||||||
<MkLoading v-if="translating" mini/>
|
<MkLoading v-if="translating" mini/>
|
||||||
<div v-else class="translated">
|
<div v-else class="translated">
|
||||||
<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||||
<Mfm :text="translation.text" :author="appearNote.user" :i="$i"/>
|
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
|
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
|
||||||
<div>
|
<div>
|
||||||
<p v-if="note.cw != null" :class="$style.cw">
|
<p v-if="note.cw != null" :class="$style.cw">
|
||||||
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :i="$i"/>
|
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :i="$i" :emoji-urls="note.emojis"/>
|
||||||
<MkCwButton v-model="showContent" :note="note"/>
|
<MkCwButton v-model="showContent" :note="note"/>
|
||||||
</p>
|
</p>
|
||||||
<div v-show="note.cw == null || showContent">
|
<div v-show="note.cw == null || showContent">
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
<i v-else-if="notification.type === 'mention'" class="ti ti-at"></i>
|
<i v-else-if="notification.type === 'mention'" class="ti ti-at"></i>
|
||||||
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
||||||
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
||||||
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-military-award"></i>
|
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
|
||||||
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
|
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
|
||||||
<MkReactionIcon
|
<MkReactionIcon
|
||||||
v-else-if="notification.type === 'reaction'"
|
v-else-if="notification.type === 'reaction'"
|
||||||
@@ -38,26 +38,26 @@
|
|||||||
<div v-once :class="$style.content">
|
<div v-once :class="$style.content">
|
||||||
<MkA v-if="notification.type === 'reaction'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
<MkA v-if="notification.type === 'reaction'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :author="notification.note.user"/>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
||||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA v-else-if="notification.type === 'renote'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
|
<MkA v-else-if="notification.type === 'renote'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
|
||||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||||
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full" :author="notification.note.renote.user"/>
|
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :author="notification.note.renote.user"/>
|
||||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA v-else-if="notification.type === 'reply'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
<MkA v-else-if="notification.type === 'reply'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :author="notification.note.user"/>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA v-else-if="notification.type === 'mention'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
<MkA v-else-if="notification.type === 'mention'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :author="notification.note.user"/>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA v-else-if="notification.type === 'quote'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
<MkA v-else-if="notification.type === 'quote'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :author="notification.note.user"/>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA v-else-if="notification.type === 'pollEnded'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
<MkA v-else-if="notification.type === 'pollEnded'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :author="notification.note.user"/>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
||||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
|
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
<span v-else-if="notification.type === 'receiveFollowRequest'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span>
|
<span v-else-if="notification.type === 'receiveFollowRequest'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span>
|
||||||
<span v-else-if="notification.type === 'groupInvited'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button></div></span>
|
<span v-else-if="notification.type === 'groupInvited'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button></div></span>
|
||||||
<span v-else-if="notification.type === 'app'" :class="$style.text">
|
<span v-else-if="notification.type === 'app'" :class="$style.text">
|
||||||
<Mfm :text="notification.body" :nowrap="!full"/>
|
<Mfm :text="notification.body" :nowrap="false"/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -249,7 +249,7 @@ useTooltip(reactionRef, (showing) => {
|
|||||||
|
|
||||||
.t_achievementEarned {
|
.t_achievementEarned {
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
background: #88a6b7;
|
background: #cb9a11;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
<template #default="{ items: notifications }">
|
<template #default="{ items: notifications }">
|
||||||
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :no-gap="true">
|
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :no-gap="true">
|
||||||
<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
|
<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
|
||||||
<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="false" class="_panel notification"/>
|
<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/>
|
||||||
</MkDateSeparatedList>
|
</MkDateSeparatedList>
|
||||||
</template>
|
</template>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
|
@@ -24,7 +24,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ComputedRef, inject, provide } from 'vue';
|
import { ComputedRef, inject, onMounted, onUnmounted, provide } from 'vue';
|
||||||
import RouterView from '@/components/global/RouterView.vue';
|
import RouterView from '@/components/global/RouterView.vue';
|
||||||
import MkWindow from '@/components/MkWindow.vue';
|
import MkWindow from '@/components/MkWindow.vue';
|
||||||
import { popout as _popout } from '@/scripts/popout';
|
import { popout as _popout } from '@/scripts/popout';
|
||||||
@@ -35,6 +35,8 @@ import { mainRouter, routes } from '@/router';
|
|||||||
import { Router } from '@/nirax';
|
import { Router } from '@/nirax';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
|
import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
|
||||||
|
import { openingWindowsCount } from '@/os';
|
||||||
|
import { claimAchievement } from '@/scripts/achievements';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
initialPath: string;
|
initialPath: string;
|
||||||
@@ -128,6 +130,17 @@ function popout() {
|
|||||||
windowEl.close();
|
windowEl.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
openingWindowsCount.value++;
|
||||||
|
if (openingWindowsCount.value >= 3) {
|
||||||
|
claimAchievement('open3windows');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
openingWindowsCount.value--;
|
||||||
|
});
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
close,
|
close,
|
||||||
});
|
});
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user