Compare commits
162 Commits
2023.9.0-r
...
2023.10.0-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6d5e18aa8d | ||
![]() |
986623dbdc | ||
![]() |
8c684d5391 | ||
![]() |
69de8cad7c | ||
![]() |
fb3338029b | ||
![]() |
aae1034d62 | ||
![]() |
dc435fb8ee | ||
![]() |
0fe8c0134c | ||
![]() |
5e8c0deab3 | ||
![]() |
d6ef28d4ca | ||
![]() |
93bd34113c | ||
![]() |
c8d7a5ae76 | ||
![]() |
71edc65d0d | ||
![]() |
3a7558f36c | ||
![]() |
4a595153dc | ||
![]() |
10e50f71d1 | ||
![]() |
d49e5b19e1 | ||
![]() |
873a93fea3 | ||
![]() |
e4345679dc | ||
![]() |
11e657bdd7 | ||
![]() |
691fe711ff | ||
![]() |
a5b6e807bb | ||
![]() |
adf9d9c969 | ||
![]() |
8c663f65a8 | ||
![]() |
481ca4ec03 | ||
![]() |
e6ca53c5e1 | ||
![]() |
95dc70021f | ||
![]() |
fd3295eba4 | ||
![]() |
a76cebd897 | ||
![]() |
7d289c1b77 | ||
![]() |
0bdbdba9f8 | ||
![]() |
4489ca3c74 | ||
![]() |
87416710c3 | ||
![]() |
132b01461d | ||
![]() |
dab205edb8 | ||
![]() |
e4dcab8671 | ||
![]() |
780721e9a2 | ||
![]() |
ee483f2dee | ||
![]() |
2a7bc847b0 | ||
![]() |
2333bdb98a | ||
![]() |
979741ce09 | ||
![]() |
5b00fa6f82 | ||
![]() |
d2bb35bcf3 | ||
![]() |
e4ade46a2d | ||
![]() |
89e4f28d06 | ||
![]() |
0dbf5175df | ||
![]() |
55c14aec2c | ||
![]() |
fb63fc1213 | ||
![]() |
ca515d5a7e | ||
![]() |
6ebea82dba | ||
![]() |
05d1f5e564 | ||
![]() |
ee70f05a86 | ||
![]() |
fb6a5c8356 | ||
![]() |
a997b7bdcc | ||
![]() |
bcbcaa9c60 | ||
![]() |
6b0f1d0cc1 | ||
![]() |
b40329887f | ||
![]() |
cc4fd6b5c5 | ||
![]() |
3dd84f7824 | ||
![]() |
610b68c8ff | ||
![]() |
a40734d417 | ||
![]() |
be81c1a6d6 | ||
![]() |
17b83ff4c1 | ||
![]() |
5fd0cb31f6 | ||
![]() |
f3e09af35b | ||
![]() |
cd8a8e204d | ||
![]() |
a511d8eddc | ||
![]() |
0f6ee7dc1c | ||
![]() |
6277a5545c | ||
![]() |
5ee93dc4a2 | ||
![]() |
10ae0b329a | ||
![]() |
000abcd2f0 | ||
![]() |
e00fdc2d59 | ||
![]() |
6840434661 | ||
![]() |
09dfb9bde3 | ||
![]() |
b0714cbd7b | ||
![]() |
d0917aac1a | ||
![]() |
ff6600da2e | ||
![]() |
7e74cff126 | ||
![]() |
e53749773e | ||
![]() |
392de4df36 | ||
![]() |
cc6a96e1c9 | ||
![]() |
0e681f3cc4 | ||
![]() |
a512915a84 | ||
![]() |
5edc885c22 | ||
![]() |
e5c339b86a | ||
![]() |
d92e2b6ae0 | ||
![]() |
eb38f08e13 | ||
![]() |
f269841a83 | ||
![]() |
b55ffa2cbe | ||
![]() |
0b0e58d405 | ||
![]() |
b349d0baf8 | ||
![]() |
961f5a0caa | ||
![]() |
ac19b055c7 | ||
![]() |
eb23fd4e60 | ||
![]() |
fbab67df35 | ||
![]() |
2529830bca | ||
![]() |
c01731f091 | ||
![]() |
9771f1c435 | ||
![]() |
424bb78387 | ||
![]() |
9c448055a3 | ||
![]() |
b9da1415a5 | ||
![]() |
4216a67462 | ||
![]() |
7ce86a6196 | ||
![]() |
2438c047a7 | ||
![]() |
c106db89e1 | ||
![]() |
a388e25f3e | ||
![]() |
63c6a9bb80 | ||
![]() |
772d2432b6 | ||
![]() |
eb740e2c72 | ||
![]() |
d854942a1f | ||
![]() |
ce1218a2b2 | ||
![]() |
d860e53b67 | ||
![]() |
055464a624 | ||
![]() |
9d0c077311 | ||
![]() |
440f3144ae | ||
![]() |
5ad0906c89 | ||
![]() |
2039e244c5 | ||
![]() |
bd19d75c9c | ||
![]() |
ee44f35fea | ||
![]() |
89edf8f81e | ||
![]() |
ece5469277 | ||
![]() |
576158e883 | ||
![]() |
dcaea66dbf | ||
![]() |
5318532a8d | ||
![]() |
646a8d1a54 | ||
![]() |
dc8ab01168 | ||
![]() |
281369d8c5 | ||
![]() |
65aef45050 | ||
![]() |
48314a39e0 | ||
![]() |
fe570fe16b | ||
![]() |
cf573add27 | ||
![]() |
4a7f6e6de4 | ||
![]() |
00659220a5 | ||
![]() |
51546ad1ce | ||
![]() |
80d52f65eb | ||
![]() |
841e6ff901 | ||
![]() |
82a51d49a0 | ||
![]() |
30b231225c | ||
![]() |
d05563c448 | ||
![]() |
03c868b727 | ||
![]() |
8d2fb99662 | ||
![]() |
20689638db | ||
![]() |
2b561d2648 | ||
![]() |
509cea511c | ||
![]() |
72075314a8 | ||
![]() |
7a3ddc869e | ||
![]() |
eb7c65ccb3 | ||
![]() |
8e5a90589d | ||
![]() |
ed983a5baf | ||
![]() |
2ad3b1fd74 | ||
![]() |
ed53b5f9bc | ||
![]() |
19bc9c20a6 | ||
![]() |
fdf149cf52 | ||
![]() |
76c4fedb7f | ||
![]() |
c3ccec723f | ||
![]() |
7893da4d99 | ||
![]() |
531c61ed2b | ||
![]() |
b60b214c0c | ||
![]() |
10924fd229 | ||
![]() |
9e4d3ebe5f | ||
![]() |
ba6e85482e |
@@ -95,6 +95,14 @@ redis:
|
|||||||
# #prefix: example-prefix
|
# #prefix: example-prefix
|
||||||
# #db: 1
|
# #db: 1
|
||||||
|
|
||||||
|
#redisForTimelines:
|
||||||
|
# host: redis
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
# ┌───────────────────────────┐
|
# ┌───────────────────────────┐
|
||||||
#───┘ MeiliSearch configuration └─────────────────────────────
|
#───┘ MeiliSearch configuration └─────────────────────────────
|
||||||
|
|
||||||
|
@@ -30,7 +30,7 @@ url: https://example.tld/
|
|||||||
# The port that your Misskey server should listen on.
|
# The port that your Misskey server should listen on.
|
||||||
port: 3000
|
port: 3000
|
||||||
|
|
||||||
# You can also use UNIX domain socket.
|
# You can also use UNIX domain socket.
|
||||||
# socket: /path/to/misskey.sock
|
# socket: /path/to/misskey.sock
|
||||||
# chmodSocket: '777'
|
# chmodSocket: '777'
|
||||||
|
|
||||||
@@ -60,17 +60,17 @@ dbReplications: false
|
|||||||
# You can configure any number of replicas here
|
# You can configure any number of replicas here
|
||||||
#dbSlaves:
|
#dbSlaves:
|
||||||
# -
|
# -
|
||||||
# host:
|
# host:
|
||||||
# port:
|
# port:
|
||||||
# db:
|
# db:
|
||||||
# user:
|
# user:
|
||||||
# pass:
|
# pass:
|
||||||
# -
|
# -
|
||||||
# host:
|
# host:
|
||||||
# port:
|
# port:
|
||||||
# db:
|
# db:
|
||||||
# user:
|
# user:
|
||||||
# pass:
|
# pass:
|
||||||
|
|
||||||
# ┌─────────────────────┐
|
# ┌─────────────────────┐
|
||||||
#───┘ Redis configuration └─────────────────────────────────────
|
#───┘ Redis configuration └─────────────────────────────────────
|
||||||
@@ -105,6 +105,16 @@ redis:
|
|||||||
# # You can specify more ioredis options...
|
# # You can specify more ioredis options...
|
||||||
# #username: example-username
|
# #username: example-username
|
||||||
|
|
||||||
|
#redisForTimelines:
|
||||||
|
# host: localhost
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
# # You can specify more ioredis options...
|
||||||
|
# #username: example-username
|
||||||
|
|
||||||
# ┌───────────────────────────┐
|
# ┌───────────────────────────┐
|
||||||
#───┘ MeiliSearch configuration └─────────────────────────────
|
#───┘ MeiliSearch configuration └─────────────────────────────
|
||||||
|
|
||||||
@@ -206,3 +216,6 @@ signToActivityPubGet: true
|
|||||||
|
|
||||||
# Upload or download file size limits (bytes)
|
# Upload or download file size limits (bytes)
|
||||||
#maxFileSize: 262144000
|
#maxFileSize: 262144000
|
||||||
|
|
||||||
|
# PID File of master process
|
||||||
|
#pidFile: /tmp/misskey.pid
|
||||||
|
@@ -4,7 +4,9 @@
|
|||||||
"service": "app",
|
"service": "app",
|
||||||
"workspaceFolder": "/workspace",
|
"workspaceFolder": "/workspace",
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers-contrib/features/pnpm:2": {},
|
"ghcr.io/devcontainers-contrib/features/pnpm:2": {
|
||||||
|
"version": "8.8.0"
|
||||||
|
},
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
"version": "20.5.1"
|
"version": "20.5.1"
|
||||||
}
|
}
|
||||||
|
@@ -95,6 +95,14 @@ redis:
|
|||||||
# #prefix: example-prefix
|
# #prefix: example-prefix
|
||||||
# #db: 1
|
# #db: 1
|
||||||
|
|
||||||
|
#redisForTimelines:
|
||||||
|
# host: redis
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
# ┌───────────────────────────┐
|
# ┌───────────────────────────┐
|
||||||
#───┘ MeiliSearch configuration └─────────────────────────────
|
#───┘ MeiliSearch configuration └─────────────────────────────
|
||||||
|
|
||||||
|
2
.github/workflows/api-misskey-js.yml
vendored
2
.github/workflows/api-misskey-js.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4.0.0
|
uses: actions/checkout@v4.1.0
|
||||||
|
|
||||||
- run: corepack enable
|
- run: corepack enable
|
||||||
|
|
||||||
|
2
.github/workflows/check_copyright_year.yml
vendored
2
.github/workflows/check_copyright_year.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
check_copyright_year:
|
check_copyright_year:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.0.0
|
- uses: actions/checkout@v4.1.0
|
||||||
- run: |
|
- run: |
|
||||||
if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then
|
if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then
|
||||||
echo "Please change copyright year!"
|
echo "Please change copyright year!"
|
||||||
|
2
.github/workflows/docker-develop.yml
vendored
2
.github/workflows/docker-develop.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
if: github.repository == 'misskey-dev/misskey'
|
if: github.repository == 'misskey-dev/misskey'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v4.0.0
|
uses: actions/checkout@v4.1.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v3.0.0
|
uses: docker/setup-buildx-action@v3.0.0
|
||||||
|
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v4.0.0
|
uses: actions/checkout@v4.1.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v3.0.0
|
uses: docker/setup-buildx-action@v3.0.0
|
||||||
|
2
.github/workflows/dockle.yml
vendored
2
.github/workflows/dockle.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
DOCKER_CONTENT_TRUST: 1
|
DOCKER_CONTENT_TRUST: 1
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.0.0
|
- uses: actions/checkout@v4.1.0
|
||||||
- run: |
|
- run: |
|
||||||
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb"
|
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb"
|
||||||
sudo dpkg -i dockle.deb
|
sudo dpkg -i dockle.deb
|
||||||
|
6
.github/workflows/lint.yml
vendored
6
.github/workflows/lint.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
pnpm_install:
|
pnpm_install:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.0.0
|
- uses: actions/checkout@v4.1.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: true
|
submodules: true
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
- sw
|
- sw
|
||||||
- misskey-js
|
- misskey-js
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.0.0
|
- uses: actions/checkout@v4.1.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: true
|
submodules: true
|
||||||
@@ -64,7 +64,7 @@ jobs:
|
|||||||
- backend
|
- backend
|
||||||
- misskey-js
|
- misskey-js
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.0.0
|
- uses: actions/checkout@v4.1.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: true
|
submodules: true
|
||||||
|
2
.github/workflows/pr-preview-deploy.yml
vendored
2
.github/workflows/pr-preview-deploy.yml
vendored
@@ -53,7 +53,7 @@ jobs:
|
|||||||
|
|
||||||
# Check out merge commit
|
# Check out merge commit
|
||||||
- name: Fork based /deploy checkout
|
- name: Fork based /deploy checkout
|
||||||
uses: actions/checkout@v4.0.0
|
uses: actions/checkout@v4.1.0
|
||||||
with:
|
with:
|
||||||
ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge'
|
ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge'
|
||||||
|
|
||||||
|
2
.github/workflows/test-backend.yml
vendored
2
.github/workflows/test-backend.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
|||||||
- 56312:6379
|
- 56312:6379
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.0.0
|
- uses: actions/checkout@v4.1.0
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
|
4
.github/workflows/test-frontend.yml
vendored
4
.github/workflows/test-frontend.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
node-version: [20.5.1]
|
node-version: [20.5.1]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.0.0
|
- uses: actions/checkout@v4.1.0
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
- 56312:6379
|
- 56312:6379
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.0.0
|
- uses: actions/checkout@v4.1.0
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
# https://github.com/cypress-io/cypress-docker-images/issues/150
|
# https://github.com/cypress-io/cypress-docker-images/issues/150
|
||||||
|
2
.github/workflows/test-misskey-js.yml
vendored
2
.github/workflows/test-misskey-js.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4.0.0
|
uses: actions/checkout@v4.1.0
|
||||||
|
|
||||||
- run: corepack enable
|
- run: corepack enable
|
||||||
|
|
||||||
|
2
.github/workflows/test-production.yml
vendored
2
.github/workflows/test-production.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
node-version: [20.5.1]
|
node-version: [20.5.1]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.0.0
|
- uses: actions/checkout@v4.1.0
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
|
94
CHANGELOG.md
94
CHANGELOG.md
@@ -12,7 +12,91 @@
|
|||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## 2023.9.0 (unreleased)
|
## 2023.10.0
|
||||||
|
### NOTE
|
||||||
|
- 2023.9.2で導入されたノート編集機能はクオリティの高い実装が困難であることが判明したため撤回されました
|
||||||
|
- アップデート後、アップデートより前の時点にTLを遡ることはできません
|
||||||
|
- アップデート後であっても、今後のアップデートで2023.10.0以前のTLに遡れるようになる可能性はあります
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
- API: users/notes, notes/local-timeline で fileType 指定はできなくなりました
|
||||||
|
- API: notes/featured でページネーションは他APIと同様 untilId を使って行うようになりました
|
||||||
|
|
||||||
|
### General
|
||||||
|
- Feat: ユーザーごとに他ユーザーへの返信をタイムラインに含めるか設定可能になりました
|
||||||
|
- Feat: ユーザーリスト内のメンバーごとに他ユーザーへの返信をユーザーリストタイムラインに含めるか設定可能になりました
|
||||||
|
- Feat: ユーザーごとのハイライト
|
||||||
|
- Feat: プライバシーポリシー・運営者情報(Impressum)の指定が可能になりました
|
||||||
|
- プライバシーポリシーはサーバー登録時に同意確認が入ります
|
||||||
|
- Enhance: ソフトワードミュートとハードワードミュートは統合されました
|
||||||
|
- Enhance: モデレーションログ機能の強化
|
||||||
|
- Enhance: ローカリゼーションの更新
|
||||||
|
- Enhance: 依存関係の更新
|
||||||
|
- Fix: ダイレクト投稿をリノートできてしまう問題を修正
|
||||||
|
- Fix: ユーザーリストTLにチャンネル投稿が含まれる問題を修正
|
||||||
|
|
||||||
|
### Client
|
||||||
|
- Enhance: 二要素認証のバックアップコード一覧をテキストファイルでダウンロード可能に
|
||||||
|
- Fix: リアクションしたユーザ一覧のUIが稀に左上に残ってしまう不具合を修正
|
||||||
|
|
||||||
|
### Server
|
||||||
|
- Enhance: タイムライン取得時のパフォーマンスを大幅に向上
|
||||||
|
- Enhance: ハイライト取得時のパフォーマンスを大幅に向上
|
||||||
|
- Enhance: トレンドハッシュタグ取得時のパフォーマンスを大幅に向上
|
||||||
|
- Enhance: 不要なPostgreSQLのインデックスを削除しパフォーマンスを向上
|
||||||
|
- Fix: 連合なしアンケートに投票をするとUpdateがリモートに配信されてしまうのを修正
|
||||||
|
|
||||||
|
## 2023.9.3
|
||||||
|
### General
|
||||||
|
- Enhance: ノートの翻訳機能の利用可否をロールで設定可能に
|
||||||
|
|
||||||
|
### Client
|
||||||
|
- Enhance: AiScriptでホストのアドレスを参照する定数`SERVER_URL`を追加
|
||||||
|
- Enhance: モデレーションログ機能の強化
|
||||||
|
- Enhance: ローカリゼーションの更新
|
||||||
|
|
||||||
|
### Server
|
||||||
|
- Fix: Redisに古いバージョンのキャッシュが残っている場合、キャッシュが消えるまでの間通知が届かなくなる問題を修正
|
||||||
|
- Fix: 後方互換性の修正
|
||||||
|
|
||||||
|
## 2023.9.2
|
||||||
|
|
||||||
|
### General
|
||||||
|
- Feat: ノートの編集をできるように
|
||||||
|
- ロールで編集可否を設定可能
|
||||||
|
- Feat: 通知を種類ごとに 全員から受け取る/フォロー中のユーザーのみ受け取る/フォロワーのみ受け取る/相互のみ受け取る/指定したリストのメンバーのみ受け取る/受け取らない から選べるように
|
||||||
|
- Enhance: タイムラインからRenoteを除外するオプションを追加
|
||||||
|
- Enhance: ユーザーページのノート一覧でRenoteを除外できるように
|
||||||
|
- Enhance: タイムラインでファイルが添付されたノートのみ表示するオプションを追加
|
||||||
|
- Enhance: モデレーションログ機能の強化
|
||||||
|
- Enhance: 依存関係の更新
|
||||||
|
- Enhance: ローカリゼーションの更新
|
||||||
|
|
||||||
|
### Client
|
||||||
|
- Enhance: Plugin:register_post_form_actionを用いてCWを取得・変更できるように
|
||||||
|
- Enhance: admin/ad/listにて掲載中の広告が絞り込めるように
|
||||||
|
- Enhance: AiScriptにリモートサーバーのAPIを叩く用の関数を追加(`Mk:apiExternal`)
|
||||||
|
|
||||||
|
### Server
|
||||||
|
- Enhance: MasterプロセスのPIDを書き出せるように
|
||||||
|
- Enhance: admin/ad/createにてレスポンス200、設定した広告情報を返すように
|
||||||
|
|
||||||
|
## 2023.9.1
|
||||||
|
|
||||||
|
### General
|
||||||
|
- Enhance: モデレーションログ機能の強化
|
||||||
|
|
||||||
|
### Client
|
||||||
|
- Fix: ノートのメニューにある「詳細」ボタンの表示がログイン/ログアウト状態で統一されていない問題を修正
|
||||||
|
|
||||||
|
### Server
|
||||||
|
- Fix: お知らせのページネーションが機能しない
|
||||||
|
- Fix: 「ユーザーの新規投稿」の通知設定を切り替えるとサーバー内部エラーが出る
|
||||||
|
|
||||||
|
## 2023.9.0
|
||||||
|
|
||||||
|
### Note
|
||||||
|
- meilisearchを使用する場合、v1.2以上が必要です
|
||||||
|
|
||||||
### General
|
### General
|
||||||
- Feat: OAuth 2.0のサポート
|
- Feat: OAuth 2.0のサポート
|
||||||
@@ -28,6 +112,7 @@
|
|||||||
- Feat: 二要素認証でパスキーをサポートするようになりました
|
- Feat: 二要素認証でパスキーをサポートするようになりました
|
||||||
- Feat: 指定したユーザーが投稿したときに通知できるようになりました
|
- Feat: 指定したユーザーが投稿したときに通知できるようになりました
|
||||||
- Feat: プロフィールでのリンク検証
|
- Feat: プロフィールでのリンク検証
|
||||||
|
- Feat: モデレーションログ機能
|
||||||
- Feat: 通知をテストできるようになりました
|
- Feat: 通知をテストできるようになりました
|
||||||
- Feat: PWAのアイコンが設定できるようになりました
|
- Feat: PWAのアイコンが設定できるようになりました
|
||||||
- Enhance: サーバー名の略称が設定できるようになりました
|
- Enhance: サーバー名の略称が設定できるようになりました
|
||||||
@@ -79,6 +164,10 @@
|
|||||||
- Fix: 他のサーバーのユーザーへ「メッセージを送信」した時の初期テキストのメンションが間違っている問題を修正
|
- Fix: 他のサーバーのユーザーへ「メッセージを送信」した時の初期テキストのメンションが間違っている問題を修正
|
||||||
- Fix: 環境によってはMisskey Webが開けない問題を修正
|
- Fix: 環境によってはMisskey Webが開けない問題を修正
|
||||||
- Fix: プラグインの権限リストが見れない問題を修正
|
- Fix: プラグインの権限リストが見れない問題を修正
|
||||||
|
- Fix: 複数の階層があるメニューで、短くタップすると正常に動かない場合がある問題を修正
|
||||||
|
- Fix: アニメーションがオフのとき、スマホで子メニューの選択ができない問題を修正
|
||||||
|
- Fix: ドロワーメニューで、親メニュー項目をマウスでホバーすると子メニューが表示されてしまう問題を修正
|
||||||
|
- Fix: AiScriptでMk:apiが外部と通信できる問題を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- Change: cacheRemoteFilesの初期値はfalseになりました
|
- Change: cacheRemoteFilesの初期値はfalseになりました
|
||||||
@@ -89,6 +178,8 @@
|
|||||||
- Enhance: nodeinfo 2.1対応
|
- Enhance: nodeinfo 2.1対応
|
||||||
- Enhance: 自分へのメンション一覧を取得する際のパフォーマンスを向上
|
- Enhance: 自分へのメンション一覧を取得する際のパフォーマンスを向上
|
||||||
- Enhance: Docker環境でjemallocを使用することでメモリ使用量を削減
|
- Enhance: Docker環境でjemallocを使用することでメモリ使用量を削減
|
||||||
|
- Enhance: ID生成方式としてaidxを追加、かつデフォルトに
|
||||||
|
- Enhance: Add address bind config option (outgoingAddress)
|
||||||
- Fix: MK_ONLY_SERVERオプションを指定した際にクラッシュする問題を修正
|
- Fix: MK_ONLY_SERVERオプションを指定した際にクラッシュする問題を修正
|
||||||
- Fix: notes/reactionsのページネーションが機能しない問題を修正
|
- Fix: notes/reactionsのページネーションが機能しない問題を修正
|
||||||
- Fix: ノート検索 `notes/search` にてhostを指定した際に検索結果に反映されるように
|
- Fix: ノート検索 `notes/search` にてhostを指定した際に検索結果に反映されるように
|
||||||
@@ -111,7 +202,6 @@
|
|||||||
### Server
|
### Server
|
||||||
- Fix: APIのオフセットが壊れていたせいで「もっと見る」でもっと見れない問題を修正
|
- Fix: APIのオフセットが壊れていたせいで「もっと見る」でもっと見れない問題を修正
|
||||||
- Fix: 外部サーバーの投稿がタイムラインに表示されないことがある問題を修正
|
- Fix: 外部サーバーの投稿がタイムラインに表示されないことがある問題を修正
|
||||||
- Enhance: Add address bind config option (outgoingAddress)
|
|
||||||
|
|
||||||
## 13.14.1
|
## 13.14.1
|
||||||
|
|
||||||
|
@@ -116,6 +116,14 @@ redis:
|
|||||||
# #prefix: example-prefix
|
# #prefix: example-prefix
|
||||||
# #db: 1
|
# #db: 1
|
||||||
|
|
||||||
|
#redisForTimelines:
|
||||||
|
# host: redis
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
# ┌───────────────────────────┐
|
# ┌───────────────────────────┐
|
||||||
#───┘ MeiliSearch configuration └─────────────────────────────
|
#───┘ MeiliSearch configuration └─────────────────────────────
|
||||||
|
|
||||||
|
@@ -1140,6 +1140,7 @@ _plugin:
|
|||||||
install: "ثبّت إضافات"
|
install: "ثبّت إضافات"
|
||||||
installWarn: "رجاءً لا تثبت إضافات غير موثوقة."
|
installWarn: "رجاءً لا تثبت إضافات غير موثوقة."
|
||||||
manage: "إدارة الإضافات"
|
manage: "إدارة الإضافات"
|
||||||
|
viewSource: "اظهر المصدر"
|
||||||
_preferencesBackups:
|
_preferencesBackups:
|
||||||
createdAt: "تم إنشاؤه: {date} {time}"
|
createdAt: "تم إنشاؤه: {date} {time}"
|
||||||
updatedAt: "آخر تحديث: {date} {time}"
|
updatedAt: "آخر تحديث: {date} {time}"
|
||||||
@@ -1183,11 +1184,6 @@ _wordMute:
|
|||||||
muteWords: "الكلمات المحظورة"
|
muteWords: "الكلمات المحظورة"
|
||||||
muteWordsDescription: "افصل بينهم بمسافة لاستخدام معامل \"و\" أو بسطر لاستخدام معامل \"أو\"."
|
muteWordsDescription: "افصل بينهم بمسافة لاستخدام معامل \"و\" أو بسطر لاستخدام معامل \"أو\"."
|
||||||
muteWordsDescription2: "احصر الكلمات المفتاحية بين بين شرطتين مائلتين لاستخدامها كتعابير نمطية"
|
muteWordsDescription2: "احصر الكلمات المفتاحية بين بين شرطتين مائلتين لاستخدامها كتعابير نمطية"
|
||||||
softDescription: "اخف الملاحظات التي تستوف الشروط من الخيط الزمني."
|
|
||||||
hardDescription: "اخف الملاحظات التي تستوف الشروط من الخيط الزمني.بالإضافة إلى أن هذه الملاحظات ستبقى مخفية حتى وإن تغيرت الشروط."
|
|
||||||
soft: "لينة"
|
|
||||||
hard: "قاسية"
|
|
||||||
mutedNotes: "الملاحظات المكتومة"
|
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription: "هذه سيحجب كل ملاحظات الخوادم المحجوبة ومشاركاتها والردود على تلك الملاحظات حتى وإن كانت من خادم غير محجوب."
|
instanceMuteDescription: "هذه سيحجب كل ملاحظات الخوادم المحجوبة ومشاركاتها والردود على تلك الملاحظات حتى وإن كانت من خادم غير محجوب."
|
||||||
instanceMuteDescription2: "مدخلة لكل سطر"
|
instanceMuteDescription2: "مدخلة لكل سطر"
|
||||||
@@ -1247,8 +1243,6 @@ _sfx:
|
|||||||
note: "الملاحظات"
|
note: "الملاحظات"
|
||||||
noteMy: "ملاحظتي"
|
noteMy: "ملاحظتي"
|
||||||
notification: "الإشعارات"
|
notification: "الإشعارات"
|
||||||
chat: "المحادثة"
|
|
||||||
chatBg: "المحادثة (الخلفية)"
|
|
||||||
antenna: "الهوائيات"
|
antenna: "الهوائيات"
|
||||||
channel: "إشعارات القنات"
|
channel: "إشعارات القنات"
|
||||||
_ago:
|
_ago:
|
||||||
@@ -1555,3 +1549,6 @@ _webhookSettings:
|
|||||||
active: "مُفعّل"
|
active: "مُفعّل"
|
||||||
_events:
|
_events:
|
||||||
reaction: "عند التفاعل"
|
reaction: "عند التفاعل"
|
||||||
|
_moderationLogTypes:
|
||||||
|
suspend: "علِق"
|
||||||
|
resetPassword: "أعد تعيين كلمتك السرية"
|
||||||
|
@@ -889,6 +889,7 @@ _plugin:
|
|||||||
install: "প্লাগইন ইন্সটল করুন"
|
install: "প্লাগইন ইন্সটল করুন"
|
||||||
installWarn: "অবিশ্বস্ত প্লাগইন ইনস্টল করবেন না।"
|
installWarn: "অবিশ্বস্ত প্লাগইন ইনস্টল করবেন না।"
|
||||||
manage: "প্লাগইন ম্যানেজ করুন"
|
manage: "প্লাগইন ম্যানেজ করুন"
|
||||||
|
viewSource: "উৎস দেখুন"
|
||||||
_registry:
|
_registry:
|
||||||
scope: "স্কোপ"
|
scope: "স্কোপ"
|
||||||
key: "কী"
|
key: "কী"
|
||||||
@@ -931,11 +932,6 @@ _wordMute:
|
|||||||
muteWords: "নিঃশব্দ করা শব্দগুলি"
|
muteWords: "নিঃশব্দ করা শব্দগুলি"
|
||||||
muteWordsDescription: "স্পেস দিয়ে আলাদা করলে AND শর্ত তৈরি হবে এবং আলাদা লাইনে লিখলে OR শর্ত তৈরি হবে।"
|
muteWordsDescription: "স্পেস দিয়ে আলাদা করলে AND শর্ত তৈরি হবে এবং আলাদা লাইনে লিখলে OR শর্ত তৈরি হবে।"
|
||||||
muteWordsDescription2: "রেগুলার এক্সপ্রেশন ব্যবহার করতে স্ল্যাশ দিয়ে কীওয়ার্ডকে ঘিরে রাখুন।"
|
muteWordsDescription2: "রেগুলার এক্সপ্রেশন ব্যবহার করতে স্ল্যাশ দিয়ে কীওয়ার্ডকে ঘিরে রাখুন।"
|
||||||
softDescription: "টাইমলাইন থেকে নির্দিষ্ট শর্তানুযায়ী নোট লুকিয়ে রাখে।"
|
|
||||||
hardDescription: "নির্দিষ্ট শর্তানুযায়ী নোটগুলিকে টাইমলাইন থেকে বাদ দেয়। আপনি শর্ত পরিবর্তন করলেও যে নোটগুলি যোগ করা হয়নি সেগুলি বাদ দেওয়া হবে।"
|
|
||||||
soft: "নমনীয়"
|
|
||||||
hard: "কঠোর"
|
|
||||||
mutedNotes: "মিউট করা নোটগুলি"
|
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription: "কনফিগার করা ইন্সট্যান্সের সব নোট এবং রিনোট মিউট করুন, মিউট করা ইন্সট্যান্সের ব্যবহারকারীদের উত্তর সহ।"
|
instanceMuteDescription: "কনফিগার করা ইন্সট্যান্সের সব নোট এবং রিনোট মিউট করুন, মিউট করা ইন্সট্যান্সের ব্যবহারকারীদের উত্তর সহ।"
|
||||||
instanceMuteDescription2: "প্রতিটিকে আলাদা লাইনে লিখুন"
|
instanceMuteDescription2: "প্রতিটিকে আলাদা লাইনে লিখুন"
|
||||||
@@ -1019,8 +1015,6 @@ _sfx:
|
|||||||
note: "নোটগুলি"
|
note: "নোটগুলি"
|
||||||
noteMy: "নোট (আপনার)"
|
noteMy: "নোট (আপনার)"
|
||||||
notification: "বিজ্ঞপ্তি"
|
notification: "বিজ্ঞপ্তি"
|
||||||
chat: "চ্যাট"
|
|
||||||
chatBg: "চ্যাট (ব্যাকগ্রাউন্ড)"
|
|
||||||
antenna: "অ্যান্টেনাগুলি"
|
antenna: "অ্যান্টেনাগুলি"
|
||||||
channel: "চ্যানেলের বিজ্ঞপ্তি"
|
channel: "চ্যানেলের বিজ্ঞপ্তি"
|
||||||
_ago:
|
_ago:
|
||||||
@@ -1332,3 +1326,6 @@ _deck:
|
|||||||
_webhookSettings:
|
_webhookSettings:
|
||||||
name: "নাম"
|
name: "নাম"
|
||||||
active: "চালু"
|
active: "চালু"
|
||||||
|
_moderationLogTypes:
|
||||||
|
suspend: "স্থগিত করা"
|
||||||
|
resetPassword: "পাসওয়ার্ড রিসেট করুন"
|
||||||
|
@@ -398,7 +398,6 @@ _theme:
|
|||||||
_sfx:
|
_sfx:
|
||||||
note: "Notes"
|
note: "Notes"
|
||||||
notification: "Notificacions"
|
notification: "Notificacions"
|
||||||
chat: "Xat"
|
|
||||||
antenna: "Antenes"
|
antenna: "Antenes"
|
||||||
_2fa:
|
_2fa:
|
||||||
renewTOTPCancel: "No, gràcies"
|
renewTOTPCancel: "No, gràcies"
|
||||||
@@ -479,3 +478,6 @@ _deck:
|
|||||||
list: "Llistes"
|
list: "Llistes"
|
||||||
mentions: "Mencions"
|
mentions: "Mencions"
|
||||||
direct: "Publicacions directes"
|
direct: "Publicacions directes"
|
||||||
|
_moderationLogTypes:
|
||||||
|
suspend: "Suspèn"
|
||||||
|
resetPassword: "Restableix la contrasenya"
|
||||||
|
@@ -1492,6 +1492,7 @@ _plugin:
|
|||||||
install: "Instalovat plugin"
|
install: "Instalovat plugin"
|
||||||
installWarn: "Neinstalujte nedůvěryhodné pluginy."
|
installWarn: "Neinstalujte nedůvěryhodné pluginy."
|
||||||
manage: "Správce pluginů"
|
manage: "Správce pluginů"
|
||||||
|
viewSource: "Zobrazit zdroj"
|
||||||
_preferencesBackups:
|
_preferencesBackups:
|
||||||
list: "Vytvořit backup"
|
list: "Vytvořit backup"
|
||||||
saveNew: "Uložit novou zálohu"
|
saveNew: "Uložit novou zálohu"
|
||||||
@@ -1558,11 +1559,6 @@ _wordMute:
|
|||||||
muteWords: "Ztlumená slova"
|
muteWords: "Ztlumená slova"
|
||||||
muteWordsDescription: "Podmínku AND oddělujte mezerami, podmínku OR oddělujte řádkovými zlomy."
|
muteWordsDescription: "Podmínku AND oddělujte mezerami, podmínku OR oddělujte řádkovými zlomy."
|
||||||
muteWordsDescription2: "Chcete-li použít regulární výrazy, obklopte klíčová slova lomítky."
|
muteWordsDescription2: "Chcete-li použít regulární výrazy, obklopte klíčová slova lomítky."
|
||||||
softDescription: "Skrýt poznámky, které splňují nastavené podmínky, z časové osy."
|
|
||||||
hardDescription: "Zabrání přidání poznámek splňujících nastavené podmínky na časovou osu. Kromě toho nebudou tyto poznámky přidány na časovou osu, ani když se podmínky změní."
|
|
||||||
soft: "Měkký"
|
|
||||||
hard: "Tvrdý"
|
|
||||||
mutedNotes: "Ztlumené poznámky"
|
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription: "Tímhle se ztlumí všechny poznámky/poznámky z uvedených instancí, včetně poznámek uživatelů, kteří odpovídají uživateli ze ztlumené instance."
|
instanceMuteDescription: "Tímhle se ztlumí všechny poznámky/poznámky z uvedených instancí, včetně poznámek uživatelů, kteří odpovídají uživateli ze ztlumené instance."
|
||||||
instanceMuteDescription2: "Oddělte novými řádky"
|
instanceMuteDescription2: "Oddělte novými řádky"
|
||||||
@@ -1646,8 +1642,6 @@ _sfx:
|
|||||||
note: "Poznámky"
|
note: "Poznámky"
|
||||||
noteMy: "Moje poznámka"
|
noteMy: "Moje poznámka"
|
||||||
notification: "Oznámení"
|
notification: "Oznámení"
|
||||||
chat: "Zprávy"
|
|
||||||
chatBg: "Chat (Pozadí)"
|
|
||||||
antenna: "Antény"
|
antenna: "Antény"
|
||||||
channel: "Oznámení kanálu"
|
channel: "Oznámení kanálu"
|
||||||
_ago:
|
_ago:
|
||||||
@@ -2035,3 +2029,7 @@ _webhookSettings:
|
|||||||
renote: "Při renotaci poznámky"
|
renote: "Při renotaci poznámky"
|
||||||
reaction: "Při obdržení reakce"
|
reaction: "Při obdržení reakce"
|
||||||
mention: "Při zmínce"
|
mention: "Při zmínce"
|
||||||
|
_moderationLogTypes:
|
||||||
|
suspend: "Zmrazit"
|
||||||
|
resetPassword: "Resetovat heslo"
|
||||||
|
createInvitation: "Vygenerovat pozvánku"
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
_lang_: "Deutsch"
|
_lang_: "Deutsch"
|
||||||
headlineMisskey: "Ein durch Notizen verbundenes Netzwerk"
|
headlineMisskey: "Ein durch Notizen verbundenes Netzwerk"
|
||||||
introMisskey: "Willkommen! Misskey ist eine dezentralisierte Open-Source Microblogging-Platform.\nVerfasse „Notizen“ um mitzuteilen, was gerade passiert oder um Ereignisse mit anderen zu teilen. 📡\nMit „Reaktionen“ kannst du außerdem schnell deine Gefühle über Notizen anderer Benutzer zum Ausdruck bringen. 👍\nEine neue Welt wartet auf dich! 🚀"
|
introMisskey: "Willkommen! Misskey ist eine dezentralisierte Open-Source Microblogging-Platform.\nVerfasse „Notizen“ um mitzuteilen, was gerade passiert oder um Ereignisse mit anderen zu teilen. 📡\nMit „Reaktionen“ kannst du außerdem schnell deine Gefühle über Notizen anderer Benutzer zum Ausdruck bringen. 👍\nEine neue Welt wartet auf dich! 🚀"
|
||||||
poweredByMisskeyDescription: "{name} ist einer der durch die Open-Source-Plattform <b>Misskey</b> betriebenen Dienste (meist als \"Misskey-Instanz\" bezeichnet)."
|
poweredByMisskeyDescription: "{name} ist einer der durch die Open-Source-Plattform <b>Misskey</b> betriebenen Dienste."
|
||||||
monthAndDay: "{day}.{month}."
|
monthAndDay: "{day}.{month}."
|
||||||
search: "Suchen"
|
search: "Suchen"
|
||||||
notifications: "Benachrichtigungen"
|
notifications: "Benachrichtigungen"
|
||||||
@@ -75,7 +75,7 @@ import: "Import"
|
|||||||
export: "Export"
|
export: "Export"
|
||||||
files: "Dateien"
|
files: "Dateien"
|
||||||
download: "Herunterladen"
|
download: "Herunterladen"
|
||||||
driveFileDeleteConfirm: "Möchtest du die Datei „{name}“ wirklich löschen? Sie wird in allen Inhalten, die sie verwenden, auch verschwinden."
|
driveFileDeleteConfirm: "Möchtest du die Datei „{name}“ wirklich löschen? Einige Inhalte, die diese Datei verwenden, werden auch verschwinden."
|
||||||
unfollowConfirm: "Möchtest du {name} wirklich nicht mehr folgen?"
|
unfollowConfirm: "Möchtest du {name} wirklich nicht mehr folgen?"
|
||||||
exportRequested: "Du hast einen Export angefragt. Dies kann etwas Zeit in Anspruch nehmen. Sobald der Export abgeschlossen ist, wird er deiner Drive hinzugefügt."
|
exportRequested: "Du hast einen Export angefragt. Dies kann etwas Zeit in Anspruch nehmen. Sobald der Export abgeschlossen ist, wird er deiner Drive hinzugefügt."
|
||||||
importRequested: "Du hast einen Import angefragt. Dies kann etwas Zeit in Anspruch nehmen."
|
importRequested: "Du hast einen Import angefragt. Dies kann etwas Zeit in Anspruch nehmen."
|
||||||
@@ -418,6 +418,7 @@ moderator: "Moderator"
|
|||||||
moderation: "Moderation"
|
moderation: "Moderation"
|
||||||
moderationNote: "Moderationsnotiz"
|
moderationNote: "Moderationsnotiz"
|
||||||
addModerationNote: "Moderationsnotiz hinzufügen"
|
addModerationNote: "Moderationsnotiz hinzufügen"
|
||||||
|
moderationLogs: "Moderationsprotokolle"
|
||||||
nUsersMentioned: "Von {n} Benutzern erwähnt"
|
nUsersMentioned: "Von {n} Benutzern erwähnt"
|
||||||
securityKeyAndPasskey: "Hardware-Sicherheitsschlüssel und Passkeys"
|
securityKeyAndPasskey: "Hardware-Sicherheitsschlüssel und Passkeys"
|
||||||
securityKey: "Hardware-Sicherheitsschlüssel"
|
securityKey: "Hardware-Sicherheitsschlüssel"
|
||||||
@@ -639,7 +640,7 @@ display: "Anzeigeart"
|
|||||||
copy: "Kopieren"
|
copy: "Kopieren"
|
||||||
metrics: "Metriken"
|
metrics: "Metriken"
|
||||||
overview: "Übersicht"
|
overview: "Übersicht"
|
||||||
logs: "Logs"
|
logs: "Protokolle"
|
||||||
delayed: "Verzögert"
|
delayed: "Verzögert"
|
||||||
database: "Datenbank"
|
database: "Datenbank"
|
||||||
channel: "Kanäle"
|
channel: "Kanäle"
|
||||||
@@ -710,6 +711,7 @@ lockedAccountInfo: "Auch wenn du Follow-Anfragen auf manuelle Bestätigung setzt
|
|||||||
alwaysMarkSensitive: "Medien standardmäßig als sensibel markieren"
|
alwaysMarkSensitive: "Medien standardmäßig als sensibel markieren"
|
||||||
loadRawImages: "Anstatt Vorschaubilder immer Originalbilder anzeigen"
|
loadRawImages: "Anstatt Vorschaubilder immer Originalbilder anzeigen"
|
||||||
disableShowingAnimatedImages: "Animierte Bilder nicht abspielen"
|
disableShowingAnimatedImages: "Animierte Bilder nicht abspielen"
|
||||||
|
highlightSensitiveMedia: "Sensitive Medien markieren"
|
||||||
verificationEmailSent: "Eine Bestätigungsmail wurde an deine Email-Adresse versendet. Besuche den dort enthaltenen Link, um die Verifizierung abzuschließen."
|
verificationEmailSent: "Eine Bestätigungsmail wurde an deine Email-Adresse versendet. Besuche den dort enthaltenen Link, um die Verifizierung abzuschließen."
|
||||||
notSet: "Nicht konfiguriert"
|
notSet: "Nicht konfiguriert"
|
||||||
emailVerified: "Email-Adresse bestätigt"
|
emailVerified: "Email-Adresse bestätigt"
|
||||||
@@ -913,7 +915,7 @@ typeToConfirm: "Bitte gib zur Bestätigung {x} ein"
|
|||||||
deleteAccount: "Benutzerkonto löschen"
|
deleteAccount: "Benutzerkonto löschen"
|
||||||
document: "Dokumentation"
|
document: "Dokumentation"
|
||||||
numberOfPageCache: "Seitencachegröße"
|
numberOfPageCache: "Seitencachegröße"
|
||||||
numberOfPageCacheDescription: "Das Erhöhen dieses Caches führt zu einer angenehmerern Benutzererfahrung, erhöht aber Serverlast und Arbeitsspeicherauslastung."
|
numberOfPageCacheDescription: "Das Erhöhen dieses Caches führt zu einer angenehmerern Benutzererfahrung, aber erhöht Last und Arbeitsspeicherauslastung auf dem Nutzergerät."
|
||||||
logoutConfirm: "Wirklich abmelden?"
|
logoutConfirm: "Wirklich abmelden?"
|
||||||
lastActiveDate: "Zuletzt verwendet am"
|
lastActiveDate: "Zuletzt verwendet am"
|
||||||
statusbar: "Statusleiste"
|
statusbar: "Statusleiste"
|
||||||
@@ -1048,7 +1050,7 @@ vertical: "Vertikal"
|
|||||||
horizontal: "Horizontal"
|
horizontal: "Horizontal"
|
||||||
position: "Position"
|
position: "Position"
|
||||||
serverRules: "Serverregeln"
|
serverRules: "Serverregeln"
|
||||||
pleaseConfirmBelowBeforeSignup: "Lies bitte Untenstehendes vor der Registration."
|
pleaseConfirmBelowBeforeSignup: "Lies bitte diese Informationen und stimme ihnen vor der Registration zu."
|
||||||
pleaseAgreeAllToContinue: "Zum Fortfahren muss allen obigen Feldern zugestimmt werden."
|
pleaseAgreeAllToContinue: "Zum Fortfahren muss allen obigen Feldern zugestimmt werden."
|
||||||
continue: "Fortfahren"
|
continue: "Fortfahren"
|
||||||
preservedUsernames: "Reservierte Benutzernamen"
|
preservedUsernames: "Reservierte Benutzernamen"
|
||||||
@@ -1116,6 +1118,17 @@ keepScreenOn: "Bildschirm angeschaltet lassen"
|
|||||||
verifiedLink: "Link-Besitz wurde verifiziert"
|
verifiedLink: "Link-Besitz wurde verifiziert"
|
||||||
notifyNotes: "Über neue Notizen benachrichtigen"
|
notifyNotes: "Über neue Notizen benachrichtigen"
|
||||||
unnotifyNotes: "Nicht über neue Notizen benachrichtigen"
|
unnotifyNotes: "Nicht über neue Notizen benachrichtigen"
|
||||||
|
authentication: "Authentifikation"
|
||||||
|
authenticationRequiredToContinue: "Bitte authentifiziere dich, um fortzufahren"
|
||||||
|
dateAndTime: "Zeit"
|
||||||
|
showRenotes: "Renotes anzeigen"
|
||||||
|
edited: "Bearbeitet"
|
||||||
|
notificationRecieveConfig: "Benachrichtigungseinstellungen"
|
||||||
|
mutualFollow: "Gegenseitig gefolgt"
|
||||||
|
fileAttachedOnly: "Nur Notizen mit Dateien"
|
||||||
|
showRepliesToOthersInTimeline: "Antworten in Chronik anzeigen"
|
||||||
|
hideRepliesToOthersInTimeline: "Antworten nicht in Chronik anzeigen"
|
||||||
|
externalServices: "Externe Dienste"
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "Nur für existierende Nutzer"
|
forExistingUsers: "Nur für existierende Nutzer"
|
||||||
forExistingUsersDescription: "Ist diese Option aktiviert, wird diese Ankündigung nur Nutzern angezeigt, die zum Zeitpunkt der Ankündigung bereits registriert sind. Ist sie deaktiviert, wird sie auch Nutzern, die sich nach dessen Veröffentlichung registrieren, angezeigt."
|
forExistingUsersDescription: "Ist diese Option aktiviert, wird diese Ankündigung nur Nutzern angezeigt, die zum Zeitpunkt der Ankündigung bereits registriert sind. Ist sie deaktiviert, wird sie auch Nutzern, die sich nach dessen Veröffentlichung registrieren, angezeigt."
|
||||||
@@ -1149,6 +1162,8 @@ _serverSettings:
|
|||||||
appIconStyleRecommendation: "Da das Icon zu einem Kreis oder Quadrat zugeschnitten wird, wird ein Icon mit gefülltem Margin um den Inhalt herum empfohlen."
|
appIconStyleRecommendation: "Da das Icon zu einem Kreis oder Quadrat zugeschnitten wird, wird ein Icon mit gefülltem Margin um den Inhalt herum empfohlen."
|
||||||
appIconResolutionMustBe: "Die Mindestauflösung ist {resolution}."
|
appIconResolutionMustBe: "Die Mindestauflösung ist {resolution}."
|
||||||
manifestJsonOverride: "Überschreiben von manifest.json"
|
manifestJsonOverride: "Überschreiben von manifest.json"
|
||||||
|
shortName: "Abkürzung"
|
||||||
|
shortNameDescription: "Ein Kürzel für den Namen der Instanz, der angezeigt werden kann, falls der volle Instanzname lang ist."
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "Von einem anderen Konto zu diesem migrieren"
|
moveFrom: "Von einem anderen Konto zu diesem migrieren"
|
||||||
moveFromSub: "Alias für ein anderes Konto erstellen"
|
moveFromSub: "Alias für ein anderes Konto erstellen"
|
||||||
@@ -1463,6 +1478,7 @@ _role:
|
|||||||
descriptionOfRateLimitFactor: "Je niedriger desto weniger restriktiv, je höher destro restriktiver."
|
descriptionOfRateLimitFactor: "Je niedriger desto weniger restriktiv, je höher destro restriktiver."
|
||||||
canHideAds: "Kann Werbung ausblenden"
|
canHideAds: "Kann Werbung ausblenden"
|
||||||
canSearchNotes: "Nutzung der Notizsuchfunktion"
|
canSearchNotes: "Nutzung der Notizsuchfunktion"
|
||||||
|
canUseTranslator: "Verwendung des Übersetzers"
|
||||||
_condition:
|
_condition:
|
||||||
isLocal: "Lokaler Benutzer"
|
isLocal: "Lokaler Benutzer"
|
||||||
isRemote: "Benutzer fremder Instanz"
|
isRemote: "Benutzer fremder Instanz"
|
||||||
@@ -1529,6 +1545,7 @@ _plugin:
|
|||||||
install: "Plugins installieren"
|
install: "Plugins installieren"
|
||||||
installWarn: "Installiere bitte nur vertrauenswürdige Plugins."
|
installWarn: "Installiere bitte nur vertrauenswürdige Plugins."
|
||||||
manage: "Plugins verwalten"
|
manage: "Plugins verwalten"
|
||||||
|
viewSource: "Quelltext anzeigen"
|
||||||
_preferencesBackups:
|
_preferencesBackups:
|
||||||
list: "Erstellte Backups"
|
list: "Erstellte Backups"
|
||||||
saveNew: "Neu erstellen"
|
saveNew: "Neu erstellen"
|
||||||
@@ -1595,11 +1612,6 @@ _wordMute:
|
|||||||
muteWords: "Stummgeschaltete Wörter"
|
muteWords: "Stummgeschaltete Wörter"
|
||||||
muteWordsDescription: "Zum Nutzen einer \"UND\"-Verknüpfung Einträge mit Leerzeichen trennen, zum Nutzen einer \"ODER\"-Verknüpfung Einträge mit einem Zeilenumbruch trennen."
|
muteWordsDescription: "Zum Nutzen einer \"UND\"-Verknüpfung Einträge mit Leerzeichen trennen, zum Nutzen einer \"ODER\"-Verknüpfung Einträge mit einem Zeilenumbruch trennen."
|
||||||
muteWordsDescription2: "Umgib Schlüsselworter mit Schrägstrichen, um Reguläre Ausdrücke zu verwenden."
|
muteWordsDescription2: "Umgib Schlüsselworter mit Schrägstrichen, um Reguläre Ausdrücke zu verwenden."
|
||||||
softDescription: "Notizen, die die angegebenen Konditionen erfüllen, in der Chronik ausblenden."
|
|
||||||
hardDescription: "Verhindern, dass Notizen, die die angegebenen Konditionen erfüllen, der Chronik hinzugefügt werden. Zudem werden diese Notizen auch nicht der Chronik hinzugefügt, falls die Konditionen geändert werden."
|
|
||||||
soft: "Leicht"
|
|
||||||
hard: "Schwer"
|
|
||||||
mutedNotes: "Stummgeschaltete Notizen"
|
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription: "Schaltet alle Notizen/Renotes stumm, die von den gelisteten Instanzen stammen, inklusive Antworten von Benutzern an einen Benutzer einer stummgeschalteten Instanz."
|
instanceMuteDescription: "Schaltet alle Notizen/Renotes stumm, die von den gelisteten Instanzen stammen, inklusive Antworten von Benutzern an einen Benutzer einer stummgeschalteten Instanz."
|
||||||
instanceMuteDescription2: "Instanzen getrennt durch Zeilenumbrüchen angeben"
|
instanceMuteDescription2: "Instanzen getrennt durch Zeilenumbrüchen angeben"
|
||||||
@@ -1683,8 +1695,6 @@ _sfx:
|
|||||||
note: "Notizen"
|
note: "Notizen"
|
||||||
noteMy: "Meine Notizen"
|
noteMy: "Meine Notizen"
|
||||||
notification: "Benachrichtigungen"
|
notification: "Benachrichtigungen"
|
||||||
chat: "Chat"
|
|
||||||
chatBg: "Chat (Hintergrund)"
|
|
||||||
antenna: "Antennen"
|
antenna: "Antennen"
|
||||||
channel: "Kanalbenachrichtigung"
|
channel: "Kanalbenachrichtigung"
|
||||||
_ago:
|
_ago:
|
||||||
@@ -1721,7 +1731,7 @@ _2fa:
|
|||||||
step2Click: "Durch Klicken dieses QR-Codes kannst du Verifikation mit deinem Security-Token oder einer App registrieren."
|
step2Click: "Durch Klicken dieses QR-Codes kannst du Verifikation mit deinem Security-Token oder einer App registrieren."
|
||||||
step2Uri: "Nutzt du ein Desktopprogramm, gib folgende URI eingeben"
|
step2Uri: "Nutzt du ein Desktopprogramm, gib folgende URI eingeben"
|
||||||
step3Title: "Authentifizierungsscode eingeben"
|
step3Title: "Authentifizierungsscode eingeben"
|
||||||
step3: "Gib zum Abschluss den Token ein, der von deiner App angezeigt wird."
|
step3: "Gib zum Abschluss den Code (Token) ein, der von deiner App angezeigt wird."
|
||||||
setupCompleted: "Einrichtung abgeschlossen"
|
setupCompleted: "Einrichtung abgeschlossen"
|
||||||
step4: "Alle folgenden Anmeldeversuche werden ab sofort die Eingabe eines solchen Tokens benötigen."
|
step4: "Alle folgenden Anmeldeversuche werden ab sofort die Eingabe eines solchen Tokens benötigen."
|
||||||
securityKeyNotSupported: "Dein Browser unterstützt keine Hardware-Sicherheitsschlüssel."
|
securityKeyNotSupported: "Dein Browser unterstützt keine Hardware-Sicherheitsschlüssel."
|
||||||
@@ -1794,6 +1804,7 @@ _antennaSources:
|
|||||||
homeTimeline: "Notizen von Benutzern, denen gefolgt wird"
|
homeTimeline: "Notizen von Benutzern, denen gefolgt wird"
|
||||||
users: "Notizen von einem oder mehreren angegebenen Benutzern"
|
users: "Notizen von einem oder mehreren angegebenen Benutzern"
|
||||||
userList: "Notizen von allen Benutzern einer Liste"
|
userList: "Notizen von allen Benutzern einer Liste"
|
||||||
|
userBlacklist: "Alle Notizen abgesehen derer angegebener Benutzer"
|
||||||
_weekday:
|
_weekday:
|
||||||
sunday: "Sonntag"
|
sunday: "Sonntag"
|
||||||
monday: "Montag"
|
monday: "Montag"
|
||||||
@@ -2022,6 +2033,7 @@ _notification:
|
|||||||
notificationWillBeDisplayedLikeThis: "Benachrichtigungen sehen so aus"
|
notificationWillBeDisplayedLikeThis: "Benachrichtigungen sehen so aus"
|
||||||
_types:
|
_types:
|
||||||
all: "Alle"
|
all: "Alle"
|
||||||
|
note: "Neue Notizen"
|
||||||
follow: "Neue Follower"
|
follow: "Neue Follower"
|
||||||
mention: "Erwähnungen"
|
mention: "Erwähnungen"
|
||||||
reply: "Antworten"
|
reply: "Antworten"
|
||||||
@@ -2091,3 +2103,34 @@ _webhookSettings:
|
|||||||
renote: "Wenn du ein Renote erhältst"
|
renote: "Wenn du ein Renote erhältst"
|
||||||
reaction: "Wenn du eine Reaktion erhältst"
|
reaction: "Wenn du eine Reaktion erhältst"
|
||||||
mention: "Wenn du erwähnt wirst"
|
mention: "Wenn du erwähnt wirst"
|
||||||
|
_moderationLogTypes:
|
||||||
|
createRole: "Rolle erstellt"
|
||||||
|
deleteRole: "Rolle gelöscht"
|
||||||
|
updateRole: "Rolle aktualisiert"
|
||||||
|
assignRole: "Zu Rolle zugewiesen"
|
||||||
|
unassignRole: "Aus Rolle entfernt"
|
||||||
|
suspend: "Gesperrt"
|
||||||
|
unsuspend: "Entsperrt"
|
||||||
|
addCustomEmoji: "Benutzerdefiniertes Emoji hinzugefügt"
|
||||||
|
updateCustomEmoji: "Benutzerdefiniertes Emoji aktualisiert"
|
||||||
|
deleteCustomEmoji: "Benutzerdefiniertes Emoji gelöscht"
|
||||||
|
updateServerSettings: "Servereinstellungen aktualisiert"
|
||||||
|
updateUserNote: "Moderationsnotiz aktualisiert"
|
||||||
|
deleteDriveFile: "Datei gelöscht"
|
||||||
|
deleteNote: "Notiz gelöscht"
|
||||||
|
createGlobalAnnouncement: "Globale Ankündigung erstellt"
|
||||||
|
createUserAnnouncement: "Benutzerspezifische Ankündigung erstellt"
|
||||||
|
updateGlobalAnnouncement: "Globale Ankündigung aktualisiert"
|
||||||
|
updateUserAnnouncement: "Benutzerspezifische Ankündigung aktualisiert"
|
||||||
|
deleteGlobalAnnouncement: "Globale Ankündigung gelöscht"
|
||||||
|
deleteUserAnnouncement: "Benutzerspezifische Ankündigung gelöscht"
|
||||||
|
resetPassword: "Passwort zurückgesetzt"
|
||||||
|
suspendRemoteInstance: "Fremde Instanz gesperrt"
|
||||||
|
unsuspendRemoteInstance: "Fremde Instanz entsperrt"
|
||||||
|
markSensitiveDriveFile: "Datei als sensitiv markiert"
|
||||||
|
unmarkSensitiveDriveFile: "Datei als nicht sensitiv markiert"
|
||||||
|
resolveAbuseReport: "Meldung bearbeitet"
|
||||||
|
createInvitation: "Einladung erstellt"
|
||||||
|
createAd: "Werbung erstellt"
|
||||||
|
deleteAd: "Werbung gelöscht"
|
||||||
|
updateAd: "Werbung aktualisiert"
|
||||||
|
@@ -303,8 +303,6 @@ _theme:
|
|||||||
_sfx:
|
_sfx:
|
||||||
note: "Σημειώματα"
|
note: "Σημειώματα"
|
||||||
notification: "Ειδοποιήσεις"
|
notification: "Ειδοποιήσεις"
|
||||||
chat: "Συνομιλία"
|
|
||||||
chatBg: "Συνομιλία (Παρασκήνιο)"
|
|
||||||
antenna: "Αντένες"
|
antenna: "Αντένες"
|
||||||
channel: "Ειδοποιήσεις καναλιών"
|
channel: "Ειδοποιήσεις καναλιών"
|
||||||
_ago:
|
_ago:
|
||||||
@@ -397,3 +395,5 @@ _deck:
|
|||||||
mentions: "Επισημάνσεις"
|
mentions: "Επισημάνσεις"
|
||||||
_webhookSettings:
|
_webhookSettings:
|
||||||
name: "Όνομα"
|
name: "Όνομα"
|
||||||
|
_moderationLogTypes:
|
||||||
|
suspend: "Αποβολή"
|
||||||
|
@@ -418,6 +418,7 @@ moderator: "Moderator"
|
|||||||
moderation: "Moderation"
|
moderation: "Moderation"
|
||||||
moderationNote: "Moderation note"
|
moderationNote: "Moderation note"
|
||||||
addModerationNote: "Add moderation note"
|
addModerationNote: "Add moderation note"
|
||||||
|
moderationLogs: "Moderation logs"
|
||||||
nUsersMentioned: "Mentioned by {n} users"
|
nUsersMentioned: "Mentioned by {n} users"
|
||||||
securityKeyAndPasskey: "Security- and passkeys"
|
securityKeyAndPasskey: "Security- and passkeys"
|
||||||
securityKey: "Security key"
|
securityKey: "Security key"
|
||||||
@@ -710,6 +711,7 @@ lockedAccountInfo: "Unless you set your note visiblity to \"Followers only\", yo
|
|||||||
alwaysMarkSensitive: "Mark as sensitive by default"
|
alwaysMarkSensitive: "Mark as sensitive by default"
|
||||||
loadRawImages: "Load original images instead of showing thumbnails"
|
loadRawImages: "Load original images instead of showing thumbnails"
|
||||||
disableShowingAnimatedImages: "Don't play animated images"
|
disableShowingAnimatedImages: "Don't play animated images"
|
||||||
|
highlightSensitiveMedia: "Highlight sensitive media"
|
||||||
verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification."
|
verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification."
|
||||||
notSet: "Not set"
|
notSet: "Not set"
|
||||||
emailVerified: "Email has been verified"
|
emailVerified: "Email has been verified"
|
||||||
@@ -913,7 +915,7 @@ typeToConfirm: "Please enter {x} to confirm"
|
|||||||
deleteAccount: "Delete account"
|
deleteAccount: "Delete account"
|
||||||
document: "Documentation"
|
document: "Documentation"
|
||||||
numberOfPageCache: "Number of cached pages"
|
numberOfPageCache: "Number of cached pages"
|
||||||
numberOfPageCacheDescription: "Increasing this number will improve convenience for users but cause more server load as well as more memory to be used."
|
numberOfPageCacheDescription: "Increasing this number will improve convenience for but cause more load as more memory usage on the user's device."
|
||||||
logoutConfirm: "Really log out?"
|
logoutConfirm: "Really log out?"
|
||||||
lastActiveDate: "Last used at"
|
lastActiveDate: "Last used at"
|
||||||
statusbar: "Status bar"
|
statusbar: "Status bar"
|
||||||
@@ -1116,6 +1118,17 @@ keepScreenOn: "Keep screen on"
|
|||||||
verifiedLink: "Link ownership has been verified"
|
verifiedLink: "Link ownership has been verified"
|
||||||
notifyNotes: "Notify about new notes"
|
notifyNotes: "Notify about new notes"
|
||||||
unnotifyNotes: "Stop notifying about new notes"
|
unnotifyNotes: "Stop notifying about new notes"
|
||||||
|
authentication: "Authentication"
|
||||||
|
authenticationRequiredToContinue: "Please authenticate to continue"
|
||||||
|
dateAndTime: "Timestamp"
|
||||||
|
showRenotes: "Show renotes"
|
||||||
|
edited: "Edited"
|
||||||
|
notificationRecieveConfig: "Notification Settings"
|
||||||
|
mutualFollow: "Mutual follow"
|
||||||
|
fileAttachedOnly: "Only notes with files"
|
||||||
|
showRepliesToOthersInTimeline: "Show replies to others in TL"
|
||||||
|
hideRepliesToOthersInTimeline: "Hide replies to others from TL"
|
||||||
|
externalServices: "External Services"
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "Existing users only"
|
forExistingUsers: "Existing users only"
|
||||||
forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it."
|
forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it."
|
||||||
@@ -1149,6 +1162,8 @@ _serverSettings:
|
|||||||
appIconStyleRecommendation: "As the icon may be cropped to a square or circle, an icon with colored margin around the content is recommended."
|
appIconStyleRecommendation: "As the icon may be cropped to a square or circle, an icon with colored margin around the content is recommended."
|
||||||
appIconResolutionMustBe: "The minimum resolution is {resolution}."
|
appIconResolutionMustBe: "The minimum resolution is {resolution}."
|
||||||
manifestJsonOverride: "manifest.json Override"
|
manifestJsonOverride: "manifest.json Override"
|
||||||
|
shortName: "Short name"
|
||||||
|
shortNameDescription: "A shorthand for the instance's name that can be displayed if the full official name is long."
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "Migrate another account to this one"
|
moveFrom: "Migrate another account to this one"
|
||||||
moveFromSub: "Create alias to another account"
|
moveFromSub: "Create alias to another account"
|
||||||
@@ -1463,6 +1478,7 @@ _role:
|
|||||||
descriptionOfRateLimitFactor: "Lower rate limits are less restrictive, higher ones more restrictive. "
|
descriptionOfRateLimitFactor: "Lower rate limits are less restrictive, higher ones more restrictive. "
|
||||||
canHideAds: "Can hide ads"
|
canHideAds: "Can hide ads"
|
||||||
canSearchNotes: "Usage of note search"
|
canSearchNotes: "Usage of note search"
|
||||||
|
canUseTranslator: "Translator usage"
|
||||||
_condition:
|
_condition:
|
||||||
isLocal: "Local user"
|
isLocal: "Local user"
|
||||||
isRemote: "Remote user"
|
isRemote: "Remote user"
|
||||||
@@ -1529,6 +1545,7 @@ _plugin:
|
|||||||
install: "Install plugins"
|
install: "Install plugins"
|
||||||
installWarn: "Please do not install untrustworthy plugins."
|
installWarn: "Please do not install untrustworthy plugins."
|
||||||
manage: "Manage plugins"
|
manage: "Manage plugins"
|
||||||
|
viewSource: "View source"
|
||||||
_preferencesBackups:
|
_preferencesBackups:
|
||||||
list: "Created backups"
|
list: "Created backups"
|
||||||
saveNew: "Save new backup"
|
saveNew: "Save new backup"
|
||||||
@@ -1595,11 +1612,6 @@ _wordMute:
|
|||||||
muteWords: "Muted words"
|
muteWords: "Muted words"
|
||||||
muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition."
|
muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition."
|
||||||
muteWordsDescription2: "Surround keywords with slashes to use regular expressions."
|
muteWordsDescription2: "Surround keywords with slashes to use regular expressions."
|
||||||
softDescription: "Hide notes that fulfil the set conditions from the timeline."
|
|
||||||
hardDescription: "Prevents notes fulfilling the set conditions from being added to the timeline. In addition, these notes will not be added to the timeline even if the conditions are changed."
|
|
||||||
soft: "Soft"
|
|
||||||
hard: "Hard"
|
|
||||||
mutedNotes: "Muted notes"
|
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription: "This will mute any notes/renotes from the listed instances, including those of users replying to a user from a muted instance."
|
instanceMuteDescription: "This will mute any notes/renotes from the listed instances, including those of users replying to a user from a muted instance."
|
||||||
instanceMuteDescription2: "Separate with newlines"
|
instanceMuteDescription2: "Separate with newlines"
|
||||||
@@ -1683,8 +1695,6 @@ _sfx:
|
|||||||
note: "New note"
|
note: "New note"
|
||||||
noteMy: "Own note"
|
noteMy: "Own note"
|
||||||
notification: "Notifications"
|
notification: "Notifications"
|
||||||
chat: "Chat"
|
|
||||||
chatBg: "Chat (Background)"
|
|
||||||
antenna: "Antennas"
|
antenna: "Antennas"
|
||||||
channel: "Channel notifications"
|
channel: "Channel notifications"
|
||||||
_ago:
|
_ago:
|
||||||
@@ -1794,6 +1804,7 @@ _antennaSources:
|
|||||||
homeTimeline: "Notes from followed users"
|
homeTimeline: "Notes from followed users"
|
||||||
users: "Notes from specific users"
|
users: "Notes from specific users"
|
||||||
userList: "Notes from a specified list of users"
|
userList: "Notes from a specified list of users"
|
||||||
|
userBlacklist: "All notes except for those of one or more specified users"
|
||||||
_weekday:
|
_weekday:
|
||||||
sunday: "Sunday"
|
sunday: "Sunday"
|
||||||
monday: "Monday"
|
monday: "Monday"
|
||||||
@@ -2022,6 +2033,7 @@ _notification:
|
|||||||
notificationWillBeDisplayedLikeThis: "Notifications look like this"
|
notificationWillBeDisplayedLikeThis: "Notifications look like this"
|
||||||
_types:
|
_types:
|
||||||
all: "All"
|
all: "All"
|
||||||
|
note: "New notes"
|
||||||
follow: "New followers"
|
follow: "New followers"
|
||||||
mention: "Mentions"
|
mention: "Mentions"
|
||||||
reply: "Replies"
|
reply: "Replies"
|
||||||
@@ -2091,3 +2103,34 @@ _webhookSettings:
|
|||||||
renote: "When renoted"
|
renote: "When renoted"
|
||||||
reaction: "When receiving a reaction"
|
reaction: "When receiving a reaction"
|
||||||
mention: "When being mentioned"
|
mention: "When being mentioned"
|
||||||
|
_moderationLogTypes:
|
||||||
|
createRole: "Role created"
|
||||||
|
deleteRole: "Role deleted"
|
||||||
|
updateRole: "Role updated"
|
||||||
|
assignRole: "Assigned to role"
|
||||||
|
unassignRole: "Removed from role"
|
||||||
|
suspend: "Suspended"
|
||||||
|
unsuspend: "Unsuspended"
|
||||||
|
addCustomEmoji: "Custom emoji added"
|
||||||
|
updateCustomEmoji: "Custom emoji updated"
|
||||||
|
deleteCustomEmoji: "Custom emoji deleted"
|
||||||
|
updateServerSettings: "Server settings updated"
|
||||||
|
updateUserNote: "Moderation note updated"
|
||||||
|
deleteDriveFile: "File deleted"
|
||||||
|
deleteNote: "Note deleted"
|
||||||
|
createGlobalAnnouncement: "Global announcement created"
|
||||||
|
createUserAnnouncement: "User announcement created"
|
||||||
|
updateGlobalAnnouncement: "Global announcement updated"
|
||||||
|
updateUserAnnouncement: "User announcement updated"
|
||||||
|
deleteGlobalAnnouncement: "Global announcement deleted"
|
||||||
|
deleteUserAnnouncement: "User announcement deleted"
|
||||||
|
resetPassword: "Password reset"
|
||||||
|
suspendRemoteInstance: "Remote instance suspended"
|
||||||
|
unsuspendRemoteInstance: "Remote instance unsuspended"
|
||||||
|
markSensitiveDriveFile: "File marked as sensitive"
|
||||||
|
unmarkSensitiveDriveFile: "File unmarked as sensitive"
|
||||||
|
resolveAbuseReport: "Report resolved"
|
||||||
|
createInvitation: "Invite generated"
|
||||||
|
createAd: "Ad created"
|
||||||
|
deleteAd: "Ad deleted"
|
||||||
|
updateAd: "Ad updated"
|
||||||
|
@@ -418,6 +418,7 @@ moderator: "Moderador"
|
|||||||
moderation: "Moderación"
|
moderation: "Moderación"
|
||||||
moderationNote: "Nota de moderación"
|
moderationNote: "Nota de moderación"
|
||||||
addModerationNote: "Añadir nota de moderación"
|
addModerationNote: "Añadir nota de moderación"
|
||||||
|
moderationLogs: "Log de moderación"
|
||||||
nUsersMentioned: "{n} usuarios mencionados"
|
nUsersMentioned: "{n} usuarios mencionados"
|
||||||
securityKeyAndPasskey: "Clave de seguridad / clave de paso"
|
securityKeyAndPasskey: "Clave de seguridad / clave de paso"
|
||||||
securityKey: "Clave de seguridad"
|
securityKey: "Clave de seguridad"
|
||||||
@@ -710,6 +711,7 @@ lockedAccountInfo: "A menos que configures la visibilidad de tus notas como \"S
|
|||||||
alwaysMarkSensitive: "Marcar los medios de comunicación como contenido sensible por defecto"
|
alwaysMarkSensitive: "Marcar los medios de comunicación como contenido sensible por defecto"
|
||||||
loadRawImages: "Cargar las imágenes originales en lugar de mostrar las miniaturas"
|
loadRawImages: "Cargar las imágenes originales en lugar de mostrar las miniaturas"
|
||||||
disableShowingAnimatedImages: "No reproducir imágenes animadas"
|
disableShowingAnimatedImages: "No reproducir imágenes animadas"
|
||||||
|
highlightSensitiveMedia: "Resaltar medios marcados como sensibles"
|
||||||
verificationEmailSent: "Se le ha enviado un correo electrónico de confirmación. Por favor, acceda al enlace proporcionado en el correo electrónico para completar la configuración."
|
verificationEmailSent: "Se le ha enviado un correo electrónico de confirmación. Por favor, acceda al enlace proporcionado en el correo electrónico para completar la configuración."
|
||||||
notSet: "Sin especificar"
|
notSet: "Sin especificar"
|
||||||
emailVerified: "Su dirección de correo electrónico ha sido verificada."
|
emailVerified: "Su dirección de correo electrónico ha sido verificada."
|
||||||
@@ -1109,6 +1111,16 @@ youHaveUnreadAnnouncements: "Hay anuncios sin leer"
|
|||||||
useSecurityKey: "Por favor, sigue las instrucciones de tu dispositivo o navegador para usar tu clave de seguridad o tu clave de paso."
|
useSecurityKey: "Por favor, sigue las instrucciones de tu dispositivo o navegador para usar tu clave de seguridad o tu clave de paso."
|
||||||
replies: "Responder"
|
replies: "Responder"
|
||||||
renotes: "Renotar"
|
renotes: "Renotar"
|
||||||
|
loadReplies: "Ver respuestas"
|
||||||
|
loadConversation: "Ver conversación"
|
||||||
|
pinnedList: "Lista fijada"
|
||||||
|
keepScreenOn: "Mantener pantalla encendida"
|
||||||
|
verifiedLink: "Propiedad del enlace verificada"
|
||||||
|
notifyNotes: "Notificar nuevas notas"
|
||||||
|
unnotifyNotes: "Dejar de notificar nuevas notas"
|
||||||
|
authentication: "Autenticación"
|
||||||
|
authenticationRequiredToContinue: "Por favor, autentifícate para continuar"
|
||||||
|
dateAndTime: "Fecha y hora"
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "Solo para usuarios registrados"
|
forExistingUsers: "Solo para usuarios registrados"
|
||||||
forExistingUsersDescription: "Este anuncio solo se mostrará a aquellos usuarios registrados en el momento de su publicación. Si se deshabilita esta opción, aquellos usuarios que se registren tras su publicación también lo verán."
|
forExistingUsersDescription: "Este anuncio solo se mostrará a aquellos usuarios registrados en el momento de su publicación. Si se deshabilita esta opción, aquellos usuarios que se registren tras su publicación también lo verán."
|
||||||
@@ -1137,7 +1149,13 @@ _serverRules:
|
|||||||
description: "Un conjunto de reglas que serán mostradas antes del registro. Configurar un sumario de términos de servicio es recomendado."
|
description: "Un conjunto de reglas que serán mostradas antes del registro. Configurar un sumario de términos de servicio es recomendado."
|
||||||
_serverSettings:
|
_serverSettings:
|
||||||
iconUrl: "URL del ícono"
|
iconUrl: "URL del ícono"
|
||||||
|
appIconDescription: "Indica el icono que se va a usar cuando {host} se muestre como una app."
|
||||||
|
appIconUsageExample: "Por ejemplo, como PWA o cuando se muestre como un marcador en la pantalla inicial del dispositivo"
|
||||||
|
appIconStyleRecommendation: "Como el icono puede ser recortado como un cuadrado o un círculo, se recomienda un icono con un margen coloreado alrededor del contenido."
|
||||||
|
appIconResolutionMustBe: "La resolución mínima es {resolution}."
|
||||||
manifestJsonOverride: "Sobreescribir manifest.json"
|
manifestJsonOverride: "Sobreescribir manifest.json"
|
||||||
|
shortName: "Nombre corto"
|
||||||
|
shortNameDescription: "Forma corta del nombre de la instancia que puede mostrarse si el nombre completo es demasiado largo."
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "Trasladar de otra cuenta a ésta"
|
moveFrom: "Trasladar de otra cuenta a ésta"
|
||||||
moveFromSub: "Crear un alias para otra cuenta."
|
moveFromSub: "Crear un alias para otra cuenta."
|
||||||
@@ -1518,6 +1536,7 @@ _plugin:
|
|||||||
install: "Instalar plugins"
|
install: "Instalar plugins"
|
||||||
installWarn: "Por favor no instale plugins que no son de confianza"
|
installWarn: "Por favor no instale plugins que no son de confianza"
|
||||||
manage: "Gestionar plugins"
|
manage: "Gestionar plugins"
|
||||||
|
viewSource: "Ver la fuente"
|
||||||
_preferencesBackups:
|
_preferencesBackups:
|
||||||
list: "Respaldos creados"
|
list: "Respaldos creados"
|
||||||
saveNew: "Guardar nuevo respaldo"
|
saveNew: "Guardar nuevo respaldo"
|
||||||
@@ -1584,11 +1603,6 @@ _wordMute:
|
|||||||
muteWords: "Palabras que silenciar"
|
muteWords: "Palabras que silenciar"
|
||||||
muteWordsDescription: "Separar con espacios indica una declaracion And, separar con lineas nuevas indica una declaracion Or。"
|
muteWordsDescription: "Separar con espacios indica una declaracion And, separar con lineas nuevas indica una declaracion Or。"
|
||||||
muteWordsDescription2: "Encerrar las palabras clave entre numerales para usar expresiones regulares"
|
muteWordsDescription2: "Encerrar las palabras clave entre numerales para usar expresiones regulares"
|
||||||
softDescription: "Ocultar en la linea de tiempo las notas que cumplen las condiciones"
|
|
||||||
hardDescription: "Evitar que se agreguen a la linea de tiempo las notas que cumplen las condiciones. Las notas no agregadas seguirán quitadas aunque cambien las condiciones."
|
|
||||||
soft: "Suave"
|
|
||||||
hard: "Duro"
|
|
||||||
mutedNotes: "Notas silenciadas"
|
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription: "Silencia todas las notas y reposts de la instancias seleccionadas, incluyendo respuestas a los usuarios de las mismas"
|
instanceMuteDescription: "Silencia todas las notas y reposts de la instancias seleccionadas, incluyendo respuestas a los usuarios de las mismas"
|
||||||
instanceMuteDescription2: "Separar por líneas"
|
instanceMuteDescription2: "Separar por líneas"
|
||||||
@@ -1672,8 +1686,6 @@ _sfx:
|
|||||||
note: "Notas"
|
note: "Notas"
|
||||||
noteMy: "Nota (a mí mismo)"
|
noteMy: "Nota (a mí mismo)"
|
||||||
notification: "Notificaciones"
|
notification: "Notificaciones"
|
||||||
chat: "Chat"
|
|
||||||
chatBg: "Chat (Fondo)"
|
|
||||||
antenna: "Antena receptora"
|
antenna: "Antena receptora"
|
||||||
channel: "Notificaciones del canal"
|
channel: "Notificaciones del canal"
|
||||||
_ago:
|
_ago:
|
||||||
@@ -1783,6 +1795,7 @@ _antennaSources:
|
|||||||
homeTimeline: "Notas de los usuarios que sigues"
|
homeTimeline: "Notas de los usuarios que sigues"
|
||||||
users: "Notas de un usuario o varios"
|
users: "Notas de un usuario o varios"
|
||||||
userList: "Notas de los usuarios de una lista"
|
userList: "Notas de los usuarios de una lista"
|
||||||
|
userBlacklist: "Todas las notas excepto aquellas de uno o más usuarios especificados"
|
||||||
_weekday:
|
_weekday:
|
||||||
sunday: "Domingo"
|
sunday: "Domingo"
|
||||||
monday: "Lunes"
|
monday: "Lunes"
|
||||||
@@ -1882,6 +1895,7 @@ _profile:
|
|||||||
metadataContent: "Contenido"
|
metadataContent: "Contenido"
|
||||||
changeAvatar: "Cambiar avatar"
|
changeAvatar: "Cambiar avatar"
|
||||||
changeBanner: "Cambiar banner"
|
changeBanner: "Cambiar banner"
|
||||||
|
verifiedLinkDescription: "Introduciendo una URL que contiene un enlace a tu perfil, se puede mostrar un icono de verificación de propiedad al lado del campo."
|
||||||
_exportOrImport:
|
_exportOrImport:
|
||||||
allNotes: "Todas las notas"
|
allNotes: "Todas las notas"
|
||||||
favoritedNotes: "Notas favoritas"
|
favoritedNotes: "Notas favoritas"
|
||||||
@@ -2000,6 +2014,7 @@ _notification:
|
|||||||
youReceivedFollowRequest: "Has mandado una solicitud de seguimiento"
|
youReceivedFollowRequest: "Has mandado una solicitud de seguimiento"
|
||||||
yourFollowRequestAccepted: "Tu solicitud de seguimiento fue aceptada"
|
yourFollowRequestAccepted: "Tu solicitud de seguimiento fue aceptada"
|
||||||
pollEnded: "Estan disponibles los resultados de la encuesta"
|
pollEnded: "Estan disponibles los resultados de la encuesta"
|
||||||
|
newNote: "Nueva nota"
|
||||||
unreadAntennaNote: "Antena {name}"
|
unreadAntennaNote: "Antena {name}"
|
||||||
emptyPushNotificationMessage: "Se han actualizado las notificaciones push"
|
emptyPushNotificationMessage: "Se han actualizado las notificaciones push"
|
||||||
achievementEarned: "Logro desbloqueado"
|
achievementEarned: "Logro desbloqueado"
|
||||||
@@ -2009,6 +2024,7 @@ _notification:
|
|||||||
notificationWillBeDisplayedLikeThis: "Las notificaciones tendrán este aspecto"
|
notificationWillBeDisplayedLikeThis: "Las notificaciones tendrán este aspecto"
|
||||||
_types:
|
_types:
|
||||||
all: "Todo"
|
all: "Todo"
|
||||||
|
note: "Nuevas notas"
|
||||||
follow: "Siguiendo"
|
follow: "Siguiendo"
|
||||||
mention: "Menciones"
|
mention: "Menciones"
|
||||||
reply: "Respuestas"
|
reply: "Respuestas"
|
||||||
@@ -2078,3 +2094,31 @@ _webhookSettings:
|
|||||||
renote: "Cuando reciba un \"re-note\""
|
renote: "Cuando reciba un \"re-note\""
|
||||||
reaction: "Cuando se recibe una reacción"
|
reaction: "Cuando se recibe una reacción"
|
||||||
mention: "Cuando hay una mención"
|
mention: "Cuando hay una mención"
|
||||||
|
_moderationLogTypes:
|
||||||
|
createRole: "Rol creado"
|
||||||
|
deleteRole: "Rol eliminado"
|
||||||
|
updateRole: "Rol actualizado"
|
||||||
|
assignRole: "Rol asignado"
|
||||||
|
unassignRole: "Rol retirado"
|
||||||
|
suspend: "Suspender"
|
||||||
|
unsuspend: "Suspensión retirada"
|
||||||
|
addCustomEmoji: "Añadido emoji personalizado"
|
||||||
|
updateCustomEmoji: "Emoji personalizado actualizado"
|
||||||
|
deleteCustomEmoji: "Emoji personalizado eliminado"
|
||||||
|
updateServerSettings: "Ajustes de servidor actualizados"
|
||||||
|
updateUserNote: "Nota de moderación actualizada"
|
||||||
|
deleteDriveFile: "Archivo eliminado"
|
||||||
|
deleteNote: "Nota eliminada"
|
||||||
|
createGlobalAnnouncement: "Anuncio global creado"
|
||||||
|
createUserAnnouncement: "Anuncio de usuario creado"
|
||||||
|
updateGlobalAnnouncement: "Anuncio global actualizado"
|
||||||
|
updateUserAnnouncement: "Anuncio de usuario actualizado"
|
||||||
|
deleteGlobalAnnouncement: "Anuncio global eliminado"
|
||||||
|
deleteUserAnnouncement: "Anuncio de usuario eliminado"
|
||||||
|
resetPassword: "Resetear contraseña"
|
||||||
|
suspendRemoteInstance: "Instancia remota suspendida"
|
||||||
|
unsuspendRemoteInstance: "Suspensión de instancia remota retirada"
|
||||||
|
markSensitiveDriveFile: "Archivo marcado como sensible"
|
||||||
|
unmarkSensitiveDriveFile: "Archivo marcado como no sensible"
|
||||||
|
resolveAbuseReport: "Reporte resuelto"
|
||||||
|
createInvitation: "Generar invitación"
|
||||||
|
@@ -272,6 +272,7 @@ startMessaging: "Commencer à discuter"
|
|||||||
nUsersRead: "Lu par {n} personnes"
|
nUsersRead: "Lu par {n} personnes"
|
||||||
agreeTo: "J’accepte {0}"
|
agreeTo: "J’accepte {0}"
|
||||||
agree: "Accepter"
|
agree: "Accepter"
|
||||||
|
agreeBelow: "J’accepte ce qui suit"
|
||||||
basicNotesBeforeCreateAccount: "Notes importantes"
|
basicNotesBeforeCreateAccount: "Notes importantes"
|
||||||
termsOfService: "Conditions d'utilisation"
|
termsOfService: "Conditions d'utilisation"
|
||||||
start: "Commencer"
|
start: "Commencer"
|
||||||
@@ -406,6 +407,7 @@ aboutMisskey: "À propos de Misskey"
|
|||||||
administrator: "Administrateur"
|
administrator: "Administrateur"
|
||||||
token: "Jeton"
|
token: "Jeton"
|
||||||
2fa: "Authentification à deux facteurs"
|
2fa: "Authentification à deux facteurs"
|
||||||
|
setupOf2fa: "Configuration de l’authentification à deux facteurs"
|
||||||
totp: "Application d'authentification"
|
totp: "Application d'authentification"
|
||||||
totpDescription: "Entrez un mot de passe à usage unique à l'aide d'une application d'authentification"
|
totpDescription: "Entrez un mot de passe à usage unique à l'aide d'une application d'authentification"
|
||||||
moderator: "Modérateur·rice·s"
|
moderator: "Modérateur·rice·s"
|
||||||
@@ -413,6 +415,7 @@ moderation: "Modérations"
|
|||||||
moderationNote: "Note de modération"
|
moderationNote: "Note de modération"
|
||||||
addModerationNote: "Ajouter une note de modération"
|
addModerationNote: "Ajouter une note de modération"
|
||||||
nUsersMentioned: "{n} utilisateur·rice·s mentionné·e·s"
|
nUsersMentioned: "{n} utilisateur·rice·s mentionné·e·s"
|
||||||
|
securityKeyAndPasskey: "Sécurité et clés de sécurité"
|
||||||
securityKey: "Clé de sécurité"
|
securityKey: "Clé de sécurité"
|
||||||
lastUsed: "Dernier utilisé"
|
lastUsed: "Dernier utilisé"
|
||||||
lastUsedAt: "Dernière utilisation : {t}"
|
lastUsedAt: "Dernière utilisation : {t}"
|
||||||
@@ -797,6 +800,7 @@ popularPosts: "Les plus consultées"
|
|||||||
shareWithNote: "Partager dans une note"
|
shareWithNote: "Partager dans une note"
|
||||||
ads: "Publicité"
|
ads: "Publicité"
|
||||||
expiration: "Échéance"
|
expiration: "Échéance"
|
||||||
|
startingperiod: "Commencer"
|
||||||
memo: "Pense-bête"
|
memo: "Pense-bête"
|
||||||
priority: "Priorité"
|
priority: "Priorité"
|
||||||
high: "Haute"
|
high: "Haute"
|
||||||
@@ -958,6 +962,7 @@ internalServerError: "Erreur interne du serveur"
|
|||||||
copyErrorInfo: "Copier les détails de l’erreur"
|
copyErrorInfo: "Copier les détails de l’erreur"
|
||||||
exploreOtherServers: "Trouver une autre instance"
|
exploreOtherServers: "Trouver une autre instance"
|
||||||
disableFederationOk: "Désactiver"
|
disableFederationOk: "Désactiver"
|
||||||
|
likeOnly: "Les favoris uniquement"
|
||||||
license: "Licence"
|
license: "Licence"
|
||||||
video: "Vidéo"
|
video: "Vidéo"
|
||||||
videos: "Vidéos"
|
videos: "Vidéos"
|
||||||
@@ -978,6 +983,7 @@ horizontal: "Latéral"
|
|||||||
serverRules: "Règles du serveur"
|
serverRules: "Règles du serveur"
|
||||||
archive: "Archive"
|
archive: "Archive"
|
||||||
youFollowing: "Abonné·e"
|
youFollowing: "Abonné·e"
|
||||||
|
options: "Options"
|
||||||
later: "Plus tard"
|
later: "Plus tard"
|
||||||
goToMisskey: "Retour vers Misskey"
|
goToMisskey: "Retour vers Misskey"
|
||||||
expirationDate: "Date d’expiration"
|
expirationDate: "Date d’expiration"
|
||||||
@@ -990,12 +996,24 @@ icon: "Avatar"
|
|||||||
forYou: "Pour vous"
|
forYou: "Pour vous"
|
||||||
replies: "Répondre"
|
replies: "Répondre"
|
||||||
renotes: "Renoter"
|
renotes: "Renoter"
|
||||||
|
loadReplies: "Inclure les réponses"
|
||||||
|
pinnedList: "Liste épinglée"
|
||||||
|
notifyNotes: "Notifier à propos des nouvelles notes"
|
||||||
|
authentication: "Authentification"
|
||||||
|
authenticationRequiredToContinue: "Veuillez vous authentifier pour continuer"
|
||||||
_announcement:
|
_announcement:
|
||||||
readConfirmTitle: "Marquer comme lu ?"
|
readConfirmTitle: "Marquer comme lu ?"
|
||||||
_initialAccountSetting:
|
_initialAccountSetting:
|
||||||
profileSetting: "Paramètres du profil"
|
profileSetting: "Paramètres du profil"
|
||||||
privacySetting: "Paramètres de confidentialité"
|
privacySetting: "Paramètres de confidentialité"
|
||||||
|
initialAccountSettingCompleted: "Configuration du profil terminée avec succès !"
|
||||||
|
ifYouNeedLearnMore: "Si vous voulez en savoir plus comment utiliser {name}(Misskey), veuillez visiter {link}."
|
||||||
|
skipAreYouSure: "Désirez-vous ignorer la configuration du profile ?"
|
||||||
|
_serverSettings:
|
||||||
|
iconUrl: "URL de l’icône"
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
|
moveFrom: "Migrer un autre compte vers le présent compte"
|
||||||
|
moveFromSub: "Créer un alias vers un autre compte"
|
||||||
moveToLabel: "Compte vers lequel vous migrez :"
|
moveToLabel: "Compte vers lequel vous migrez :"
|
||||||
startMigration: "Migrer"
|
startMigration: "Migrer"
|
||||||
movedTo: "Compte vers lequel vous migrez :"
|
movedTo: "Compte vers lequel vous migrez :"
|
||||||
@@ -1052,20 +1070,33 @@ _achievements:
|
|||||||
_login1000:
|
_login1000:
|
||||||
flavor: "Merci d'utiliser Misskey !"
|
flavor: "Merci d'utiliser Misskey !"
|
||||||
_profileFilled:
|
_profileFilled:
|
||||||
|
title: "Bien préparé"
|
||||||
description: "Configuration de votre profil"
|
description: "Configuration de votre profil"
|
||||||
_markedAsCat:
|
_markedAsCat:
|
||||||
title: "Je suis un chat"
|
title: "Je suis un chat"
|
||||||
|
description: "Rendre votre compte comme un chat"
|
||||||
flavor: "Je n'ai pas encore de nom"
|
flavor: "Je n'ai pas encore de nom"
|
||||||
|
_following1:
|
||||||
|
title: "Vous suivez votre premier utilisateur·rice"
|
||||||
_following50:
|
_following50:
|
||||||
title: "Beaucoup d'amis"
|
title: "Beaucoup d'amis"
|
||||||
_followers10:
|
_followers10:
|
||||||
title: "Abonnez-moi !"
|
title: "Abonnez-moi !"
|
||||||
|
_followers100:
|
||||||
|
title: "Populaire"
|
||||||
|
_followers500:
|
||||||
|
title: "Tour radio"
|
||||||
|
_followers1000:
|
||||||
|
title: "Influenceur·euse"
|
||||||
_iLoveMisskey:
|
_iLoveMisskey:
|
||||||
title: "J’adore Misskey"
|
title: "J’adore Misskey"
|
||||||
description: "Publication « J’❤ #Misskey »"
|
description: "Publication « J’❤ #Misskey »"
|
||||||
|
flavor: "L'équipe de développement de Misskey apprécie vraiment votre aide !"
|
||||||
_foundTreasure:
|
_foundTreasure:
|
||||||
title: "Chasse au trésor"
|
title: "Chasse au trésor"
|
||||||
description: "Vous avez trouvé le trésor caché"
|
description: "Vous avez trouvé le trésor caché"
|
||||||
|
_client30min:
|
||||||
|
title: "Pause bien méritée"
|
||||||
_postedAtLateNight:
|
_postedAtLateNight:
|
||||||
flavor: "C’est l’heure d’aller au lit."
|
flavor: "C’est l’heure d’aller au lit."
|
||||||
_postedAt0min0sec:
|
_postedAt0min0sec:
|
||||||
@@ -1074,18 +1105,45 @@ _achievements:
|
|||||||
flavor: "Tic tac, tic tac, tic tac, ding !"
|
flavor: "Tic tac, tic tac, tic tac, ding !"
|
||||||
_viewInstanceChart:
|
_viewInstanceChart:
|
||||||
title: "Analyste"
|
title: "Analyste"
|
||||||
|
_outputHelloWorldOnScratchpad:
|
||||||
|
title: "Bonjour tout le monde !"
|
||||||
|
_open3windows:
|
||||||
|
title: "Multi-fenêtres"
|
||||||
|
_driveFolderCircularReference:
|
||||||
|
title: "Référence circulaire"
|
||||||
|
_setNameToSyuilo:
|
||||||
|
description: "Vous avez spécifié « syuilo » comme nom"
|
||||||
|
_passedSinceAccountCreated1:
|
||||||
|
title: "Premier anniversaire"
|
||||||
|
_passedSinceAccountCreated2:
|
||||||
|
title: "Second anniversaire"
|
||||||
|
_passedSinceAccountCreated3:
|
||||||
|
title: "3ème anniversaire"
|
||||||
_loggedInOnBirthday:
|
_loggedInOnBirthday:
|
||||||
title: "Joyeux Anniversaire !"
|
title: "Joyeux Anniversaire !"
|
||||||
|
description: "Vous vous êtes connecté à la date de votre anniversaire"
|
||||||
_loggedInOnNewYearsDay:
|
_loggedInOnNewYearsDay:
|
||||||
title: "Bonne année !"
|
title: "Bonne année !"
|
||||||
_cookieClicked:
|
_cookieClicked:
|
||||||
flavor: "Attendez une minute, vous êtes sur le mauvais site web ?"
|
flavor: "Attendez une minute, vous êtes sur le mauvais site web ?"
|
||||||
|
_brainDiver:
|
||||||
|
flavor: "Misskey-Misskey La-Tu-Ma"
|
||||||
_role:
|
_role:
|
||||||
|
new: "Nouveau rôle"
|
||||||
|
edit: "Modifier le rôle"
|
||||||
name: "Nom du rôle"
|
name: "Nom du rôle"
|
||||||
description: "Description du rôle"
|
description: "Description du rôle"
|
||||||
permission: "Rôle et autorisations"
|
permission: "Rôle et autorisations"
|
||||||
assignTarget: "Attribuer"
|
assignTarget: "Attribuer"
|
||||||
condition: "Condition"
|
condition: "Condition"
|
||||||
|
isPublic: "Rôle public"
|
||||||
|
options: "Options"
|
||||||
|
policies: "Stratégies"
|
||||||
|
baseRole: "Modèle de rôle"
|
||||||
|
useBaseValue: "Utiliser la valeur du modèle de rôle"
|
||||||
|
chooseRoleToAssign: "Sélectionner le rôle à assigner"
|
||||||
|
iconUrl: "URL de l’icône"
|
||||||
|
displayOrder: "Classement"
|
||||||
priority: "Priorité"
|
priority: "Priorité"
|
||||||
_priority:
|
_priority:
|
||||||
low: "Basse"
|
low: "Basse"
|
||||||
@@ -1144,6 +1202,7 @@ _plugin:
|
|||||||
install: "Installation de plugin"
|
install: "Installation de plugin"
|
||||||
installWarn: "N’installez que des extensions provenant de sources de confiance."
|
installWarn: "N’installez que des extensions provenant de sources de confiance."
|
||||||
manage: "Gestion des plugins"
|
manage: "Gestion des plugins"
|
||||||
|
viewSource: "Afficher la source"
|
||||||
_preferencesBackups:
|
_preferencesBackups:
|
||||||
list: "Sauvegardes créées"
|
list: "Sauvegardes créées"
|
||||||
saveNew: "Nouvelle sauvegarde"
|
saveNew: "Nouvelle sauvegarde"
|
||||||
@@ -1208,11 +1267,6 @@ _wordMute:
|
|||||||
muteWords: "Mots à filtrer"
|
muteWords: "Mots à filtrer"
|
||||||
muteWordsDescription: "Séparer avec des espaces pour la condition AND. Séparer avec un saut de ligne pour une condition OR."
|
muteWordsDescription: "Séparer avec des espaces pour la condition AND. Séparer avec un saut de ligne pour une condition OR."
|
||||||
muteWordsDescription2: "Pour utiliser des expressions régulières (regex), mettez les mots-clés entre barres obliques."
|
muteWordsDescription2: "Pour utiliser des expressions régulières (regex), mettez les mots-clés entre barres obliques."
|
||||||
softDescription: "Masquez les notes de votre fil selon les paramètres que vous définissez."
|
|
||||||
hardDescription: "Empêchez votre fil de charger les notes selon les paramètres que vous définissez. Cette action est irréversible : si vous modifiez ces paramètres plus tard, les notes précédemment filtrées ne seront pas récupérées."
|
|
||||||
soft: "Doux"
|
|
||||||
hard: "Strict"
|
|
||||||
mutedNotes: "Notes filtrées"
|
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription: "Met en sourdine toutes les notes et renotes de l'instance configurée, y compris les réponses aux utilisateurs de l'instance muette."
|
instanceMuteDescription: "Met en sourdine toutes les notes et renotes de l'instance configurée, y compris les réponses aux utilisateurs de l'instance muette."
|
||||||
instanceMuteDescription2: "Séparer avec de nouvelles lignes"
|
instanceMuteDescription2: "Séparer avec de nouvelles lignes"
|
||||||
@@ -1296,8 +1350,6 @@ _sfx:
|
|||||||
note: "Nouvelle note"
|
note: "Nouvelle note"
|
||||||
noteMy: "Ma note"
|
noteMy: "Ma note"
|
||||||
notification: "Notifications"
|
notification: "Notifications"
|
||||||
chat: "Discuter"
|
|
||||||
chatBg: "Discussion (arrière-plan)"
|
|
||||||
antenna: "Réception de l’antenne"
|
antenna: "Réception de l’antenne"
|
||||||
channel: "Notifications de canal"
|
channel: "Notifications de canal"
|
||||||
_ago:
|
_ago:
|
||||||
@@ -1330,6 +1382,7 @@ _2fa:
|
|||||||
securityKeyNotSupported: "Votre navigateur ne prend pas en charge les clés de sécurité."
|
securityKeyNotSupported: "Votre navigateur ne prend pas en charge les clés de sécurité."
|
||||||
securityKeyInfo: "Vous pouvez configurer l'authentification WebAuthN pour sécuriser davantage le processus de connexion grâce à une clé de sécurité matérielle qui prend en charge FIDO2, ou bien en configurant l'authentification par empreinte digitale ou par code PIN sur votre appareil."
|
securityKeyInfo: "Vous pouvez configurer l'authentification WebAuthN pour sécuriser davantage le processus de connexion grâce à une clé de sécurité matérielle qui prend en charge FIDO2, ou bien en configurant l'authentification par empreinte digitale ou par code PIN sur votre appareil."
|
||||||
securityKeyName: "Nom de la clé"
|
securityKeyName: "Nom de la clé"
|
||||||
|
removeKey: "Supprimer la clé de sécurité"
|
||||||
removeKeyConfirm: "Voulez-vous supprimer {name} ?"
|
removeKeyConfirm: "Voulez-vous supprimer {name} ?"
|
||||||
renewTOTPOk: "Reconfigurer"
|
renewTOTPOk: "Reconfigurer"
|
||||||
renewTOTPCancel: "Pas maintenant"
|
renewTOTPCancel: "Pas maintenant"
|
||||||
@@ -1631,3 +1684,6 @@ _deck:
|
|||||||
_webhookSettings:
|
_webhookSettings:
|
||||||
name: "Nom"
|
name: "Nom"
|
||||||
active: "Activé"
|
active: "Activé"
|
||||||
|
_moderationLogTypes:
|
||||||
|
suspend: "Suspendre"
|
||||||
|
resetPassword: "Réinitialiser le mot de passe"
|
||||||
|
@@ -1100,6 +1100,7 @@ currentAnnouncements: "Pengumuman Saat Ini"
|
|||||||
pastAnnouncements: "Pengumuman Terdahulu"
|
pastAnnouncements: "Pengumuman Terdahulu"
|
||||||
replies: "Balas"
|
replies: "Balas"
|
||||||
renotes: "Renote"
|
renotes: "Renote"
|
||||||
|
dateAndTime: "Tanggal dan Waktu"
|
||||||
_initialAccountSetting:
|
_initialAccountSetting:
|
||||||
accountCreated: "Akun kamu telah sukses dibuat!"
|
accountCreated: "Akun kamu telah sukses dibuat!"
|
||||||
letsStartAccountSetup: "Untuk pemula, ayo atur profilmu dulu."
|
letsStartAccountSetup: "Untuk pemula, ayo atur profilmu dulu."
|
||||||
@@ -1496,6 +1497,7 @@ _plugin:
|
|||||||
install: "Memasang plugin"
|
install: "Memasang plugin"
|
||||||
installWarn: "Mohon jangan memasang plugin yang tidak dapat dipercayai."
|
installWarn: "Mohon jangan memasang plugin yang tidak dapat dipercayai."
|
||||||
manage: "Manajemen plugin"
|
manage: "Manajemen plugin"
|
||||||
|
viewSource: "Lihat sumber"
|
||||||
_preferencesBackups:
|
_preferencesBackups:
|
||||||
list: "Cadangan yang dibuat"
|
list: "Cadangan yang dibuat"
|
||||||
saveNew: "Simpan cadangan baru"
|
saveNew: "Simpan cadangan baru"
|
||||||
@@ -1562,11 +1564,6 @@ _wordMute:
|
|||||||
muteWords: "Kata yang dibisukan"
|
muteWords: "Kata yang dibisukan"
|
||||||
muteWordsDescription: "Pisahkan dengan spasi untuk kondisi AND. Pisahkan dengan baris baru untuk kondisi OR."
|
muteWordsDescription: "Pisahkan dengan spasi untuk kondisi AND. Pisahkan dengan baris baru untuk kondisi OR."
|
||||||
muteWordsDescription2: "Kurung kata kunci dengan garis miring untuk menggunakan ekspresi reguler."
|
muteWordsDescription2: "Kurung kata kunci dengan garis miring untuk menggunakan ekspresi reguler."
|
||||||
softDescription: "Sembunyikan catatan yang memenuhi aturan kondisi dari lini masa."
|
|
||||||
hardDescription: "Cegah catatan memenuhi aturan kondisi dari ditambahkan ke lini masa. Dengan tambahan, catatan berikut tidak akan ditambahkan ke lini masa meskipun jika kondisi tersebut diubah."
|
|
||||||
soft: "Lembut"
|
|
||||||
hard: "Keras"
|
|
||||||
mutedNotes: "Catatan yang dibisukan"
|
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription: "Pengaturan ini akan membisukan note/renote apa saja dari instansi yang terdaftar, termasuk pengguna yang membalas pengguna lain dalam instansi yang dibisukan."
|
instanceMuteDescription: "Pengaturan ini akan membisukan note/renote apa saja dari instansi yang terdaftar, termasuk pengguna yang membalas pengguna lain dalam instansi yang dibisukan."
|
||||||
instanceMuteDescription2: "Pisah dengan baris baru"
|
instanceMuteDescription2: "Pisah dengan baris baru"
|
||||||
@@ -1650,8 +1647,6 @@ _sfx:
|
|||||||
note: "Catatan"
|
note: "Catatan"
|
||||||
noteMy: "Catatan (Saya)"
|
noteMy: "Catatan (Saya)"
|
||||||
notification: "Notifikasi"
|
notification: "Notifikasi"
|
||||||
chat: "Pesan"
|
|
||||||
chatBg: "Obrolan (Latar Belakang)"
|
|
||||||
antenna: "Penerimaan Antenna"
|
antenna: "Penerimaan Antenna"
|
||||||
channel: "Notifikasi Kanal"
|
channel: "Notifikasi Kanal"
|
||||||
_ago:
|
_ago:
|
||||||
@@ -2040,3 +2035,7 @@ _webhookSettings:
|
|||||||
renote: "Ketika direnote"
|
renote: "Ketika direnote"
|
||||||
reaction: "Ketika menerima reaksi"
|
reaction: "Ketika menerima reaksi"
|
||||||
mention: "Ketika sedang disebut"
|
mention: "Ketika sedang disebut"
|
||||||
|
_moderationLogTypes:
|
||||||
|
suspend: "Tangguhkan"
|
||||||
|
resetPassword: "Atur ulang kata sandi"
|
||||||
|
createInvitation: "Buat kode undangan"
|
||||||
|
56
locales/index.d.ts
vendored
56
locales/index.d.ts
vendored
@@ -421,6 +421,7 @@ export interface Locale {
|
|||||||
"moderation": string;
|
"moderation": string;
|
||||||
"moderationNote": string;
|
"moderationNote": string;
|
||||||
"addModerationNote": string;
|
"addModerationNote": string;
|
||||||
|
"moderationLogs": string;
|
||||||
"nUsersMentioned": string;
|
"nUsersMentioned": string;
|
||||||
"securityKeyAndPasskey": string;
|
"securityKeyAndPasskey": string;
|
||||||
"securityKey": string;
|
"securityKey": string;
|
||||||
@@ -1122,6 +1123,21 @@ export interface Locale {
|
|||||||
"unnotifyNotes": string;
|
"unnotifyNotes": string;
|
||||||
"authentication": string;
|
"authentication": string;
|
||||||
"authenticationRequiredToContinue": string;
|
"authenticationRequiredToContinue": string;
|
||||||
|
"dateAndTime": string;
|
||||||
|
"showRenotes": string;
|
||||||
|
"edited": string;
|
||||||
|
"notificationRecieveConfig": string;
|
||||||
|
"mutualFollow": string;
|
||||||
|
"fileAttachedOnly": string;
|
||||||
|
"showRepliesToOthersInTimeline": string;
|
||||||
|
"hideRepliesToOthersInTimeline": string;
|
||||||
|
"externalServices": string;
|
||||||
|
"impressum": string;
|
||||||
|
"impressumUrl": string;
|
||||||
|
"impressumDescription": string;
|
||||||
|
"privacyPolicy": string;
|
||||||
|
"privacyPolicyUrl": string;
|
||||||
|
"tosAndPrivacyPolicy": string;
|
||||||
"_announcement": {
|
"_announcement": {
|
||||||
"forExistingUsers": string;
|
"forExistingUsers": string;
|
||||||
"forExistingUsersDescription": string;
|
"forExistingUsersDescription": string;
|
||||||
@@ -1554,6 +1570,7 @@ export interface Locale {
|
|||||||
"descriptionOfRateLimitFactor": string;
|
"descriptionOfRateLimitFactor": string;
|
||||||
"canHideAds": string;
|
"canHideAds": string;
|
||||||
"canSearchNotes": string;
|
"canSearchNotes": string;
|
||||||
|
"canUseTranslator": string;
|
||||||
};
|
};
|
||||||
"_condition": {
|
"_condition": {
|
||||||
"isLocal": string;
|
"isLocal": string;
|
||||||
@@ -1710,11 +1727,6 @@ export interface Locale {
|
|||||||
"muteWords": string;
|
"muteWords": string;
|
||||||
"muteWordsDescription": string;
|
"muteWordsDescription": string;
|
||||||
"muteWordsDescription2": string;
|
"muteWordsDescription2": string;
|
||||||
"softDescription": string;
|
|
||||||
"hardDescription": string;
|
|
||||||
"soft": string;
|
|
||||||
"hard": string;
|
|
||||||
"mutedNotes": string;
|
|
||||||
};
|
};
|
||||||
"_instanceMute": {
|
"_instanceMute": {
|
||||||
"instanceMuteDescription": string;
|
"instanceMuteDescription": string;
|
||||||
@@ -1802,8 +1814,6 @@ export interface Locale {
|
|||||||
"note": string;
|
"note": string;
|
||||||
"noteMy": string;
|
"noteMy": string;
|
||||||
"notification": string;
|
"notification": string;
|
||||||
"chat": string;
|
|
||||||
"chatBg": string;
|
|
||||||
"antenna": string;
|
"antenna": string;
|
||||||
"channel": string;
|
"channel": string;
|
||||||
};
|
};
|
||||||
@@ -2248,6 +2258,38 @@ export interface Locale {
|
|||||||
"mention": string;
|
"mention": string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
"_moderationLogTypes": {
|
||||||
|
"createRole": string;
|
||||||
|
"deleteRole": string;
|
||||||
|
"updateRole": string;
|
||||||
|
"assignRole": string;
|
||||||
|
"unassignRole": string;
|
||||||
|
"suspend": string;
|
||||||
|
"unsuspend": string;
|
||||||
|
"addCustomEmoji": string;
|
||||||
|
"updateCustomEmoji": string;
|
||||||
|
"deleteCustomEmoji": string;
|
||||||
|
"updateServerSettings": string;
|
||||||
|
"updateUserNote": string;
|
||||||
|
"deleteDriveFile": string;
|
||||||
|
"deleteNote": string;
|
||||||
|
"createGlobalAnnouncement": string;
|
||||||
|
"createUserAnnouncement": string;
|
||||||
|
"updateGlobalAnnouncement": string;
|
||||||
|
"updateUserAnnouncement": string;
|
||||||
|
"deleteGlobalAnnouncement": string;
|
||||||
|
"deleteUserAnnouncement": string;
|
||||||
|
"resetPassword": string;
|
||||||
|
"suspendRemoteInstance": string;
|
||||||
|
"unsuspendRemoteInstance": string;
|
||||||
|
"markSensitiveDriveFile": string;
|
||||||
|
"unmarkSensitiveDriveFile": string;
|
||||||
|
"resolveAbuseReport": string;
|
||||||
|
"createInvitation": string;
|
||||||
|
"createAd": string;
|
||||||
|
"deleteAd": string;
|
||||||
|
"updateAd": string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
declare const locales: {
|
declare const locales: {
|
||||||
[lang: string]: Locale;
|
[lang: string]: Locale;
|
||||||
|
@@ -78,7 +78,7 @@ download: "Scarica"
|
|||||||
driveFileDeleteConfirm: "Vuoi davvero eliminare il file \"{name}\", e le Note a cui è stato allegato?"
|
driveFileDeleteConfirm: "Vuoi davvero eliminare il file \"{name}\", e le Note a cui è stato allegato?"
|
||||||
unfollowConfirm: "Vuoi davvero smettere di seguire {name}?"
|
unfollowConfirm: "Vuoi davvero smettere di seguire {name}?"
|
||||||
exportRequested: "Hai richiesto un'esportazione, e potrebbe volerci tempo. Quando sarà compiuta, il file verrà aggiunto direttamente al Drive."
|
exportRequested: "Hai richiesto un'esportazione, e potrebbe volerci tempo. Quando sarà compiuta, il file verrà aggiunto direttamente al Drive."
|
||||||
importRequested: "Hai richiesto un'importazione. Può volerci tempo. "
|
importRequested: "Hai richiesto un'importazione. Potrebbe richiedere un po' di tempo."
|
||||||
lists: "Liste"
|
lists: "Liste"
|
||||||
noLists: "Nessuna lista"
|
noLists: "Nessuna lista"
|
||||||
note: "Nota"
|
note: "Nota"
|
||||||
@@ -117,7 +117,7 @@ pinnedNote: "Nota fissata"
|
|||||||
pinned: "Fissa sul profilo"
|
pinned: "Fissa sul profilo"
|
||||||
you: "Tu"
|
you: "Tu"
|
||||||
clickToShow: "Clicca per visualizzare"
|
clickToShow: "Clicca per visualizzare"
|
||||||
sensitive: "Contenuto sensibile"
|
sensitive: "Esplicito"
|
||||||
add: "Aggiungi"
|
add: "Aggiungi"
|
||||||
reaction: "Reazioni"
|
reaction: "Reazioni"
|
||||||
reactions: "Reazioni"
|
reactions: "Reazioni"
|
||||||
@@ -125,13 +125,13 @@ 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"
|
||||||
attachCancel: "Rimuovi allegato"
|
attachCancel: "Rimuovi allegato"
|
||||||
markAsSensitive: "Segna come sensibile"
|
markAsSensitive: "Segna come esplicito"
|
||||||
unmarkAsSensitive: "Segna come non sensibile"
|
unmarkAsSensitive: "Non segnare come esplicito "
|
||||||
enterFileName: "Nome del file"
|
enterFileName: "Nome del file"
|
||||||
mute: "Silenzia"
|
mute: "Silenzia"
|
||||||
unmute: "Riattiva l'audio"
|
unmute: "Riattiva l'audio"
|
||||||
renoteMute: "Silenzia i Rinota"
|
renoteMute: "Silenzia le Rinota"
|
||||||
renoteUnmute: "Non silenziare i Rinota"
|
renoteUnmute: "Non silenziare le Rinota"
|
||||||
block: "Blocca"
|
block: "Blocca"
|
||||||
unblock: "Sblocca"
|
unblock: "Sblocca"
|
||||||
suspend: "Sospensione"
|
suspend: "Sospensione"
|
||||||
@@ -148,7 +148,7 @@ editAntenna: "Modifica Antenna"
|
|||||||
selectWidget: "Seleziona il riquadro"
|
selectWidget: "Seleziona il riquadro"
|
||||||
editWidgets: "Modifica i riquadri"
|
editWidgets: "Modifica i riquadri"
|
||||||
editWidgetsExit: "Conferma le modifiche"
|
editWidgetsExit: "Conferma le modifiche"
|
||||||
customEmojis: "Emoji personalizzati"
|
customEmojis: "Emoji personalizzate"
|
||||||
emoji: "Emoji"
|
emoji: "Emoji"
|
||||||
emojis: "Emoji"
|
emojis: "Emoji"
|
||||||
emojiName: "Nome dell'emoji"
|
emojiName: "Nome dell'emoji"
|
||||||
@@ -158,8 +158,8 @@ settingGuide: "Configurazione suggerita"
|
|||||||
cacheRemoteFiles: "Memorizza i file remoti nella cache"
|
cacheRemoteFiles: "Memorizza i file remoti nella cache"
|
||||||
cacheRemoteFilesDescription: "Disabilitando questa opzione, i file remoti verranno linkati direttamente senza essere memorizzati nella cache. Sarà possibile risparmiare spazio di archiviazione sul server, ma il traffico aumenterà in quanto non verranno generate anteprime."
|
cacheRemoteFilesDescription: "Disabilitando questa opzione, i file remoti verranno linkati direttamente senza essere memorizzati nella cache. Sarà possibile risparmiare spazio di archiviazione sul server, ma il traffico aumenterà in quanto non verranno generate anteprime."
|
||||||
youCanCleanRemoteFilesCache: "Puoi svuotare tutta la cache cliccando il bottone 🗑️ nella gestione file"
|
youCanCleanRemoteFilesCache: "Puoi svuotare tutta la cache cliccando il bottone 🗑️ nella gestione file"
|
||||||
cacheRemoteSensitiveFiles: "Memorizza nella cache i file sensibili remoti"
|
cacheRemoteSensitiveFiles: "Copia nella cache locale i file espliciti remoti"
|
||||||
cacheRemoteSensitiveFilesDescription: "Disattivando questa opzione, i file sensibili verranno caricati direttamente dall'istanza remota senza essere salvati dal server."
|
cacheRemoteSensitiveFilesDescription: "Disattivando questa opzione, i file espliciti verranno richiesti direttamente all'istanza remota senza essere salvati nel server locale."
|
||||||
flagAsBot: "Io sono un robot"
|
flagAsBot: "Io sono un robot"
|
||||||
flagAsBotDescription: "Attiva questo campo se il profilo esegue principalmente operazioni automatiche. L'attivazione segnala agli altri sviluppatori come comportarsi per evitare catene d’interazione infinite con altri bot. I sistemi interni di Misskey si adegueranno al fine di trattare questo profilo come bot."
|
flagAsBotDescription: "Attiva questo campo se il profilo esegue principalmente operazioni automatiche. L'attivazione segnala agli altri sviluppatori come comportarsi per evitare catene d’interazione infinite con altri bot. I sistemi interni di Misskey si adegueranno al fine di trattare questo profilo come bot."
|
||||||
flagAsCat: "Sono un gatto"
|
flagAsCat: "Sono un gatto"
|
||||||
@@ -186,7 +186,7 @@ recipient: "Destinatario"
|
|||||||
annotation: "Annotazione preventiva"
|
annotation: "Annotazione preventiva"
|
||||||
federation: "Federazione"
|
federation: "Federazione"
|
||||||
instances: "Istanza"
|
instances: "Istanza"
|
||||||
registeredAt: "Registrato presso"
|
registeredAt: "Prima federazione"
|
||||||
latestRequestReceivedAt: "Ultima richiesta ricevuta"
|
latestRequestReceivedAt: "Ultima richiesta ricevuta"
|
||||||
latestStatus: "Ultimo stato"
|
latestStatus: "Ultimo stato"
|
||||||
storageUsage: "Capienza dei dischi"
|
storageUsage: "Capienza dei dischi"
|
||||||
@@ -321,7 +321,7 @@ copyUrl: "Copia URL"
|
|||||||
rename: "Modifica nome"
|
rename: "Modifica nome"
|
||||||
avatar: "Foto del profilo"
|
avatar: "Foto del profilo"
|
||||||
banner: "Intestazione"
|
banner: "Intestazione"
|
||||||
displayOfSensitiveMedia: "Visibilità dei media sensibili"
|
displayOfSensitiveMedia: "Visibilità dei media espliciti"
|
||||||
whenServerDisconnected: "Quando la connessione col server è persa"
|
whenServerDisconnected: "Quando la connessione col server è persa"
|
||||||
disconnectedFromServer: "Il server si è disconnesso"
|
disconnectedFromServer: "Il server si è disconnesso"
|
||||||
reload: "Ricarica"
|
reload: "Ricarica"
|
||||||
@@ -418,6 +418,7 @@ moderator: "Moderatore"
|
|||||||
moderation: "moderazione"
|
moderation: "moderazione"
|
||||||
moderationNote: "Promemoria di moderazione"
|
moderationNote: "Promemoria di moderazione"
|
||||||
addModerationNote: "Aggiungi promemoria di moderazione"
|
addModerationNote: "Aggiungi promemoria di moderazione"
|
||||||
|
moderationLogs: "Cronologia di moderazione"
|
||||||
nUsersMentioned: "{n} profili menzionati"
|
nUsersMentioned: "{n} profili menzionati"
|
||||||
securityKeyAndPasskey: "Chiave di sicurezza e accesso"
|
securityKeyAndPasskey: "Chiave di sicurezza e accesso"
|
||||||
securityKey: "Chiave di sicurezza"
|
securityKey: "Chiave di sicurezza"
|
||||||
@@ -460,7 +461,7 @@ invitationCode: "Codice di invito"
|
|||||||
checking: "Confermando"
|
checking: "Confermando"
|
||||||
available: "Disponibile"
|
available: "Disponibile"
|
||||||
unavailable: "Il nome utente è già in uso"
|
unavailable: "Il nome utente è già in uso"
|
||||||
usernameInvalidFormat: "Il nome utente può contenere solo lettere, numeri e '_'"
|
usernameInvalidFormat: "Il nome utente deve avere solo caratteri alfanumerici e trattino basso '_'"
|
||||||
tooShort: "Troppo breve"
|
tooShort: "Troppo breve"
|
||||||
tooLong: "Troppo lungo"
|
tooLong: "Troppo lungo"
|
||||||
weakPassword: "Password debole"
|
weakPassword: "Password debole"
|
||||||
@@ -707,9 +708,10 @@ driveUsage: "Utilizzazione del Drive"
|
|||||||
noCrawle: "Rifiuta l'indicizzazione dai robot."
|
noCrawle: "Rifiuta l'indicizzazione dai robot."
|
||||||
noCrawleDescription: "Richiedi che i motori di ricerca non indicizzino la tua pagina di profilo, le tue note, pagine, ecc."
|
noCrawleDescription: "Richiedi che i motori di ricerca non indicizzino la tua pagina di profilo, le tue note, pagine, ecc."
|
||||||
lockedAccountInfo: "A meno che non imposti la visibilità delle tue note su \"Solo ai follower\", le tue note sono visibili da tutti, anche se hai configurato l'account per confermare manualmente le richieste di follow."
|
lockedAccountInfo: "A meno che non imposti la visibilità delle tue note su \"Solo ai follower\", le tue note sono visibili da tutti, anche se hai configurato l'account per confermare manualmente le richieste di follow."
|
||||||
alwaysMarkSensitive: "Segnare i media come sensibili per impostazione predefinita"
|
alwaysMarkSensitive: "Segnare gli allegati come espliciti come opzione predefinita"
|
||||||
loadRawImages: "Visualizza le intere immagini allegate invece delle miniature."
|
loadRawImages: "Visualizza le intere immagini allegate invece delle miniature."
|
||||||
disableShowingAnimatedImages: "Disabilita le immagini animate"
|
disableShowingAnimatedImages: "Disabilita le immagini animate"
|
||||||
|
highlightSensitiveMedia: "Evidenzia i media espliciti"
|
||||||
verificationEmailSent: "Una mail di verifica è stata inviata. Si prega di accedere al collegamento per compiere la verifica."
|
verificationEmailSent: "Una mail di verifica è stata inviata. Si prega di accedere al collegamento per compiere la verifica."
|
||||||
notSet: "Non impostato"
|
notSet: "Non impostato"
|
||||||
emailVerified: "Il tuo indirizzo email è stato verificato"
|
emailVerified: "Il tuo indirizzo email è stato verificato"
|
||||||
@@ -926,7 +928,7 @@ type: "Tipo"
|
|||||||
speed: "Velocità"
|
speed: "Velocità"
|
||||||
slow: "Lento"
|
slow: "Lento"
|
||||||
fast: "Veloce"
|
fast: "Veloce"
|
||||||
sensitiveMediaDetection: "Rilevamento dei contenuti sensibili."
|
sensitiveMediaDetection: "Rilevamento dei contenuti espliciti"
|
||||||
localOnly: "Soltanto locale"
|
localOnly: "Soltanto locale"
|
||||||
remoteOnly: "Solo remoto"
|
remoteOnly: "Solo remoto"
|
||||||
failedToUpload: "errore di caricamento"
|
failedToUpload: "errore di caricamento"
|
||||||
@@ -989,7 +991,7 @@ thisPostMayBeAnnoying: "Questa nota potrebbe essere offensiva"
|
|||||||
thisPostMayBeAnnoyingHome: "Pubblica sulla timeline principale"
|
thisPostMayBeAnnoyingHome: "Pubblica sulla timeline principale"
|
||||||
thisPostMayBeAnnoyingCancel: "Annulla"
|
thisPostMayBeAnnoyingCancel: "Annulla"
|
||||||
thisPostMayBeAnnoyingIgnore: "Pubblica lo stesso"
|
thisPostMayBeAnnoyingIgnore: "Pubblica lo stesso"
|
||||||
collapseRenotes: "Comprimi i Rinota già letti"
|
collapseRenotes: "Comprimi le Rinota già viste"
|
||||||
internalServerError: "Errore interno del server"
|
internalServerError: "Errore interno del server"
|
||||||
internalServerErrorDescription: "Si è verificato un errore imprevisto all'interno del server"
|
internalServerErrorDescription: "Si è verificato un errore imprevisto all'interno del server"
|
||||||
copyErrorInfo: "Copia le informazioni sull'errore"
|
copyErrorInfo: "Copia le informazioni sull'errore"
|
||||||
@@ -1006,11 +1008,11 @@ cannotBeChangedLater: "Non sarà più modificabile"
|
|||||||
reactionAcceptance: "Reazioni consentite"
|
reactionAcceptance: "Reazioni consentite"
|
||||||
likeOnly: "Solo i Like"
|
likeOnly: "Solo i Like"
|
||||||
likeOnlyForRemote: "Solo Like remoti"
|
likeOnlyForRemote: "Solo Like remoti"
|
||||||
nonSensitiveOnly: "Solamente non sensibili"
|
nonSensitiveOnly: "Soltanto non espliciti"
|
||||||
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Solamente non sensibili (solo Mi piace remoti)"
|
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Soltanto non espliciti (reazioni remote)"
|
||||||
rolesAssignedToMe: "I miei ruoli"
|
rolesAssignedToMe: "I miei ruoli"
|
||||||
resetPasswordConfirm: "Vuoi davvero ripristinare la password?"
|
resetPasswordConfirm: "Vuoi davvero ripristinare la password?"
|
||||||
sensitiveWords: "Parole sensibili"
|
sensitiveWords: "Parole esplicite"
|
||||||
sensitiveWordsDescription: "Imposta automaticamente \"Home\" alla visibilità delle Note che contengono una qualsiasi parola tra queste configurate. Puoi separarle per riga."
|
sensitiveWordsDescription: "Imposta automaticamente \"Home\" alla visibilità delle Note che contengono una qualsiasi parola tra queste configurate. Puoi separarle per riga."
|
||||||
sensitiveWordsDescription2: "Gli spazi creano la relazione \"E\" tra parole (questo E quello). Racchiudere una parola nelle slash \"/\" la trasforma in Espressione Regolare."
|
sensitiveWordsDescription2: "Gli spazi creano la relazione \"E\" tra parole (questo E quello). Racchiudere una parola nelle slash \"/\" la trasforma in Espressione Regolare."
|
||||||
notesSearchNotAvailable: "Non è possibile cercare tra le Note."
|
notesSearchNotAvailable: "Non è possibile cercare tra le Note."
|
||||||
@@ -1116,6 +1118,17 @@ keepScreenOn: "Mantieni lo schermo acceso"
|
|||||||
verifiedLink: "Abbiamo confermato la validità di questo collegamento"
|
verifiedLink: "Abbiamo confermato la validità di questo collegamento"
|
||||||
notifyNotes: "Notifica nuove Note"
|
notifyNotes: "Notifica nuove Note"
|
||||||
unnotifyNotes: "Interrompi le notifiche di nuove Note"
|
unnotifyNotes: "Interrompi le notifiche di nuove Note"
|
||||||
|
authentication: "Autenticazione"
|
||||||
|
authenticationRequiredToContinue: "Per procedere, è richiesta l'autenticazione"
|
||||||
|
dateAndTime: "Data e Ora"
|
||||||
|
showRenotes: "Leggi le Rinota"
|
||||||
|
edited: "Modificato"
|
||||||
|
notificationRecieveConfig: "Preferenze di notifica"
|
||||||
|
mutualFollow: "Follow reciproco"
|
||||||
|
fileAttachedOnly: "Con file in allegato"
|
||||||
|
showRepliesToOthersInTimeline: "Risposte altrui nella TL"
|
||||||
|
hideRepliesToOthersInTimeline: "Nascondi Riposte altrui nella TL"
|
||||||
|
externalServices: "Servizi esterni"
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "Solo ai profili attuali"
|
forExistingUsers: "Solo ai profili attuali"
|
||||||
forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio."
|
forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio."
|
||||||
@@ -1149,6 +1162,8 @@ _serverSettings:
|
|||||||
appIconStyleRecommendation: "Poiché l'icona potrebbe essere ritagliata in un quadrato o in un cerchio, si raccomanda che abbia un margine colorato."
|
appIconStyleRecommendation: "Poiché l'icona potrebbe essere ritagliata in un quadrato o in un cerchio, si raccomanda che abbia un margine colorato."
|
||||||
appIconResolutionMustBe: "La risoluzione minima è {resolution}"
|
appIconResolutionMustBe: "La risoluzione minima è {resolution}"
|
||||||
manifestJsonOverride: "Sostituire il file manifest.json"
|
manifestJsonOverride: "Sostituire il file manifest.json"
|
||||||
|
shortName: "Abbreviazione"
|
||||||
|
shortNameDescription: "Un'abbreviazione o un nome comune che può essere visualizzato al posto del nome ufficiale lungo del server."
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "Migra un altro profilo dentro a questo"
|
moveFrom: "Migra un altro profilo dentro a questo"
|
||||||
moveFromSub: "Crea un alias verso un altro profilo remoto"
|
moveFromSub: "Crea un alias verso un altro profilo remoto"
|
||||||
@@ -1443,14 +1458,14 @@ _role:
|
|||||||
_options:
|
_options:
|
||||||
gtlAvailable: "Disponibilità della Timeline Federata"
|
gtlAvailable: "Disponibilità della Timeline Federata"
|
||||||
ltlAvailable: "Disponibilità della Timeline Locale"
|
ltlAvailable: "Disponibilità della Timeline Locale"
|
||||||
canPublicNote: "Può scrivere Note con Visibilità Pubblica"
|
canPublicNote: "Scrivere Note con Visibilità Pubblica"
|
||||||
canInvite: "Genera codici di invito all'istanza"
|
canInvite: "Generare codici di invito all'istanza"
|
||||||
inviteLimit: "Limite di codici invito"
|
inviteLimit: "Limite di codici invito"
|
||||||
inviteLimitCycle: "Intervallo di emissione del codice di invito"
|
inviteLimitCycle: "Intervallo di emissione del codice di invito"
|
||||||
inviteExpirationTime: "Scadenza del codice di invito"
|
inviteExpirationTime: "Scadenza del codice di invito"
|
||||||
canManageCustomEmojis: "Gestire le emoji personalizzate"
|
canManageCustomEmojis: "Gestire le emoji personalizzate"
|
||||||
driveCapacity: "Capienza del Drive"
|
driveCapacity: "Capienza del Drive"
|
||||||
alwaysMarkNsfw: "Imposta sempre come NSFW"
|
alwaysMarkNsfw: "Impostare sempre come esplicito (NSFW)"
|
||||||
pinMax: "Quantità massima di Note in primo piano"
|
pinMax: "Quantità massima di Note in primo piano"
|
||||||
antennaMax: "Quantità massima di Antenne"
|
antennaMax: "Quantità massima di Antenne"
|
||||||
wordMuteMax: "Lunghezza massima del filtro parole"
|
wordMuteMax: "Lunghezza massima del filtro parole"
|
||||||
@@ -1461,8 +1476,9 @@ _role:
|
|||||||
userEachUserListsMax: "Quantità massima di profili per lista"
|
userEachUserListsMax: "Quantità massima di profili per lista"
|
||||||
rateLimitFactor: "Limite del rapporto"
|
rateLimitFactor: "Limite del rapporto"
|
||||||
descriptionOfRateLimitFactor: "I rapporti più bassi sono meno restrittivi, quelli più alti lo sono di più."
|
descriptionOfRateLimitFactor: "I rapporti più bassi sono meno restrittivi, quelli più alti lo sono di più."
|
||||||
canHideAds: "Può nascondere i banner"
|
canHideAds: "Nascondere i banner"
|
||||||
canSearchNotes: "Ricercare nelle Note"
|
canSearchNotes: "Ricercare nelle Note"
|
||||||
|
canUseTranslator: "Tradurre le Note"
|
||||||
_condition:
|
_condition:
|
||||||
isLocal: "Profilo locale"
|
isLocal: "Profilo locale"
|
||||||
isRemote: "Profilo remoto"
|
isRemote: "Profilo remoto"
|
||||||
@@ -1478,9 +1494,9 @@ _role:
|
|||||||
or: "O"
|
or: "O"
|
||||||
not: "NON"
|
not: "NON"
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "L'apprendimento automatico può essere utilizzato per individuare automaticamente i media sensibili da moderare. Il carico del server aumenta leggermente."
|
description: "Utilizzare l'apprendimento automatico (machine learning) per riconoscere media espliciti e sottoporli alla moderazione. Aumenterà lievemente il carico del server."
|
||||||
sensitivity: "Sensibilità di rilevamento"
|
sensitivity: "Sensibilità del rilevamento"
|
||||||
sensitivityDescription: "Una minore sensibilità riduce i falsi positivi (false positività). Una maggiore sensibilità riduce le omissioni (falsi negativi)."
|
sensitivityDescription: "Abbassando la sensibilità si riducono i falsi positivi (rilevazioni errate). Aumentando la sensibilità si riduce il numero di rilevazioni mancate. (rilevazioni ignorate)."
|
||||||
setSensitiveFlagAutomatically: "Impostare il flag NSFW."
|
setSensitiveFlagAutomatically: "Impostare il flag NSFW."
|
||||||
setSensitiveFlagAutomaticallyDescription: "Anche se questa impostazione è disattivata, il risultato della decisione viene conservato internamente."
|
setSensitiveFlagAutomaticallyDescription: "Anche se questa impostazione è disattivata, il risultato della decisione viene conservato internamente."
|
||||||
analyzeVideos: "Abilitazione dell'analisi video."
|
analyzeVideos: "Abilitazione dell'analisi video."
|
||||||
@@ -1529,6 +1545,7 @@ _plugin:
|
|||||||
install: "Installa estensioni"
|
install: "Installa estensioni"
|
||||||
installWarn: "Si prega di installare soltanto estensioni che provengono da fonti affidabili."
|
installWarn: "Si prega di installare soltanto estensioni che provengono da fonti affidabili."
|
||||||
manage: "Gestisci estensioni"
|
manage: "Gestisci estensioni"
|
||||||
|
viewSource: "Visualizza sorgente"
|
||||||
_preferencesBackups:
|
_preferencesBackups:
|
||||||
list: "Elenco di impostazioni salvate in precedenza"
|
list: "Elenco di impostazioni salvate in precedenza"
|
||||||
saveNew: "Nuovo salvataggio"
|
saveNew: "Nuovo salvataggio"
|
||||||
@@ -1563,8 +1580,8 @@ _aboutMisskey:
|
|||||||
morePatrons: "Apprezziamo sinceramente il supporto di tante altre persone. Grazie mille! 🥰"
|
morePatrons: "Apprezziamo sinceramente il supporto di tante altre persone. Grazie mille! 🥰"
|
||||||
patrons: "Sostenitori"
|
patrons: "Sostenitori"
|
||||||
_displayOfSensitiveMedia:
|
_displayOfSensitiveMedia:
|
||||||
respect: "Nascondere i media sensibili"
|
respect: "Nascondere i media espliciti"
|
||||||
ignore: "Non nascondere i media sensibili"
|
ignore: "Non nascondere i media espliciti"
|
||||||
force: "Nascondi tutti i media"
|
force: "Nascondi tutti i media"
|
||||||
_instanceTicker:
|
_instanceTicker:
|
||||||
none: "Nascondi"
|
none: "Nascondi"
|
||||||
@@ -1595,11 +1612,6 @@ _wordMute:
|
|||||||
muteWords: "Parole da filtrare"
|
muteWords: "Parole da filtrare"
|
||||||
muteWordsDescription: "Separare con uno spazio indica la condizione \"E\". Separare con una interruzione di riga, indica la condizione \"O\""
|
muteWordsDescription: "Separare con uno spazio indica la condizione \"E\". Separare con una interruzione di riga, indica la condizione \"O\""
|
||||||
muteWordsDescription2: "Se vuoi indicare delle Espressioni Regolari (regexp), metti la condizione all'interno di due slash (/)"
|
muteWordsDescription2: "Se vuoi indicare delle Espressioni Regolari (regexp), metti la condizione all'interno di due slash (/)"
|
||||||
softDescription: "Verranno nascoste da tutte le Timeline quelle Note che soddisfano le seguenti condizioni"
|
|
||||||
hardDescription: "Impedisci alla istanza di caricare Note che soddisfano le seguenti condizioni. Le Note già filtrate sono già scomparse in modo irreversibile, fino al cambiamento delle condizioni. Dopo di che scompariranno quelle che soddisfano le nuove condizioni."
|
|
||||||
soft: "Leggero"
|
|
||||||
hard: "Pesante"
|
|
||||||
mutedNotes: "Note filtrate"
|
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription: "Disattiva tutte le note, le note di rinvio (condivisione) dell'istanza configurata, comprese le risposte agli utenti dell'istanza."
|
instanceMuteDescription: "Disattiva tutte le note, le note di rinvio (condivisione) dell'istanza configurata, comprese le risposte agli utenti dell'istanza."
|
||||||
instanceMuteDescription2: "Impostazione separata da una nuova riga"
|
instanceMuteDescription2: "Impostazione separata da una nuova riga"
|
||||||
@@ -1683,8 +1695,6 @@ _sfx:
|
|||||||
note: "Nota"
|
note: "Nota"
|
||||||
noteMy: "Mia nota"
|
noteMy: "Mia nota"
|
||||||
notification: "Notifiche"
|
notification: "Notifiche"
|
||||||
chat: "Messaggi"
|
|
||||||
chatBg: "Chat (sfondo)"
|
|
||||||
antenna: "Ricezione dell'antenna"
|
antenna: "Ricezione dell'antenna"
|
||||||
channel: "Notifiche di canale"
|
channel: "Notifiche di canale"
|
||||||
_ago:
|
_ago:
|
||||||
@@ -1794,6 +1804,7 @@ _antennaSources:
|
|||||||
homeTimeline: "Note dagli utenti che segui"
|
homeTimeline: "Note dagli utenti che segui"
|
||||||
users: "Note dagli utenti selezionati"
|
users: "Note dagli utenti selezionati"
|
||||||
userList: "Note dagli utenti della lista selezionata"
|
userList: "Note dagli utenti della lista selezionata"
|
||||||
|
userBlacklist: "Tutte le Note tranne quelle di uno o più profili specificati"
|
||||||
_weekday:
|
_weekday:
|
||||||
sunday: "Domenica"
|
sunday: "Domenica"
|
||||||
monday: "Lunedì"
|
monday: "Lunedì"
|
||||||
@@ -2022,7 +2033,8 @@ _notification:
|
|||||||
notificationWillBeDisplayedLikeThis: "La notifica apparirà così"
|
notificationWillBeDisplayedLikeThis: "La notifica apparirà così"
|
||||||
_types:
|
_types:
|
||||||
all: "Tutto"
|
all: "Tutto"
|
||||||
follow: "Novità follower"
|
note: "Nuove Note"
|
||||||
|
follow: "Nuovi profili follower"
|
||||||
mention: "Menzioni"
|
mention: "Menzioni"
|
||||||
reply: "Risposte"
|
reply: "Risposte"
|
||||||
renote: "Rinota"
|
renote: "Rinota"
|
||||||
@@ -2091,3 +2103,34 @@ _webhookSettings:
|
|||||||
renote: "Quando la Nota è Rinotata"
|
renote: "Quando la Nota è Rinotata"
|
||||||
reaction: "Quando ricevo una reazione"
|
reaction: "Quando ricevo una reazione"
|
||||||
mention: "Quando mi menzionano"
|
mention: "Quando mi menzionano"
|
||||||
|
_moderationLogTypes:
|
||||||
|
createRole: "Ruolo creato"
|
||||||
|
deleteRole: "Ruolo eliminato"
|
||||||
|
updateRole: "Ruolo aggiornato"
|
||||||
|
assignRole: "Ruolo assegnato"
|
||||||
|
unassignRole: "Ruolo disassegnato"
|
||||||
|
suspend: "Sospensione"
|
||||||
|
unsuspend: "Sospensione rimossa"
|
||||||
|
addCustomEmoji: "Emoji personalizzata aggiunta"
|
||||||
|
updateCustomEmoji: "Emoji personalizzata aggiornata"
|
||||||
|
deleteCustomEmoji: "Emoji personalizzata eliminata"
|
||||||
|
updateServerSettings: "Impostazioni del server aggiornate"
|
||||||
|
updateUserNote: "Promemoria di moderazione aggiornato"
|
||||||
|
deleteDriveFile: "File da Drive eliminato"
|
||||||
|
deleteNote: "Nota eliminata"
|
||||||
|
createGlobalAnnouncement: "Annuncio globale creato"
|
||||||
|
createUserAnnouncement: "Annuncio ai profili iscritti creato"
|
||||||
|
updateGlobalAnnouncement: "Annuncio globale aggiornato"
|
||||||
|
updateUserAnnouncement: "Annuncio ai profili iscritti aggiornato"
|
||||||
|
deleteGlobalAnnouncement: "Annuncio globale eliminato"
|
||||||
|
deleteUserAnnouncement: "Annuncio ai profili iscritti eliminato"
|
||||||
|
resetPassword: "Password azzerata"
|
||||||
|
suspendRemoteInstance: "Istanza remota sospesa"
|
||||||
|
unsuspendRemoteInstance: "Istanza remota riattivata"
|
||||||
|
markSensitiveDriveFile: "File nel Drive segnato come esplicito"
|
||||||
|
unmarkSensitiveDriveFile: "File nel Drive segnato come non esplicito"
|
||||||
|
resolveAbuseReport: "Segnalazione risolta"
|
||||||
|
createInvitation: "Genera codice di invito"
|
||||||
|
createAd: "Banner creato"
|
||||||
|
deleteAd: "Banner eliminato"
|
||||||
|
updateAd: "Banner aggiornato"
|
||||||
|
@@ -418,6 +418,7 @@ moderator: "モデレーター"
|
|||||||
moderation: "モデレーション"
|
moderation: "モデレーション"
|
||||||
moderationNote: "モデレーションノート"
|
moderationNote: "モデレーションノート"
|
||||||
addModerationNote: "モデレーションノートを追加する"
|
addModerationNote: "モデレーションノートを追加する"
|
||||||
|
moderationLogs: "モデログ"
|
||||||
nUsersMentioned: "{n}人が投稿"
|
nUsersMentioned: "{n}人が投稿"
|
||||||
securityKeyAndPasskey: "セキュリティキー・パスキー"
|
securityKeyAndPasskey: "セキュリティキー・パスキー"
|
||||||
securityKey: "セキュリティキー"
|
securityKey: "セキュリティキー"
|
||||||
@@ -1119,6 +1120,21 @@ notifyNotes: "投稿を通知"
|
|||||||
unnotifyNotes: "投稿の通知を解除"
|
unnotifyNotes: "投稿の通知を解除"
|
||||||
authentication: "認証"
|
authentication: "認証"
|
||||||
authenticationRequiredToContinue: "続けるには認証を行ってください"
|
authenticationRequiredToContinue: "続けるには認証を行ってください"
|
||||||
|
dateAndTime: "日時"
|
||||||
|
showRenotes: "リノートを表示"
|
||||||
|
edited: "編集済み"
|
||||||
|
notificationRecieveConfig: "通知の受信設定"
|
||||||
|
mutualFollow: "相互フォロー"
|
||||||
|
fileAttachedOnly: "ファイル付きのみ"
|
||||||
|
showRepliesToOthersInTimeline: "TLに他の人への返信を含める"
|
||||||
|
hideRepliesToOthersInTimeline: "TLに他の人への返信を含めない"
|
||||||
|
externalServices: "外部サービス"
|
||||||
|
impressum: "運営者情報"
|
||||||
|
impressumUrl: "運営者情報URL"
|
||||||
|
impressumDescription: "ドイツなどの一部の国と地域では表示が義務付けられています(Impressum)。"
|
||||||
|
privacyPolicy: "プライバシーポリシー"
|
||||||
|
privacyPolicyUrl: "プライバシーポリシーURL"
|
||||||
|
tosAndPrivacyPolicy: "利用規約・プライバシーポリシー"
|
||||||
|
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "既存ユーザーのみ"
|
forExistingUsers: "既存ユーザーのみ"
|
||||||
@@ -1474,7 +1490,8 @@ _role:
|
|||||||
rateLimitFactor: "レートリミット"
|
rateLimitFactor: "レートリミット"
|
||||||
descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。"
|
descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。"
|
||||||
canHideAds: "広告の非表示"
|
canHideAds: "広告の非表示"
|
||||||
canSearchNotes: "ノート検索の利用可否"
|
canSearchNotes: "ノート検索の利用"
|
||||||
|
canUseTranslator: "翻訳機能の利用"
|
||||||
_condition:
|
_condition:
|
||||||
isLocal: "ローカルユーザー"
|
isLocal: "ローカルユーザー"
|
||||||
isRemote: "リモートユーザー"
|
isRemote: "リモートユーザー"
|
||||||
@@ -1627,11 +1644,6 @@ _wordMute:
|
|||||||
muteWords: "ミュートするワード"
|
muteWords: "ミュートするワード"
|
||||||
muteWordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。"
|
muteWordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。"
|
||||||
muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。"
|
muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。"
|
||||||
softDescription: "指定した条件のノートをタイムラインから隠します。"
|
|
||||||
hardDescription: "指定した条件のノートをタイムラインに追加しないようにします。追加されなかったノートは、条件を変更しても除外されたままになります。"
|
|
||||||
soft: "ソフト"
|
|
||||||
hard: "ハード"
|
|
||||||
mutedNotes: "ミュートされたノート"
|
|
||||||
|
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したサーバーの全てのノートとRenoteをミュートします。"
|
instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したサーバーの全てのノートとRenoteをミュートします。"
|
||||||
@@ -1719,8 +1731,6 @@ _sfx:
|
|||||||
note: "ノート"
|
note: "ノート"
|
||||||
noteMy: "ノート(自分)"
|
noteMy: "ノート(自分)"
|
||||||
notification: "通知"
|
notification: "通知"
|
||||||
chat: "チャット"
|
|
||||||
chatBg: "チャット(バックグラウンド)"
|
|
||||||
antenna: "アンテナ受信"
|
antenna: "アンテナ受信"
|
||||||
channel: "チャンネル通知"
|
channel: "チャンネル通知"
|
||||||
|
|
||||||
@@ -2160,3 +2170,35 @@ _webhookSettings:
|
|||||||
renote: "Renoteされたとき"
|
renote: "Renoteされたとき"
|
||||||
reaction: "リアクションがあったとき"
|
reaction: "リアクションがあったとき"
|
||||||
mention: "メンションされたとき"
|
mention: "メンションされたとき"
|
||||||
|
|
||||||
|
_moderationLogTypes:
|
||||||
|
createRole: "ロールを作成"
|
||||||
|
deleteRole: "ロールを削除"
|
||||||
|
updateRole: "ロールを更新"
|
||||||
|
assignRole: "ロールへアサイン"
|
||||||
|
unassignRole: "ロールのアサイン解除"
|
||||||
|
suspend: "凍結"
|
||||||
|
unsuspend: "凍結解除"
|
||||||
|
addCustomEmoji: "カスタム絵文字追加"
|
||||||
|
updateCustomEmoji: "カスタム絵文字更新"
|
||||||
|
deleteCustomEmoji: "カスタム絵文字削除"
|
||||||
|
updateServerSettings: "サーバー設定更新"
|
||||||
|
updateUserNote: "モデレーションノート更新"
|
||||||
|
deleteDriveFile: "ファイルを削除"
|
||||||
|
deleteNote: "ノートを削除"
|
||||||
|
createGlobalAnnouncement: "全体のお知らせを作成"
|
||||||
|
createUserAnnouncement: "ユーザーへお知らせを作成"
|
||||||
|
updateGlobalAnnouncement: "全体のお知らせを更新"
|
||||||
|
updateUserAnnouncement: "ユーザーのお知らせを更新"
|
||||||
|
deleteGlobalAnnouncement: "全体のお知らせを削除"
|
||||||
|
deleteUserAnnouncement: "ユーザーのお知らせを削除"
|
||||||
|
resetPassword: "パスワードをリセット"
|
||||||
|
suspendRemoteInstance: "リモートサーバーを停止"
|
||||||
|
unsuspendRemoteInstance: "リモートサーバーを再開"
|
||||||
|
markSensitiveDriveFile: "ファイルをセンシティブ付与"
|
||||||
|
unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
|
||||||
|
resolveAbuseReport: "通報を解決"
|
||||||
|
createInvitation: "招待コードを作成"
|
||||||
|
createAd: "広告を作成"
|
||||||
|
deleteAd: "広告を削除"
|
||||||
|
updateAd: "広告を更新"
|
||||||
|
@@ -1105,6 +1105,10 @@ youHaveUnreadAnnouncements: "あんたまだこのお知らせ読んどらんや
|
|||||||
useSecurityKey: "ブラウザまたはデバイスの言う通りに、セキュリティキーまたはパスキーを使ってや。"
|
useSecurityKey: "ブラウザまたはデバイスの言う通りに、セキュリティキーまたはパスキーを使ってや。"
|
||||||
replies: "返事"
|
replies: "返事"
|
||||||
renotes: "Renote"
|
renotes: "Renote"
|
||||||
|
loadReplies: "返信を見るで"
|
||||||
|
loadConversation: "会話を見るで"
|
||||||
|
verifiedLink: "このリンク先の所有者であることが確認されたで。"
|
||||||
|
authenticationRequiredToContinue: "続けるには認証をやってや。"
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "もうおるユーザーのみ"
|
forExistingUsers: "もうおるユーザーのみ"
|
||||||
forExistingUsersDescription: "有効にすると、このお知らせ作成時点でおるユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。"
|
forExistingUsersDescription: "有効にすると、このお知らせ作成時点でおるユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。"
|
||||||
@@ -1133,6 +1137,11 @@ _serverRules:
|
|||||||
description: "新規登録前に見せる、サーバーの簡潔なルールを設定すんで。内容は使うための決め事の要約とすることを推奨するわ。"
|
description: "新規登録前に見せる、サーバーの簡潔なルールを設定すんで。内容は使うための決め事の要約とすることを推奨するわ。"
|
||||||
_serverSettings:
|
_serverSettings:
|
||||||
iconUrl: "アイコン画像のURL"
|
iconUrl: "アイコン画像のURL"
|
||||||
|
appIconDescription: "{host}がアプリとして表示してるんやつをアイコンを指定すんで。"
|
||||||
|
appIconUsageExample: "PWAや、スマートフォンのホーム画面にブックマークとして追加された時など"
|
||||||
|
appIconStyleRecommendation: "円形もしくは角丸にクロップされる場合があるさかいに、塗り潰された余白のある背景があるものが推奨されるで。"
|
||||||
|
appIconResolutionMustBe: "解像度は必ず{resolution}である必要があるで。"
|
||||||
|
shortNameDescription: "サーバーの名前が長い時に、代わりに表示することのできるあだ名。"
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "別のアカウントからこのアカウントに引っ越す"
|
moveFrom: "別のアカウントからこのアカウントに引っ越す"
|
||||||
moveFromSub: "別のアカウントへエイリアスを作る"
|
moveFromSub: "別のアカウントへエイリアスを作る"
|
||||||
@@ -1510,6 +1519,7 @@ _plugin:
|
|||||||
install: "プラグインのインストール"
|
install: "プラグインのインストール"
|
||||||
installWarn: "信頼できへんプラグインはインストールせんとってな"
|
installWarn: "信頼できへんプラグインはインストールせんとってな"
|
||||||
manage: "プラグインの管理"
|
manage: "プラグインの管理"
|
||||||
|
viewSource: "ソースを表示"
|
||||||
_preferencesBackups:
|
_preferencesBackups:
|
||||||
list: "作ったバックアップ"
|
list: "作ったバックアップ"
|
||||||
saveNew: "新しく保存"
|
saveNew: "新しく保存"
|
||||||
@@ -1576,11 +1586,6 @@ _wordMute:
|
|||||||
muteWords: "ミュートするワード"
|
muteWords: "ミュートするワード"
|
||||||
muteWordsDescription: "スペースで区切るとAND指定になって、改行で区切るとOR指定になるで。"
|
muteWordsDescription: "スペースで区切るとAND指定になって、改行で区切るとOR指定になるで。"
|
||||||
muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になるで。"
|
muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になるで。"
|
||||||
softDescription: "指定した条件のノートをタイムラインから隠すで。"
|
|
||||||
hardDescription: "指定した条件のノートをタイムラインに追加しないようにするで。追加せーへんかったかったノートは、条件を変えても除外されたままになるで。"
|
|
||||||
soft: "ソフト"
|
|
||||||
hard: "ハード"
|
|
||||||
mutedNotes: "ミュートされたノート"
|
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したインスタンスの全てのノートとRenoteをミュートにするで。"
|
instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したインスタンスの全てのノートとRenoteをミュートにするで。"
|
||||||
instanceMuteDescription2: "改行で区切って設定するんやで"
|
instanceMuteDescription2: "改行で区切って設定するんやで"
|
||||||
@@ -1664,8 +1669,6 @@ _sfx:
|
|||||||
note: "ノート"
|
note: "ノート"
|
||||||
noteMy: "ノート(自分)"
|
noteMy: "ノート(自分)"
|
||||||
notification: "通知"
|
notification: "通知"
|
||||||
chat: "チャット"
|
|
||||||
chatBg: "チャット(バックグラウンド)"
|
|
||||||
antenna: "アンテナ受信"
|
antenna: "アンテナ受信"
|
||||||
channel: "チャンネル通知"
|
channel: "チャンネル通知"
|
||||||
_ago:
|
_ago:
|
||||||
@@ -1702,6 +1705,7 @@ _2fa:
|
|||||||
step2Click: "QRコードをクリックすると、今使とる端末に入っとる認証アプリとかキーリングに登録できるで。"
|
step2Click: "QRコードをクリックすると、今使とる端末に入っとる認証アプリとかキーリングに登録できるで。"
|
||||||
step3Title: "確認コードを入れてーや"
|
step3Title: "確認コードを入れてーや"
|
||||||
step3: "アプリに表示されているトークンを入力して終わりや。"
|
step3: "アプリに表示されているトークンを入力して終わりや。"
|
||||||
|
setupCompleted: "設定が完了したで。"
|
||||||
step4: "これからログインするときも、同じようにトークンを入力するんやで"
|
step4: "これからログインするときも、同じようにトークンを入力するんやで"
|
||||||
securityKeyNotSupported: "今使とるブラウザはセキュリティキーに対応してへんのやってさ。"
|
securityKeyNotSupported: "今使とるブラウザはセキュリティキーに対応してへんのやってさ。"
|
||||||
registerTOTPBeforeKey: "セキュリティキー・パスキーを登録するんやったら、まず認証アプリを設定してーな。"
|
registerTOTPBeforeKey: "セキュリティキー・パスキーを登録するんやったら、まず認証アプリを設定してーな。"
|
||||||
@@ -1716,6 +1720,10 @@ _2fa:
|
|||||||
renewTOTPConfirm: "今までの認証アプリの確認コードは使えんくなるけどええか?"
|
renewTOTPConfirm: "今までの認証アプリの確認コードは使えんくなるけどええか?"
|
||||||
renewTOTPOk: "もっかい設定する"
|
renewTOTPOk: "もっかい設定する"
|
||||||
renewTOTPCancel: "やめとく"
|
renewTOTPCancel: "やめとく"
|
||||||
|
checkBackupCodesBeforeCloseThisWizard: "このウィザードを閉じる前に、したのバックアップコードを確認しいや。"
|
||||||
|
backupCodesDescription: "認証アプリが使用できんなった場合、以下のバックアップコードを使ってアカウントにアクセスできるで。これらのコードは必ず安全な場所に置いときや。各コードは一回だけ使用できるで。"
|
||||||
|
backupCodeUsedWarning: "バックアップコードが使用されたで。認証アプリが使えなくなってるん場合、なるべく早く認証アプリを再設定しや。"
|
||||||
|
backupCodesExhaustedWarning: "バックアップコードが全て使用されたで。認証アプリを利用できん場合、これ以上アカウントにアクセスできなくなるで。認証アプリを再登録しや。"
|
||||||
_permissions:
|
_permissions:
|
||||||
"read:account": "アカウントの情報を見るで"
|
"read:account": "アカウントの情報を見るで"
|
||||||
"write:account": "アカウントの情報を変更するで"
|
"write:account": "アカウントの情報を変更するで"
|
||||||
@@ -1988,6 +1996,9 @@ _notification:
|
|||||||
unreadAntennaNote: "アンテナ {name}"
|
unreadAntennaNote: "アンテナ {name}"
|
||||||
emptyPushNotificationMessage: "プッシュ通知の更新をしといたで"
|
emptyPushNotificationMessage: "プッシュ通知の更新をしといたで"
|
||||||
achievementEarned: "実績を獲得しとるで"
|
achievementEarned: "実績を獲得しとるで"
|
||||||
|
checkNotificationBehavior: "通知の表示を確かめるで"
|
||||||
|
sendTestNotification: "テスト通知を送信するで"
|
||||||
|
notificationWillBeDisplayedLikeThis: "通知はこのように表示されるで"
|
||||||
_types:
|
_types:
|
||||||
all: "すべて"
|
all: "すべて"
|
||||||
follow: "フォロー"
|
follow: "フォロー"
|
||||||
@@ -2023,6 +2034,7 @@ _deck:
|
|||||||
introduction2: "画面の右にある + を押して、いつでもカラムを追加できるで。"
|
introduction2: "画面の右にある + を押して、いつでもカラムを追加できるで。"
|
||||||
widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選んでウィジェットを追加してなー"
|
widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選んでウィジェットを追加してなー"
|
||||||
useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示"
|
useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示"
|
||||||
|
usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となるで"
|
||||||
_columns:
|
_columns:
|
||||||
main: "メイン"
|
main: "メイン"
|
||||||
widgets: "ウィジェット"
|
widgets: "ウィジェット"
|
||||||
@@ -2057,3 +2069,7 @@ _webhookSettings:
|
|||||||
renote: "Renoteされるとき~!"
|
renote: "Renoteされるとき~!"
|
||||||
reaction: "ツッコミがあるとき~!"
|
reaction: "ツッコミがあるとき~!"
|
||||||
mention: "メンションがあるとき~!"
|
mention: "メンションがあるとき~!"
|
||||||
|
_moderationLogTypes:
|
||||||
|
suspend: "凍結"
|
||||||
|
resetPassword: "パスワードをリセット"
|
||||||
|
createInvitation: "招待コードを作成"
|
||||||
|
@@ -416,6 +416,9 @@ totp: "인증 앱"
|
|||||||
totpDescription: "인증 앱을 사용하여 일회성 비밀번호 입력"
|
totpDescription: "인증 앱을 사용하여 일회성 비밀번호 입력"
|
||||||
moderator: "모더레이터"
|
moderator: "모더레이터"
|
||||||
moderation: "모더레이션"
|
moderation: "모더레이션"
|
||||||
|
moderationNote: "모더레이션 노트"
|
||||||
|
addModerationNote: "모더레이션 노트 추가하기"
|
||||||
|
moderationLogs: "모더레이션 로그"
|
||||||
nUsersMentioned: "{n}명이 언급함"
|
nUsersMentioned: "{n}명이 언급함"
|
||||||
securityKeyAndPasskey: "보안 키 또는 패스 키"
|
securityKeyAndPasskey: "보안 키 또는 패스 키"
|
||||||
securityKey: "보안 키"
|
securityKey: "보안 키"
|
||||||
@@ -1107,6 +1110,18 @@ youHaveUnreadAnnouncements: "읽지 않은 공지사항이 있습니다."
|
|||||||
useSecurityKey: "브라우저 또는 기기의 안내에 따라 보안 키 또는 패스키를 사용해 주십시오."
|
useSecurityKey: "브라우저 또는 기기의 안내에 따라 보안 키 또는 패스키를 사용해 주십시오."
|
||||||
replies: "답글"
|
replies: "답글"
|
||||||
renotes: "리노트"
|
renotes: "리노트"
|
||||||
|
loadReplies: "답글 보기"
|
||||||
|
loadConversation: "대화 보기"
|
||||||
|
pinnedList: "고정해놓은 리스트"
|
||||||
|
keepScreenOn: "기기 화면을 항상 켜기"
|
||||||
|
verifiedLink: "이 링크의 소유자임이 확인되었습니다."
|
||||||
|
notifyNotes: "새 노트 알림 켜기"
|
||||||
|
unnotifyNotes: "새 노트 알림 끄기"
|
||||||
|
authentication: "인증"
|
||||||
|
showRenotes: "리노트 표시"
|
||||||
|
edited: "수정됨"
|
||||||
|
notificationRecieveConfig: "알림 설정"
|
||||||
|
mutualFollow: "맞팔로우"
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "기존 유저에게만 알림"
|
forExistingUsers: "기존 유저에게만 알림"
|
||||||
forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 유저에게만 표시합니다. 비활성화하면 게시 후에 가입한 유저에게도 표시합니다."
|
forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 유저에게만 표시합니다. 비활성화하면 게시 후에 가입한 유저에게도 표시합니다."
|
||||||
@@ -1135,6 +1150,12 @@ _serverRules:
|
|||||||
description: "회원 가입 이전에 간단하게 표시할 서버 규칙입니다. 이용 약관의 요약으로 구성하는 것을 추천합니다."
|
description: "회원 가입 이전에 간단하게 표시할 서버 규칙입니다. 이용 약관의 요약으로 구성하는 것을 추천합니다."
|
||||||
_serverSettings:
|
_serverSettings:
|
||||||
iconUrl: "아이콘 URL"
|
iconUrl: "아이콘 URL"
|
||||||
|
appIconUsageExample: "예를 들어, PWA나 스마트폰 홈 화면에 북마크로 추가되었을 때 등"
|
||||||
|
appIconStyleRecommendation: "아이콘이 원형 또는 둥근 사각형으로 잘리는 경우가 있으므로, 가장자리 여백이 충분한 사진을 사용하는 것을 추천합니다."
|
||||||
|
appIconResolutionMustBe: "해상도는 반드시 {resolution} 이어야 합니다."
|
||||||
|
manifestJsonOverride: "manifest.json 오버라이드"
|
||||||
|
shortName: "약칭"
|
||||||
|
shortNameDescription: "서버의 정식 명칭이 긴 경우에, 대신에 표시할 수 있는 약칭이나 통칭."
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "다른 계정에서 이 계정으로 이사"
|
moveFrom: "다른 계정에서 이 계정으로 이사"
|
||||||
moveFromSub: "다른 계정에 대한 별칭을 생성"
|
moveFromSub: "다른 계정에 대한 별칭을 생성"
|
||||||
@@ -1512,6 +1533,7 @@ _plugin:
|
|||||||
install: "플러그인 설치"
|
install: "플러그인 설치"
|
||||||
installWarn: "신뢰할 수 없는 플러그인은 설치하지 않는 것이 좋습니다."
|
installWarn: "신뢰할 수 없는 플러그인은 설치하지 않는 것이 좋습니다."
|
||||||
manage: "플러그인 관리"
|
manage: "플러그인 관리"
|
||||||
|
viewSource: "소스 보기"
|
||||||
_preferencesBackups:
|
_preferencesBackups:
|
||||||
list: "생성한 백업"
|
list: "생성한 백업"
|
||||||
saveNew: "새 백업 만들기"
|
saveNew: "새 백업 만들기"
|
||||||
@@ -1578,11 +1600,6 @@ _wordMute:
|
|||||||
muteWords: "뮤트할 단어"
|
muteWords: "뮤트할 단어"
|
||||||
muteWordsDescription: "공백으로 구분하는 경우 AND, 줄바꿈으로 구분하는 경우 OR로 지정됩니다."
|
muteWordsDescription: "공백으로 구분하는 경우 AND, 줄바꿈으로 구분하는 경우 OR로 지정됩니다."
|
||||||
muteWordsDescription2: "정규 표현식을 사용하려면 키워드를 빗금표(/)로 감싸 주세요."
|
muteWordsDescription2: "정규 표현식을 사용하려면 키워드를 빗금표(/)로 감싸 주세요."
|
||||||
softDescription: "지정한 조건의 노트를 타임라인에서 숨깁니다."
|
|
||||||
hardDescription: "지정한 조건의 노트를 타임라인에 추가하지 않습니다. 타임라인에 추가되지 않은 노트는 조건을 변경해도 표시되지 않습니다."
|
|
||||||
soft: "보통"
|
|
||||||
hard: "보다 높은 수준"
|
|
||||||
mutedNotes: "뮤트된 노트"
|
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription: "뮤트한 서버에서 오는 답글을 포함한 모든 노트와 Renote를 뮤트합니다."
|
instanceMuteDescription: "뮤트한 서버에서 오는 답글을 포함한 모든 노트와 Renote를 뮤트합니다."
|
||||||
instanceMuteDescription2: "한 줄에 하나씩 입력해 주세요"
|
instanceMuteDescription2: "한 줄에 하나씩 입력해 주세요"
|
||||||
@@ -1666,8 +1683,6 @@ _sfx:
|
|||||||
note: "새 노트"
|
note: "새 노트"
|
||||||
noteMy: "내 노트"
|
noteMy: "내 노트"
|
||||||
notification: "알림"
|
notification: "알림"
|
||||||
chat: "대화"
|
|
||||||
chatBg: "대화 (백그라운드)"
|
|
||||||
antenna: "안테나 수신"
|
antenna: "안테나 수신"
|
||||||
channel: "채널 알림"
|
channel: "채널 알림"
|
||||||
_ago:
|
_ago:
|
||||||
@@ -2072,3 +2087,7 @@ _webhookSettings:
|
|||||||
renote: "누군가 내 글을 Renote했을 때"
|
renote: "누군가 내 글을 Renote했을 때"
|
||||||
reaction: "누군가 내 노트에 리액션했을 때"
|
reaction: "누군가 내 노트에 리액션했을 때"
|
||||||
mention: "누군가 나를 멘션했을 때"
|
mention: "누군가 나를 멘션했을 때"
|
||||||
|
_moderationLogTypes:
|
||||||
|
suspend: "정지"
|
||||||
|
resetPassword: "비밀번호 재설정"
|
||||||
|
createInvitation: "초대 코드 생성"
|
||||||
|
@@ -407,7 +407,6 @@ _theme:
|
|||||||
_sfx:
|
_sfx:
|
||||||
note: "ບັນທຶກ"
|
note: "ບັນທຶກ"
|
||||||
notification: "ການແຈ້ງເຕືອນ"
|
notification: "ການແຈ້ງເຕືອນ"
|
||||||
chat: "ແຊ໋ດ"
|
|
||||||
_2fa:
|
_2fa:
|
||||||
renewTOTPCancel: "ບໍ່ແມ່ນຕອນນີ້"
|
renewTOTPCancel: "ບໍ່ແມ່ນຕອນນີ້"
|
||||||
_widgets:
|
_widgets:
|
||||||
@@ -463,3 +462,5 @@ _deck:
|
|||||||
mentions: "ກ່າວເຖິງ"
|
mentions: "ກ່າວເຖິງ"
|
||||||
_webhookSettings:
|
_webhookSettings:
|
||||||
name: "ຊື່"
|
name: "ຊື່"
|
||||||
|
_moderationLogTypes:
|
||||||
|
suspend: "ລະງັບ"
|
||||||
|
@@ -438,7 +438,6 @@ _theme:
|
|||||||
_sfx:
|
_sfx:
|
||||||
note: "Notities"
|
note: "Notities"
|
||||||
notification: "Meldingen"
|
notification: "Meldingen"
|
||||||
chat: "Chat"
|
|
||||||
_2fa:
|
_2fa:
|
||||||
renewTOTPCancel: "Nee, bedankt"
|
renewTOTPCancel: "Nee, bedankt"
|
||||||
_widgets:
|
_widgets:
|
||||||
@@ -494,3 +493,6 @@ _deck:
|
|||||||
mentions: "Vermeldingen"
|
mentions: "Vermeldingen"
|
||||||
_webhookSettings:
|
_webhookSettings:
|
||||||
name: "Naam"
|
name: "Naam"
|
||||||
|
_moderationLogTypes:
|
||||||
|
suspend: "Opschorten"
|
||||||
|
resetPassword: "Wachtwoord terugzetten"
|
||||||
|
@@ -575,9 +575,6 @@ _channel:
|
|||||||
nameAndDescription: "Navn og beskrivelse"
|
nameAndDescription: "Navn og beskrivelse"
|
||||||
_menuDisplay:
|
_menuDisplay:
|
||||||
hide: "Skjul"
|
hide: "Skjul"
|
||||||
_wordMute:
|
|
||||||
soft: "Myk"
|
|
||||||
hard: "Hard"
|
|
||||||
_theme:
|
_theme:
|
||||||
description: "Beskrivelse"
|
description: "Beskrivelse"
|
||||||
color: "Farge"
|
color: "Farge"
|
||||||
@@ -725,3 +722,5 @@ _deck:
|
|||||||
direct: "Direkte"
|
direct: "Direkte"
|
||||||
_webhookSettings:
|
_webhookSettings:
|
||||||
name: "Navn"
|
name: "Navn"
|
||||||
|
_moderationLogTypes:
|
||||||
|
suspend: "Suspender"
|
||||||
|
@@ -925,6 +925,7 @@ _plugin:
|
|||||||
install: "Zainstaluj wtyczki"
|
install: "Zainstaluj wtyczki"
|
||||||
installWarn: "Nie instaluj niezaufanych wtyczek."
|
installWarn: "Nie instaluj niezaufanych wtyczek."
|
||||||
manage: "Zarządzanie wtyczkami"
|
manage: "Zarządzanie wtyczkami"
|
||||||
|
viewSource: "Zobacz źródło"
|
||||||
_preferencesBackups:
|
_preferencesBackups:
|
||||||
list: "Utworzone kopie zapasowe"
|
list: "Utworzone kopie zapasowe"
|
||||||
saveNew: "Zapisz nową kopię zapasową"
|
saveNew: "Zapisz nową kopię zapasową"
|
||||||
@@ -981,9 +982,6 @@ _menuDisplay:
|
|||||||
_wordMute:
|
_wordMute:
|
||||||
muteWords: "Słowo do wyciszenia"
|
muteWords: "Słowo do wyciszenia"
|
||||||
muteWordsDescription2: "Otocz słowa kluczowe ukośnikami, aby używać wyrażeń regularnych."
|
muteWordsDescription2: "Otocz słowa kluczowe ukośnikami, aby używać wyrażeń regularnych."
|
||||||
soft: "Łagodny"
|
|
||||||
hard: "Twardy"
|
|
||||||
mutedNotes: "Wyciszone wpisy"
|
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
title: "Ukrywa wpisy z wymienionych instancji."
|
title: "Ukrywa wpisy z wymienionych instancji."
|
||||||
heading: "Lista instancji do wyciszenia"
|
heading: "Lista instancji do wyciszenia"
|
||||||
@@ -1065,8 +1063,6 @@ _sfx:
|
|||||||
note: "Wpisy"
|
note: "Wpisy"
|
||||||
noteMy: "Mój wpis"
|
noteMy: "Mój wpis"
|
||||||
notification: "Powiadomienia"
|
notification: "Powiadomienia"
|
||||||
chat: "Wiadomości"
|
|
||||||
chatBg: "Rozmowy (tło)"
|
|
||||||
antenna: "Anteny"
|
antenna: "Anteny"
|
||||||
channel: "Powiadomienia kanału"
|
channel: "Powiadomienia kanału"
|
||||||
_ago:
|
_ago:
|
||||||
@@ -1401,3 +1397,6 @@ _webhookSettings:
|
|||||||
renote: "Po udostępnieniu wpisu"
|
renote: "Po udostępnieniu wpisu"
|
||||||
reaction: "Po otrzymaniu reakcji"
|
reaction: "Po otrzymaniu reakcji"
|
||||||
mention: "Po zostaniu wspomnianym"
|
mention: "Po zostaniu wspomnianym"
|
||||||
|
_moderationLogTypes:
|
||||||
|
suspend: "Zawieś"
|
||||||
|
resetPassword: "Zresetuj hasło"
|
||||||
|
@@ -1320,7 +1320,6 @@ _theme:
|
|||||||
_sfx:
|
_sfx:
|
||||||
note: "Posts"
|
note: "Posts"
|
||||||
notification: "Notificações"
|
notification: "Notificações"
|
||||||
chat: "Chat"
|
|
||||||
_ago:
|
_ago:
|
||||||
invalid: "Não há nada aqui"
|
invalid: "Não há nada aqui"
|
||||||
_timelineTutorial:
|
_timelineTutorial:
|
||||||
@@ -1497,3 +1496,6 @@ _webhookSettings:
|
|||||||
follow: "Quando seguindo um usuário"
|
follow: "Quando seguindo um usuário"
|
||||||
followed: "Quando sendo seguido"
|
followed: "Quando sendo seguido"
|
||||||
renote: "Quando repostado"
|
renote: "Quando repostado"
|
||||||
|
_moderationLogTypes:
|
||||||
|
suspend: "Suspender"
|
||||||
|
resetPassword: "Redefinir senha"
|
||||||
|
@@ -647,7 +647,6 @@ _theme:
|
|||||||
_sfx:
|
_sfx:
|
||||||
note: "Note"
|
note: "Note"
|
||||||
notification: "Notificări"
|
notification: "Notificări"
|
||||||
chat: "Chat"
|
|
||||||
_ago:
|
_ago:
|
||||||
invalid: "Nu e nimic de văzut aici"
|
invalid: "Nu e nimic de văzut aici"
|
||||||
_widgets:
|
_widgets:
|
||||||
@@ -704,3 +703,6 @@ _deck:
|
|||||||
mentions: "Mențiuni"
|
mentions: "Mențiuni"
|
||||||
_webhookSettings:
|
_webhookSettings:
|
||||||
name: "Nume"
|
name: "Nume"
|
||||||
|
_moderationLogTypes:
|
||||||
|
suspend: "Suspendă"
|
||||||
|
resetPassword: "Resetează parola"
|
||||||
|
@@ -1427,6 +1427,7 @@ _plugin:
|
|||||||
install: "Установка расширений"
|
install: "Установка расширений"
|
||||||
installWarn: "Пожалуйста, не устанавливайте расширения, которым не доверяете."
|
installWarn: "Пожалуйста, не устанавливайте расширения, которым не доверяете."
|
||||||
manage: "Управление расширениями"
|
manage: "Управление расширениями"
|
||||||
|
viewSource: "Просмотр исходника"
|
||||||
_preferencesBackups:
|
_preferencesBackups:
|
||||||
list: "Существующие резервные копии"
|
list: "Существующие резервные копии"
|
||||||
saveNew: "Создать резервную копию"
|
saveNew: "Создать резервную копию"
|
||||||
@@ -1487,11 +1488,6 @@ _wordMute:
|
|||||||
muteWords: "Скрыть слово"
|
muteWords: "Скрыть слово"
|
||||||
muteWordsDescription: "Пишите слова через пробел в одной строке, чтобы фильтровать их появление вместе; а если хотите фильтровать любое из них, пишите в отдельных строках."
|
muteWordsDescription: "Пишите слова через пробел в одной строке, чтобы фильтровать их появление вместе; а если хотите фильтровать любое из них, пишите в отдельных строках."
|
||||||
muteWordsDescription2: "Здесь можно использовать регулярные выражения — просто заключите их между двумя дробными чертами (/)."
|
muteWordsDescription2: "Здесь можно использовать регулярные выражения — просто заключите их между двумя дробными чертами (/)."
|
||||||
softDescription: "Соответствующие условиям заметки будут спрятаны из вашей ленты."
|
|
||||||
hardDescription: "Соответстующие условиям заметки вообще не будут попадать в вашу ленту. Даже если вы поменяете условия, отсеенные таким образом заметки уже не появятся."
|
|
||||||
soft: "Мягко"
|
|
||||||
hard: "Жёстко"
|
|
||||||
mutedNotes: "Скрытые заметки"
|
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription: "Заметки и репосты с указанных здесь инстансов, а также ответы пользователям оттуда же не будут отображаться."
|
instanceMuteDescription: "Заметки и репосты с указанных здесь инстансов, а также ответы пользователям оттуда же не будут отображаться."
|
||||||
instanceMuteDescription2: "Пишите каждый инстанс на отдельной строке"
|
instanceMuteDescription2: "Пишите каждый инстанс на отдельной строке"
|
||||||
@@ -1575,8 +1571,6 @@ _sfx:
|
|||||||
note: "Заметки"
|
note: "Заметки"
|
||||||
noteMy: "Собственные заметки"
|
noteMy: "Собственные заметки"
|
||||||
notification: "Уведомления"
|
notification: "Уведомления"
|
||||||
chat: "Сообщения"
|
|
||||||
chatBg: "Сообщения (фон)"
|
|
||||||
antenna: "Антенна"
|
antenna: "Антенна"
|
||||||
channel: "Канал"
|
channel: "Канал"
|
||||||
_ago:
|
_ago:
|
||||||
@@ -1950,3 +1944,6 @@ _webhookSettings:
|
|||||||
createWebhook: "Создать вебхук"
|
createWebhook: "Создать вебхук"
|
||||||
name: "Название"
|
name: "Название"
|
||||||
active: "Вкл."
|
active: "Вкл."
|
||||||
|
_moderationLogTypes:
|
||||||
|
suspend: "Заморозить"
|
||||||
|
resetPassword: "Сброс пароля:"
|
||||||
|
@@ -978,6 +978,7 @@ _plugin:
|
|||||||
install: "Inštalova pluginy"
|
install: "Inštalova pluginy"
|
||||||
installWarn: "Prosím neinštalujte nedôveryhodné pluginy."
|
installWarn: "Prosím neinštalujte nedôveryhodné pluginy."
|
||||||
manage: "Spravovanie pluginov"
|
manage: "Spravovanie pluginov"
|
||||||
|
viewSource: "Ukázať zdroj"
|
||||||
_preferencesBackups:
|
_preferencesBackups:
|
||||||
list: "Vytvorené zálohy"
|
list: "Vytvorené zálohy"
|
||||||
saveNew: "Uložiť novú"
|
saveNew: "Uložiť novú"
|
||||||
@@ -1038,11 +1039,6 @@ _wordMute:
|
|||||||
muteWords: "Umlčané slová"
|
muteWords: "Umlčané slová"
|
||||||
muteWordsDescription: "Medzerami oddeľte pre podmienku AND a novými riadkami pre podmienku OR."
|
muteWordsDescription: "Medzerami oddeľte pre podmienku AND a novými riadkami pre podmienku OR."
|
||||||
muteWordsDescription2: "Regulárne výrazy sa použijú keď použijete okolo lomítka."
|
muteWordsDescription2: "Regulárne výrazy sa použijú keď použijete okolo lomítka."
|
||||||
softDescription: "Skryje poznámky z časovej osi, ktoré spĺňajú podmienky."
|
|
||||||
hardDescription: "Zabráni poznámky spĺňajúce množinu podmienok, aby boli pridané do časovej osi. Navyše tieto poznámky nepribudnú v časovej osi ani keď sa podmienky zmenia."
|
|
||||||
soft: "Mäkké"
|
|
||||||
hard: "Tvrdé"
|
|
||||||
mutedNotes: "Umlčané poznámky"
|
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription: "Toto umlčí všetky poznámky/preposlania zo zoznamu serverov, vrátane tých, na ktoré používatelia odpovedajú z umlčaného servera."
|
instanceMuteDescription: "Toto umlčí všetky poznámky/preposlania zo zoznamu serverov, vrátane tých, na ktoré používatelia odpovedajú z umlčaného servera."
|
||||||
instanceMuteDescription2: "Oddeľte novými riadkami"
|
instanceMuteDescription2: "Oddeľte novými riadkami"
|
||||||
@@ -1126,8 +1122,6 @@ _sfx:
|
|||||||
note: "Poznámky"
|
note: "Poznámky"
|
||||||
noteMy: "Vlastná poznámka"
|
noteMy: "Vlastná poznámka"
|
||||||
notification: "Oznámenia"
|
notification: "Oznámenia"
|
||||||
chat: "Chat"
|
|
||||||
chatBg: "Chat (pozadie)"
|
|
||||||
antenna: "Antény"
|
antenna: "Antény"
|
||||||
channel: "Upozornenia kanála"
|
channel: "Upozornenia kanála"
|
||||||
_ago:
|
_ago:
|
||||||
@@ -1450,3 +1444,6 @@ _deck:
|
|||||||
_webhookSettings:
|
_webhookSettings:
|
||||||
name: "Názov"
|
name: "Názov"
|
||||||
active: "Zapnuté"
|
active: "Zapnuté"
|
||||||
|
_moderationLogTypes:
|
||||||
|
suspend: "Zmraziť"
|
||||||
|
resetPassword: "Resetovať heslo"
|
||||||
|
@@ -507,7 +507,6 @@ _theme:
|
|||||||
_sfx:
|
_sfx:
|
||||||
note: "Noter"
|
note: "Noter"
|
||||||
notification: "Notifikationer"
|
notification: "Notifikationer"
|
||||||
chat: "Chatt"
|
|
||||||
antenna: "Antenner"
|
antenna: "Antenner"
|
||||||
_2fa:
|
_2fa:
|
||||||
renewTOTPCancel: "Nej tack"
|
renewTOTPCancel: "Nej tack"
|
||||||
@@ -573,3 +572,6 @@ _deck:
|
|||||||
_webhookSettings:
|
_webhookSettings:
|
||||||
name: "Namn"
|
name: "Namn"
|
||||||
active: "Aktiverad"
|
active: "Aktiverad"
|
||||||
|
_moderationLogTypes:
|
||||||
|
suspend: "Suspendera"
|
||||||
|
resetPassword: "Återställ Lösenord"
|
||||||
|
@@ -416,6 +416,9 @@ totp: "แอป Authenticator"
|
|||||||
totpDescription: "ใช้แอปยืนยันตัวตนเพื่อป้อนรหัสผ่านแบบใช้ครั้งเดียว"
|
totpDescription: "ใช้แอปยืนยันตัวตนเพื่อป้อนรหัสผ่านแบบใช้ครั้งเดียว"
|
||||||
moderator: "ผู้ควบคุม"
|
moderator: "ผู้ควบคุม"
|
||||||
moderation: "การกลั่นกรอง"
|
moderation: "การกลั่นกรอง"
|
||||||
|
moderationNote: "โน้ตการกลั่นกรอง"
|
||||||
|
addModerationNote: "เพิ่มโน้ตการกลั่นกรอง"
|
||||||
|
moderationLogs: "บันทึกการกลั่นกรอง"
|
||||||
nUsersMentioned: "กล่าวถึงโดยผู้ใช้ {n} รายนี้"
|
nUsersMentioned: "กล่าวถึงโดยผู้ใช้ {n} รายนี้"
|
||||||
securityKeyAndPasskey: "ความปลอดภัยและรหัสผ่าน"
|
securityKeyAndPasskey: "ความปลอดภัยและรหัสผ่าน"
|
||||||
securityKey: "กุญแจความปลอดภัย"
|
securityKey: "กุญแจความปลอดภัย"
|
||||||
@@ -708,6 +711,7 @@ lockedAccountInfo: "เว้นแต่ว่าคุณจะต้องต
|
|||||||
alwaysMarkSensitive: "ทำเครื่องหมายเป็น NSFW เป็นค่าเริ่มต้น"
|
alwaysMarkSensitive: "ทำเครื่องหมายเป็น NSFW เป็นค่าเริ่มต้น"
|
||||||
loadRawImages: "โหลดภาพต้นฉบับแทนการแสดงภาพขนาดย่อ"
|
loadRawImages: "โหลดภาพต้นฉบับแทนการแสดงภาพขนาดย่อ"
|
||||||
disableShowingAnimatedImages: "ไม่ต้องเล่นภาพเคลื่อนไหว"
|
disableShowingAnimatedImages: "ไม่ต้องเล่นภาพเคลื่อนไหว"
|
||||||
|
highlightSensitiveMedia: "ไฮไลท์สื่อที่ละเอียดอ่อน"
|
||||||
verificationEmailSent: "ส่งอีเมลยืนยันแล้วนะ ได้โปรดกรุณาไปที่ลิงก์ที่รวมไว้เพื่อทำการตรวจสอบให้เสร็จสิ้น"
|
verificationEmailSent: "ส่งอีเมลยืนยันแล้วนะ ได้โปรดกรุณาไปที่ลิงก์ที่รวมไว้เพื่อทำการตรวจสอบให้เสร็จสิ้น"
|
||||||
notSet: "ไม่ได้ตั้งค่า"
|
notSet: "ไม่ได้ตั้งค่า"
|
||||||
emailVerified: "อีเมลได้รับการยืนยันแล้ว"
|
emailVerified: "อีเมลได้รับการยืนยันแล้ว"
|
||||||
@@ -1022,6 +1026,7 @@ retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการ
|
|||||||
enableChartsForRemoteUser: "สร้างแผนภูมิข้อมูลผู้ใช้ระยะไกล"
|
enableChartsForRemoteUser: "สร้างแผนภูมิข้อมูลผู้ใช้ระยะไกล"
|
||||||
enableChartsForFederatedInstances: "สร้างแผนภูมิข้อมูลอินสแตนซ์ระยะไกล"
|
enableChartsForFederatedInstances: "สร้างแผนภูมิข้อมูลอินสแตนซ์ระยะไกล"
|
||||||
showClipButtonInNoteFooter: "เพิ่ม \"คลิป\" เพื่อบันทึกเมนูการทำงาน"
|
showClipButtonInNoteFooter: "เพิ่ม \"คลิป\" เพื่อบันทึกเมนูการทำงาน"
|
||||||
|
reactionsDisplaySize: "รีแอคชั่นแสดงผลขนาด"
|
||||||
noteIdOrUrl: "โน้ต ID หรือ URL"
|
noteIdOrUrl: "โน้ต ID หรือ URL"
|
||||||
video: "วีดีโอ"
|
video: "วีดีโอ"
|
||||||
videos: "วีดีโอ"
|
videos: "วีดีโอ"
|
||||||
@@ -1100,11 +1105,26 @@ iHaveReadXCarefullyAndAgree: "ฉันได้อ่านข้อควา
|
|||||||
dialog: "ไดอะล็อก"
|
dialog: "ไดอะล็อก"
|
||||||
icon: "ไอคอน"
|
icon: "ไอคอน"
|
||||||
forYou: "สำหรับคุณ"
|
forYou: "สำหรับคุณ"
|
||||||
|
currentAnnouncements: "ประกาศในปัจจุบัน"
|
||||||
|
pastAnnouncements: "ประกาศที่ผ่านมา"
|
||||||
|
youHaveUnreadAnnouncements: "มีการประกาศที่ยังไม่ได้อ่าน"
|
||||||
replies: "ตอบกลับ"
|
replies: "ตอบกลับ"
|
||||||
renotes: "รีโน้ต"
|
renotes: "รีโน้ต"
|
||||||
loadReplies: "แสดงการตอบกลับ"
|
loadReplies: "แสดงการตอบกลับ"
|
||||||
loadConversation: "แสดงบทสนทนา"
|
loadConversation: "แสดงบทสนทนา"
|
||||||
|
pinnedList: "รายการที่ปักหมุดไว้แล้ว"
|
||||||
|
keepScreenOn: "เปิดหน้าจอไว้"
|
||||||
|
notifyNotes: "แจ้งเตือนเกี่ยวกับโพสต์ใหม่"
|
||||||
|
unnotifyNotes: "หยุดการแจ้งเตือนเกี่ยวกับโน้ตใหม่"
|
||||||
|
authentication: "การตรวจสอบสิทธิ์"
|
||||||
|
dateAndTime: "เวลาประทับ"
|
||||||
|
showRenotes: "แสดงรีโน้ต"
|
||||||
|
edited: "แก้ไขแล้ว"
|
||||||
|
notificationRecieveConfig: "การตั้งค่าการแจ้งเตือน"
|
||||||
|
mutualFollow: "ติดตามซึ่งกันและกัน"
|
||||||
|
fileAttachedOnly: "เฉพาะโน้ตที่มีไฟล์เท่านั้น"
|
||||||
_announcement:
|
_announcement:
|
||||||
|
forExistingUsers: "ผู้ใช้งานที่มีอยู่เท่านั้น"
|
||||||
forExistingUsersDescription: "การประกาศนี้จะแสดงต่อผู้ใช้ที่มีอยู่ ณ จุดที่เผยแพร่นั้นๆถ้าหากเปิดใช้งาน ถ้าหากปิดใช้งานผู้ที่กำลังสมัครใหม่หลังจากโพสต์แล้วนั้นก็จะเห็นเช่นกัน"
|
forExistingUsersDescription: "การประกาศนี้จะแสดงต่อผู้ใช้ที่มีอยู่ ณ จุดที่เผยแพร่นั้นๆถ้าหากเปิดใช้งาน ถ้าหากปิดใช้งานผู้ที่กำลังสมัครใหม่หลังจากโพสต์แล้วนั้นก็จะเห็นเช่นกัน"
|
||||||
needConfirmationToReadDescription: "ข้อความแจ้งแยก ถ้าหากต้องการเพื่อยืนยันว่ากำลังทำเครื่องหมายประกาศนี้ว่าอ่านแล้วจะแสดงขึ้นถ้าหากเปิดใช้งาน การประกาศนั้นจะไม่รวมอยู่ในฟังก์ชั่นว่า \"ทำเครื่องหมายทั้งหมดว่าอ่านแล้ว\""
|
needConfirmationToReadDescription: "ข้อความแจ้งแยก ถ้าหากต้องการเพื่อยืนยันว่ากำลังทำเครื่องหมายประกาศนี้ว่าอ่านแล้วจะแสดงขึ้นถ้าหากเปิดใช้งาน การประกาศนั้นจะไม่รวมอยู่ในฟังก์ชั่นว่า \"ทำเครื่องหมายทั้งหมดว่าอ่านแล้ว\""
|
||||||
end: "ประกาศเก็บถาวร"
|
end: "ประกาศเก็บถาวร"
|
||||||
@@ -1130,6 +1150,8 @@ _serverRules:
|
|||||||
description: "ชุดของกฎที่จะแสดงก่อนการลงทะเบียนเราขอแนะนำให้ตั้งค่าสรุปข้อกำหนดในการให้บริการ"
|
description: "ชุดของกฎที่จะแสดงก่อนการลงทะเบียนเราขอแนะนำให้ตั้งค่าสรุปข้อกำหนดในการให้บริการ"
|
||||||
_serverSettings:
|
_serverSettings:
|
||||||
iconUrl: "ไอคอน URL"
|
iconUrl: "ไอคอน URL"
|
||||||
|
manifestJsonOverride: "manifest.json โอเวอร์ลาย"
|
||||||
|
shortName: "ชื่อย่อ"
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "ย้ายข้อมูลบัญชีอื่นไปยังอีกบัญชีนี้หนึ่ง"
|
moveFrom: "ย้ายข้อมูลบัญชีอื่นไปยังอีกบัญชีนี้หนึ่ง"
|
||||||
moveFromSub: "สร้างนามแฝงไปยังบัญชีอื่น"
|
moveFromSub: "สร้างนามแฝงไปยังบัญชีอื่น"
|
||||||
@@ -1386,6 +1408,7 @@ _achievements:
|
|||||||
flavor: "Misskey-Misskey La-Tu-Ma"
|
flavor: "Misskey-Misskey La-Tu-Ma"
|
||||||
_smashTestNotificationButton:
|
_smashTestNotificationButton:
|
||||||
title: "ทดสอบโอเวอร์โฟลว์"
|
title: "ทดสอบโอเวอร์โฟลว์"
|
||||||
|
description: "ทดสอบการแจ้งเตือนทริกเกอร์ซ้ำๆ ภายในระยะเวลาอันสั้นๆ"
|
||||||
_role:
|
_role:
|
||||||
new: "บทบาทใหม่"
|
new: "บทบาทใหม่"
|
||||||
edit: "แก้ไขบทบาท"
|
edit: "แก้ไขบทบาท"
|
||||||
@@ -1443,6 +1466,7 @@ _role:
|
|||||||
descriptionOfRateLimitFactor: "ขีดจํากัดอัตราที่ต่ำกว่ามีข้อจํากัดน้อยกว่าข้อจํากัดที่สูงกว่า"
|
descriptionOfRateLimitFactor: "ขีดจํากัดอัตราที่ต่ำกว่ามีข้อจํากัดน้อยกว่าข้อจํากัดที่สูงกว่า"
|
||||||
canHideAds: "ซ่อนโฆษณา"
|
canHideAds: "ซ่อนโฆษณา"
|
||||||
canSearchNotes: "การใช้การค้นหาโน้ต"
|
canSearchNotes: "การใช้การค้นหาโน้ต"
|
||||||
|
canUseTranslator: "การใช้งานแปล"
|
||||||
_condition:
|
_condition:
|
||||||
isLocal: "ผู้ใช้ภายใน"
|
isLocal: "ผู้ใช้ภายใน"
|
||||||
isRemote: "ผู้ใช้ระยะไกล"
|
isRemote: "ผู้ใช้ระยะไกล"
|
||||||
@@ -1509,6 +1533,7 @@ _plugin:
|
|||||||
install: "ติดตั้งปลั๊กอิน"
|
install: "ติดตั้งปลั๊กอิน"
|
||||||
installWarn: "กรุณาอย่าติดตั้งปลั๊กอินที่ไม่น่าเชื่อถือนะคะ"
|
installWarn: "กรุณาอย่าติดตั้งปลั๊กอินที่ไม่น่าเชื่อถือนะคะ"
|
||||||
manage: "จัดการปลั๊กอิน"
|
manage: "จัดการปลั๊กอิน"
|
||||||
|
viewSource: "ดูต้นฉบับ"
|
||||||
_preferencesBackups:
|
_preferencesBackups:
|
||||||
list: "สร้างการสำรองข้อมูล"
|
list: "สร้างการสำรองข้อมูล"
|
||||||
saveNew: "บันทึกใหม่"
|
saveNew: "บันทึกใหม่"
|
||||||
@@ -1575,11 +1600,6 @@ _wordMute:
|
|||||||
muteWords: "ปิดเสียงคำ"
|
muteWords: "ปิดเสียงคำ"
|
||||||
muteWordsDescription: "คั่นด้วยช่องว่างสำหรับเงื่อนไข AND หรือด้วยการขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR นะ"
|
muteWordsDescription: "คั่นด้วยช่องว่างสำหรับเงื่อนไข AND หรือด้วยการขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR นะ"
|
||||||
muteWordsDescription2: "ล้อมรอบคีย์เวิร์ดด้วยเครื่องหมายทับเพื่อใช้นิพจน์ทั่วไป"
|
muteWordsDescription2: "ล้อมรอบคีย์เวิร์ดด้วยเครื่องหมายทับเพื่อใช้นิพจน์ทั่วไป"
|
||||||
softDescription: "ซ่อนโน้ตให้ตรงตามเงื่อนไขที่ตั้งไว้จากไทม์ไลน์"
|
|
||||||
hardDescription: "ป้องกันไม่ให้โน้ตย่อที่ตรงตามเงื่อนไขที่ตั้งไว้ไม่ให้ถูกเพิ่มลงในไทม์ไลน์ นอกจากนี้ โน้ตเหล่านี้จะไม่ถูกเพิ่มลงในไทม์ไลน์แม้ว่าจะมีการเปลี่ยนแปลงเงื่อนไขยังไงก็ตาม"
|
|
||||||
soft: "ซอฟ"
|
|
||||||
hard: "ยาก"
|
|
||||||
mutedNotes: "ปิดเสียงโน้ต"
|
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription: "การดำเนินการนี้จะปิดเสียง\"โน้ต/รีโน้ต\"จากอินสแตนซ์ที่อยู่ในรายการ รวมถึงบันทึกของผู้ใช้ที่ตอบกลับผู้ใช้จากอินสแตนซ์ที่ปิดเสียง"
|
instanceMuteDescription: "การดำเนินการนี้จะปิดเสียง\"โน้ต/รีโน้ต\"จากอินสแตนซ์ที่อยู่ในรายการ รวมถึงบันทึกของผู้ใช้ที่ตอบกลับผู้ใช้จากอินสแตนซ์ที่ปิดเสียง"
|
||||||
instanceMuteDescription2: "คั่นด้วยการขึ้นบรรทัดใหม่"
|
instanceMuteDescription2: "คั่นด้วยการขึ้นบรรทัดใหม่"
|
||||||
@@ -1663,8 +1683,6 @@ _sfx:
|
|||||||
note: "หมายเหตุ"
|
note: "หมายเหตุ"
|
||||||
noteMy: "โน้ตของตัวเอง"
|
noteMy: "โน้ตของตัวเอง"
|
||||||
notification: "การเเจ้งเตือน"
|
notification: "การเเจ้งเตือน"
|
||||||
chat: "แชท"
|
|
||||||
chatBg: "แชท (พื้นหลัง)"
|
|
||||||
antenna: "เสาอากาศ"
|
antenna: "เสาอากาศ"
|
||||||
channel: "การแจ้งเตือนช่อง"
|
channel: "การแจ้งเตือนช่อง"
|
||||||
_ago:
|
_ago:
|
||||||
@@ -1769,6 +1787,7 @@ _antennaSources:
|
|||||||
homeTimeline: "โน้ตจากผู้ใช้ที่ติดตาม"
|
homeTimeline: "โน้ตจากผู้ใช้ที่ติดตาม"
|
||||||
users: "โน้ตจากผู้ใช้ที่เฉพาะเจาะจง"
|
users: "โน้ตจากผู้ใช้ที่เฉพาะเจาะจง"
|
||||||
userList: "โน้ตจากรายชื่อผู้ใช้ที่ระบุ"
|
userList: "โน้ตจากรายชื่อผู้ใช้ที่ระบุ"
|
||||||
|
userBlacklist: "โน้ตทั้งหมดยกเว้นโน้ตของผู้ใช้ที่ต้องระบุเจาะจงตั้งแต่หนึ่งรายขึ้นไป"
|
||||||
_weekday:
|
_weekday:
|
||||||
sunday: "วันอาทิตย์"
|
sunday: "วันอาทิตย์"
|
||||||
monday: "วันจันทร์"
|
monday: "วันจันทร์"
|
||||||
@@ -1868,6 +1887,7 @@ _profile:
|
|||||||
metadataContent: "เนื้อหา"
|
metadataContent: "เนื้อหา"
|
||||||
changeAvatar: "เปลี่ยนอวาตาร์"
|
changeAvatar: "เปลี่ยนอวาตาร์"
|
||||||
changeBanner: "เปลี่ยนแบนเนอร์"
|
changeBanner: "เปลี่ยนแบนเนอร์"
|
||||||
|
verifiedLinkDescription: "โดยการป้อน URL ที่มีลิงก์ไปยังโปรไฟล์ของคุณตรงนี้ ส่วนไอคอนการยืนยันความเป็นเจ้าของนั้นก็สามารถแสดงถัดจากฟิลด์ได้นะ"
|
||||||
_exportOrImport:
|
_exportOrImport:
|
||||||
allNotes: "โน้ตทั้งหมด"
|
allNotes: "โน้ตทั้งหมด"
|
||||||
favoritedNotes: "บันทึกที่ชื่นชอบ"
|
favoritedNotes: "บันทึกที่ชื่นชอบ"
|
||||||
@@ -1986,6 +2006,7 @@ _notification:
|
|||||||
youReceivedFollowRequest: "คุณมีคำขอติดตามใหม่น่ะ"
|
youReceivedFollowRequest: "คุณมีคำขอติดตามใหม่น่ะ"
|
||||||
yourFollowRequestAccepted: "คำขอติดตามของคุณได้รับการยอมรับแล้วน่ะ"
|
yourFollowRequestAccepted: "คำขอติดตามของคุณได้รับการยอมรับแล้วน่ะ"
|
||||||
pollEnded: "โพลสำรวจความคิดเห็นผลลัพธ์มีพร้อมใช้งาน"
|
pollEnded: "โพลสำรวจความคิดเห็นผลลัพธ์มีพร้อมใช้งาน"
|
||||||
|
newNote: "โพสต์ใหม่"
|
||||||
unreadAntennaNote: "เสาอากาศ {name}"
|
unreadAntennaNote: "เสาอากาศ {name}"
|
||||||
emptyPushNotificationMessage: "การแจ้งเตือนแบบพุชได้รับการอัพเดทแล้ว"
|
emptyPushNotificationMessage: "การแจ้งเตือนแบบพุชได้รับการอัพเดทแล้ว"
|
||||||
achievementEarned: "รับความสำเร็จ"
|
achievementEarned: "รับความสำเร็จ"
|
||||||
@@ -1995,6 +2016,7 @@ _notification:
|
|||||||
notificationWillBeDisplayedLikeThis: "การแจ้งเตือนมีลักษณะแบบนี้"
|
notificationWillBeDisplayedLikeThis: "การแจ้งเตือนมีลักษณะแบบนี้"
|
||||||
_types:
|
_types:
|
||||||
all: "ทั้งหมด"
|
all: "ทั้งหมด"
|
||||||
|
note: "โน้ตใหม่"
|
||||||
follow: "กำลังติดตาม"
|
follow: "กำลังติดตาม"
|
||||||
mention: "กล่าวถึง"
|
mention: "กล่าวถึง"
|
||||||
reply: "ตอบกลับ"
|
reply: "ตอบกลับ"
|
||||||
@@ -2064,3 +2086,34 @@ _webhookSettings:
|
|||||||
renote: "รีโน้ตแล้วเมื่อ"
|
renote: "รีโน้ตแล้วเมื่อ"
|
||||||
reaction: "เมื่อได้รับรีแอคชั่น"
|
reaction: "เมื่อได้รับรีแอคชั่น"
|
||||||
mention: "เมื่อกำลังถูกกล่าวถึง"
|
mention: "เมื่อกำลังถูกกล่าวถึง"
|
||||||
|
_moderationLogTypes:
|
||||||
|
createRole: "สร้างบทบาทแล้ว"
|
||||||
|
deleteRole: "ลบบทบาทแล้ว"
|
||||||
|
updateRole: "อัปเดตบทบาทแล้ว"
|
||||||
|
assignRole: "ได้รับมอบหมายบทบาท"
|
||||||
|
unassignRole: "ถอดออกจากบทบาทแล้ว"
|
||||||
|
suspend: "ถูกระงับ"
|
||||||
|
unsuspend: "เลิกถูกระงับ"
|
||||||
|
addCustomEmoji: "เพิ่มอีโมจิที่กำหนดเองแล้ว"
|
||||||
|
updateCustomEmoji: "อัปเดตอีโมจิที่กำหนดเองแล้ว"
|
||||||
|
deleteCustomEmoji: "ลบอีโมจิที่กำหนดเองออกแล้ว"
|
||||||
|
updateServerSettings: "อัปเดตการตั้งค่าเซิร์ฟเวอร์แล้ว"
|
||||||
|
updateUserNote: "อัปเดตโน้ตการกลั่นกรองแล้ว"
|
||||||
|
deleteDriveFile: "ลบไฟล์ออกแล้ว"
|
||||||
|
deleteNote: "ลบโน้ตออกแล้ว"
|
||||||
|
createGlobalAnnouncement: "สร้างประกาศทั่วโลกแล้ว"
|
||||||
|
createUserAnnouncement: "สร้างประกาศผู้ใช้แล้ว"
|
||||||
|
updateGlobalAnnouncement: "อัปเดตประกาศทั่วโลกแล้ว"
|
||||||
|
updateUserAnnouncement: "อัปเดตประกาศผู้ใช้แล้ว"
|
||||||
|
deleteGlobalAnnouncement: "ลบประกาศทั่วโลกออกแล้ว"
|
||||||
|
deleteUserAnnouncement: "ลบประกาศผู้ใช้ออกแล้ว"
|
||||||
|
resetPassword: "รีเซ็ตรหัสผ่าน"
|
||||||
|
suspendRemoteInstance: "อินสแตนซ์ระยะไกลถูกระงับ"
|
||||||
|
unsuspendRemoteInstance: "อินสแตนซ์ระยะไกลเลิกการระงับ"
|
||||||
|
markSensitiveDriveFile: "ทำเครื่องหมายไฟล์บอกว่าละเอียดอ่อน"
|
||||||
|
unmarkSensitiveDriveFile: "ยกเลิกทำเครื่องหมายไฟล์ว่าละเอียดอ่อน"
|
||||||
|
resolveAbuseReport: "รายงานได้รับการแก้ไขแล้ว"
|
||||||
|
createInvitation: "สร้างคำเชิญ"
|
||||||
|
createAd: "สร้างโฆษณาแล้ว"
|
||||||
|
deleteAd: "ลบโฆษณาออกแล้ว"
|
||||||
|
updateAd: "อัปเดตโฆษณาแล้ว"
|
||||||
|
@@ -386,7 +386,6 @@ _theme:
|
|||||||
_sfx:
|
_sfx:
|
||||||
note: "notlar"
|
note: "notlar"
|
||||||
notification: "Bildirim"
|
notification: "Bildirim"
|
||||||
chat: "Mesajlar"
|
|
||||||
_2fa:
|
_2fa:
|
||||||
renewTOTPCancel: "Hayır, teşekkürler"
|
renewTOTPCancel: "Hayır, teşekkürler"
|
||||||
_permissions:
|
_permissions:
|
||||||
@@ -448,3 +447,6 @@ _deck:
|
|||||||
tl: "Zaman çizelgesi"
|
tl: "Zaman çizelgesi"
|
||||||
list: "Listeler"
|
list: "Listeler"
|
||||||
mentions: "Bahsetmeler"
|
mentions: "Bahsetmeler"
|
||||||
|
_moderationLogTypes:
|
||||||
|
suspend: "askıya al"
|
||||||
|
resetPassword: "Şifre sıfırlama"
|
||||||
|
@@ -1180,6 +1180,7 @@ _plugin:
|
|||||||
install: "Встановити плагін"
|
install: "Встановити плагін"
|
||||||
installWarn: "Будь ласка, не встановлюйте плагінів, яким ви не довіряєте."
|
installWarn: "Будь ласка, не встановлюйте плагінів, яким ви не довіряєте."
|
||||||
manage: "Керування плагінами"
|
manage: "Керування плагінами"
|
||||||
|
viewSource: "Переглянути вихідний код"
|
||||||
_preferencesBackups:
|
_preferencesBackups:
|
||||||
list: "Створені бекапи"
|
list: "Створені бекапи"
|
||||||
saveNew: "Зберегти як новий"
|
saveNew: "Зберегти як новий"
|
||||||
@@ -1232,11 +1233,6 @@ _wordMute:
|
|||||||
muteWords: "Заглушені слова"
|
muteWords: "Заглушені слова"
|
||||||
muteWordsDescription: "Розділення ключових слів пробілами для \"І\" або з нової лінійки для \"АБО\""
|
muteWordsDescription: "Розділення ключових слів пробілами для \"І\" або з нової лінійки для \"АБО\""
|
||||||
muteWordsDescription2: "Для використання RegEx, ключові слова потрібно вписати поміж слешів \"/\"."
|
muteWordsDescription2: "Для використання RegEx, ключові слова потрібно вписати поміж слешів \"/\"."
|
||||||
softDescription: "Приховати записи які відповідають критеріям зі стрічки подій."
|
|
||||||
hardDescription: "Приховати записи які відповідають критеріям зі стрічки подій. Також приховані записи не будуть додані до стрічки подій навіть якщо критерії буде змінено."
|
|
||||||
soft: "М'яко"
|
|
||||||
hard: "Жорстко"
|
|
||||||
mutedNotes: "Заблоковані нотатки"
|
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription2: "Розділяйте новими рядками"
|
instanceMuteDescription2: "Розділяйте новими рядками"
|
||||||
title: "Приховує нотатки з перелічених інстансів."
|
title: "Приховує нотатки з перелічених інстансів."
|
||||||
@@ -1314,8 +1310,6 @@ _sfx:
|
|||||||
note: "Нотатки"
|
note: "Нотатки"
|
||||||
noteMy: "Мої нотатки"
|
noteMy: "Мої нотатки"
|
||||||
notification: "Сповіщення"
|
notification: "Сповіщення"
|
||||||
chat: "Чати"
|
|
||||||
chatBg: "Чати (фон)"
|
|
||||||
antenna: "Прийом антени"
|
antenna: "Прийом антени"
|
||||||
channel: "Повідомлення каналу"
|
channel: "Повідомлення каналу"
|
||||||
_ago:
|
_ago:
|
||||||
@@ -1619,3 +1613,6 @@ _deck:
|
|||||||
_webhookSettings:
|
_webhookSettings:
|
||||||
name: "Ім'я"
|
name: "Ім'я"
|
||||||
active: "Увімкнено"
|
active: "Увімкнено"
|
||||||
|
_moderationLogTypes:
|
||||||
|
suspend: "Призупинити"
|
||||||
|
resetPassword: "Скинути пароль"
|
||||||
|
@@ -910,7 +910,6 @@ _theme:
|
|||||||
_sfx:
|
_sfx:
|
||||||
note: "Qaydlar"
|
note: "Qaydlar"
|
||||||
notification: "Xabarnomalar"
|
notification: "Xabarnomalar"
|
||||||
chat: "Suhbat"
|
|
||||||
_ago:
|
_ago:
|
||||||
minutesAgo: "{n} daqiqa oldin"
|
minutesAgo: "{n} daqiqa oldin"
|
||||||
hoursAgo: "{n} soat oldin"
|
hoursAgo: "{n} soat oldin"
|
||||||
@@ -1084,3 +1083,6 @@ _webhookSettings:
|
|||||||
_events:
|
_events:
|
||||||
renote: "Qayta qayd qilinganda"
|
renote: "Qayta qayd qilinganda"
|
||||||
mention: "Eslanganda"
|
mention: "Eslanganda"
|
||||||
|
_moderationLogTypes:
|
||||||
|
suspend: "To'xtatish"
|
||||||
|
resetPassword: "Parolni tiklash"
|
||||||
|
@@ -1343,6 +1343,7 @@ _plugin:
|
|||||||
install: "Cài đặt tiện ích"
|
install: "Cài đặt tiện ích"
|
||||||
installWarn: "Vui lòng không cài đặt những tiện ích đáng ngờ."
|
installWarn: "Vui lòng không cài đặt những tiện ích đáng ngờ."
|
||||||
manage: "Quản lý plugin"
|
manage: "Quản lý plugin"
|
||||||
|
viewSource: "Xem mã nguồn"
|
||||||
_preferencesBackups:
|
_preferencesBackups:
|
||||||
list: "Tạo sao lưu"
|
list: "Tạo sao lưu"
|
||||||
saveNew: "Lưu bản sao lưu"
|
saveNew: "Lưu bản sao lưu"
|
||||||
@@ -1403,11 +1404,6 @@ _wordMute:
|
|||||||
muteWords: "Ẩn từ ngữ"
|
muteWords: "Ẩn từ ngữ"
|
||||||
muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition."
|
muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition."
|
||||||
muteWordsDescription2: "Bao quanh các từ khóa bằng dấu gạch chéo để sử dụng cụm từ thông dụng."
|
muteWordsDescription2: "Bao quanh các từ khóa bằng dấu gạch chéo để sử dụng cụm từ thông dụng."
|
||||||
softDescription: "Ẩn các tút phù hợp điều kiện đã đặt khỏi bảng tin."
|
|
||||||
hardDescription: "Ngăn các tút đáp ứng các điều kiện đã đặt xuất hiện trên bảng tin. Lưu ý, những tút này sẽ không được thêm vào bảng tin ngay cả khi các điều kiện được thay đổi."
|
|
||||||
soft: "Yếu"
|
|
||||||
hard: "Mạnh"
|
|
||||||
mutedNotes: "Những tút đã ẩn"
|
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription: "Thao tác này sẽ ẩn mọi tút/lượt đăng lại từ các máy chủ được liệt kê, bao gồm cả những tút dạng trả lời từ máy chủ bị ẩn."
|
instanceMuteDescription: "Thao tác này sẽ ẩn mọi tút/lượt đăng lại từ các máy chủ được liệt kê, bao gồm cả những tút dạng trả lời từ máy chủ bị ẩn."
|
||||||
instanceMuteDescription2: "Tách bằng cách xuống dòng"
|
instanceMuteDescription2: "Tách bằng cách xuống dòng"
|
||||||
@@ -1491,8 +1487,6 @@ _sfx:
|
|||||||
note: "Tút"
|
note: "Tút"
|
||||||
noteMy: "Tút của tôi"
|
noteMy: "Tút của tôi"
|
||||||
notification: "Thông báo"
|
notification: "Thông báo"
|
||||||
chat: "Trò chuyện"
|
|
||||||
chatBg: "Chat (Nền)"
|
|
||||||
antenna: "Trạm phát sóng"
|
antenna: "Trạm phát sóng"
|
||||||
channel: "Kênh"
|
channel: "Kênh"
|
||||||
_ago:
|
_ago:
|
||||||
@@ -1859,3 +1853,6 @@ _webhookSettings:
|
|||||||
_events:
|
_events:
|
||||||
reaction: "Khi nhận được sự kiện"
|
reaction: "Khi nhận được sự kiện"
|
||||||
mention: "Khi có người nhắc tới bạn"
|
mention: "Khi có người nhắc tới bạn"
|
||||||
|
_moderationLogTypes:
|
||||||
|
suspend: "Vô hiệu hóa"
|
||||||
|
resetPassword: "Đặt lại mật khẩu"
|
||||||
|
@@ -418,6 +418,7 @@ moderator: "监察员"
|
|||||||
moderation: "管理"
|
moderation: "管理"
|
||||||
moderationNote: "管理笔记"
|
moderationNote: "管理笔记"
|
||||||
addModerationNote: "添加管理笔记"
|
addModerationNote: "添加管理笔记"
|
||||||
|
moderationLogs: "管理日志"
|
||||||
nUsersMentioned: "{n} 被提到"
|
nUsersMentioned: "{n} 被提到"
|
||||||
securityKeyAndPasskey: "安全密钥或 Passkey"
|
securityKeyAndPasskey: "安全密钥或 Passkey"
|
||||||
securityKey: "安全密钥"
|
securityKey: "安全密钥"
|
||||||
@@ -710,6 +711,7 @@ lockedAccountInfo: "即使启用该功能,只要您不将帖子可见范围设
|
|||||||
alwaysMarkSensitive: "默认将媒体文件标记为敏感内容"
|
alwaysMarkSensitive: "默认将媒体文件标记为敏感内容"
|
||||||
loadRawImages: "添加附件图像的缩略图时使用原始图像质量"
|
loadRawImages: "添加附件图像的缩略图时使用原始图像质量"
|
||||||
disableShowingAnimatedImages: "不播放动画"
|
disableShowingAnimatedImages: "不播放动画"
|
||||||
|
highlightSensitiveMedia: "高亮显示敏感媒体"
|
||||||
verificationEmailSent: "已发送确认电子邮件。请访问电子邮件中的链接以完成设置。"
|
verificationEmailSent: "已发送确认电子邮件。请访问电子邮件中的链接以完成设置。"
|
||||||
notSet: "未设置"
|
notSet: "未设置"
|
||||||
emailVerified: "电子邮件地址已验证"
|
emailVerified: "电子邮件地址已验证"
|
||||||
@@ -1116,6 +1118,16 @@ keepScreenOn: "保持设备屏幕开启"
|
|||||||
verifiedLink: "已验证的链接"
|
verifiedLink: "已验证的链接"
|
||||||
notifyNotes: "打开发帖通知"
|
notifyNotes: "打开发帖通知"
|
||||||
unnotifyNotes: "关闭发帖通知"
|
unnotifyNotes: "关闭发帖通知"
|
||||||
|
authentication: "验证"
|
||||||
|
authenticationRequiredToContinue: "要继续,请先进行验证"
|
||||||
|
dateAndTime: "日期和时间"
|
||||||
|
showRenotes: "显示转帖"
|
||||||
|
edited: "已编辑"
|
||||||
|
notificationRecieveConfig: "通知接收设置"
|
||||||
|
mutualFollow: "互相关注"
|
||||||
|
fileAttachedOnly: "仅限媒体"
|
||||||
|
showRepliesToOthersInTimeline: "在时间线上显示给其他人的回复"
|
||||||
|
hideRepliesToOthersInTimeline: "在时间线上隐藏给其他人的回复"
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "仅限现有用户"
|
forExistingUsers: "仅限现有用户"
|
||||||
forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。"
|
forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。"
|
||||||
@@ -1149,6 +1161,8 @@ _serverSettings:
|
|||||||
appIconStyleRecommendation: "因为有可能会被裁切为圆形或者圆角矩形,建议使用边缘带有留白背景的图标。"
|
appIconStyleRecommendation: "因为有可能会被裁切为圆形或者圆角矩形,建议使用边缘带有留白背景的图标。"
|
||||||
appIconResolutionMustBe: "分辨率必须为 {resolution}。"
|
appIconResolutionMustBe: "分辨率必须为 {resolution}。"
|
||||||
manifestJsonOverride: "覆盖 mainfest.json"
|
manifestJsonOverride: "覆盖 mainfest.json"
|
||||||
|
shortName: "简称"
|
||||||
|
shortNameDescription: "如果服务器的正式名称很长,可以用简称或者別名来替代。"
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "从别的账号迁移到此账户"
|
moveFrom: "从别的账号迁移到此账户"
|
||||||
moveFromSub: "为另一个账户建立别名"
|
moveFromSub: "为另一个账户建立别名"
|
||||||
@@ -1529,6 +1543,7 @@ _plugin:
|
|||||||
install: "安装插件"
|
install: "安装插件"
|
||||||
installWarn: "请不要安装不可信的插件。"
|
installWarn: "请不要安装不可信的插件。"
|
||||||
manage: "管理插件..."
|
manage: "管理插件..."
|
||||||
|
viewSource: "查看源代码"
|
||||||
_preferencesBackups:
|
_preferencesBackups:
|
||||||
list: "已创建的备份"
|
list: "已创建的备份"
|
||||||
saveNew: "另存为"
|
saveNew: "另存为"
|
||||||
@@ -1595,11 +1610,6 @@ _wordMute:
|
|||||||
muteWords: "禁用词"
|
muteWords: "禁用词"
|
||||||
muteWordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。"
|
muteWordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。"
|
||||||
muteWordsDescription2: "正则表达式用斜线包裹"
|
muteWordsDescription2: "正则表达式用斜线包裹"
|
||||||
softDescription: "隐藏时间线中指定条件的帖子。"
|
|
||||||
hardDescription: "防止将具有指定条件的帖子添加到时间线。 即使您更改条件,未添加的帖文也会被排除在外。"
|
|
||||||
soft: "软屏蔽"
|
|
||||||
hard: "硬屏蔽"
|
|
||||||
mutedNotes: "被屏蔽的帖子"
|
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription: "屏蔽服务器中的所有帖子和转帖,包括这些服务器上的用户回复。"
|
instanceMuteDescription: "屏蔽服务器中的所有帖子和转帖,包括这些服务器上的用户回复。"
|
||||||
instanceMuteDescription2: "一行一个"
|
instanceMuteDescription2: "一行一个"
|
||||||
@@ -1683,8 +1693,6 @@ _sfx:
|
|||||||
note: "帖子"
|
note: "帖子"
|
||||||
noteMy: "我的帖子"
|
noteMy: "我的帖子"
|
||||||
notification: "通知"
|
notification: "通知"
|
||||||
chat: "聊天"
|
|
||||||
chatBg: "聊天背景"
|
|
||||||
antenna: "天线接收"
|
antenna: "天线接收"
|
||||||
channel: "频道通知"
|
channel: "频道通知"
|
||||||
_ago:
|
_ago:
|
||||||
@@ -1794,6 +1802,7 @@ _antennaSources:
|
|||||||
homeTimeline: "已关注用户的帖子"
|
homeTimeline: "已关注用户的帖子"
|
||||||
users: "来自指定用户的帖子"
|
users: "来自指定用户的帖子"
|
||||||
userList: "来自指定列表中的帖子"
|
userList: "来自指定列表中的帖子"
|
||||||
|
userBlacklist: "除掉已选择用户后所有的帖子"
|
||||||
_weekday:
|
_weekday:
|
||||||
sunday: "星期日"
|
sunday: "星期日"
|
||||||
monday: "星期一"
|
monday: "星期一"
|
||||||
@@ -2022,6 +2031,7 @@ _notification:
|
|||||||
notificationWillBeDisplayedLikeThis: "通知将会这样表示"
|
notificationWillBeDisplayedLikeThis: "通知将会这样表示"
|
||||||
_types:
|
_types:
|
||||||
all: "全部"
|
all: "全部"
|
||||||
|
note: "用户的新帖子"
|
||||||
follow: "关注中"
|
follow: "关注中"
|
||||||
mention: "提及"
|
mention: "提及"
|
||||||
reply: "回复"
|
reply: "回复"
|
||||||
@@ -2091,3 +2101,32 @@ _webhookSettings:
|
|||||||
renote: "被转发时"
|
renote: "被转发时"
|
||||||
reaction: "被回应时"
|
reaction: "被回应时"
|
||||||
mention: "被提及时"
|
mention: "被提及时"
|
||||||
|
_moderationLogTypes:
|
||||||
|
createRole: "创建角色"
|
||||||
|
deleteRole: "删除角色"
|
||||||
|
updateRole: "更新角色"
|
||||||
|
assignRole: "分配角色"
|
||||||
|
unassignRole: "取消分配角色"
|
||||||
|
suspend: "冻结"
|
||||||
|
unsuspend: "解除冻结"
|
||||||
|
addCustomEmoji: "添加自定义表情符号"
|
||||||
|
updateCustomEmoji: "更新自定义表情符号"
|
||||||
|
deleteCustomEmoji: "删除自定义表情符号"
|
||||||
|
updateServerSettings: "更新服务器设置"
|
||||||
|
updateUserNote: "更新管理笔记"
|
||||||
|
deleteDriveFile: "删除文件"
|
||||||
|
deleteNote: "删除帖子"
|
||||||
|
createGlobalAnnouncement: "创建全体通知"
|
||||||
|
createUserAnnouncement: "创建用户通知"
|
||||||
|
updateGlobalAnnouncement: "更新全体通知"
|
||||||
|
updateUserAnnouncement: "更新用户通知"
|
||||||
|
deleteGlobalAnnouncement: "删除全体通知"
|
||||||
|
deleteUserAnnouncement: "删除用户通知"
|
||||||
|
resetPassword: "重置密码"
|
||||||
|
markSensitiveDriveFile: "标记网盘文件为敏感媒体"
|
||||||
|
unmarkSensitiveDriveFile: "取消标记网盘文件为敏感媒体"
|
||||||
|
resolveAbuseReport: "处理举报"
|
||||||
|
createInvitation: "发行邀请码"
|
||||||
|
createAd: "创建了广告"
|
||||||
|
deleteAd: "删除了广告"
|
||||||
|
updateAd: "更新了广告"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
_lang_: "繁體中文"
|
_lang_: "繁體中文(台灣)"
|
||||||
headlineMisskey: "貼文連繫網絡"
|
headlineMisskey: "貼文連繫網路"
|
||||||
introMisskey: "歡迎!Misskey 是一個開放原始碼且去中心化的社群網路服務。\n發布「貼文」向身邊的人分享您的想法!📡\n利用「反應」表達您對貼文的感覺!👍\n讓我們一起探索新的世界吧!🚀"
|
introMisskey: "歡迎!Misskey 是一個開放原始碼且去中心化的社群網路服務。\n發布「貼文」向身邊的人分享您的想法!📡\n利用「反應」表達您對貼文的感覺!👍\n讓我們一起探索新的世界吧!🚀"
|
||||||
poweredByMisskeyDescription: "{name}是開放原始碼平臺 <b>Misskey</b> 的伺服器之一。"
|
poweredByMisskeyDescription: "{name}是開放原始碼平臺 <b>Misskey</b> 的伺服器之一。"
|
||||||
monthAndDay: "{month} 月 {day} 日"
|
monthAndDay: "{month} 月 {day} 日"
|
||||||
@@ -15,7 +15,7 @@ gotIt: "知道了"
|
|||||||
cancel: "取消"
|
cancel: "取消"
|
||||||
noThankYou: "現在不要"
|
noThankYou: "現在不要"
|
||||||
enterUsername: "輸入使用者名稱"
|
enterUsername: "輸入使用者名稱"
|
||||||
renotedBy: "{user} 轉發"
|
renotedBy: "{user} 轉發了"
|
||||||
noNotes: "無貼文"
|
noNotes: "無貼文"
|
||||||
noNotifications: "沒有通知"
|
noNotifications: "沒有通知"
|
||||||
instance: "伺服器"
|
instance: "伺服器"
|
||||||
@@ -45,7 +45,7 @@ pin: "置頂"
|
|||||||
unpin: "取消置頂"
|
unpin: "取消置頂"
|
||||||
copyContent: "複製內容"
|
copyContent: "複製內容"
|
||||||
copyLink: "複製連結"
|
copyLink: "複製連結"
|
||||||
copyLinkRenote: "複製轉貼連結"
|
copyLinkRenote: "複製轉發的連結"
|
||||||
delete: "刪除"
|
delete: "刪除"
|
||||||
deleteAndEdit: "刪除並編輯"
|
deleteAndEdit: "刪除並編輯"
|
||||||
deleteAndEditConfirm: "要刪除並再次編輯嗎?此貼文的所有反應、轉發和回覆也將會消失。"
|
deleteAndEditConfirm: "要刪除並再次編輯嗎?此貼文的所有反應、轉發和回覆也將會消失。"
|
||||||
@@ -56,7 +56,7 @@ copyRSS: "複製RSS"
|
|||||||
copyUsername: "複製使用者名稱"
|
copyUsername: "複製使用者名稱"
|
||||||
copyUserId: "複製使用者 ID"
|
copyUserId: "複製使用者 ID"
|
||||||
copyNoteId: "複製貼文 ID"
|
copyNoteId: "複製貼文 ID"
|
||||||
copyFileId: "複製檔案ID"
|
copyFileId: "複製檔案 ID"
|
||||||
copyFolderId: "複製資料夾ID"
|
copyFolderId: "複製資料夾ID"
|
||||||
copyProfileUrl: "複製個人資料網址"
|
copyProfileUrl: "複製個人資料網址"
|
||||||
searchUser: "搜尋使用者"
|
searchUser: "搜尋使用者"
|
||||||
@@ -75,9 +75,9 @@ import: "匯入"
|
|||||||
export: "匯出"
|
export: "匯出"
|
||||||
files: "檔案"
|
files: "檔案"
|
||||||
download: "下載"
|
download: "下載"
|
||||||
driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的貼文也會跟著消失。\n"
|
driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此檔案的貼文也會跟著被刪除。"
|
||||||
unfollowConfirm: "確定要取消追隨{name}嗎?"
|
unfollowConfirm: "確定要取消追隨{name}嗎?"
|
||||||
exportRequested: "已請求匯出。這可能會花一點時間。匯出的檔案將會被放到雲端裡。"
|
exportRequested: "已請求匯出。這可能會花一點時間。匯出的檔案將會被放到雲端硬碟裡。"
|
||||||
importRequested: "已請求匯入。這可能會花一點時間。"
|
importRequested: "已請求匯入。這可能會花一點時間。"
|
||||||
lists: "清單"
|
lists: "清單"
|
||||||
noLists: "你沒有任何清單"
|
noLists: "你沒有任何清單"
|
||||||
@@ -107,7 +107,7 @@ followRequestPending: "追隨許可待批准"
|
|||||||
enterEmoji: "輸入表情符號"
|
enterEmoji: "輸入表情符號"
|
||||||
renote: "轉發"
|
renote: "轉發"
|
||||||
unrenote: "取消轉發"
|
unrenote: "取消轉發"
|
||||||
renoted: "轉發成功"
|
renoted: "轉發成功。"
|
||||||
cantRenote: "無法轉發此貼文。"
|
cantRenote: "無法轉發此貼文。"
|
||||||
cantReRenote: "無法轉發之前已經轉發過的內容。"
|
cantReRenote: "無法轉發之前已經轉發過的內容。"
|
||||||
quote: "引用"
|
quote: "引用"
|
||||||
@@ -138,8 +138,8 @@ suspend: "凍結"
|
|||||||
unsuspend: "解除凍結"
|
unsuspend: "解除凍結"
|
||||||
blockConfirm: "確定要封鎖此使用者嗎?"
|
blockConfirm: "確定要封鎖此使用者嗎?"
|
||||||
unblockConfirm: "確定要解除封鎖此使用者嗎?"
|
unblockConfirm: "確定要解除封鎖此使用者嗎?"
|
||||||
suspendConfirm: "確定凍結此帳戶?"
|
suspendConfirm: "確定凍結此使用者?"
|
||||||
unsuspendConfirm: "確定解凍此帳戶?"
|
unsuspendConfirm: "確定解凍此使用者?"
|
||||||
selectList: "選擇清單"
|
selectList: "選擇清單"
|
||||||
editList: "編輯清單"
|
editList: "編輯清單"
|
||||||
selectChannel: "選擇頻道"
|
selectChannel: "選擇頻道"
|
||||||
@@ -152,20 +152,20 @@ customEmojis: "自訂表情符號"
|
|||||||
emoji: "表情符號"
|
emoji: "表情符號"
|
||||||
emojis: "表情符號"
|
emojis: "表情符號"
|
||||||
emojiName: "表情符號名稱"
|
emojiName: "表情符號名稱"
|
||||||
emojiUrl: "表情符號URL"
|
emojiUrl: "表情符號 URL"
|
||||||
addEmoji: "新增表情符號"
|
addEmoji: "新增表情符號"
|
||||||
settingGuide: "推薦設定"
|
settingGuide: "推薦設定"
|
||||||
cacheRemoteFiles: "快取遠端檔案"
|
cacheRemoteFiles: "快取遠端檔案"
|
||||||
cacheRemoteFilesDescription: "禁用此設定會停止建立遠端檔案快取,從而節省伺服器儲存空間,但會因從遠端讀取資料而增加網路數據用量。"
|
cacheRemoteFilesDescription: "啟用此設定後,遠端檔案會被快取在本伺服器的儲存空間中。雖然顯示圖片會變快,但會消耗較多伺服器的儲存空間。至於要快取遠端使用者到什麼程度,是依照角色的雲端硬碟容量而定。當超過這個限制時,從較舊的檔案開始自快取中刪除並改為連結。關閉這個設定時,遠端檔案從一開始就維持連結的方式,但建議將 default.yml 的 proxyRemoteFiles 設為 true,以便產生圖片的縮圖並保護使用者的隱私。"
|
||||||
youCanCleanRemoteFilesCache: "按檔案管理的🗑️按鈕,將快取全部刪除。"
|
youCanCleanRemoteFilesCache: "按檔案管理的🗑️按鈕,可將快取全部刪除。"
|
||||||
cacheRemoteSensitiveFiles: "快取遠端的敏感檔案"
|
cacheRemoteSensitiveFiles: "快取遠端的敏感檔案"
|
||||||
cacheRemoteSensitiveFilesDescription: "若停用這個設定,則不會快取遠端的敏感檔案,而是直接連結。"
|
cacheRemoteSensitiveFilesDescription: "若停用這個設定,則不會快取遠端的敏感檔案,而是直接連結。"
|
||||||
flagAsBot: "此使用者是機器人"
|
flagAsBot: "此使用者是機器人"
|
||||||
flagAsBotDescription: "標記本帳戶由程式控制,防止其他程式與本帳戶產生無限互動的行為。"
|
flagAsBotDescription: "如果本帳戶是由程式控制,請啟用此選項。啟用後,會作為標示幫助其他開發者防止機器人之間產生無限互動的行為,並會調整Misskey內部系統將本帳戶識別為機器人"
|
||||||
flagAsCat: "此帳戶是一隻貓,喵~~~!!!"
|
flagAsCat: "此帳戶是一隻貓,喵~~~!!!"
|
||||||
flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示"
|
flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示"
|
||||||
flagShowTimelineReplies: "在時間軸上顯示貼文的回覆"
|
flagShowTimelineReplies: "在時間軸上顯示貼文的回覆"
|
||||||
flagShowTimelineRepliesDescription: "啟用時,時間軸除了顯示使用者的貼文以外,還會顯示使用者對其他貼文的回覆。"
|
flagShowTimelineRepliesDescription: "啟用後,時間軸除了顯示使用者的貼文以外,還會顯示使用者對其他貼文的回覆。"
|
||||||
autoAcceptFollowed: "自動允許來自追隨中使用者的追隨請求"
|
autoAcceptFollowed: "自動允許來自追隨中使用者的追隨請求"
|
||||||
addAccount: "新增帳戶"
|
addAccount: "新增帳戶"
|
||||||
reloadAccountsList: "更新帳戶清單的資訊"
|
reloadAccountsList: "更新帳戶清單的資訊"
|
||||||
@@ -184,7 +184,7 @@ host: "主機"
|
|||||||
selectUser: "選取使用者"
|
selectUser: "選取使用者"
|
||||||
recipient: "收件人"
|
recipient: "收件人"
|
||||||
annotation: "註解"
|
annotation: "註解"
|
||||||
federation: "聯邦宇宙"
|
federation: "站台聯邦"
|
||||||
instances: "伺服器"
|
instances: "伺服器"
|
||||||
registeredAt: "初次觀測"
|
registeredAt: "初次觀測"
|
||||||
latestRequestReceivedAt: "上次收到的請求"
|
latestRequestReceivedAt: "上次收到的請求"
|
||||||
@@ -321,7 +321,7 @@ copyUrl: "複製URL"
|
|||||||
rename: "重新命名"
|
rename: "重新命名"
|
||||||
avatar: "大頭貼"
|
avatar: "大頭貼"
|
||||||
banner: "橫幅"
|
banner: "橫幅"
|
||||||
displayOfSensitiveMedia: "顯示敏感媒體"
|
displayOfSensitiveMedia: "敏感檔案的顯示"
|
||||||
whenServerDisconnected: "與伺服器的連接中斷時"
|
whenServerDisconnected: "與伺服器的連接中斷時"
|
||||||
disconnectedFromServer: "與伺服器中斷連線"
|
disconnectedFromServer: "與伺服器中斷連線"
|
||||||
reload: "重新整理"
|
reload: "重新整理"
|
||||||
@@ -418,12 +418,13 @@ moderator: "審查員"
|
|||||||
moderation: "審查"
|
moderation: "審查"
|
||||||
moderationNote: "管理筆記"
|
moderationNote: "管理筆記"
|
||||||
addModerationNote: "新增管理筆記"
|
addModerationNote: "新增管理筆記"
|
||||||
nUsersMentioned: "被提及到 {n} 次"
|
moderationLogs: "管理日誌"
|
||||||
|
nUsersMentioned: "被 {n} 個人提及"
|
||||||
securityKeyAndPasskey: "安全金鑰、Passkey"
|
securityKeyAndPasskey: "安全金鑰、Passkey"
|
||||||
securityKey: "安全金鑰"
|
securityKey: "安全金鑰"
|
||||||
lastUsed: "上次使用"
|
lastUsed: "上次使用"
|
||||||
lastUsedAt: "上次使用:{t}"
|
lastUsedAt: "上次使用:{t}"
|
||||||
unregister: "註銷帳戶"
|
unregister: "註銷"
|
||||||
passwordLessLogin: "設置無密碼登入"
|
passwordLessLogin: "設置無密碼登入"
|
||||||
passwordLessLoginDescription: "不使用密碼,以安全金鑰或 Passkey 登入"
|
passwordLessLoginDescription: "不使用密碼,以安全金鑰或 Passkey 登入"
|
||||||
resetPassword: "重設密碼"
|
resetPassword: "重設密碼"
|
||||||
@@ -490,7 +491,7 @@ createAccount: "建立帳戶"
|
|||||||
existingAccount: "現有帳戶"
|
existingAccount: "現有帳戶"
|
||||||
regenerate: "再次生成"
|
regenerate: "再次生成"
|
||||||
fontSize: "字體大小"
|
fontSize: "字體大小"
|
||||||
mediaListWithOneImageAppearance: "只有一張圖片時的媒體列表高度"
|
mediaListWithOneImageAppearance: "只有一張圖片時的檔案列表高度"
|
||||||
limitTo: "上限為 {x}"
|
limitTo: "上限為 {x}"
|
||||||
noFollowRequests: "沒有追隨您的請求"
|
noFollowRequests: "沒有追隨您的請求"
|
||||||
openImageInNewTab: "於新分頁中開啟圖片"
|
openImageInNewTab: "於新分頁中開啟圖片"
|
||||||
@@ -508,8 +509,8 @@ promote: "推廣"
|
|||||||
numberOfDays: "有效天數"
|
numberOfDays: "有效天數"
|
||||||
hideThisNote: "隱藏此貼文"
|
hideThisNote: "隱藏此貼文"
|
||||||
showFeaturedNotesInTimeline: "在時間軸上顯示熱門推薦"
|
showFeaturedNotesInTimeline: "在時間軸上顯示熱門推薦"
|
||||||
objectStorage: "對象存儲"
|
objectStorage: "物件儲存"
|
||||||
useObjectStorage: "使用對象存儲"
|
useObjectStorage: "使用物件儲存"
|
||||||
objectStorageBaseUrl: "Base URL"
|
objectStorageBaseUrl: "Base URL"
|
||||||
objectStorageBaseUrlDesc: "用於引用的 URL。如果您使用的是 CDN 或反向代理,請指定其 URL,例如 S3(https://<bucket>.s3.amazonaws.com)、GCS(https://storage.googleapis.com/<bucket>)。"
|
objectStorageBaseUrlDesc: "用於引用的 URL。如果您使用的是 CDN 或反向代理,請指定其 URL,例如 S3(https://<bucket>.s3.amazonaws.com)、GCS(https://storage.googleapis.com/<bucket>)。"
|
||||||
objectStorageBucket: "儲存空間(Bucket)"
|
objectStorageBucket: "儲存空間(Bucket)"
|
||||||
@@ -572,7 +573,7 @@ tokenRevokedDescription: "登入權杖失效,請重新登入。"
|
|||||||
accountDeleted: "帳戶已被刪除"
|
accountDeleted: "帳戶已被刪除"
|
||||||
accountDeletedDescription: "這個帳戶已被刪除。"
|
accountDeletedDescription: "這個帳戶已被刪除。"
|
||||||
menu: "選單"
|
menu: "選單"
|
||||||
divider: "分割線"
|
divider: "分隔線"
|
||||||
addItem: "新增項目"
|
addItem: "新增項目"
|
||||||
rearrange: "排序方式"
|
rearrange: "排序方式"
|
||||||
relays: "中繼"
|
relays: "中繼"
|
||||||
@@ -581,7 +582,7 @@ inboxUrl: "收件夾URL"
|
|||||||
addedRelays: "已加入的中繼"
|
addedRelays: "已加入的中繼"
|
||||||
serviceworkerInfo: "您需要啟用推送通知。"
|
serviceworkerInfo: "您需要啟用推送通知。"
|
||||||
deletedNote: "已刪除的貼文"
|
deletedNote: "已刪除的貼文"
|
||||||
invisibleNote: "隱藏的貼文"
|
invisibleNote: "私密的貼文"
|
||||||
enableInfiniteScroll: "啟用自動滾動頁面模式"
|
enableInfiniteScroll: "啟用自動滾動頁面模式"
|
||||||
visibility: "可見性"
|
visibility: "可見性"
|
||||||
poll: "投票"
|
poll: "投票"
|
||||||
@@ -707,9 +708,10 @@ driveUsage: "雲端硬碟使用量"
|
|||||||
noCrawle: "拒絕搜尋引擎索引"
|
noCrawle: "拒絕搜尋引擎索引"
|
||||||
noCrawleDescription: "要求網路搜尋引擎不要索引你的個人資料頁、貼文及頁面等。"
|
noCrawleDescription: "要求網路搜尋引擎不要索引你的個人資料頁、貼文及頁面等。"
|
||||||
lockedAccountInfo: "即使你通過了追隨者請求,除非你將貼文的可見性設定為 「追隨者」,否則任何人都能看見你的貼文。"
|
lockedAccountInfo: "即使你通過了追隨者請求,除非你將貼文的可見性設定為 「追隨者」,否則任何人都能看見你的貼文。"
|
||||||
alwaysMarkSensitive: "預設將多媒體標記為敏感內容"
|
alwaysMarkSensitive: "預設標記檔案為敏感內容"
|
||||||
loadRawImages: "以原始圖檔顯示附件圖檔的縮圖"
|
loadRawImages: "以原始圖檔顯示附件圖檔的縮圖"
|
||||||
disableShowingAnimatedImages: "不播放動態圖檔"
|
disableShowingAnimatedImages: "不播放動態圖檔"
|
||||||
|
highlightSensitiveMedia: "強調敏感標記"
|
||||||
verificationEmailSent: "已發送驗證電子郵件。請點擊進入電子郵件中的鏈接完成驗證。"
|
verificationEmailSent: "已發送驗證電子郵件。請點擊進入電子郵件中的鏈接完成驗證。"
|
||||||
notSet: "未設定"
|
notSet: "未設定"
|
||||||
emailVerified: "已成功驗證您的電郵"
|
emailVerified: "已成功驗證您的電郵"
|
||||||
@@ -926,7 +928,7 @@ type: "類型"
|
|||||||
speed: "速度"
|
speed: "速度"
|
||||||
slow: "慢"
|
slow: "慢"
|
||||||
fast: "快"
|
fast: "快"
|
||||||
sensitiveMediaDetection: "敏感性媒體的檢測"
|
sensitiveMediaDetection: "敏感檔案的檢測"
|
||||||
localOnly: "僅限本地"
|
localOnly: "僅限本地"
|
||||||
remoteOnly: "僅限遠端"
|
remoteOnly: "僅限遠端"
|
||||||
failedToUpload: "上傳失敗"
|
failedToUpload: "上傳失敗"
|
||||||
@@ -935,7 +937,7 @@ cannotUploadBecauseNoFreeSpace: "由於雲端硬碟沒有可用空間,因此
|
|||||||
cannotUploadBecauseExceedsFileSizeLimit: "由於超過了檔案大小的限制,無法上傳。"
|
cannotUploadBecauseExceedsFileSizeLimit: "由於超過了檔案大小的限制,無法上傳。"
|
||||||
beta: "測試版"
|
beta: "測試版"
|
||||||
enableAutoSensitive: "自動 NSFW 判定"
|
enableAutoSensitive: "自動 NSFW 判定"
|
||||||
enableAutoSensitiveDescription: "如果可用,它將使用機器學習技術判斷多媒體內容是否需要標記 NSFW。即使關閉此功能,也可能會依實例規則而自動啟用。"
|
enableAutoSensitiveDescription: "如果可用,它將使用機器學習技術判斷檔案是否需要標記為敏感。即使關閉此功能,也可能會依實例規則而自動啟用。"
|
||||||
activeEmailValidationDescription: "積極驗證使用者的電郵地址,以判斷它是否可以通訊。關閉此選項代表只會檢查地址是否符合格式。"
|
activeEmailValidationDescription: "積極驗證使用者的電郵地址,以判斷它是否可以通訊。關閉此選項代表只會檢查地址是否符合格式。"
|
||||||
navbar: "導覽列"
|
navbar: "導覽列"
|
||||||
shuffle: "隨機"
|
shuffle: "隨機"
|
||||||
@@ -1116,6 +1118,17 @@ keepScreenOn: "保持設備螢幕開啟"
|
|||||||
verifiedLink: "已驗證連結"
|
verifiedLink: "已驗證連結"
|
||||||
notifyNotes: "開啟貼文通知"
|
notifyNotes: "開啟貼文通知"
|
||||||
unnotifyNotes: "關閉貼文通知"
|
unnotifyNotes: "關閉貼文通知"
|
||||||
|
authentication: "驗證"
|
||||||
|
authenticationRequiredToContinue: "請於繼續前完成驗證"
|
||||||
|
dateAndTime: "日期與時間"
|
||||||
|
showRenotes: "顯示轉發貼文"
|
||||||
|
edited: "已編輯"
|
||||||
|
notificationRecieveConfig: "接受通知的設定"
|
||||||
|
mutualFollow: "互相追隨"
|
||||||
|
fileAttachedOnly: "包含附件"
|
||||||
|
showRepliesToOthersInTimeline: "在時間軸上顯示給其他人的回覆"
|
||||||
|
hideRepliesToOthersInTimeline: "在時間軸上隱藏給其他人的回覆"
|
||||||
|
externalServices: "外部服務"
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "僅限既有的使用者"
|
forExistingUsers: "僅限既有的使用者"
|
||||||
forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。"
|
forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。"
|
||||||
@@ -1149,6 +1162,8 @@ _serverSettings:
|
|||||||
appIconStyleRecommendation: "因為可能會裁剪成圓形或圓角,所以建議用單色填滿邊框及背景。"
|
appIconStyleRecommendation: "因為可能會裁剪成圓形或圓角,所以建議用單色填滿邊框及背景。"
|
||||||
appIconResolutionMustBe: "解析度必須為 {resolution}。"
|
appIconResolutionMustBe: "解析度必須為 {resolution}。"
|
||||||
manifestJsonOverride: "覆寫 manifest.json"
|
manifestJsonOverride: "覆寫 manifest.json"
|
||||||
|
shortName: "簡稱"
|
||||||
|
shortNameDescription: "如果伺服器的正式名稱很長,可用簡稱或通稱代替。"
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "從其他帳戶遷移到這個帳戶"
|
moveFrom: "從其他帳戶遷移到這個帳戶"
|
||||||
moveFromSub: "為另一個帳戶建立別名"
|
moveFromSub: "為另一個帳戶建立別名"
|
||||||
@@ -1463,6 +1478,7 @@ _role:
|
|||||||
descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。"
|
descriptionOfRateLimitFactor: "值越小限制越少,值越大限制越多。"
|
||||||
canHideAds: "不顯示廣告"
|
canHideAds: "不顯示廣告"
|
||||||
canSearchNotes: "可否搜尋貼文"
|
canSearchNotes: "可否搜尋貼文"
|
||||||
|
canUseTranslator: "使用翻譯功能"
|
||||||
_condition:
|
_condition:
|
||||||
isLocal: "本地使用者"
|
isLocal: "本地使用者"
|
||||||
isRemote: "遠端使用者"
|
isRemote: "遠端使用者"
|
||||||
@@ -1478,7 +1494,7 @@ _role:
|
|||||||
or: "~或~"
|
or: "~或~"
|
||||||
not: "~否"
|
not: "~否"
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審查。 伺服器的負荷會稍微增加。"
|
description: "您可以使用機器學習自動檢測敏感檔案以便審查。這會稍微增加伺服器負荷。"
|
||||||
sensitivity: "檢測敏感度"
|
sensitivity: "檢測敏感度"
|
||||||
sensitivityDescription: "敏感度低時,誤檢測(偽陽性)會減少。敏感度高時,漏檢(偽陰性)會減少。"
|
sensitivityDescription: "敏感度低時,誤檢測(偽陽性)會減少。敏感度高時,漏檢(偽陰性)會減少。"
|
||||||
setSensitiveFlagAutomatically: "設定 NSFW 標籤"
|
setSensitiveFlagAutomatically: "設定 NSFW 標籤"
|
||||||
@@ -1529,6 +1545,7 @@ _plugin:
|
|||||||
install: "安裝外掛組件"
|
install: "安裝外掛組件"
|
||||||
installWarn: "請不要安裝來源不明的外掛。"
|
installWarn: "請不要安裝來源不明的外掛。"
|
||||||
manage: "管理外掛"
|
manage: "管理外掛"
|
||||||
|
viewSource: "檢視原始碼"
|
||||||
_preferencesBackups:
|
_preferencesBackups:
|
||||||
list: "已備份的設定檔"
|
list: "已備份的設定檔"
|
||||||
saveNew: "另存新檔"
|
saveNew: "另存新檔"
|
||||||
@@ -1563,13 +1580,13 @@ _aboutMisskey:
|
|||||||
morePatrons: "還有許許多多幫助我們的其他人,非常感謝你們。 🥰"
|
morePatrons: "還有許許多多幫助我們的其他人,非常感謝你們。 🥰"
|
||||||
patrons: "贊助者"
|
patrons: "贊助者"
|
||||||
_displayOfSensitiveMedia:
|
_displayOfSensitiveMedia:
|
||||||
respect: "隱藏被標記為敏感的多媒體內容"
|
respect: "隱藏敏感檔案"
|
||||||
ignore: "不隱藏被標記為敏感的多媒體內容"
|
ignore: "顯示敏感檔案"
|
||||||
force: "隱藏所有多媒體內容"
|
force: "隱藏所有檔案"
|
||||||
_instanceTicker:
|
_instanceTicker:
|
||||||
none: "隱藏"
|
none: "隱藏"
|
||||||
remote: "向遠端使用者顯示"
|
remote: "只顯示遠端使用者"
|
||||||
always: "總是顯示"
|
always: "一律顯示"
|
||||||
_serverDisconnectedBehavior:
|
_serverDisconnectedBehavior:
|
||||||
reload: "自動重載"
|
reload: "自動重載"
|
||||||
dialog: "彈出式警告"
|
dialog: "彈出式警告"
|
||||||
@@ -1595,14 +1612,9 @@ _wordMute:
|
|||||||
muteWords: "加入靜音文字"
|
muteWords: "加入靜音文字"
|
||||||
muteWordsDescription: "空格代表「以及」(AND),換行代表「或者」(OR)。"
|
muteWordsDescription: "空格代表「以及」(AND),換行代表「或者」(OR)。"
|
||||||
muteWordsDescription2: "用斜線包圍關鍵字代表正規表達式。"
|
muteWordsDescription2: "用斜線包圍關鍵字代表正規表達式。"
|
||||||
softDescription: "隱藏時間軸中符合特定條件的貼文。"
|
|
||||||
hardDescription: "符合特定條件的貼文將不會新增至時間軸。 即使您更改條件,未被新增的貼文也會被排除在外。"
|
|
||||||
soft: "軟性靜音"
|
|
||||||
hard: "硬性靜音"
|
|
||||||
mutedNotes: "已靜音的貼文"
|
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription: "包括對被靜音實例上的使用者的回覆,被設定的實例上所有貼文及轉發都會被靜音。"
|
instanceMuteDescription: "包括對被靜音實例上的使用者的回覆,被設定的實例上所有貼文及轉發都會被靜音。"
|
||||||
instanceMuteDescription2: "換行以分隔"
|
instanceMuteDescription2: "設定時以換行進行分隔"
|
||||||
title: "將隱藏被設定的實例貼文。"
|
title: "將隱藏被設定的實例貼文。"
|
||||||
heading: "將實例靜音"
|
heading: "將實例靜音"
|
||||||
_theme:
|
_theme:
|
||||||
@@ -1655,7 +1667,7 @@ _theme:
|
|||||||
mentionMe: "提到了我"
|
mentionMe: "提到了我"
|
||||||
renote: "轉發貼文"
|
renote: "轉發貼文"
|
||||||
modalBg: "對話框背景"
|
modalBg: "對話框背景"
|
||||||
divider: "分割線"
|
divider: "分隔線"
|
||||||
scrollbarHandle: "捲動條"
|
scrollbarHandle: "捲動條"
|
||||||
scrollbarHandleHover: "捲動條(懸浮)"
|
scrollbarHandleHover: "捲動條(懸浮)"
|
||||||
dateLabelFg: "日期標籤文字"
|
dateLabelFg: "日期標籤文字"
|
||||||
@@ -1683,8 +1695,6 @@ _sfx:
|
|||||||
note: "貼文"
|
note: "貼文"
|
||||||
noteMy: "我的貼文"
|
noteMy: "我的貼文"
|
||||||
notification: "通知"
|
notification: "通知"
|
||||||
chat: "聊天"
|
|
||||||
chatBg: "聊天背景"
|
|
||||||
antenna: "天線接收"
|
antenna: "天線接收"
|
||||||
channel: "頻道通知"
|
channel: "頻道通知"
|
||||||
_ago:
|
_ago:
|
||||||
@@ -1794,6 +1804,7 @@ _antennaSources:
|
|||||||
homeTimeline: "來自已追隨使用者的貼文"
|
homeTimeline: "來自已追隨使用者的貼文"
|
||||||
users: "來自特定使用者的貼文"
|
users: "來自特定使用者的貼文"
|
||||||
userList: "來自特定清單中的貼文"
|
userList: "來自特定清單中的貼文"
|
||||||
|
userBlacklist: "除指定使用者外的所有貼文"
|
||||||
_weekday:
|
_weekday:
|
||||||
sunday: "週日"
|
sunday: "週日"
|
||||||
monday: "週一"
|
monday: "週一"
|
||||||
@@ -2022,6 +2033,7 @@ _notification:
|
|||||||
notificationWillBeDisplayedLikeThis: "通知會以這樣的方式顯示"
|
notificationWillBeDisplayedLikeThis: "通知會以這樣的方式顯示"
|
||||||
_types:
|
_types:
|
||||||
all: "全部 "
|
all: "全部 "
|
||||||
|
note: "使用者的最新貼文"
|
||||||
follow: "追隨中"
|
follow: "追隨中"
|
||||||
mention: "提及"
|
mention: "提及"
|
||||||
reply: "回覆"
|
reply: "回覆"
|
||||||
@@ -2091,3 +2103,34 @@ _webhookSettings:
|
|||||||
renote: "當被轉發時"
|
renote: "當被轉發時"
|
||||||
reaction: "當獲得反應時"
|
reaction: "當獲得反應時"
|
||||||
mention: "當被提到時"
|
mention: "當被提到時"
|
||||||
|
_moderationLogTypes:
|
||||||
|
createRole: "新增角色"
|
||||||
|
deleteRole: "刪除角色 "
|
||||||
|
updateRole: "更新角色設定"
|
||||||
|
assignRole: "指派角色"
|
||||||
|
unassignRole: "撤銷角色"
|
||||||
|
suspend: "凍結"
|
||||||
|
unsuspend: "解除凍結"
|
||||||
|
addCustomEmoji: "新增自訂表情符號"
|
||||||
|
updateCustomEmoji: "更新自訂表情符號"
|
||||||
|
deleteCustomEmoji: "刪除自訂表情符號"
|
||||||
|
updateServerSettings: "更新伺服器設定"
|
||||||
|
updateUserNote: "更新管理筆記"
|
||||||
|
deleteDriveFile: "刪除檔案"
|
||||||
|
deleteNote: "刪除貼文"
|
||||||
|
createGlobalAnnouncement: "建立全網通知"
|
||||||
|
createUserAnnouncement: "建立使用者通知"
|
||||||
|
updateGlobalAnnouncement: "更新全部的公告"
|
||||||
|
updateUserAnnouncement: "更新使用者的公告"
|
||||||
|
deleteGlobalAnnouncement: "刪除全部的公告"
|
||||||
|
deleteUserAnnouncement: "刪除使用者的公告"
|
||||||
|
resetPassword: "重設密碼"
|
||||||
|
suspendRemoteInstance: "封鎖遠端伺服器"
|
||||||
|
unsuspendRemoteInstance: "解除封鎖遠端伺服器"
|
||||||
|
markSensitiveDriveFile: "標記為敏感檔案"
|
||||||
|
unmarkSensitiveDriveFile: "撤銷標記為敏感檔案"
|
||||||
|
resolveAbuseReport: "解決檢舉"
|
||||||
|
createInvitation: "建立邀請碼"
|
||||||
|
createAd: "建立廣告"
|
||||||
|
deleteAd: "刪除廣告"
|
||||||
|
updateAd: "更新廣告"
|
||||||
|
14
package.json
14
package.json
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2023.9.0-rc.1",
|
"version": "2023.10.0-beta.7",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/misskey-dev/misskey.git"
|
"url": "https://github.com/misskey-dev/misskey.git"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@8.7.6",
|
"packageManager": "pnpm@8.8.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/frontend",
|
"packages/frontend",
|
||||||
"packages/backend",
|
"packages/backend",
|
||||||
@@ -46,15 +46,15 @@
|
|||||||
"execa": "8.0.1",
|
"execa": "8.0.1",
|
||||||
"cssnano": "6.0.1",
|
"cssnano": "6.0.1",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"postcss": "8.4.30",
|
"postcss": "8.4.31",
|
||||||
"terser": "5.20.0",
|
"terser": "5.21.0",
|
||||||
"typescript": "5.2.2"
|
"typescript": "5.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "6.7.2",
|
"@typescript-eslint/eslint-plugin": "6.7.4",
|
||||||
"@typescript-eslint/parser": "6.7.2",
|
"@typescript-eslint/parser": "6.7.4",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "13.2.0",
|
"cypress": "13.3.0",
|
||||||
"eslint": "8.50.0",
|
"eslint": "8.50.0",
|
||||||
"start-server-and-test": "2.0.1"
|
"start-server-and-test": "2.0.1"
|
||||||
},
|
},
|
||||||
|
@@ -216,4 +216,6 @@ module.exports = {
|
|||||||
maxWorkers: 1, // Make it use worker (that can be killed and restarted)
|
maxWorkers: 1, // Make it use worker (that can be killed and restarted)
|
||||||
logHeapUsage: true, // To debug when out-of-memory happens on CI
|
logHeapUsage: true, // To debug when out-of-memory happens on CI
|
||||||
workerIdleMemoryLimit: '1GiB', // Limit the worker to 1GB (GitHub Workflows dies at 2GB)
|
workerIdleMemoryLimit: '1GiB', // Limit the worker to 1GB (GitHub Workflows dies at 2GB)
|
||||||
|
|
||||||
|
maxConcurrency: 32,
|
||||||
};
|
};
|
||||||
|
@@ -0,0 +1,21 @@
|
|||||||
|
export class MutingNotificationTypes1695605508898 {
|
||||||
|
name = 'MutingNotificationTypes1695605508898'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" RENAME TO "user_profile_mutingnotificationtypes_enum_old"`);
|
||||||
|
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum" AS ENUM('note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app', 'test', 'pollVote', 'groupInvited')`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum"[]`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
||||||
|
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum_old"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app')`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
||||||
|
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`);
|
||||||
|
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`);
|
||||||
|
}
|
||||||
|
}
|
11
packages/backend/migration/1695901659683-note-updated-at.js
Normal file
11
packages/backend/migration/1695901659683-note-updated-at.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export class NoteUpdatedAt1695901659683 {
|
||||||
|
name = 'NoteUpdatedAt1695901659683'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "note" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "updatedAt"`);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class NotificationRecieveConfig1695944637565 {
|
||||||
|
name = 'NotificationRecieveConfig1695944637565'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "mutingNotificationTypes"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ADD "notificationRecieveConfig" jsonb NOT NULL DEFAULT '{}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "notificationRecieveConfig"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ADD "mutingNotificationTypes" "public"."user_profile_notificationrecieveconfig_enum" array NOT NULL DEFAULT '{}'`);
|
||||||
|
}
|
||||||
|
}
|
17
packages/backend/migration/1696003580220-AddSomeUrls.js
Normal file
17
packages/backend/migration/1696003580220-AddSomeUrls.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class AddSomeUrls1696003580220 {
|
||||||
|
name = 'AddSomeUrls1696003580220'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "impressumUrl" character varying(1024)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "privacyPolicyUrl" character varying(1024)`);
|
||||||
|
}
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "impressumUrl"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "privacyPolicyUrl"`);
|
||||||
|
}
|
||||||
|
}
|
20
packages/backend/migration/1696222183852-withReplies.js
Normal file
20
packages/backend/migration/1696222183852-withReplies.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class WithReplies1696222183852 {
|
||||||
|
name = 'WithReplies1696222183852'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "following" ADD "withReplies" boolean NOT NULL DEFAULT false`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_joining" ADD "withReplies" boolean NOT NULL DEFAULT false`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_d74d8ab5efa7e3bb82825c0fa2" ON "following" ("followeeId", "followerHost") `);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_d74d8ab5efa7e3bb82825c0fa2"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_joining" DROP COLUMN "withReplies"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "withReplies"`);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,11 @@
|
|||||||
|
export class UserListMembership1696323464251 {
|
||||||
|
name = 'UserListMembership1696323464251'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_joining" RENAME TO "user_list_membership"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_membership" RENAME TO "user_list_joining"`);
|
||||||
|
}
|
||||||
|
}
|
17
packages/backend/migration/1696331570827-hibernation.js
Normal file
17
packages/backend/migration/1696331570827-hibernation.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export class Hibernation1696331570827 {
|
||||||
|
name = 'Hibernation1696331570827'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_d74d8ab5efa7e3bb82825c0fa2"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "isHibernated" boolean NOT NULL DEFAULT false`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "following" ADD "isFollowerHibernated" boolean NOT NULL DEFAULT false`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_ce62b50d882d4e9dee10ad0d2f" ON "following" ("followeeId", "followerHost", "isFollowerHibernated") `);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_ce62b50d882d4e9dee10ad0d2f"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "isFollowerHibernated"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isHibernated"`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_d74d8ab5efa7e3bb82825c0fa2" ON "following" ("followeeId", "followerHost") `);
|
||||||
|
}
|
||||||
|
}
|
33
packages/backend/migration/1696332072038-clean.js
Normal file
33
packages/backend/migration/1696332072038-clean.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
export class Clean1696332072038 {
|
||||||
|
name = 'Clean1696332072038'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_d844bfc6f3f523a05189076efaa"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_605472305f26818cc93d1baaa74"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_d844bfc6f3f523a05189076efa"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_605472305f26818cc93d1baaa7"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_90f7da835e4c10aca6853621e1"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "preservedUsernames" SET DEFAULT '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }'`);
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "user_list_membership"."createdAt" IS 'The created date of the UserListMembership.'`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_021015e6683570ae9f6b0c62be" ON "user_list_membership" ("userId") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_cddcaf418dc4d392ecfcca842a" ON "user_list_membership" ("userListId") `);
|
||||||
|
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e4f3094c43f2d665e6030b0337" ON "user_list_membership" ("userId", "userListId") `);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_021015e6683570ae9f6b0c62bee" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_cddcaf418dc4d392ecfcca842a7" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_cddcaf418dc4d392ecfcca842a7"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_021015e6683570ae9f6b0c62bee"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_e4f3094c43f2d665e6030b0337"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_cddcaf418dc4d392ecfcca842a"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_021015e6683570ae9f6b0c62be"`);
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "user_list_membership"."createdAt" IS 'The created date of the UserListJoining.'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "preservedUsernames" SET DEFAULT '{admin,administrator,root,system,maintainer,host,mod,moderator,owner,superuser,staff,auth,i,me,everyone,all,mention,mentions,example,user,users,account,accounts,official,help,helps,support,supports,info,information,informations,announce,announces,announcement,announcements,notice,notification,notifications,dev,developer,developers,tech,misskey}'`);
|
||||||
|
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_90f7da835e4c10aca6853621e1" ON "user_list_membership" ("userId", "userListId") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_605472305f26818cc93d1baaa7" ON "user_list_membership" ("userListId") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_d844bfc6f3f523a05189076efa" ON "user_list_membership" ("userId") `);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_605472305f26818cc93d1baaa74" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_d844bfc6f3f523a05189076efaa" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class MetaCacheSettings1696373953614 {
|
||||||
|
name = 'MetaCacheSettings1696373953614'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "perLocalUserUserTimelineCacheMax" integer NOT NULL DEFAULT '300'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "perRemoteUserUserTimelineCacheMax" integer NOT NULL DEFAULT '100'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "perUserHomeTimelineCacheMax" integer NOT NULL DEFAULT '300'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "perUserListTimelineCacheMax" integer NOT NULL DEFAULT '300'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perUserListTimelineCacheMax"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perUserHomeTimelineCacheMax"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perRemoteUserUserTimelineCacheMax"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perLocalUserUserTimelineCacheMax"`);
|
||||||
|
}
|
||||||
|
}
|
16
packages/backend/migration/1696388600237-revert-note-edit.js
Normal file
16
packages/backend/migration/1696388600237-revert-note-edit.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class RevertNoteEdit1696388600237 {
|
||||||
|
name = 'RevertNoteEdit1696388600237'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "updatedAt"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "note" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`);
|
||||||
|
}
|
||||||
|
}
|
18
packages/backend/migration/1696405744672-clean-up.js
Normal file
18
packages/backend/migration/1696405744672-clean-up.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class CleanUp1696405744672 {
|
||||||
|
name = 'CleanUp1696405744672'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_e7c0567f5261063592f022e9b5"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_25dfc71b0369b003a4cd434d0b"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_25dfc71b0369b003a4cd434d0b" ON "note" ("attachedFileTypes") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_e7c0567f5261063592f022e9b5" ON "note" ("createdAt") `);
|
||||||
|
}
|
||||||
|
}
|
18
packages/backend/migration/1696569742153-clean-up.js
Normal file
18
packages/backend/migration/1696569742153-clean-up.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class CleanUp1696569742153 {
|
||||||
|
name = 'CleanUp1696569742153'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_01f4581f114e0ebd2bbb876f0b"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "score"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "note" ADD "score" integer NOT NULL DEFAULT '0'`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_01f4581f114e0ebd2bbb876f0b" ON "note_reaction" ("createdAt") `);
|
||||||
|
}
|
||||||
|
}
|
15
packages/backend/migration/1696581429196-clean-up.js
Normal file
15
packages/backend/migration/1696581429196-clean-up.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class CleanUp1696581429196 {
|
||||||
|
name = 'CleanUp1696581429196'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP TABLE IF EXISTS "muted_note"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
}
|
||||||
|
}
|
@@ -39,7 +39,7 @@
|
|||||||
"@swc/core-win32-x64-msvc": "1.3.56",
|
"@swc/core-win32-x64-msvc": "1.3.56",
|
||||||
"@tensorflow/tfjs": "4.4.0",
|
"@tensorflow/tfjs": "4.4.0",
|
||||||
"@tensorflow/tfjs-node": "4.4.0",
|
"@tensorflow/tfjs-node": "4.4.0",
|
||||||
"bufferutil": "^4.0.7",
|
"bufferutil": "4.0.7",
|
||||||
"slacc-android-arm-eabi": "0.0.10",
|
"slacc-android-arm-eabi": "0.0.10",
|
||||||
"slacc-android-arm64": "0.0.10",
|
"slacc-android-arm64": "0.0.10",
|
||||||
"slacc-darwin-arm64": "0.0.10",
|
"slacc-darwin-arm64": "0.0.10",
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
"slacc-linux-x64-musl": "0.0.10",
|
"slacc-linux-x64-musl": "0.0.10",
|
||||||
"slacc-win32-arm64-msvc": "0.0.10",
|
"slacc-win32-arm64-msvc": "0.0.10",
|
||||||
"slacc-win32-x64-msvc": "0.0.10",
|
"slacc-win32-x64-msvc": "0.0.10",
|
||||||
"utf-8-validate": "^6.0.3"
|
"utf-8-validate": "6.0.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.412.0",
|
"@aws-sdk/client-s3": "3.412.0",
|
||||||
@@ -68,17 +68,17 @@
|
|||||||
"@fastify/cors": "8.4.0",
|
"@fastify/cors": "8.4.0",
|
||||||
"@fastify/express": "2.3.0",
|
"@fastify/express": "2.3.0",
|
||||||
"@fastify/http-proxy": "9.2.1",
|
"@fastify/http-proxy": "9.2.1",
|
||||||
"@fastify/multipart": "7.7.3",
|
"@fastify/multipart": "8.0.0",
|
||||||
"@fastify/static": "6.11.2",
|
"@fastify/static": "6.11.2",
|
||||||
"@fastify/view": "8.2.0",
|
"@fastify/view": "8.2.0",
|
||||||
"@nestjs/common": "10.2.6",
|
"@nestjs/common": "10.2.7",
|
||||||
"@nestjs/core": "10.2.6",
|
"@nestjs/core": "10.2.7",
|
||||||
"@nestjs/testing": "10.2.6",
|
"@nestjs/testing": "10.2.7",
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@simplewebauthn/server": "8.1.1",
|
"@simplewebauthn/server": "8.2.0",
|
||||||
"@sinonjs/fake-timers": "11.1.0",
|
"@sinonjs/fake-timers": "11.1.0",
|
||||||
"@swc/cli": "0.1.62",
|
"@swc/cli": "0.1.62",
|
||||||
"@swc/core": "1.3.87",
|
"@swc/core": "1.3.92",
|
||||||
"accepts": "1.3.8",
|
"accepts": "1.3.8",
|
||||||
"ajv": "8.12.0",
|
"ajv": "8.12.0",
|
||||||
"archiver": "6.0.1",
|
"archiver": "6.0.1",
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"body-parser": "1.20.2",
|
"body-parser": "1.20.2",
|
||||||
"bullmq": "4.11.4",
|
"bullmq": "4.12.2",
|
||||||
"cacheable-lookup": "7.0.0",
|
"cacheable-lookup": "7.0.0",
|
||||||
"cbor": "9.0.1",
|
"cbor": "9.0.1",
|
||||||
"chalk": "5.3.0",
|
"chalk": "5.3.0",
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"jsonld": "8.3.1",
|
"jsonld": "8.3.1",
|
||||||
"jsrsasign": "10.8.6",
|
"jsrsasign": "10.8.6",
|
||||||
"meilisearch": "0.34.2",
|
"meilisearch": "0.35.0",
|
||||||
"mfm-js": "0.23.3",
|
"mfm-js": "0.23.3",
|
||||||
"microformats-parser": "1.5.2",
|
"microformats-parser": "1.5.2",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
"summaly": "github:misskey-dev/summaly",
|
"summaly": "github:misskey-dev/summaly",
|
||||||
"systeminformation": "5.21.8",
|
"systeminformation": "5.21.11",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tmp": "0.2.1",
|
"tmp": "0.2.1",
|
||||||
"tsc-alias": "1.8.8",
|
"tsc-alias": "1.8.8",
|
||||||
@@ -187,33 +187,33 @@
|
|||||||
"@types/jsdom": "21.1.3",
|
"@types/jsdom": "21.1.3",
|
||||||
"@types/jsonld": "1.5.10",
|
"@types/jsonld": "1.5.10",
|
||||||
"@types/jsrsasign": "10.5.9",
|
"@types/jsrsasign": "10.5.9",
|
||||||
"@types/mime-types": "2.1.1",
|
"@types/mime-types": "2.1.2",
|
||||||
"@types/ms": "0.7.31",
|
"@types/ms": "0.7.32",
|
||||||
"@types/node": "20.6.3",
|
"@types/node": "20.8.2",
|
||||||
"@types/node-fetch": "3.0.3",
|
"@types/node-fetch": "3.0.3",
|
||||||
"@types/nodemailer": "6.4.10",
|
"@types/nodemailer": "6.4.11",
|
||||||
"@types/oauth": "0.9.2",
|
"@types/oauth": "0.9.2",
|
||||||
"@types/oauth2orize": "1.11.1",
|
"@types/oauth2orize": "1.11.1",
|
||||||
"@types/oauth2orize-pkce": "0.1.0",
|
"@types/oauth2orize-pkce": "0.1.0",
|
||||||
"@types/pg": "8.10.2",
|
"@types/pg": "8.10.3",
|
||||||
"@types/pug": "2.0.6",
|
"@types/pug": "2.0.7",
|
||||||
"@types/punycode": "2.1.0",
|
"@types/punycode": "2.1.0",
|
||||||
"@types/qrcode": "1.5.2",
|
"@types/qrcode": "1.5.2",
|
||||||
"@types/random-seed": "0.3.3",
|
"@types/random-seed": "0.3.3",
|
||||||
"@types/ratelimiter": "3.4.4",
|
"@types/ratelimiter": "3.4.4",
|
||||||
"@types/rename": "1.0.4",
|
"@types/rename": "1.0.5",
|
||||||
"@types/sanitize-html": "2.9.0",
|
"@types/sanitize-html": "2.9.1",
|
||||||
"@types/semver": "7.5.2",
|
"@types/semver": "7.5.3",
|
||||||
"@types/sharp": "0.32.0",
|
"@types/sharp": "0.32.0",
|
||||||
"@types/simple-oauth2": "5.0.4",
|
"@types/simple-oauth2": "5.0.5",
|
||||||
"@types/sinonjs__fake-timers": "8.1.2",
|
"@types/sinonjs__fake-timers": "8.1.3",
|
||||||
"@types/tinycolor2": "1.4.4",
|
"@types/tinycolor2": "1.4.4",
|
||||||
"@types/tmp": "0.2.4",
|
"@types/tmp": "0.2.4",
|
||||||
"@types/vary": "1.1.0",
|
"@types/vary": "1.1.1",
|
||||||
"@types/web-push": "3.6.0",
|
"@types/web-push": "3.6.1",
|
||||||
"@types/ws": "8.5.5",
|
"@types/ws": "8.5.6",
|
||||||
"@typescript-eslint/eslint-plugin": "6.7.2",
|
"@typescript-eslint/eslint-plugin": "6.7.4",
|
||||||
"@typescript-eslint/parser": "6.7.2",
|
"@typescript-eslint/parser": "6.7.4",
|
||||||
"aws-sdk-client-mock": "3.0.0",
|
"aws-sdk-client-mock": "3.0.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "8.50.0",
|
"eslint": "8.50.0",
|
||||||
|
@@ -70,11 +70,19 @@ const $redisForSub: Provider = {
|
|||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const $redisForTimelines: Provider = {
|
||||||
|
provide: DI.redisForTimelines,
|
||||||
|
useFactory: (config: Config) => {
|
||||||
|
return new Redis.Redis(config.redisForTimelines);
|
||||||
|
},
|
||||||
|
inject: [DI.config],
|
||||||
|
};
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
imports: [RepositoryModule],
|
imports: [RepositoryModule],
|
||||||
providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub],
|
providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines],
|
||||||
exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, RepositoryModule],
|
exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, RepositoryModule],
|
||||||
})
|
})
|
||||||
export class GlobalModule implements OnApplicationShutdown {
|
export class GlobalModule implements OnApplicationShutdown {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -82,6 +90,7 @@ export class GlobalModule implements OnApplicationShutdown {
|
|||||||
@Inject(DI.redis) private redisClient: Redis.Redis,
|
@Inject(DI.redis) private redisClient: Redis.Redis,
|
||||||
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
|
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
|
||||||
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
|
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
|
||||||
|
@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async dispose(): Promise<void> {
|
public async dispose(): Promise<void> {
|
||||||
@@ -98,6 +107,7 @@ export class GlobalModule implements OnApplicationShutdown {
|
|||||||
this.redisClient.disconnect(),
|
this.redisClient.disconnect(),
|
||||||
this.redisForPub.disconnect(),
|
this.redisForPub.disconnect(),
|
||||||
this.redisForSub.disconnect(),
|
this.redisForSub.disconnect(),
|
||||||
|
this.redisForTimelines.disconnect(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -63,6 +63,7 @@ export async function masterMain() {
|
|||||||
showNodejsVersion();
|
showNodejsVersion();
|
||||||
config = loadConfigBoot();
|
config = loadConfigBoot();
|
||||||
//await connectDb();
|
//await connectDb();
|
||||||
|
if (config.pidFile) fs.writeFileSync(config.pidFile, process.pid.toString());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
bootLogger.error('Fatal error occurred during initialization', null, true);
|
bootLogger.error('Fatal error occurred during initialization', null, true);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
@@ -47,6 +47,7 @@ type Source = {
|
|||||||
redis: RedisOptionsSource;
|
redis: RedisOptionsSource;
|
||||||
redisForPubsub?: RedisOptionsSource;
|
redisForPubsub?: RedisOptionsSource;
|
||||||
redisForJobQueue?: RedisOptionsSource;
|
redisForJobQueue?: RedisOptionsSource;
|
||||||
|
redisForTimelines?: RedisOptionsSource;
|
||||||
meilisearch?: {
|
meilisearch?: {
|
||||||
host: string;
|
host: string;
|
||||||
port: string;
|
port: string;
|
||||||
@@ -89,6 +90,7 @@ type Source = {
|
|||||||
perChannelMaxNoteCacheCount?: number;
|
perChannelMaxNoteCacheCount?: number;
|
||||||
perUserNotificationsMaxCount?: number;
|
perUserNotificationsMaxCount?: number;
|
||||||
deactivateAntennaThreshold?: number;
|
deactivateAntennaThreshold?: number;
|
||||||
|
pidFile: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Config = {
|
export type Config = {
|
||||||
@@ -160,9 +162,11 @@ export type Config = {
|
|||||||
redis: RedisOptions & RedisOptionsSource;
|
redis: RedisOptions & RedisOptionsSource;
|
||||||
redisForPubsub: RedisOptions & RedisOptionsSource;
|
redisForPubsub: RedisOptions & RedisOptionsSource;
|
||||||
redisForJobQueue: RedisOptions & RedisOptionsSource;
|
redisForJobQueue: RedisOptions & RedisOptionsSource;
|
||||||
|
redisForTimelines: RedisOptions & RedisOptionsSource;
|
||||||
perChannelMaxNoteCacheCount: number;
|
perChannelMaxNoteCacheCount: number;
|
||||||
perUserNotificationsMaxCount: number;
|
perUserNotificationsMaxCount: number;
|
||||||
deactivateAntennaThreshold: number;
|
deactivateAntennaThreshold: number;
|
||||||
|
pidFile: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const _filename = fileURLToPath(import.meta.url);
|
const _filename = fileURLToPath(import.meta.url);
|
||||||
@@ -225,6 +229,7 @@ export function loadConfig(): Config {
|
|||||||
redis,
|
redis,
|
||||||
redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
|
redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
|
||||||
redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
|
redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
|
||||||
|
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
|
||||||
id: config.id,
|
id: config.id,
|
||||||
proxy: config.proxy,
|
proxy: config.proxy,
|
||||||
proxySmtp: config.proxySmtp,
|
proxySmtp: config.proxySmtp,
|
||||||
@@ -255,6 +260,7 @@ export function loadConfig(): Config {
|
|||||||
perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
|
perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
|
||||||
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 300,
|
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 300,
|
||||||
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
|
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
|
||||||
|
pidFile: config.pidFile,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -9,7 +9,7 @@ import { IsNull, In, MoreThan, Not } from 'typeorm';
|
|||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||||
import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/_.js';
|
import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js';
|
||||||
import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
|
import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
|
||||||
|
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
@@ -42,8 +42,8 @@ export class AccountMoveService {
|
|||||||
@Inject(DI.mutingsRepository)
|
@Inject(DI.mutingsRepository)
|
||||||
private mutingsRepository: MutingsRepository,
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
@Inject(DI.userListJoiningsRepository)
|
@Inject(DI.userListMembershipsRepository)
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||||
|
|
||||||
@Inject(DI.instancesRepository)
|
@Inject(DI.instancesRepository)
|
||||||
private instancesRepository: InstancesRepository,
|
private instancesRepository: InstancesRepository,
|
||||||
@@ -215,40 +215,40 @@ export class AccountMoveService {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public async updateLists(src: ThinUser, dst: MiUser): Promise<void> {
|
public async updateLists(src: ThinUser, dst: MiUser): Promise<void> {
|
||||||
// Return if there is no list to be updated.
|
// Return if there is no list to be updated.
|
||||||
const oldJoinings = await this.userListJoiningsRepository.find({
|
const oldMemberships = await this.userListMembershipsRepository.find({
|
||||||
where: {
|
where: {
|
||||||
userId: src.id,
|
userId: src.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (oldJoinings.length === 0) return;
|
if (oldMemberships.length === 0) return;
|
||||||
|
|
||||||
const existingUserListIds = await this.userListJoiningsRepository.find({
|
const existingUserListIds = await this.userListMembershipsRepository.find({
|
||||||
where: {
|
where: {
|
||||||
userId: dst.id,
|
userId: dst.id,
|
||||||
},
|
},
|
||||||
}).then(joinings => joinings.map(joining => joining.userListId));
|
}).then(memberships => memberships.map(membership => membership.userListId));
|
||||||
|
|
||||||
const newJoinings: Map<string, { createdAt: Date; userId: string; userListId: string; }> = new Map();
|
const newMemberships: Map<string, { createdAt: Date; userId: string; userListId: string; }> = new Map();
|
||||||
|
|
||||||
// 重複しないようにIDを生成
|
// 重複しないようにIDを生成
|
||||||
const genId = (): string => {
|
const genId = (): string => {
|
||||||
let id: string;
|
let id: string;
|
||||||
do {
|
do {
|
||||||
id = this.idService.genId();
|
id = this.idService.genId();
|
||||||
} while (newJoinings.has(id));
|
} while (newMemberships.has(id));
|
||||||
return id;
|
return id;
|
||||||
};
|
};
|
||||||
for (const joining of oldJoinings) {
|
for (const membership of oldMemberships) {
|
||||||
if (existingUserListIds.includes(joining.userListId)) continue; // skip if dst exists in this user's list
|
if (existingUserListIds.includes(membership.userListId)) continue; // skip if dst exists in this user's list
|
||||||
newJoinings.set(genId(), {
|
newMemberships.set(genId(), {
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
userId: dst.id,
|
userId: dst.id,
|
||||||
userListId: joining.userListId,
|
userListId: membership.userListId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const arrayToInsert = Array.from(newJoinings.entries()).map(entry => ({ ...entry[1], id: entry[0] }));
|
const arrayToInsert = Array.from(newMemberships.entries()).map(entry => ({ ...entry[1], id: entry[0] }));
|
||||||
await this.userListJoiningsRepository.insert(arrayToInsert);
|
await this.userListMembershipsRepository.insert(arrayToInsert);
|
||||||
|
|
||||||
// Have the proxy account follow the new account in the same way as UserListService.push
|
// Have the proxy account follow the new account in the same way as UserListService.push
|
||||||
if (this.userEntityService.isRemoteUser(dst)) {
|
if (this.userEntityService.isRemoteUser(dst)) {
|
||||||
|
@@ -7,11 +7,12 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import type { AnnouncementReadsRepository, AnnouncementsRepository, MiAnnouncement, MiAnnouncementRead } from '@/models/_.js';
|
import type { AnnouncementReadsRepository, AnnouncementsRepository, MiAnnouncement, MiAnnouncementRead, UsersRepository } from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { Packed } from '@/misc/json-schema.js';
|
import { Packed } from '@/misc/json-schema.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AnnouncementService {
|
export class AnnouncementService {
|
||||||
@@ -22,8 +23,12 @@ export class AnnouncementService {
|
|||||||
@Inject(DI.announcementReadsRepository)
|
@Inject(DI.announcementReadsRepository)
|
||||||
private announcementReadsRepository: AnnouncementReadsRepository,
|
private announcementReadsRepository: AnnouncementReadsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +63,7 @@ export class AnnouncementService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async create(values: Partial<MiAnnouncement>): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> {
|
public async create(values: Partial<MiAnnouncement>, moderator?: MiUser): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> {
|
||||||
const announcement = await this.announcementsRepository.insert({
|
const announcement = await this.announcementsRepository.insert({
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -79,10 +84,28 @@ export class AnnouncementService {
|
|||||||
this.globalEventService.publishMainStream(values.userId, 'announcementCreated', {
|
this.globalEventService.publishMainStream(values.userId, 'announcementCreated', {
|
||||||
announcement: packed,
|
announcement: packed,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (moderator) {
|
||||||
|
const user = await this.usersRepository.findOneByOrFail({ id: values.userId });
|
||||||
|
this.moderationLogService.log(moderator, 'createUserAnnouncement', {
|
||||||
|
announcementId: announcement.id,
|
||||||
|
announcement: announcement,
|
||||||
|
userId: values.userId,
|
||||||
|
userUsername: user.username,
|
||||||
|
userHost: user.host,
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.globalEventService.publishBroadcastStream('announcementCreated', {
|
this.globalEventService.publishBroadcastStream('announcementCreated', {
|
||||||
announcement: packed,
|
announcement: packed,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (moderator) {
|
||||||
|
this.moderationLogService.log(moderator, 'createGlobalAnnouncement', {
|
||||||
|
announcementId: announcement.id,
|
||||||
|
announcement: announcement,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -91,6 +114,67 @@ export class AnnouncementService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async update(announcement: MiAnnouncement, values: Partial<MiAnnouncement>, moderator?: MiUser): Promise<void> {
|
||||||
|
await this.announcementsRepository.update(announcement.id, {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
title: values.title,
|
||||||
|
text: values.text,
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */
|
||||||
|
imageUrl: values.imageUrl || null,
|
||||||
|
display: values.display,
|
||||||
|
icon: values.icon,
|
||||||
|
forExistingUsers: values.forExistingUsers,
|
||||||
|
needConfirmationToRead: values.needConfirmationToRead,
|
||||||
|
isActive: values.isActive,
|
||||||
|
});
|
||||||
|
|
||||||
|
const after = await this.announcementsRepository.findOneByOrFail({ id: announcement.id });
|
||||||
|
|
||||||
|
if (moderator) {
|
||||||
|
if (announcement.userId) {
|
||||||
|
const user = await this.usersRepository.findOneByOrFail({ id: announcement.userId });
|
||||||
|
this.moderationLogService.log(moderator, 'updateUserAnnouncement', {
|
||||||
|
announcementId: announcement.id,
|
||||||
|
before: announcement,
|
||||||
|
after: after,
|
||||||
|
userId: announcement.userId,
|
||||||
|
userUsername: user.username,
|
||||||
|
userHost: user.host,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.moderationLogService.log(moderator, 'updateGlobalAnnouncement', {
|
||||||
|
announcementId: announcement.id,
|
||||||
|
before: announcement,
|
||||||
|
after: after,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async delete(announcement: MiAnnouncement, moderator?: MiUser): Promise<void> {
|
||||||
|
await this.announcementsRepository.delete(announcement.id);
|
||||||
|
|
||||||
|
if (moderator) {
|
||||||
|
if (announcement.userId) {
|
||||||
|
const user = await this.usersRepository.findOneByOrFail({ id: announcement.userId });
|
||||||
|
this.moderationLogService.log(moderator, 'deleteUserAnnouncement', {
|
||||||
|
announcementId: announcement.id,
|
||||||
|
announcement: announcement,
|
||||||
|
userId: announcement.userId,
|
||||||
|
userUsername: user.username,
|
||||||
|
userHost: user.host,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.moderationLogService.log(moderator, 'deleteGlobalAnnouncement', {
|
||||||
|
announcementId: announcement.id,
|
||||||
|
announcement: announcement,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async read(user: MiUser, announcementId: MiAnnouncement['id']): Promise<void> {
|
public async read(user: MiUser, announcementId: MiAnnouncement['id']): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
@@ -12,10 +12,10 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|||||||
import * as Acct from '@/misc/acct.js';
|
import * as Acct from '@/misc/acct.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { AntennasRepository, UserListJoiningsRepository } from '@/models/_.js';
|
import type { AntennasRepository, UserListMembershipsRepository } from '@/models/_.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -24,8 +24,8 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
private antennas: MiAntenna[];
|
private antennas: MiAntenna[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redis)
|
@Inject(DI.redisForTimelines)
|
||||||
private redisClient: Redis.Redis,
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.redisForSub)
|
@Inject(DI.redisForSub)
|
||||||
private redisForSub: Redis.Redis,
|
private redisForSub: Redis.Redis,
|
||||||
@@ -33,8 +33,8 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
@Inject(DI.antennasRepository)
|
@Inject(DI.antennasRepository)
|
||||||
private antennasRepository: AntennasRepository,
|
private antennasRepository: AntennasRepository,
|
||||||
|
|
||||||
@Inject(DI.userListJoiningsRepository)
|
@Inject(DI.userListMembershipsRepository)
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
@@ -50,7 +50,7 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
const obj = JSON.parse(data);
|
const obj = JSON.parse(data);
|
||||||
|
|
||||||
if (obj.channel === 'internal') {
|
if (obj.channel === 'internal') {
|
||||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'antennaCreated':
|
case 'antennaCreated':
|
||||||
this.antennas.push({
|
this.antennas.push({
|
||||||
@@ -81,7 +81,7 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const)));
|
const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const)));
|
||||||
const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna);
|
const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna);
|
||||||
|
|
||||||
const redisPipeline = this.redisClient.pipeline();
|
const redisPipeline = this.redisForTimelines.pipeline();
|
||||||
|
|
||||||
for (const antenna of matchedAntennas) {
|
for (const antenna of matchedAntennas) {
|
||||||
redisPipeline.xadd(
|
redisPipeline.xadd(
|
||||||
@@ -108,7 +108,7 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
if (antenna.src === 'home') {
|
if (antenna.src === 'home') {
|
||||||
// TODO
|
// TODO
|
||||||
} else if (antenna.src === 'list') {
|
} else if (antenna.src === 'list') {
|
||||||
const listUsers = (await this.userListJoiningsRepository.findBy({
|
const listUsers = (await this.userListMembershipsRepository.findBy({
|
||||||
userListId: antenna.userListId!,
|
userListId: antenna.userListId!,
|
||||||
})).map(x => x.userId);
|
})).map(x => x.userId);
|
||||||
|
|
||||||
|
@@ -5,13 +5,13 @@
|
|||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing } from '@/models/_.js';
|
||||||
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
||||||
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -25,7 +25,7 @@ export class CacheService implements OnApplicationShutdown {
|
|||||||
public userBlockingCache: RedisKVCache<Set<string>>;
|
public userBlockingCache: RedisKVCache<Set<string>>;
|
||||||
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
|
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
|
||||||
public renoteMutingsCache: RedisKVCache<Set<string>>;
|
public renoteMutingsCache: RedisKVCache<Set<string>>;
|
||||||
public userFollowingsCache: RedisKVCache<Set<string>>;
|
public userFollowingsCache: RedisKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>;
|
||||||
public userFollowingChannelsCache: RedisKVCache<Set<string>>;
|
public userFollowingChannelsCache: RedisKVCache<Set<string>>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -136,12 +136,18 @@ export class CacheService implements OnApplicationShutdown {
|
|||||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.userFollowingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowings', {
|
this.userFollowingsCache = new RedisKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>(this.redisClient, 'userFollowings', {
|
||||||
lifetime: 1000 * 60 * 30, // 30m
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
memoryCacheLifetime: 1000 * 60, // 1m
|
memoryCacheLifetime: 1000 * 60, // 1m
|
||||||
fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))),
|
fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId', 'withReplies'] }).then(xs => {
|
||||||
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
const obj: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {};
|
||||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
for (const x of xs) {
|
||||||
|
obj[x.followeeId] = { withReplies: x.withReplies };
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}),
|
||||||
|
toRedisConverter: (value) => JSON.stringify(value),
|
||||||
|
fromRedisConverter: (value) => JSON.parse(value),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.userFollowingChannelsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowingChannels', {
|
this.userFollowingChannelsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowingChannels', {
|
||||||
@@ -160,7 +166,7 @@ export class CacheService implements OnApplicationShutdown {
|
|||||||
const obj = JSON.parse(data);
|
const obj = JSON.parse(data);
|
||||||
|
|
||||||
if (obj.channel === 'internal') {
|
if (obj.channel === 'internal') {
|
||||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'userChangeSuspendedState':
|
case 'userChangeSuspendedState':
|
||||||
case 'remoteUserUpdated': {
|
case 'remoteUserUpdated': {
|
||||||
@@ -188,6 +194,7 @@ export class CacheService implements OnApplicationShutdown {
|
|||||||
if (follower) follower.followingCount++;
|
if (follower) follower.followingCount++;
|
||||||
const followee = this.userByIdCache.get(body.followeeId);
|
const followee = this.userByIdCache.get(body.followeeId);
|
||||||
if (followee) followee.followersCount++;
|
if (followee) followee.followersCount++;
|
||||||
|
this.userFollowingsCache.delete(body.followerId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@@ -46,6 +46,7 @@ import { SignupService } from './SignupService.js';
|
|||||||
import { WebAuthnService } from './WebAuthnService.js';
|
import { WebAuthnService } from './WebAuthnService.js';
|
||||||
import { UserBlockingService } from './UserBlockingService.js';
|
import { UserBlockingService } from './UserBlockingService.js';
|
||||||
import { CacheService } from './CacheService.js';
|
import { CacheService } from './CacheService.js';
|
||||||
|
import { UserService } from './UserService.js';
|
||||||
import { UserFollowingService } from './UserFollowingService.js';
|
import { UserFollowingService } from './UserFollowingService.js';
|
||||||
import { UserKeypairService } from './UserKeypairService.js';
|
import { UserKeypairService } from './UserKeypairService.js';
|
||||||
import { UserListService } from './UserListService.js';
|
import { UserListService } from './UserListService.js';
|
||||||
@@ -59,6 +60,7 @@ import { UtilityService } from './UtilityService.js';
|
|||||||
import { FileInfoService } from './FileInfoService.js';
|
import { FileInfoService } from './FileInfoService.js';
|
||||||
import { SearchService } from './SearchService.js';
|
import { SearchService } from './SearchService.js';
|
||||||
import { ClipService } from './ClipService.js';
|
import { ClipService } from './ClipService.js';
|
||||||
|
import { FeaturedService } from './FeaturedService.js';
|
||||||
import { ChartLoggerService } from './chart/ChartLoggerService.js';
|
import { ChartLoggerService } from './chart/ChartLoggerService.js';
|
||||||
import FederationChart from './chart/charts/federation.js';
|
import FederationChart from './chart/charts/federation.js';
|
||||||
import NotesChart from './chart/charts/notes.js';
|
import NotesChart from './chart/charts/notes.js';
|
||||||
@@ -173,6 +175,7 @@ const $SignupService: Provider = { provide: 'SignupService', useExisting: Signup
|
|||||||
const $WebAuthnService: Provider = { provide: 'WebAuthnService', useExisting: WebAuthnService };
|
const $WebAuthnService: Provider = { provide: 'WebAuthnService', useExisting: WebAuthnService };
|
||||||
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
|
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
|
||||||
const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService };
|
const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService };
|
||||||
|
const $UserService: Provider = { provide: 'UserService', useExisting: UserService };
|
||||||
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
|
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
|
||||||
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
|
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
|
||||||
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
||||||
@@ -185,6 +188,7 @@ const $UtilityService: Provider = { provide: 'UtilityService', useExisting: Util
|
|||||||
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
|
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
|
||||||
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
|
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
|
||||||
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
|
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
|
||||||
|
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
|
||||||
|
|
||||||
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
|
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
|
||||||
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
|
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
|
||||||
@@ -303,6 +307,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
WebAuthnService,
|
WebAuthnService,
|
||||||
UserBlockingService,
|
UserBlockingService,
|
||||||
CacheService,
|
CacheService,
|
||||||
|
UserService,
|
||||||
UserFollowingService,
|
UserFollowingService,
|
||||||
UserKeypairService,
|
UserKeypairService,
|
||||||
UserListService,
|
UserListService,
|
||||||
@@ -315,6 +320,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
FileInfoService,
|
FileInfoService,
|
||||||
SearchService,
|
SearchService,
|
||||||
ClipService,
|
ClipService,
|
||||||
|
FeaturedService,
|
||||||
ChartLoggerService,
|
ChartLoggerService,
|
||||||
FederationChart,
|
FederationChart,
|
||||||
NotesChart,
|
NotesChart,
|
||||||
@@ -426,6 +432,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
$WebAuthnService,
|
$WebAuthnService,
|
||||||
$UserBlockingService,
|
$UserBlockingService,
|
||||||
$CacheService,
|
$CacheService,
|
||||||
|
$UserService,
|
||||||
$UserFollowingService,
|
$UserFollowingService,
|
||||||
$UserKeypairService,
|
$UserKeypairService,
|
||||||
$UserListService,
|
$UserListService,
|
||||||
@@ -438,6 +445,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
$FileInfoService,
|
$FileInfoService,
|
||||||
$SearchService,
|
$SearchService,
|
||||||
$ClipService,
|
$ClipService,
|
||||||
|
$FeaturedService,
|
||||||
$ChartLoggerService,
|
$ChartLoggerService,
|
||||||
$FederationChart,
|
$FederationChart,
|
||||||
$NotesChart,
|
$NotesChart,
|
||||||
@@ -550,6 +558,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
WebAuthnService,
|
WebAuthnService,
|
||||||
UserBlockingService,
|
UserBlockingService,
|
||||||
CacheService,
|
CacheService,
|
||||||
|
UserService,
|
||||||
UserFollowingService,
|
UserFollowingService,
|
||||||
UserKeypairService,
|
UserKeypairService,
|
||||||
UserListService,
|
UserListService,
|
||||||
@@ -562,6 +571,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
FileInfoService,
|
FileInfoService,
|
||||||
SearchService,
|
SearchService,
|
||||||
ClipService,
|
ClipService,
|
||||||
|
FeaturedService,
|
||||||
FederationChart,
|
FederationChart,
|
||||||
NotesChart,
|
NotesChart,
|
||||||
UsersChart,
|
UsersChart,
|
||||||
@@ -672,6 +682,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
$WebAuthnService,
|
$WebAuthnService,
|
||||||
$UserBlockingService,
|
$UserBlockingService,
|
||||||
$CacheService,
|
$CacheService,
|
||||||
|
$UserService,
|
||||||
$UserFollowingService,
|
$UserFollowingService,
|
||||||
$UserKeypairService,
|
$UserKeypairService,
|
||||||
$UserListService,
|
$UserListService,
|
||||||
@@ -684,6 +695,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
$FileInfoService,
|
$FileInfoService,
|
||||||
$SearchService,
|
$SearchService,
|
||||||
$ClipService,
|
$ClipService,
|
||||||
|
$FeaturedService,
|
||||||
$FederationChart,
|
$FederationChart,
|
||||||
$NotesChart,
|
$NotesChart,
|
||||||
$UsersChart,
|
$UsersChart,
|
||||||
|
@@ -12,12 +12,13 @@ import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
|||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import type { MiEmoji } from '@/models/Emoji.js';
|
import type { MiEmoji } from '@/models/Emoji.js';
|
||||||
import type { EmojisRepository, MiRole } from '@/models/_.js';
|
import type { EmojisRepository, MiRole, MiUser } from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
|
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { query } from '@/misc/prelude/url.js';
|
import { query } from '@/misc/prelude/url.js';
|
||||||
import type { Serialized } from '@/server/api/stream/types.js';
|
import type { Serialized } from '@/types.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
|
||||||
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
|
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private emojiEntityService: EmojiEntityService,
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
this.cache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12);
|
this.cache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12);
|
||||||
@@ -46,7 +48,6 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||||||
fetcher: () => this.emojisRepository.find({ where: { host: IsNull() } }).then(emojis => new Map(emojis.map(emoji => [emoji.name, emoji]))),
|
fetcher: () => this.emojisRepository.find({ where: { host: IsNull() } }).then(emojis => new Map(emojis.map(emoji => [emoji.name, emoji]))),
|
||||||
toRedisConverter: (value) => JSON.stringify(Array.from(value.values())),
|
toRedisConverter: (value) => JSON.stringify(Array.from(value.values())),
|
||||||
fromRedisConverter: (value) => {
|
fromRedisConverter: (value) => {
|
||||||
if (!Array.isArray(JSON.parse(value))) return undefined; // 古いバージョンの壊れたキャッシュが残っていることがある(そのうち消す)
|
|
||||||
return new Map(JSON.parse(value).map((x: Serialized<MiEmoji>) => [x.name, {
|
return new Map(JSON.parse(value).map((x: Serialized<MiEmoji>) => [x.name, {
|
||||||
...x,
|
...x,
|
||||||
updatedAt: x.updatedAt ? new Date(x.updatedAt) : null,
|
updatedAt: x.updatedAt ? new Date(x.updatedAt) : null,
|
||||||
@@ -66,7 +67,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||||||
isSensitive: boolean;
|
isSensitive: boolean;
|
||||||
localOnly: boolean;
|
localOnly: boolean;
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][];
|
roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][];
|
||||||
}): Promise<MiEmoji> {
|
}, moderator?: MiUser): Promise<MiEmoji> {
|
||||||
const emoji = await this.emojisRepository.insert({
|
const emoji = await this.emojisRepository.insert({
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
@@ -89,6 +90,13 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||||||
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
||||||
emoji: await this.emojiEntityService.packDetailed(emoji.id),
|
emoji: await this.emojiEntityService.packDetailed(emoji.id),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (moderator) {
|
||||||
|
this.moderationLogService.log(moderator, 'addCustomEmoji', {
|
||||||
|
emojiId: emoji.id,
|
||||||
|
emoji: emoji,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return emoji;
|
return emoji;
|
||||||
@@ -104,7 +112,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||||||
isSensitive?: boolean;
|
isSensitive?: boolean;
|
||||||
localOnly?: boolean;
|
localOnly?: boolean;
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][];
|
roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][];
|
||||||
}): Promise<void> {
|
}, moderator?: MiUser): Promise<void> {
|
||||||
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
|
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
|
||||||
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
|
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
|
||||||
if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
|
if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
|
||||||
@@ -125,11 +133,11 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
this.localEmojisCache.refresh();
|
this.localEmojisCache.refresh();
|
||||||
|
|
||||||
const updated = await this.emojiEntityService.packDetailed(emoji.id);
|
const packed = await this.emojiEntityService.packDetailed(emoji.id);
|
||||||
|
|
||||||
if (emoji.name === data.name) {
|
if (emoji.name === data.name) {
|
||||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||||
emojis: [updated],
|
emojis: [packed],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
||||||
@@ -137,7 +145,16 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
||||||
emoji: updated,
|
emoji: packed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moderator) {
|
||||||
|
const updated = await this.emojisRepository.findOneByOrFail({ id: id });
|
||||||
|
this.moderationLogService.log(moderator, 'updateCustomEmoji', {
|
||||||
|
emojiId: emoji.id,
|
||||||
|
before: emoji,
|
||||||
|
after: updated,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,7 +248,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async delete(id: MiEmoji['id']) {
|
public async delete(id: MiEmoji['id'], moderator?: MiUser) {
|
||||||
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
|
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
|
||||||
|
|
||||||
await this.emojisRepository.delete(emoji.id);
|
await this.emojisRepository.delete(emoji.id);
|
||||||
@@ -241,16 +258,30 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||||||
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
||||||
emojis: [await this.emojiEntityService.packDetailed(emoji)],
|
emojis: [await this.emojiEntityService.packDetailed(emoji)],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (moderator) {
|
||||||
|
this.moderationLogService.log(moderator, 'deleteCustomEmoji', {
|
||||||
|
emojiId: emoji.id,
|
||||||
|
emoji: emoji,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async deleteBulk(ids: MiEmoji['id'][]) {
|
public async deleteBulk(ids: MiEmoji['id'][], moderator?: MiUser) {
|
||||||
const emojis = await this.emojisRepository.findBy({
|
const emojis = await this.emojisRepository.findBy({
|
||||||
id: In(ids),
|
id: In(ids),
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const emoji of emojis) {
|
for (const emoji of emojis) {
|
||||||
await this.emojisRepository.delete(emoji.id);
|
await this.emojisRepository.delete(emoji.id);
|
||||||
|
|
||||||
|
if (moderator) {
|
||||||
|
this.moderationLogService.log(moderator, 'deleteCustomEmoji', {
|
||||||
|
emojiId: emoji.id,
|
||||||
|
emoji: emoji,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.localEmojisCache.refresh();
|
this.localEmojisCache.refresh();
|
||||||
@@ -348,6 +379,20 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ローカル内の絵文字に重複がないかチェックします
|
||||||
|
* @param name 絵文字名
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public checkDuplicate(name: string): Promise<boolean> {
|
||||||
|
return this.emojisRepository.exist({ where: { name, host: IsNull() } });
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public getEmojiById(id: string): Promise<MiEmoji | null> {
|
||||||
|
return this.emojisRepository.findOneBy({ id });
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
this.cache.dispose();
|
this.cache.dispose();
|
||||||
|
@@ -42,6 +42,7 @@ import { bindThis } from '@/decorators.js';
|
|||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { correctFilename } from '@/misc/correct-filename.js';
|
import { correctFilename } from '@/misc/correct-filename.js';
|
||||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
|
||||||
type AddFileArgs = {
|
type AddFileArgs = {
|
||||||
/** User who wish to add file */
|
/** User who wish to add file */
|
||||||
@@ -86,6 +87,9 @@ type UploadFromUrlArgs = {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DriveService {
|
export class DriveService {
|
||||||
|
public static NoSuchFolderError = class extends Error {};
|
||||||
|
public static InvalidFileNameError = class extends Error {};
|
||||||
|
public static CannotUnmarkSensitiveError = class extends Error {};
|
||||||
private registerLogger: Logger;
|
private registerLogger: Logger;
|
||||||
private downloaderLogger: Logger;
|
private downloaderLogger: Logger;
|
||||||
private deleteLogger: Logger;
|
private deleteLogger: Logger;
|
||||||
@@ -119,6 +123,7 @@ export class DriveService {
|
|||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
private driveChart: DriveChart,
|
private driveChart: DriveChart,
|
||||||
private perUserDriveChart: PerUserDriveChart,
|
private perUserDriveChart: PerUserDriveChart,
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
@@ -648,7 +653,63 @@ export class DriveService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async deleteFile(file: MiDriveFile, isExpired = false) {
|
public async updateFile(file: MiDriveFile, values: Partial<MiDriveFile>, updater: MiUser) {
|
||||||
|
const alwaysMarkNsfw = (await this.roleService.getUserPolicies(file.userId)).alwaysMarkNsfw;
|
||||||
|
|
||||||
|
if (values.name && !this.driveFileEntityService.validateFileName(file.name)) {
|
||||||
|
throw new DriveService.InvalidFileNameError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.isSensitive !== undefined && values.isSensitive !== file.isSensitive && alwaysMarkNsfw && !values.isSensitive) {
|
||||||
|
throw new DriveService.CannotUnmarkSensitiveError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.folderId != null) {
|
||||||
|
const folder = await this.driveFoldersRepository.findOneBy({
|
||||||
|
id: values.folderId,
|
||||||
|
userId: file.userId!,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (folder == null) {
|
||||||
|
throw new DriveService.NoSuchFolderError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.driveFilesRepository.update(file.id, values);
|
||||||
|
|
||||||
|
const fileObj = await this.driveFileEntityService.pack(file.id, { self: true });
|
||||||
|
|
||||||
|
// Publish fileUpdated event
|
||||||
|
if (file.userId) {
|
||||||
|
this.globalEventService.publishDriveStream(file.userId, 'fileUpdated', fileObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await this.roleService.isModerator(updater) && (file.userId !== updater.id)) {
|
||||||
|
if (values.isSensitive !== undefined && values.isSensitive !== file.isSensitive) {
|
||||||
|
const user = file.userId ? await this.usersRepository.findOneByOrFail({ id: file.userId }) : null;
|
||||||
|
if (values.isSensitive) {
|
||||||
|
this.moderationLogService.log(updater, 'markSensitiveDriveFile', {
|
||||||
|
fileId: file.id,
|
||||||
|
fileUserId: file.userId,
|
||||||
|
fileUserUsername: user?.username ?? null,
|
||||||
|
fileUserHost: user?.host ?? null,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.moderationLogService.log(updater, 'unmarkSensitiveDriveFile', {
|
||||||
|
fileId: file.id,
|
||||||
|
fileUserId: file.userId,
|
||||||
|
fileUserUsername: user?.username ?? null,
|
||||||
|
fileUserHost: user?.host ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async deleteFile(file: MiDriveFile, isExpired = false, deleter?: MiUser) {
|
||||||
if (file.storedInternal) {
|
if (file.storedInternal) {
|
||||||
this.internalStorageService.del(file.accessKey!);
|
this.internalStorageService.del(file.accessKey!);
|
||||||
|
|
||||||
@@ -671,11 +732,11 @@ export class DriveService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.deletePostProcess(file, isExpired);
|
this.deletePostProcess(file, isExpired, deleter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async deleteFileSync(file: MiDriveFile, isExpired = false) {
|
public async deleteFileSync(file: MiDriveFile, isExpired = false, deleter?: MiUser) {
|
||||||
if (file.storedInternal) {
|
if (file.storedInternal) {
|
||||||
this.internalStorageService.del(file.accessKey!);
|
this.internalStorageService.del(file.accessKey!);
|
||||||
|
|
||||||
@@ -702,11 +763,11 @@ export class DriveService {
|
|||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.deletePostProcess(file, isExpired);
|
this.deletePostProcess(file, isExpired, deleter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async deletePostProcess(file: MiDriveFile, isExpired = false) {
|
private async deletePostProcess(file: MiDriveFile, isExpired = false, deleter?: MiUser) {
|
||||||
// リモートファイル期限切れ削除後は直リンクにする
|
// リモートファイル期限切れ削除後は直リンクにする
|
||||||
if (isExpired && file.userHost !== null && file.uri != null) {
|
if (isExpired && file.userHost !== null && file.uri != null) {
|
||||||
this.driveFilesRepository.update(file.id, {
|
this.driveFilesRepository.update(file.id, {
|
||||||
@@ -733,6 +794,20 @@ export class DriveService {
|
|||||||
this.instanceChart.updateDrive(file, false);
|
this.instanceChart.updateDrive(file, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (file.userId) {
|
||||||
|
this.globalEventService.publishDriveStream(file.userId, 'fileDeleted', file.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleter && await this.roleService.isModerator(deleter) && (file.userId !== deleter.id)) {
|
||||||
|
const user = file.userId ? await this.usersRepository.findOneByOrFail({ id: file.userId }) : null;
|
||||||
|
this.moderationLogService.log(deleter, 'deleteDriveFile', {
|
||||||
|
fileId: file.id,
|
||||||
|
fileUserId: file.userId,
|
||||||
|
fileUserUsername: user?.username ?? null,
|
||||||
|
fileUserHost: user?.host ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
116
packages/backend/src/core/FeaturedService.ts
Normal file
116
packages/backend/src/core/FeaturedService.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
|
import type { MiNote, MiUser } from '@/models/_.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
|
||||||
|
const GLOBAL_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと
|
||||||
|
const PER_USER_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 7; // 1週間ごと
|
||||||
|
const HASHTAG_RANKING_WINDOW = 1000 * 60 * 60; // 1時間ごと
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FeaturedService {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis, // TODO: 専用のRedisサーバーを設定できるようにする
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private getCurrentWindow(windowRange: number): number {
|
||||||
|
const passed = new Date().getTime() - new Date(new Date().getFullYear(), 0, 1).getTime();
|
||||||
|
return Math.floor(passed / windowRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async updateRankingOf(name: string, windowRange: number, element: string, score = 1): Promise<void> {
|
||||||
|
const currentWindow = this.getCurrentWindow(windowRange);
|
||||||
|
const redisTransaction = this.redisClient.multi();
|
||||||
|
redisTransaction.zincrby(
|
||||||
|
`${name}:${currentWindow}`,
|
||||||
|
score,
|
||||||
|
element);
|
||||||
|
redisTransaction.expire(
|
||||||
|
`${name}:${currentWindow}`,
|
||||||
|
(windowRange * 3) / 1000,
|
||||||
|
'NX'); // "NX -- Set expiry only when the key has no expiry" = 有効期限がないときだけ設定
|
||||||
|
await redisTransaction.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async getRankingOf(name: string, windowRange: number, limit: number): Promise<string[]> {
|
||||||
|
const currentWindow = this.getCurrentWindow(windowRange);
|
||||||
|
const previousWindow = currentWindow - 1;
|
||||||
|
|
||||||
|
const [currentRankingResult, previousRankingResult] = await Promise.all([
|
||||||
|
this.redisClient.zrange(
|
||||||
|
`${name}:${currentWindow}`, 0, limit, 'REV', 'WITHSCORES'),
|
||||||
|
this.redisClient.zrange(
|
||||||
|
`${name}:${previousWindow}`, 0, limit, 'REV', 'WITHSCORES'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const ranking = new Map<string, number>();
|
||||||
|
for (let i = 0; i < currentRankingResult.length; i += 2) {
|
||||||
|
const noteId = currentRankingResult[i];
|
||||||
|
const score = parseInt(currentRankingResult[i + 1], 10);
|
||||||
|
ranking.set(noteId, score);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < previousRankingResult.length; i += 2) {
|
||||||
|
const noteId = previousRankingResult[i];
|
||||||
|
const score = parseInt(previousRankingResult[i + 1], 10);
|
||||||
|
const exist = ranking.get(noteId);
|
||||||
|
if (exist != null) {
|
||||||
|
ranking.set(noteId, (exist + score) / 2);
|
||||||
|
} else {
|
||||||
|
ranking.set(noteId, score);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(ranking.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public updateGlobalNotesRanking(noteId: MiNote['id'], score = 1): Promise<void> {
|
||||||
|
return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public updateInChannelNotesRanking(channelId: MiNote['channelId'], noteId: MiNote['id'], score = 1): Promise<void> {
|
||||||
|
return this.updateRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public updatePerUserNotesRanking(userId: MiUser['id'], noteId: MiNote['id'], score = 1): Promise<void> {
|
||||||
|
return this.updateRankingOf(`featuredPerUserNotesRanking:${userId}`, PER_USER_NOTES_RANKING_WINDOW, noteId, score);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public updateHashtagsRanking(hashtag: string, score = 1): Promise<void> {
|
||||||
|
return this.updateRankingOf('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, hashtag, score);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public getGlobalNotesRanking(limit: number): Promise<MiNote['id'][]> {
|
||||||
|
return this.getRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public getInChannelNotesRanking(channelId: MiNote['channelId'], limit: number): Promise<MiNote['id'][]> {
|
||||||
|
return this.getRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public getPerUserNotesRanking(userId: MiUser['id'], limit: number): Promise<MiNote['id'][]> {
|
||||||
|
return this.getRankingOf(`featuredPerUserNotesRanking:${userId}`, PER_USER_NOTES_RANKING_WINDOW, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public getHashtagsRanking(limit: number): Promise<string[]> {
|
||||||
|
return this.getRankingOf('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, limit);
|
||||||
|
}
|
||||||
|
}
|
@@ -5,27 +5,254 @@
|
|||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
|
import type { MiChannel } from '@/models/Channel.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
|
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
import type { MiUserList } from '@/models/UserList.js';
|
|
||||||
import type { MiAntenna } from '@/models/Antenna.js';
|
import type { MiAntenna } from '@/models/Antenna.js';
|
||||||
import type {
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
StreamChannels,
|
import type { MiDriveFolder } from '@/models/DriveFolder.js';
|
||||||
AdminStreamTypes,
|
import type { MiUserList } from '@/models/UserList.js';
|
||||||
AntennaStreamTypes,
|
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||||
BroadcastTypes,
|
import type { MiSignin } from '@/models/Signin.js';
|
||||||
DriveStreamTypes,
|
import type { MiPage } from '@/models/Page.js';
|
||||||
InternalStreamTypes,
|
import type { MiWebhook } from '@/models/Webhook.js';
|
||||||
MainStreamTypes,
|
import type { MiMeta } from '@/models/Meta.js';
|
||||||
NoteStreamTypes,
|
import { MiRole, MiRoleAssignment } from '@/models/_.js';
|
||||||
UserListStreamTypes,
|
|
||||||
RoleTimelineStreamTypes,
|
|
||||||
} from '@/server/api/stream/types.js';
|
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
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 { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MiRole } from '@/models/_.js';
|
import { Serialized } from '@/types.js';
|
||||||
|
import type Emitter from 'strict-event-emitter-types';
|
||||||
|
import type { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
//#region Stream type-body definitions
|
||||||
|
export interface BroadcastTypes {
|
||||||
|
emojiAdded: {
|
||||||
|
emoji: Packed<'EmojiDetailed'>;
|
||||||
|
};
|
||||||
|
emojiUpdated: {
|
||||||
|
emojis: Packed<'EmojiDetailed'>[];
|
||||||
|
};
|
||||||
|
emojiDeleted: {
|
||||||
|
emojis: {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
[other: string]: any;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
announcementCreated: {
|
||||||
|
announcement: Packed<'Announcement'>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MainEventTypes {
|
||||||
|
notification: Packed<'Notification'>;
|
||||||
|
mention: Packed<'Note'>;
|
||||||
|
reply: Packed<'Note'>;
|
||||||
|
renote: Packed<'Note'>;
|
||||||
|
follow: Packed<'UserDetailedNotMe'>;
|
||||||
|
followed: Packed<'User'>;
|
||||||
|
unfollow: Packed<'User'>;
|
||||||
|
meUpdated: Packed<'User'>;
|
||||||
|
pageEvent: {
|
||||||
|
pageId: MiPage['id'];
|
||||||
|
event: string;
|
||||||
|
var: any;
|
||||||
|
userId: MiUser['id'];
|
||||||
|
user: Packed<'User'>;
|
||||||
|
};
|
||||||
|
urlUploadFinished: {
|
||||||
|
marker?: string | null;
|
||||||
|
file: Packed<'DriveFile'>;
|
||||||
|
};
|
||||||
|
readAllNotifications: undefined;
|
||||||
|
unreadNotification: Packed<'Notification'>;
|
||||||
|
unreadMention: MiNote['id'];
|
||||||
|
readAllUnreadMentions: undefined;
|
||||||
|
unreadSpecifiedNote: MiNote['id'];
|
||||||
|
readAllUnreadSpecifiedNotes: undefined;
|
||||||
|
readAllAntennas: undefined;
|
||||||
|
unreadAntenna: MiAntenna;
|
||||||
|
readAllAnnouncements: undefined;
|
||||||
|
myTokenRegenerated: undefined;
|
||||||
|
signin: MiSignin;
|
||||||
|
registryUpdated: {
|
||||||
|
scope?: string[];
|
||||||
|
key: string;
|
||||||
|
value: any | null;
|
||||||
|
};
|
||||||
|
driveFileCreated: Packed<'DriveFile'>;
|
||||||
|
readAntenna: MiAntenna;
|
||||||
|
receiveFollowRequest: Packed<'User'>;
|
||||||
|
announcementCreated: {
|
||||||
|
announcement: Packed<'Announcement'>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DriveEventTypes {
|
||||||
|
fileCreated: Packed<'DriveFile'>;
|
||||||
|
fileDeleted: MiDriveFile['id'];
|
||||||
|
fileUpdated: Packed<'DriveFile'>;
|
||||||
|
folderCreated: Packed<'DriveFolder'>;
|
||||||
|
folderDeleted: MiDriveFolder['id'];
|
||||||
|
folderUpdated: Packed<'DriveFolder'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NoteEventTypes {
|
||||||
|
pollVoted: {
|
||||||
|
choice: number;
|
||||||
|
userId: MiUser['id'];
|
||||||
|
};
|
||||||
|
deleted: {
|
||||||
|
deletedAt: Date;
|
||||||
|
};
|
||||||
|
updated: {
|
||||||
|
cw: string | null;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
reacted: {
|
||||||
|
reaction: string;
|
||||||
|
emoji?: {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
} | null;
|
||||||
|
userId: MiUser['id'];
|
||||||
|
};
|
||||||
|
unreacted: {
|
||||||
|
reaction: string;
|
||||||
|
userId: MiUser['id'];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
type NoteStreamEventTypes = {
|
||||||
|
[key in keyof NoteEventTypes]: {
|
||||||
|
id: MiNote['id'];
|
||||||
|
body: NoteEventTypes[key];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface UserListEventTypes {
|
||||||
|
userAdded: Packed<'User'>;
|
||||||
|
userRemoved: Packed<'User'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AntennaEventTypes {
|
||||||
|
note: MiNote;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoleTimelineEventTypes {
|
||||||
|
note: Packed<'Note'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminEventTypes {
|
||||||
|
newAbuseUserReport: {
|
||||||
|
id: MiAbuseUserReport['id'];
|
||||||
|
targetUserId: MiUser['id'],
|
||||||
|
reporterId: MiUser['id'],
|
||||||
|
comment: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
// 辞書(interface or type)から{ type, body }ユニオンを定義
|
||||||
|
// https://stackoverflow.com/questions/49311989/can-i-infer-the-type-of-a-value-using-extends-keyof-type
|
||||||
|
// VS Codeの展開を防止するためにEvents型を定義
|
||||||
|
type Events<T extends object> = { [K in keyof T]: { type: K; body: T[K]; } };
|
||||||
|
type EventUnionFromDictionary<
|
||||||
|
T extends object,
|
||||||
|
U = Events<T>
|
||||||
|
> = U[keyof U];
|
||||||
|
|
||||||
|
type SerializedAll<T> = {
|
||||||
|
[K in keyof T]: Serialized<T[K]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface InternalEventTypes {
|
||||||
|
userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
|
||||||
|
userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; };
|
||||||
|
remoteUserUpdated: { id: MiUser['id']; };
|
||||||
|
follow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
|
||||||
|
unfollow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
|
||||||
|
blockingCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
|
||||||
|
blockingDeleted: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
|
||||||
|
policiesUpdated: MiRole['policies'];
|
||||||
|
roleCreated: MiRole;
|
||||||
|
roleDeleted: MiRole;
|
||||||
|
roleUpdated: MiRole;
|
||||||
|
userRoleAssigned: MiRoleAssignment;
|
||||||
|
userRoleUnassigned: MiRoleAssignment;
|
||||||
|
webhookCreated: MiWebhook;
|
||||||
|
webhookDeleted: MiWebhook;
|
||||||
|
webhookUpdated: MiWebhook;
|
||||||
|
antennaCreated: MiAntenna;
|
||||||
|
antennaDeleted: MiAntenna;
|
||||||
|
antennaUpdated: MiAntenna;
|
||||||
|
metaUpdated: MiMeta;
|
||||||
|
followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
|
||||||
|
unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
|
||||||
|
updateUserProfile: MiUserProfile;
|
||||||
|
mute: { muterId: MiUser['id']; muteeId: MiUser['id']; };
|
||||||
|
unmute: { muterId: MiUser['id']; muteeId: MiUser['id']; };
|
||||||
|
userListMemberAdded: { userListId: MiUserList['id']; memberId: MiUser['id']; };
|
||||||
|
userListMemberRemoved: { userListId: MiUserList['id']; memberId: MiUser['id']; };
|
||||||
|
}
|
||||||
|
|
||||||
|
// name/messages(spec) pairs dictionary
|
||||||
|
export type GlobalEvents = {
|
||||||
|
internal: {
|
||||||
|
name: 'internal';
|
||||||
|
payload: EventUnionFromDictionary<SerializedAll<InternalEventTypes>>;
|
||||||
|
};
|
||||||
|
broadcast: {
|
||||||
|
name: 'broadcast';
|
||||||
|
payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>;
|
||||||
|
};
|
||||||
|
main: {
|
||||||
|
name: `mainStream:${MiUser['id']}`;
|
||||||
|
payload: EventUnionFromDictionary<SerializedAll<MainEventTypes>>;
|
||||||
|
};
|
||||||
|
drive: {
|
||||||
|
name: `driveStream:${MiUser['id']}`;
|
||||||
|
payload: EventUnionFromDictionary<SerializedAll<DriveEventTypes>>;
|
||||||
|
};
|
||||||
|
note: {
|
||||||
|
name: `noteStream:${MiNote['id']}`;
|
||||||
|
payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>;
|
||||||
|
};
|
||||||
|
userList: {
|
||||||
|
name: `userListStream:${MiUserList['id']}`;
|
||||||
|
payload: EventUnionFromDictionary<SerializedAll<UserListEventTypes>>;
|
||||||
|
};
|
||||||
|
roleTimeline: {
|
||||||
|
name: `roleTimelineStream:${MiRole['id']}`;
|
||||||
|
payload: EventUnionFromDictionary<SerializedAll<RoleTimelineEventTypes>>;
|
||||||
|
};
|
||||||
|
antenna: {
|
||||||
|
name: `antennaStream:${MiAntenna['id']}`;
|
||||||
|
payload: EventUnionFromDictionary<SerializedAll<AntennaEventTypes>>;
|
||||||
|
};
|
||||||
|
admin: {
|
||||||
|
name: `adminStream:${MiUser['id']}`;
|
||||||
|
payload: EventUnionFromDictionary<SerializedAll<AdminEventTypes>>;
|
||||||
|
};
|
||||||
|
notes: {
|
||||||
|
name: 'notesStream';
|
||||||
|
payload: Serialized<Packed<'Note'>>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// API event definitions
|
||||||
|
// ストリームごとのEmitterの辞書を用意
|
||||||
|
type EventEmitterDictionary = { [x in keyof GlobalEvents]: Emitter.default<EventEmitter, { [y in GlobalEvents[x]['name']]: (e: GlobalEvents[x]['payload']) => void }> };
|
||||||
|
// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
|
||||||
|
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
|
||||||
|
// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする
|
||||||
|
export type StreamEventEmitter = UnionToIntersection<EventEmitterDictionary[keyof GlobalEvents]>;
|
||||||
|
// { [y in name]: (e: spec) => void }をまとめてその交差型をEmitterにかけるとts(2590)にひっかかる
|
||||||
|
|
||||||
|
// provide stream channels union
|
||||||
|
export type StreamChannels = GlobalEvents[keyof GlobalEvents]['name'];
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GlobalEventService {
|
export class GlobalEventService {
|
||||||
@@ -51,7 +278,7 @@ export class GlobalEventService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public publishInternalEvent<K extends keyof InternalStreamTypes>(type: K, value?: InternalStreamTypes[K]): void {
|
public publishInternalEvent<K extends keyof InternalEventTypes>(type: K, value?: InternalEventTypes[K]): void {
|
||||||
this.publish('internal', type, typeof value === 'undefined' ? null : value);
|
this.publish('internal', type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,17 +288,17 @@ export class GlobalEventService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public publishMainStream<K extends keyof MainStreamTypes>(userId: MiUser['id'], type: K, value?: MainStreamTypes[K]): void {
|
public publishMainStream<K extends keyof MainEventTypes>(userId: MiUser['id'], type: K, value?: MainEventTypes[K]): void {
|
||||||
this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public publishDriveStream<K extends keyof DriveStreamTypes>(userId: MiUser['id'], type: K, value?: DriveStreamTypes[K]): void {
|
public publishDriveStream<K extends keyof DriveEventTypes>(userId: MiUser['id'], type: K, value?: DriveEventTypes[K]): void {
|
||||||
this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public publishNoteStream<K extends keyof NoteStreamTypes>(noteId: MiNote['id'], type: K, value?: NoteStreamTypes[K]): void {
|
public publishNoteStream<K extends keyof NoteEventTypes>(noteId: MiNote['id'], type: K, value?: NoteEventTypes[K]): void {
|
||||||
this.publish(`noteStream:${noteId}`, type, {
|
this.publish(`noteStream:${noteId}`, type, {
|
||||||
id: noteId,
|
id: noteId,
|
||||||
body: value,
|
body: value,
|
||||||
@@ -79,17 +306,17 @@ export class GlobalEventService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public publishUserListStream<K extends keyof UserListStreamTypes>(listId: MiUserList['id'], type: K, value?: UserListStreamTypes[K]): void {
|
public publishUserListStream<K extends keyof UserListEventTypes>(listId: MiUserList['id'], type: K, value?: UserListEventTypes[K]): void {
|
||||||
this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value);
|
this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public publishAntennaStream<K extends keyof AntennaStreamTypes>(antennaId: MiAntenna['id'], type: K, value?: AntennaStreamTypes[K]): void {
|
public publishAntennaStream<K extends keyof AntennaEventTypes>(antennaId: MiAntenna['id'], type: K, value?: AntennaEventTypes[K]): void {
|
||||||
this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value);
|
this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public publishRoleTimelineStream<K extends keyof RoleTimelineStreamTypes>(roleId: MiRole['id'], type: K, value?: RoleTimelineStreamTypes[K]): void {
|
public publishRoleTimelineStream<K extends keyof RoleTimelineEventTypes>(roleId: MiRole['id'], type: K, value?: RoleTimelineEventTypes[K]): void {
|
||||||
this.publish(`roleTimelineStream:${roleId}`, type, typeof value === 'undefined' ? null : value);
|
this.publish(`roleTimelineStream:${roleId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +326,7 @@ export class GlobalEventService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public publishAdminStream<K extends keyof AdminStreamTypes>(userId: MiUser['id'], type: K, value?: AdminStreamTypes[K]): void {
|
public publishAdminStream<K extends keyof AdminEventTypes>(userId: MiUser['id'], type: K, value?: AdminEventTypes[K]): void {
|
||||||
this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||||
@@ -12,15 +13,22 @@ import type { MiHashtag } from '@/models/Hashtag.js';
|
|||||||
import type { HashtagsRepository } from '@/models/_.js';
|
import type { HashtagsRepository } from '@/models/_.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||||
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HashtagService {
|
export class HashtagService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis, // TODO: 専用のRedisサーバーを設定できるようにする
|
||||||
|
|
||||||
@Inject(DI.hashtagsRepository)
|
@Inject(DI.hashtagsRepository)
|
||||||
private hashtagsRepository: HashtagsRepository,
|
private hashtagsRepository: HashtagsRepository,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
|
private featuredService: FeaturedService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
|
private metaService: MetaService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +54,9 @@ export class HashtagService {
|
|||||||
public async updateHashtag(user: { id: MiUser['id']; host: MiUser['host']; }, tag: string, isUserAttached = false, inc = true) {
|
public async updateHashtag(user: { id: MiUser['id']; host: MiUser['host']; }, tag: string, isUserAttached = false, inc = true) {
|
||||||
tag = normalizeForSearch(tag);
|
tag = normalizeForSearch(tag);
|
||||||
|
|
||||||
|
// TODO: サンプリング
|
||||||
|
this.updateHashtagsRanking(tag, user.id);
|
||||||
|
|
||||||
const index = await this.hashtagsRepository.findOneBy({ name: tag });
|
const index = await this.hashtagsRepository.findOneBy({ name: tag });
|
||||||
|
|
||||||
if (index == null && !inc) return;
|
if (index == null && !inc) return;
|
||||||
@@ -85,7 +96,7 @@ export class HashtagService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 自分が初めてこのタグを使ったなら
|
// 自分が初めてこのタグを使ったなら
|
||||||
if (!index.mentionedUserIds.some(id => id === user.id)) {
|
if (!index.mentionedUserIds.some(id => id === user.id)) {
|
||||||
set.mentionedUserIds = () => `array_append("mentionedUserIds", '${user.id}')`;
|
set.mentionedUserIds = () => `array_append("mentionedUserIds", '${user.id}')`;
|
||||||
set.mentionedUsersCount = () => '"mentionedUsersCount" + 1';
|
set.mentionedUsersCount = () => '"mentionedUsersCount" + 1';
|
||||||
@@ -144,4 +155,95 @@ export class HashtagService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async updateHashtagsRanking(hashtag: string, userId: MiUser['id']): Promise<void> {
|
||||||
|
const instance = await this.metaService.fetch();
|
||||||
|
const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t));
|
||||||
|
if (hiddenTags.includes(hashtag)) return;
|
||||||
|
|
||||||
|
// YYYYMMDDHHmm (10分間隔)
|
||||||
|
const now = new Date();
|
||||||
|
now.setMinutes(Math.floor(now.getMinutes() / 10) * 10, 0, 0);
|
||||||
|
const window = `${now.getUTCFullYear()}${(now.getUTCMonth() + 1).toString().padStart(2, '0')}${now.getUTCDate().toString().padStart(2, '0')}${now.getUTCHours().toString().padStart(2, '0')}${now.getUTCMinutes().toString().padStart(2, '0')}`;
|
||||||
|
|
||||||
|
const exist = await this.redisClient.sismember(`hashtagUsers:${hashtag}`, userId);
|
||||||
|
if (exist === 1) return;
|
||||||
|
|
||||||
|
this.featuredService.updateHashtagsRanking(hashtag, 1);
|
||||||
|
|
||||||
|
const redisPipeline = this.redisClient.pipeline();
|
||||||
|
|
||||||
|
// TODO: これらの Set は Bloom Filter を使うようにしても良さそう
|
||||||
|
|
||||||
|
// チャート用
|
||||||
|
redisPipeline.sadd(`hashtagUsers:${hashtag}:${window}`, userId);
|
||||||
|
redisPipeline.expire(`hashtagUsers:${hashtag}:${window}`,
|
||||||
|
60 * 60 * 24 * 3, // 3日間
|
||||||
|
'NX', // "NX -- Set expiry only when the key has no expiry" = 有効期限がないときだけ設定
|
||||||
|
);
|
||||||
|
|
||||||
|
// ユニークカウント用
|
||||||
|
redisPipeline.sadd(`hashtagUsers:${hashtag}`, userId);
|
||||||
|
redisPipeline.expire(`hashtagUsers:${hashtag}`,
|
||||||
|
60 * 60, // 1時間
|
||||||
|
'NX', // "NX -- Set expiry only when the key has no expiry" = 有効期限がないときだけ設定
|
||||||
|
);
|
||||||
|
|
||||||
|
redisPipeline.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async getChart(hashtag: string, range: number): Promise<number[]> {
|
||||||
|
const now = new Date();
|
||||||
|
now.setMinutes(Math.floor(now.getMinutes() / 10) * 10, 0, 0);
|
||||||
|
|
||||||
|
const redisPipeline = this.redisClient.pipeline();
|
||||||
|
|
||||||
|
for (let i = 0; i < range; i++) {
|
||||||
|
const window = `${now.getUTCFullYear()}${(now.getUTCMonth() + 1).toString().padStart(2, '0')}${now.getUTCDate().toString().padStart(2, '0')}${now.getUTCHours().toString().padStart(2, '0')}${now.getUTCMinutes().toString().padStart(2, '0')}`;
|
||||||
|
redisPipeline.scard(`hashtagUsers:${hashtag}:${window}`);
|
||||||
|
now.setMinutes(now.getMinutes() - (i * 10), 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await redisPipeline.exec();
|
||||||
|
|
||||||
|
if (result == null) return [];
|
||||||
|
|
||||||
|
return result.map(x => x[1]) as number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async getCharts(hashtags: string[], range: number): Promise<Record<string, number[]>> {
|
||||||
|
const now = new Date();
|
||||||
|
now.setMinutes(Math.floor(now.getMinutes() / 10) * 10, 0, 0);
|
||||||
|
|
||||||
|
const redisPipeline = this.redisClient.pipeline();
|
||||||
|
|
||||||
|
for (let i = 0; i < range; i++) {
|
||||||
|
const window = `${now.getUTCFullYear()}${(now.getUTCMonth() + 1).toString().padStart(2, '0')}${now.getUTCDate().toString().padStart(2, '0')}${now.getUTCHours().toString().padStart(2, '0')}${now.getUTCMinutes().toString().padStart(2, '0')}`;
|
||||||
|
for (const hashtag of hashtags) {
|
||||||
|
redisPipeline.scard(`hashtagUsers:${hashtag}:${window}`);
|
||||||
|
}
|
||||||
|
now.setMinutes(now.getMinutes() - (i * 10), 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await redisPipeline.exec();
|
||||||
|
|
||||||
|
if (result == null) return {};
|
||||||
|
|
||||||
|
// key is hashtag
|
||||||
|
const charts = {} as Record<string, number[]>;
|
||||||
|
for (const hashtag of hashtags) {
|
||||||
|
charts[hashtag] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < range; i++) {
|
||||||
|
for (let j = 0; j < hashtags.length; j++) {
|
||||||
|
charts[hashtags[j]].push(result[(i * hashtags.length) + j][1] as number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return charts;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,7 @@ import { DI } from '@/di-symbols.js';
|
|||||||
import { MiMeta } from '@/models/Meta.js';
|
import { MiMeta } from '@/models/Meta.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -46,7 +46,7 @@ export class MetaService implements OnApplicationShutdown {
|
|||||||
const obj = JSON.parse(data);
|
const obj = JSON.parse(data);
|
||||||
|
|
||||||
if (obj.channel === 'internal') {
|
if (obj.channel === 'internal') {
|
||||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'metaUpdated': {
|
case 'metaUpdated': {
|
||||||
this.cache = body;
|
this.cache = body;
|
||||||
|
@@ -9,6 +9,7 @@ import type { ModerationLogsRepository } from '@/models/_.js';
|
|||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { ModerationLogPayloads, moderationLogTypes } from '@/types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ModerationLogService {
|
export class ModerationLogService {
|
||||||
@@ -21,13 +22,13 @@ export class ModerationLogService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async insertModerationLog(moderator: { id: MiUser['id'] }, type: string, info?: Record<string, any>) {
|
public async log<T extends typeof moderationLogTypes[number]>(moderator: { id: MiUser['id'] }, type: T, info?: ModerationLogPayloads[T]) {
|
||||||
await this.moderationLogsRepository.insert({
|
await this.moderationLogsRepository.insert({
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
userId: moderator.id,
|
userId: moderator.id,
|
||||||
type: type,
|
type: type,
|
||||||
info: info ?? {},
|
info: (info as any) ?? {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { setImmediate } from 'node:timers/promises';
|
import { setImmediate } from 'node:timers/promises';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import { In, DataSource } from 'typeorm';
|
import { In, DataSource, IsNull, LessThan } from 'typeorm';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import RE2 from 're2';
|
import RE2 from 're2';
|
||||||
@@ -14,7 +14,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf
|
|||||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||||
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
||||||
import { MiNote } from '@/models/Note.js';
|
import { MiNote } from '@/models/Note.js';
|
||||||
import type { ChannelsRepository, FollowingsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import type { MiApp } from '@/models/App.js';
|
import type { MiApp } from '@/models/App.js';
|
||||||
import { concat } from '@/misc/prelude/array.js';
|
import { concat } from '@/misc/prelude/array.js';
|
||||||
@@ -53,8 +53,7 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
|||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { SearchService } from '@/core/SearchService.js';
|
import { SearchService } from '@/core/SearchService.js';
|
||||||
|
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||||
const mutedWordsCache = new MemorySingleCache<{ userId: MiUserProfile['userId']; mutedWords: MiUserProfile['mutedWords']; }[]>(1000 * 60 * 5);
|
|
||||||
|
|
||||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||||
|
|
||||||
@@ -110,9 +109,8 @@ class NotificationManager {
|
|||||||
// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
|
// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
|
||||||
if (!mentioneesMutedUserIds.includes(this.notifier.id)) {
|
if (!mentioneesMutedUserIds.includes(this.notifier.id)) {
|
||||||
this.notificationService.createNotification(x.target, x.reason, {
|
this.notificationService.createNotification(x.target, x.reason, {
|
||||||
notifierId: this.notifier.id,
|
|
||||||
noteId: this.note.id,
|
noteId: this.note.id,
|
||||||
});
|
}, this.notifier.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,8 +156,8 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
@Inject(DI.redis)
|
@Inject(DI.redisForTimelines)
|
||||||
private redisClient: Redis.Redis,
|
private redisForTimelines: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
@@ -176,8 +174,8 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
@Inject(DI.mutedNotesRepository)
|
@Inject(DI.userListMembershipsRepository)
|
||||||
private mutedNotesRepository: MutedNotesRepository,
|
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||||
|
|
||||||
@Inject(DI.channelsRepository)
|
@Inject(DI.channelsRepository)
|
||||||
private channelsRepository: ChannelsRepository,
|
private channelsRepository: ChannelsRepository,
|
||||||
@@ -188,6 +186,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
@Inject(DI.followingsRepository)
|
@Inject(DI.followingsRepository)
|
||||||
private followingsRepository: FollowingsRepository,
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.channelFollowingsRepository)
|
||||||
|
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
@@ -200,6 +201,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
private hashtagService: HashtagService,
|
private hashtagService: HashtagService,
|
||||||
private antennaService: AntennaService,
|
private antennaService: AntennaService,
|
||||||
private webhookService: WebhookService,
|
private webhookService: WebhookService,
|
||||||
|
private featuredService: FeaturedService,
|
||||||
private remoteUserResolveService: RemoteUserResolveService,
|
private remoteUserResolveService: RemoteUserResolveService,
|
||||||
private apDeliverManagerService: ApDeliverManagerService,
|
private apDeliverManagerService: ApDeliverManagerService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
@@ -252,19 +254,30 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renote対象が「ホームまたは全体」以外の公開範囲ならreject
|
if (data.renote) {
|
||||||
if (data.renote && data.renote.visibility !== 'public' && data.renote.visibility !== 'home' && data.renote.userId !== user.id) {
|
switch (data.renote.visibility) {
|
||||||
throw new Error('Renote target is not public or home');
|
case 'public':
|
||||||
}
|
// public noteは無条件にrenote可能
|
||||||
|
break;
|
||||||
|
case 'home':
|
||||||
|
// home noteはhome以下にrenote可能
|
||||||
|
if (data.visibility === 'public') {
|
||||||
|
data.visibility = 'home';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'followers':
|
||||||
|
// 他人のfollowers noteはreject
|
||||||
|
if (data.renote.userId !== user.id) {
|
||||||
|
throw new Error('Renote target is not public or home');
|
||||||
|
}
|
||||||
|
|
||||||
// Renote対象がpublicではないならhomeにする
|
// Renote対象がfollowersならfollowersにする
|
||||||
if (data.renote && data.renote.visibility !== 'public' && data.visibility === 'public') {
|
data.visibility = 'followers';
|
||||||
data.visibility = 'home';
|
break;
|
||||||
}
|
case 'specified':
|
||||||
|
// specified / direct noteはreject
|
||||||
// Renote対象がfollowersならfollowersにする
|
throw new Error('Renote target is not public or home');
|
||||||
if (data.renote && data.renote.visibility === 'followers') {
|
}
|
||||||
data.visibility = 'followers';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返信対象がpublicではないならhomeにする
|
// 返信対象がpublicではないならhomeにする
|
||||||
@@ -335,7 +348,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
|
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
|
||||||
|
|
||||||
if (data.channel) {
|
if (data.channel) {
|
||||||
this.redisClient.xadd(
|
this.redisForTimelines.xadd(
|
||||||
`channelTimeline:${data.channel.id}`,
|
`channelTimeline:${data.channel.id}`,
|
||||||
'MAXLEN', '~', this.config.perChannelMaxNoteCacheCount.toString(),
|
'MAXLEN', '~', this.config.perChannelMaxNoteCacheCount.toString(),
|
||||||
'*',
|
'*',
|
||||||
@@ -481,26 +494,11 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
// Increment notes count (user)
|
// Increment notes count (user)
|
||||||
this.incNotesCountOfUser(user);
|
this.incNotesCountOfUser(user);
|
||||||
|
|
||||||
// Word mute
|
if (data.visibility === 'specified') {
|
||||||
mutedWordsCache.fetch(() => this.userProfilesRepository.find({
|
// TODO?
|
||||||
where: {
|
} else {
|
||||||
enableWordMute: true,
|
this.pushToTl(note, user);
|
||||||
},
|
}
|
||||||
select: ['userId', 'mutedWords'],
|
|
||||||
})).then(us => {
|
|
||||||
for (const u of us) {
|
|
||||||
checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => {
|
|
||||||
if (shouldMute) {
|
|
||||||
this.mutedNotesRepository.insert({
|
|
||||||
id: this.idService.genId(),
|
|
||||||
userId: u.userId,
|
|
||||||
noteId: note.id,
|
|
||||||
reason: 'word',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.antennaService.addNoteToAntennas(note, user);
|
this.antennaService.addNoteToAntennas(note, user);
|
||||||
|
|
||||||
@@ -509,22 +507,22 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.reply == null) {
|
if (data.reply == null) {
|
||||||
|
// TODO: キャッシュ
|
||||||
this.followingsRepository.findBy({
|
this.followingsRepository.findBy({
|
||||||
followeeId: user.id,
|
followeeId: user.id,
|
||||||
notify: 'normal',
|
notify: 'normal',
|
||||||
}).then(followings => {
|
}).then(followings => {
|
||||||
for (const following of followings) {
|
for (const following of followings) {
|
||||||
|
// TODO: ワードミュート考慮
|
||||||
this.notificationService.createNotification(following.followerId, 'note', {
|
this.notificationService.createNotification(following.followerId, 'note', {
|
||||||
notifierId: user.id,
|
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
});
|
}, user.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
|
if (data.renote && data.renote.userId !== user.id && !user.isBot) {
|
||||||
if (data.renote && (await this.noteEntityService.countSameRenotes(user.id, data.renote.id, note.id) === 0)) {
|
this.incRenoteCount(data.renote);
|
||||||
if (!user.isBot) this.incRenoteCount(data.renote);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.poll && data.poll.expiresAt) {
|
if (data.poll && data.poll.expiresAt) {
|
||||||
@@ -724,10 +722,23 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
this.notesRepository.createQueryBuilder().update()
|
this.notesRepository.createQueryBuilder().update()
|
||||||
.set({
|
.set({
|
||||||
renoteCount: () => '"renoteCount" + 1',
|
renoteCount: () => '"renoteCount" + 1',
|
||||||
score: () => '"score" + 1',
|
|
||||||
})
|
})
|
||||||
.where('id = :id', { id: renote.id })
|
.where('id = :id', { id: renote.id })
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
|
// 30%の確率、3日以内に投稿されたノートの場合ハイライト用ランキング更新
|
||||||
|
if (Math.random() < 0.3 && (Date.now() - this.idService.parse(renote.id).date.getTime()) < 1000 * 60 * 60 * 24 * 3) {
|
||||||
|
if (renote.channelId != null) {
|
||||||
|
if (renote.replyId == null) {
|
||||||
|
this.featuredService.updateInChannelNotesRanking(renote.channelId, renote.id, 5);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (renote.visibility === 'public' && renote.userHost == null && renote.replyId == null) {
|
||||||
|
this.featuredService.updateGlobalNotesRanking(renote.id, 5);
|
||||||
|
this.featuredService.updatePerUserNotesRanking(renote.userId, renote.id, 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -813,6 +824,211 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
return mentionedUsers;
|
return mentionedUsers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) {
|
||||||
|
const meta = await this.metaService.fetch();
|
||||||
|
|
||||||
|
const redisPipeline = this.redisForTimelines.pipeline();
|
||||||
|
|
||||||
|
if (note.channelId) {
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`userTimelineWithChannel:${user.id}`,
|
||||||
|
'MAXLEN', '~', note.userHost == null ? meta.perLocalUserUserTimelineCacheMax.toString() : meta.perRemoteUserUserTimelineCacheMax.toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
|
||||||
|
const channelFollowings = await this.channelFollowingsRepository.find({
|
||||||
|
where: {
|
||||||
|
followeeId: note.channelId,
|
||||||
|
},
|
||||||
|
select: ['followerId'],
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const channelFollowing of channelFollowings) {
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`homeTimeline:${channelFollowing.followerId}`,
|
||||||
|
'MAXLEN', '~', meta.perUserHomeTimelineCacheMax.toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
|
||||||
|
if (note.fileIds.length > 0) {
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`homeTimelineWithFiles:${channelFollowing.followerId}`,
|
||||||
|
'MAXLEN', '~', (meta.perUserHomeTimelineCacheMax / 2).toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: キャッシュ?
|
||||||
|
const followings = await this.followingsRepository.find({
|
||||||
|
where: {
|
||||||
|
followeeId: user.id,
|
||||||
|
followerHost: IsNull(),
|
||||||
|
isFollowerHibernated: false,
|
||||||
|
},
|
||||||
|
select: ['followerId', 'withReplies'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const userListMemberships = await this.userListMembershipsRepository.find({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
select: ['userListId', 'withReplies'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: あまりにも数が多いと redisPipeline.exec に失敗する(理由は不明)ため、3万件程度を目安に分割して実行するようにする
|
||||||
|
for (const following of followings) {
|
||||||
|
// 自分自身以外への返信
|
||||||
|
if (note.replyId && note.replyUserId !== note.userId) {
|
||||||
|
if (!following.withReplies) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`homeTimeline:${following.followerId}`,
|
||||||
|
'MAXLEN', '~', meta.perUserHomeTimelineCacheMax.toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
|
||||||
|
if (note.fileIds.length > 0) {
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`homeTimelineWithFiles:${following.followerId}`,
|
||||||
|
'MAXLEN', '~', (meta.perUserHomeTimelineCacheMax / 2).toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
//if (note.visibility === 'followers') {
|
||||||
|
// // TODO: 重そうだから何とかしたい Set 使う?
|
||||||
|
// userLists = userLists.filter(x => followings.some(f => f.followerId === x.userListUserId));
|
||||||
|
//}
|
||||||
|
|
||||||
|
for (const userListMembership of userListMemberships) {
|
||||||
|
// 自分自身以外への返信
|
||||||
|
if (note.replyId && note.replyUserId !== note.userId) {
|
||||||
|
if (!userListMembership.withReplies) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`userListTimeline:${userListMembership.userListId}`,
|
||||||
|
'MAXLEN', '~', meta.perUserListTimelineCacheMax.toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
|
||||||
|
if (note.fileIds.length > 0) {
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`userListTimelineWithFiles:${userListMembership.userListId}`,
|
||||||
|
'MAXLEN', '~', (meta.perUserListTimelineCacheMax / 2).toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // 自分自身のHTL
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`homeTimeline:${user.id}`,
|
||||||
|
'MAXLEN', '~', meta.perUserHomeTimelineCacheMax.toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
|
||||||
|
if (note.fileIds.length > 0) {
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`homeTimelineWithFiles:${user.id}`,
|
||||||
|
'MAXLEN', '~', (meta.perUserHomeTimelineCacheMax / 2).toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自分自身以外への返信
|
||||||
|
if (note.replyId && note.replyUserId !== note.userId) {
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`userTimelineWithReplies:${user.id}`,
|
||||||
|
'MAXLEN', '~', note.userHost == null ? meta.perLocalUserUserTimelineCacheMax.toString() : meta.perRemoteUserUserTimelineCacheMax.toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
} else {
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`userTimeline:${user.id}`,
|
||||||
|
'MAXLEN', '~', note.userHost == null ? meta.perLocalUserUserTimelineCacheMax.toString() : meta.perRemoteUserUserTimelineCacheMax.toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
|
||||||
|
if (note.fileIds.length > 0) {
|
||||||
|
redisPipeline.xadd(
|
||||||
|
`userTimelineWithFiles:${user.id}`,
|
||||||
|
'MAXLEN', '~', note.userHost == null ? (meta.perLocalUserUserTimelineCacheMax / 2).toString() : (meta.perRemoteUserUserTimelineCacheMax / 2).toString(),
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.visibility === 'public' && note.userHost == null) {
|
||||||
|
redisPipeline.xadd(
|
||||||
|
'localTimeline',
|
||||||
|
'MAXLEN', '~', '1000',
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
|
||||||
|
if (note.fileIds.length > 0) {
|
||||||
|
redisPipeline.xadd(
|
||||||
|
'localTimelineWithFiles',
|
||||||
|
'MAXLEN', '~', '500',
|
||||||
|
'*',
|
||||||
|
'note', note.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.random() < 0.1) {
|
||||||
|
process.nextTick(() => {
|
||||||
|
this.checkHibernation(followings);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redisPipeline.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async checkHibernation(followings: MiFollowing[]) {
|
||||||
|
if (followings.length === 0) return;
|
||||||
|
|
||||||
|
const shuffle = (array: MiFollowing[]) => {
|
||||||
|
for (let i = array.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[array[i], array[j]] = [array[j], array[i]];
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ランダムに最大1000件サンプリング
|
||||||
|
const samples = shuffle(followings).slice(0, Math.min(followings.length, 1000));
|
||||||
|
|
||||||
|
const hibernatedUsers = await this.usersRepository.find({
|
||||||
|
where: {
|
||||||
|
id: In(samples.map(x => x.followerId)),
|
||||||
|
lastActiveDate: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 50))),
|
||||||
|
},
|
||||||
|
select: ['id'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hibernatedUsers.length > 0) {
|
||||||
|
this.usersRepository.update({
|
||||||
|
id: In(hibernatedUsers.map(x => x.id)),
|
||||||
|
}, {
|
||||||
|
isHibernated: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.followingsRepository.update({
|
||||||
|
followerId: In(hibernatedUsers.map(x => x.id)),
|
||||||
|
}, {
|
||||||
|
isFollowerHibernated: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
this.#shutdownController.abort();
|
this.#shutdownController.abort();
|
||||||
|
@@ -23,6 +23,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { SearchService } from '@/core/SearchService.js';
|
import { SearchService } from '@/core/SearchService.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NoteDeleteService {
|
export class NoteDeleteService {
|
||||||
@@ -48,6 +49,7 @@ export class NoteDeleteService {
|
|||||||
private apDeliverManagerService: ApDeliverManagerService,
|
private apDeliverManagerService: ApDeliverManagerService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
private notesChart: NotesChart,
|
private notesChart: NotesChart,
|
||||||
private perUserNotesChart: PerUserNotesChart,
|
private perUserNotesChart: PerUserNotesChart,
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
@@ -58,16 +60,10 @@ export class NoteDeleteService {
|
|||||||
* @param user 投稿者
|
* @param user 投稿者
|
||||||
* @param note 投稿
|
* @param note 投稿
|
||||||
*/
|
*/
|
||||||
async delete(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, quiet = false) {
|
async delete(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, quiet = false, deleter?: MiUser) {
|
||||||
const deletedAt = new Date();
|
const deletedAt = new Date();
|
||||||
const cascadingNotes = await this.findCascadingNotes(note);
|
const cascadingNotes = await this.findCascadingNotes(note);
|
||||||
|
|
||||||
// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
|
|
||||||
if (note.renoteId && (await this.noteEntityService.countSameRenotes(user.id, note.renoteId, note.id)) === 0) {
|
|
||||||
this.notesRepository.decrement({ id: note.renoteId }, 'renoteCount', 1);
|
|
||||||
if (!user.isBot) this.notesRepository.decrement({ id: note.renoteId }, 'score', 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (note.replyId) {
|
if (note.replyId) {
|
||||||
await this.notesRepository.decrement({ id: note.replyId }, 'repliesCount', 1);
|
await this.notesRepository.decrement({ id: note.replyId }, 'repliesCount', 1);
|
||||||
}
|
}
|
||||||
@@ -131,6 +127,17 @@ export class NoteDeleteService {
|
|||||||
id: note.id,
|
id: note.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (deleter && (note.userId !== deleter.id)) {
|
||||||
|
const user = await this.usersRepository.findOneByOrFail({ id: note.userId });
|
||||||
|
this.moderationLogService.log(deleter, 'deleteNote', {
|
||||||
|
noteId: note.id,
|
||||||
|
noteUserId: note.userId,
|
||||||
|
noteUserUsername: user.username,
|
||||||
|
noteUserHost: user.host,
|
||||||
|
note: note,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@@ -18,6 +18,7 @@ import { NotificationEntityService } from '@/core/entities/NotificationEntitySer
|
|||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
|
import { UserListService } from '@/core/UserListService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotificationService implements OnApplicationShutdown {
|
export class NotificationService implements OnApplicationShutdown {
|
||||||
@@ -38,6 +39,7 @@ export class NotificationService implements OnApplicationShutdown {
|
|||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private pushNotificationService: PushNotificationService,
|
private pushNotificationService: PushNotificationService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
|
private userListService: UserListService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,27 +76,59 @@ export class NotificationService implements OnApplicationShutdown {
|
|||||||
public async createNotification(
|
public async createNotification(
|
||||||
notifieeId: MiUser['id'],
|
notifieeId: MiUser['id'],
|
||||||
type: MiNotification['type'],
|
type: MiNotification['type'],
|
||||||
data: Partial<MiNotification>,
|
data: Omit<Partial<MiNotification>, 'notifierId'>,
|
||||||
|
notifierId?: MiUser['id'] | null,
|
||||||
): Promise<MiNotification | null> {
|
): Promise<MiNotification | null> {
|
||||||
const profile = await this.cacheService.userProfileCache.fetch(notifieeId);
|
const profile = await this.cacheService.userProfileCache.fetch(notifieeId);
|
||||||
const isMuted = profile.mutingNotificationTypes.includes(type);
|
|
||||||
if (isMuted) return null;
|
|
||||||
|
|
||||||
if (data.notifierId) {
|
// 古いMisskeyバージョンのキャッシュが残っている可能性がある
|
||||||
if (notifieeId === data.notifierId) {
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
const recieveConfig = (profile.notificationRecieveConfig ?? {})[type];
|
||||||
|
if (recieveConfig?.type === 'never') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notifierId) {
|
||||||
|
if (notifieeId === notifierId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId);
|
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId);
|
||||||
if (mutings.has(data.notifierId)) {
|
if (mutings.has(notifierId)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (recieveConfig?.type === 'following') {
|
||||||
|
const isFollowing = await this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId));
|
||||||
|
if (!isFollowing) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else if (recieveConfig?.type === 'follower') {
|
||||||
|
const isFollower = await this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId));
|
||||||
|
if (!isFollower) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else if (recieveConfig?.type === 'mutualFollow') {
|
||||||
|
const [isFollowing, isFollower] = await Promise.all([
|
||||||
|
this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => Object.hasOwn(followings, notifierId)),
|
||||||
|
this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => Object.hasOwn(followings, notifieeId)),
|
||||||
|
]);
|
||||||
|
if (!isFollowing && !isFollower) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else if (recieveConfig?.type === 'list') {
|
||||||
|
const isMember = await this.userListService.membersCache.fetch(recieveConfig.userListId).then(members => members.has(notifierId));
|
||||||
|
if (!isMember) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const notification = {
|
const notification = {
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
type: type,
|
type: type,
|
||||||
|
notifierId: notifierId,
|
||||||
...data,
|
...data,
|
||||||
} as MiNotification;
|
} as MiNotification;
|
||||||
|
|
||||||
@@ -117,8 +151,8 @@ export class NotificationService implements OnApplicationShutdown {
|
|||||||
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
|
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
|
||||||
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
|
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
|
||||||
|
|
||||||
if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));
|
if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: notifierId! }));
|
||||||
if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));
|
if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: notifierId! }));
|
||||||
}, () => { /* aborted, ignore it */ });
|
}, () => { /* aborted, ignore it */ });
|
||||||
|
|
||||||
return notification;
|
return notification;
|
||||||
|
@@ -96,6 +96,8 @@ export class PollService {
|
|||||||
const note = await this.notesRepository.findOneBy({ id: noteId });
|
const note = await this.notesRepository.findOneBy({ id: noteId });
|
||||||
if (note == null) throw new Error('note not found');
|
if (note == null) throw new Error('note not found');
|
||||||
|
|
||||||
|
if (note.localOnly) return;
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: note.userId });
|
const user = await this.usersRepository.findOneBy({ id: note.userId });
|
||||||
if (user == null) throw new Error('note not found');
|
if (user == null) throw new Error('note not found');
|
||||||
|
|
||||||
|
@@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { Brackets, ObjectLiteral } from 'typeorm';
|
import { Brackets, ObjectLiteral } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, MutedNotesRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/_.js';
|
import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { SelectQueryBuilder } from 'typeorm';
|
import type { SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
@@ -23,9 +23,6 @@ export class QueryService {
|
|||||||
@Inject(DI.channelFollowingsRepository)
|
@Inject(DI.channelFollowingsRepository)
|
||||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||||
|
|
||||||
@Inject(DI.mutedNotesRepository)
|
|
||||||
private mutedNotesRepository: MutedNotesRepository,
|
|
||||||
|
|
||||||
@Inject(DI.blockingsRepository)
|
@Inject(DI.blockingsRepository)
|
||||||
private blockingsRepository: BlockingsRepository,
|
private blockingsRepository: BlockingsRepository,
|
||||||
|
|
||||||
@@ -108,39 +105,6 @@ export class QueryService {
|
|||||||
q.setParameters(blockedQuery.getParameters());
|
q.setParameters(blockedQuery.getParameters());
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public generateChannelQuery(q: SelectQueryBuilder<any>, me?: { id: MiUser['id'] } | null): void {
|
|
||||||
if (me == null) {
|
|
||||||
q.andWhere('note.channelId IS NULL');
|
|
||||||
} else {
|
|
||||||
q.leftJoinAndSelect('note.channel', 'channel');
|
|
||||||
|
|
||||||
const channelFollowingQuery = this.channelFollowingsRepository.createQueryBuilder('channelFollowing')
|
|
||||||
.select('channelFollowing.followeeId')
|
|
||||||
.where('channelFollowing.followerId = :followerId', { followerId: me.id });
|
|
||||||
|
|
||||||
q.andWhere(new Brackets(qb => { qb
|
|
||||||
// チャンネルのノートではない
|
|
||||||
.where('note.channelId IS NULL')
|
|
||||||
// または自分がフォローしているチャンネルのノート
|
|
||||||
.orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`);
|
|
||||||
}));
|
|
||||||
|
|
||||||
q.setParameters(channelFollowingQuery.getParameters());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public generateMutedNoteQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
|
|
||||||
const mutedQuery = this.mutedNotesRepository.createQueryBuilder('muted')
|
|
||||||
.select('muted.noteId')
|
|
||||||
.where('muted.userId = :userId', { userId: me.id });
|
|
||||||
|
|
||||||
q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
|
|
||||||
|
|
||||||
q.setParameters(mutedQuery.getParameters());
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public generateMutedNoteThreadQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
|
public generateMutedNoteThreadQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
|
||||||
const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted')
|
const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted')
|
||||||
@@ -212,32 +176,6 @@ export class QueryService {
|
|||||||
q.setParameters(mutingQuery.getParameters());
|
q.setParameters(mutingQuery.getParameters());
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public generateRepliesQuery(q: SelectQueryBuilder<any>, withReplies: boolean, me?: Pick<MiUser, 'id'> | null): void {
|
|
||||||
if (me == null) {
|
|
||||||
q.andWhere(new Brackets(qb => { qb
|
|
||||||
.where('note.replyId IS NULL') // 返信ではない
|
|
||||||
.orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信
|
|
||||||
.where('note.replyId IS NOT NULL')
|
|
||||||
.andWhere('note.replyUserId = note.userId');
|
|
||||||
}));
|
|
||||||
}));
|
|
||||||
} else if (!withReplies) {
|
|
||||||
q.andWhere(new Brackets(qb => { qb
|
|
||||||
.where('note.replyId IS NULL') // 返信ではない
|
|
||||||
.orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信
|
|
||||||
.orWhere(new Brackets(qb => { qb // 返信だけど自分の行った返信
|
|
||||||
.where('note.replyId IS NOT NULL')
|
|
||||||
.andWhere('note.userId = :meId', { meId: me.id });
|
|
||||||
}))
|
|
||||||
.orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信
|
|
||||||
.where('note.replyId IS NOT NULL')
|
|
||||||
.andWhere('note.replyUserId = note.userId');
|
|
||||||
}));
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: MiUser['id'] } | null): void {
|
public generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: MiUser['id'] } | null): void {
|
||||||
// This code must always be synchronized with the checks in Notes.isVisibleForMe.
|
// This code must always be synchronized with the checks in Notes.isVisibleForMe.
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/_.js';
|
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/_.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
@@ -26,6 +27,7 @@ import { UtilityService } from '@/core/UtilityService.js';
|
|||||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||||
|
|
||||||
const FALLBACK = '❤';
|
const FALLBACK = '❤';
|
||||||
|
|
||||||
@@ -66,6 +68,9 @@ const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/;
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class ReactionService {
|
export class ReactionService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
@@ -86,6 +91,7 @@ export class ReactionService {
|
|||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private userBlockingService: UserBlockingService,
|
private userBlockingService: UserBlockingService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
|
private featuredService: FeaturedService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private apDeliverManagerService: ApDeliverManagerService,
|
private apDeliverManagerService: ApDeliverManagerService,
|
||||||
@@ -182,11 +188,28 @@ export class ReactionService {
|
|||||||
await this.notesRepository.createQueryBuilder().update()
|
await this.notesRepository.createQueryBuilder().update()
|
||||||
.set({
|
.set({
|
||||||
reactions: () => sql,
|
reactions: () => sql,
|
||||||
... (!user.isBot ? { score: () => '"score" + 1' } : {}),
|
|
||||||
})
|
})
|
||||||
.where('id = :id', { id: note.id })
|
.where('id = :id', { id: note.id })
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
|
// 30%の確率、セルフではない、3日以内に投稿されたノートの場合ハイライト用ランキング更新
|
||||||
|
if (
|
||||||
|
Math.random() < 0.3 &&
|
||||||
|
note.userId !== user.id &&
|
||||||
|
(Date.now() - this.idService.parse(note.id).date.getTime()) < 1000 * 60 * 60 * 24 * 3
|
||||||
|
) {
|
||||||
|
if (note.channelId != null) {
|
||||||
|
if (note.replyId == null) {
|
||||||
|
this.featuredService.updateInChannelNotesRanking(note.channelId, note.id, 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (note.visibility === 'public' && note.userHost == null && note.replyId == null) {
|
||||||
|
this.featuredService.updateGlobalNotesRanking(note.id, 1);
|
||||||
|
this.featuredService.updatePerUserNotesRanking(note.userId, note.id, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const meta = await this.metaService.fetch();
|
const meta = await this.metaService.fetch();
|
||||||
|
|
||||||
if (meta.enableChartsForRemoteUser || (user.host == null)) {
|
if (meta.enableChartsForRemoteUser || (user.host == null)) {
|
||||||
@@ -219,10 +242,9 @@ export class ReactionService {
|
|||||||
// リアクションされたユーザーがローカルユーザーなら通知を作成
|
// リアクションされたユーザーがローカルユーザーなら通知を作成
|
||||||
if (note.userHost === null) {
|
if (note.userHost === null) {
|
||||||
this.notificationService.createNotification(note.userId, 'reaction', {
|
this.notificationService.createNotification(note.userId, 'reaction', {
|
||||||
notifierId: user.id,
|
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
});
|
}, user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region 配信
|
//#region 配信
|
||||||
@@ -276,8 +298,6 @@ export class ReactionService {
|
|||||||
.where('id = :id', { id: note.id })
|
.where('id = :id', { id: note.id })
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
if (!user.isBot) this.notesRepository.decrement({ id: note.id }, 'score', 1);
|
|
||||||
|
|
||||||
this.globalEventService.publishNoteStream(note.id, 'unreacted', {
|
this.globalEventService.publishNoteStream(note.id, 'unreacted', {
|
||||||
reaction: this.decodeReaction(exist.reaction).reaction,
|
reaction: this.decodeReaction(exist.reaction).reaction,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
@@ -15,9 +15,10 @@ import { MetaService } from '@/core/MetaService.js';
|
|||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import type { RoleCondFormulaValue } from '@/models/Role.js';
|
import type { RoleCondFormulaValue } from '@/models/Role.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ export type RolePolicies = {
|
|||||||
inviteExpirationTime: number;
|
inviteExpirationTime: number;
|
||||||
canManageCustomEmojis: boolean;
|
canManageCustomEmojis: boolean;
|
||||||
canSearchNotes: boolean;
|
canSearchNotes: boolean;
|
||||||
|
canUseTranslator: boolean;
|
||||||
canHideAds: boolean;
|
canHideAds: boolean;
|
||||||
driveCapacityMb: number;
|
driveCapacityMb: number;
|
||||||
alwaysMarkNsfw: boolean;
|
alwaysMarkNsfw: boolean;
|
||||||
@@ -55,6 +57,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||||||
inviteExpirationTime: 0,
|
inviteExpirationTime: 0,
|
||||||
canManageCustomEmojis: false,
|
canManageCustomEmojis: false,
|
||||||
canSearchNotes: false,
|
canSearchNotes: false,
|
||||||
|
canUseTranslator: true,
|
||||||
canHideAds: false,
|
canHideAds: false,
|
||||||
driveCapacityMb: 100,
|
driveCapacityMb: 100,
|
||||||
alwaysMarkNsfw: false,
|
alwaysMarkNsfw: false,
|
||||||
@@ -98,6 +101,7 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
) {
|
) {
|
||||||
//this.onMessage = this.onMessage.bind(this);
|
//this.onMessage = this.onMessage.bind(this);
|
||||||
|
|
||||||
@@ -112,7 +116,7 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
const obj = JSON.parse(data);
|
const obj = JSON.parse(data);
|
||||||
|
|
||||||
if (obj.channel === 'internal') {
|
if (obj.channel === 'internal') {
|
||||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'roleCreated': {
|
case 'roleCreated': {
|
||||||
const cached = this.rolesCache.get();
|
const cached = this.rolesCache.get();
|
||||||
@@ -298,6 +302,7 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)),
|
inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)),
|
||||||
canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
|
canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
|
||||||
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
|
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
|
||||||
|
canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)),
|
||||||
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
||||||
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
|
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
|
||||||
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
|
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
|
||||||
@@ -374,9 +379,11 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async assign(userId: MiUser['id'], roleId: MiRole['id'], expiresAt: Date | null = null): Promise<void> {
|
public async assign(userId: MiUser['id'], roleId: MiRole['id'], expiresAt: Date | null = null, moderator?: MiUser): Promise<void> {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
|
const role = await this.rolesRepository.findOneByOrFail({ id: roleId });
|
||||||
|
|
||||||
const existing = await this.roleAssignmentsRepository.findOneBy({
|
const existing = await this.roleAssignmentsRepository.findOneBy({
|
||||||
roleId: roleId,
|
roleId: roleId,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
@@ -406,10 +413,22 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('userRoleAssigned', created);
|
this.globalEventService.publishInternalEvent('userRoleAssigned', created);
|
||||||
|
|
||||||
|
if (moderator) {
|
||||||
|
const user = await this.usersRepository.findOneByOrFail({ id: userId });
|
||||||
|
this.moderationLogService.log(moderator, 'assignRole', {
|
||||||
|
roleId: roleId,
|
||||||
|
roleName: role.name,
|
||||||
|
userId: userId,
|
||||||
|
userUsername: user.username,
|
||||||
|
userHost: user.host,
|
||||||
|
expiresAt: expiresAt ? expiresAt.toISOString() : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async unassign(userId: MiUser['id'], roleId: MiRole['id']): Promise<void> {
|
public async unassign(userId: MiUser['id'], roleId: MiRole['id'], moderator?: MiUser): Promise<void> {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId });
|
const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId });
|
||||||
@@ -430,6 +449,20 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('userRoleUnassigned', existing);
|
this.globalEventService.publishInternalEvent('userRoleUnassigned', existing);
|
||||||
|
|
||||||
|
if (moderator) {
|
||||||
|
const [user, role] = await Promise.all([
|
||||||
|
this.usersRepository.findOneByOrFail({ id: userId }),
|
||||||
|
this.rolesRepository.findOneByOrFail({ id: roleId }),
|
||||||
|
]);
|
||||||
|
this.moderationLogService.log(moderator, 'unassignRole', {
|
||||||
|
roleId: roleId,
|
||||||
|
roleName: role.name,
|
||||||
|
userId: userId,
|
||||||
|
userUsername: user.username,
|
||||||
|
userHost: user.host,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -451,6 +484,75 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
redisPipeline.exec();
|
redisPipeline.exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async create(values: Partial<MiRole>, moderator?: MiUser): Promise<MiRole> {
|
||||||
|
const date = new Date();
|
||||||
|
const created = await this.rolesRepository.insert({
|
||||||
|
id: this.idService.genId(),
|
||||||
|
createdAt: date,
|
||||||
|
updatedAt: date,
|
||||||
|
lastUsedAt: date,
|
||||||
|
name: values.name,
|
||||||
|
description: values.description,
|
||||||
|
color: values.color,
|
||||||
|
iconUrl: values.iconUrl,
|
||||||
|
target: values.target,
|
||||||
|
condFormula: values.condFormula,
|
||||||
|
isPublic: values.isPublic,
|
||||||
|
isAdministrator: values.isAdministrator,
|
||||||
|
isModerator: values.isModerator,
|
||||||
|
isExplorable: values.isExplorable,
|
||||||
|
asBadge: values.asBadge,
|
||||||
|
canEditMembersByModerator: values.canEditMembersByModerator,
|
||||||
|
displayOrder: values.displayOrder,
|
||||||
|
policies: values.policies,
|
||||||
|
}).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
|
this.globalEventService.publishInternalEvent('roleCreated', created);
|
||||||
|
|
||||||
|
if (moderator) {
|
||||||
|
this.moderationLogService.log(moderator, 'createRole', {
|
||||||
|
roleId: created.id,
|
||||||
|
role: created,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async update(role: MiRole, params: Partial<MiRole>, moderator?: MiUser): Promise<void> {
|
||||||
|
const date = new Date();
|
||||||
|
await this.rolesRepository.update(role.id, {
|
||||||
|
updatedAt: date,
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updated = await this.rolesRepository.findOneByOrFail({ id: role.id });
|
||||||
|
this.globalEventService.publishInternalEvent('roleUpdated', updated);
|
||||||
|
|
||||||
|
if (moderator) {
|
||||||
|
this.moderationLogService.log(moderator, 'updateRole', {
|
||||||
|
roleId: role.id,
|
||||||
|
before: role,
|
||||||
|
after: updated,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async delete(role: MiRole, moderator?: MiUser): Promise<void> {
|
||||||
|
await this.rolesRepository.delete({ id: role.id });
|
||||||
|
this.globalEventService.publishInternalEvent('roleDeleted', role);
|
||||||
|
|
||||||
|
if (moderator) {
|
||||||
|
this.moderationLogService.log(moderator, 'deleteRole', {
|
||||||
|
roleId: role.id,
|
||||||
|
role: role,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
this.redisForSub.off('message', this.onMessage);
|
this.redisForSub.off('message', this.onMessage);
|
||||||
|
@@ -11,7 +11,7 @@ import type { MiBlocking } from '@/models/Blocking.js';
|
|||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/_.js';
|
import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListMembershipsRepository } from '@/models/_.js';
|
||||||
import Logger from '@/logger.js';
|
import Logger from '@/logger.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
@@ -38,8 +38,8 @@ export class UserBlockingService implements OnModuleInit {
|
|||||||
@Inject(DI.userListsRepository)
|
@Inject(DI.userListsRepository)
|
||||||
private userListsRepository: UserListsRepository,
|
private userListsRepository: UserListsRepository,
|
||||||
|
|
||||||
@Inject(DI.userListJoiningsRepository)
|
@Inject(DI.userListMembershipsRepository)
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||||
|
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
@@ -149,7 +149,7 @@ export class UserBlockingService implements OnModuleInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const userList of userLists) {
|
for (const userList of userLists) {
|
||||||
await this.userListJoiningsRepository.delete({
|
await this.userListMembershipsRepository.delete({
|
||||||
userListId: userList.id,
|
userListId: userList.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
@@ -123,7 +123,11 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
|
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
|
||||||
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである
|
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである
|
||||||
// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
|
// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
|
||||||
if (followee.isLocked || (followeeProfile.carefulBot && follower.isBot) || (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee))) {
|
if (
|
||||||
|
followee.isLocked ||
|
||||||
|
(followeeProfile.carefulBot && follower.isBot) ||
|
||||||
|
(this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true')
|
||||||
|
) {
|
||||||
let autoAccept = false;
|
let autoAccept = false;
|
||||||
|
|
||||||
// 鍵アカウントであっても、既にフォローされていた場合はスルー
|
// 鍵アカウントであっても、既にフォローされていた場合はスルー
|
||||||
@@ -230,8 +234,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
|
|
||||||
// 通知を作成
|
// 通知を作成
|
||||||
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
|
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
|
||||||
notifierId: followee.id,
|
}, followee.id);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alreadyFollowed) return;
|
if (alreadyFollowed) return;
|
||||||
@@ -304,8 +307,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
|
|
||||||
// 通知を作成
|
// 通知を作成
|
||||||
this.notificationService.createNotification(followee.id, 'follow', {
|
this.notificationService.createNotification(followee.id, 'follow', {
|
||||||
notifierId: follower.id,
|
}, follower.id);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,9 +490,8 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
|
|
||||||
// 通知を作成
|
// 通知を作成
|
||||||
this.notificationService.createNotification(followee.id, 'receiveFollowRequest', {
|
this.notificationService.createNotification(followee.id, 'receiveFollowRequest', {
|
||||||
notifierId: follower.id,
|
|
||||||
followRequestId: followRequest.id,
|
followRequestId: followRequest.id,
|
||||||
});
|
}, follower.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||||
|
@@ -3,11 +3,12 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import type { UserListJoiningsRepository } from '@/models/_.js';
|
import * as Redis from 'ioredis';
|
||||||
|
import type { UserListMembershipsRepository } from '@/models/_.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import type { MiUserList } from '@/models/UserList.js';
|
import type { MiUserList } from '@/models/UserList.js';
|
||||||
import type { MiUserListJoining } from '@/models/UserListJoining.js';
|
import type { MiUserListMembership } from '@/models/UserListMembership.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
@@ -16,14 +17,24 @@ import { ProxyAccountService } from '@/core/ProxyAccountService.js';
|
|||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
import { RedisKVCache } from '@/misc/cache.js';
|
||||||
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserListService {
|
export class UserListService implements OnApplicationShutdown {
|
||||||
public static TooManyUsersError = class extends Error {};
|
public static TooManyUsersError = class extends Error {};
|
||||||
|
|
||||||
|
public membersCache: RedisKVCache<Set<string>>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.userListJoiningsRepository)
|
@Inject(DI.redis)
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
|
@Inject(DI.redisForSub)
|
||||||
|
private redisForSub: Redis.Redis,
|
||||||
|
|
||||||
|
@Inject(DI.userListMembershipsRepository)
|
||||||
|
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
@@ -32,24 +43,63 @@ export class UserListService {
|
|||||||
private proxyAccountService: ProxyAccountService,
|
private proxyAccountService: ProxyAccountService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
) {
|
) {
|
||||||
|
this.membersCache = new RedisKVCache<Set<string>>(this.redisClient, 'userListMembers', {
|
||||||
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
|
memoryCacheLifetime: 1000 * 60, // 1m
|
||||||
|
fetcher: (key) => this.userListMembershipsRepository.find({ where: { userListId: key }, select: ['userId'] }).then(xs => new Set(xs.map(x => x.userId))),
|
||||||
|
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||||
|
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.redisForSub.on('message', this.onMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async push(target: MiUser, list: MiUserList, me: MiUser) {
|
private async onMessage(_: string, data: string): Promise<void> {
|
||||||
const currentCount = await this.userListJoiningsRepository.countBy({
|
const obj = JSON.parse(data);
|
||||||
|
|
||||||
|
if (obj.channel === 'internal') {
|
||||||
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
|
switch (type) {
|
||||||
|
case 'userListMemberAdded': {
|
||||||
|
const { userListId, memberId } = body;
|
||||||
|
const members = await this.membersCache.get(userListId);
|
||||||
|
if (members) {
|
||||||
|
members.add(memberId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'userListMemberRemoved': {
|
||||||
|
const { userListId, memberId } = body;
|
||||||
|
const members = await this.membersCache.get(userListId);
|
||||||
|
if (members) {
|
||||||
|
members.delete(memberId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async addMember(target: MiUser, list: MiUserList, me: MiUser) {
|
||||||
|
const currentCount = await this.userListMembershipsRepository.countBy({
|
||||||
userListId: list.id,
|
userListId: list.id,
|
||||||
});
|
});
|
||||||
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
|
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
|
||||||
throw new UserListService.TooManyUsersError();
|
throw new UserListService.TooManyUsersError();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.userListJoiningsRepository.insert({
|
await this.userListMembershipsRepository.insert({
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
userId: target.id,
|
userId: target.id,
|
||||||
userListId: list.id,
|
userListId: list.id,
|
||||||
} as MiUserListJoining);
|
} as MiUserListMembership);
|
||||||
|
|
||||||
|
this.globalEventService.publishInternalEvent('userListMemberAdded', { userListId: list.id, memberId: target.id });
|
||||||
this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target));
|
this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target));
|
||||||
|
|
||||||
// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
|
// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
|
||||||
@@ -60,4 +110,44 @@ export class UserListService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async removeMember(target: MiUser, list: MiUserList) {
|
||||||
|
await this.userListMembershipsRepository.delete({
|
||||||
|
userId: target.id,
|
||||||
|
userListId: list.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.globalEventService.publishInternalEvent('userListMemberRemoved', { userListId: list.id, memberId: target.id });
|
||||||
|
this.globalEventService.publishUserListStream(list.id, 'userRemoved', await this.userEntityService.pack(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async updateMembership(target: MiUser, list: MiUserList, options: { withReplies?: boolean }) {
|
||||||
|
const membership = await this.userListMembershipsRepository.findOneBy({
|
||||||
|
userId: target.id,
|
||||||
|
userListId: list.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (membership == null) {
|
||||||
|
throw new Error('User is not a member of the list');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.userListMembershipsRepository.update({
|
||||||
|
id: membership.id,
|
||||||
|
}, {
|
||||||
|
withReplies: options.withReplies,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public dispose(): void {
|
||||||
|
this.redisForSub.off('message', this.onMessage);
|
||||||
|
this.membersCache.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public onApplicationShutdown(signal?: string | undefined): void {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
53
packages/backend/src/core/UserService.ts
Normal file
53
packages/backend/src/core/UserService.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import type { FollowingsRepository, UsersRepository } from '@/models/_.js';
|
||||||
|
import type { MiUser } from '@/models/User.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserService {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@Inject(DI.followingsRepository)
|
||||||
|
private followingsRepository: FollowingsRepository,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async updateLastActiveDate(user: MiUser): Promise<void> {
|
||||||
|
if (user.isHibernated) {
|
||||||
|
const result = await this.usersRepository.createQueryBuilder().update()
|
||||||
|
.set({
|
||||||
|
lastActiveDate: new Date(),
|
||||||
|
})
|
||||||
|
.where('id = :id', { id: user.id })
|
||||||
|
.returning('*')
|
||||||
|
.execute()
|
||||||
|
.then((response) => {
|
||||||
|
return response.raw[0];
|
||||||
|
});
|
||||||
|
const wokeUp = result.isHibernated;
|
||||||
|
if (wokeUp) {
|
||||||
|
this.usersRepository.update(user.id, {
|
||||||
|
isHibernated: false,
|
||||||
|
});
|
||||||
|
this.followingsRepository.update({
|
||||||
|
followerId: user.id,
|
||||||
|
}, {
|
||||||
|
isFollowerHibernated: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.usersRepository.update(user.id, {
|
||||||
|
lastActiveDate: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -9,7 +9,7 @@ import type { WebhooksRepository } from '@/models/_.js';
|
|||||||
import type { MiWebhook } from '@/models/Webhook.js';
|
import type { MiWebhook } from '@/models/Webhook.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -45,7 +45,7 @@ export class WebhookService implements OnApplicationShutdown {
|
|||||||
const obj = JSON.parse(data);
|
const obj = JSON.parse(data);
|
||||||
|
|
||||||
if (obj.channel === 'internal') {
|
if (obj.channel === 'internal') {
|
||||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'webhookCreated':
|
case 'webhookCreated':
|
||||||
if (body.active) {
|
if (body.active) {
|
||||||
|
@@ -98,13 +98,13 @@ export class NoteEntityService implements OnModuleInit {
|
|||||||
} else if (meId === packedNote.userId) {
|
} else if (meId === packedNote.userId) {
|
||||||
hide = false;
|
hide = false;
|
||||||
} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
|
} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
|
||||||
// 自分の投稿に対するリプライ
|
// 自分の投稿に対するリプライ
|
||||||
hide = false;
|
hide = false;
|
||||||
} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
|
} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
|
||||||
// 自分へのメンション
|
// 自分へのメンション
|
||||||
hide = false;
|
hide = false;
|
||||||
} else {
|
} else {
|
||||||
// フォロワーかどうか
|
// フォロワーかどうか
|
||||||
const isFollowing = await this.followingsRepository.exist({
|
const isFollowing = await this.followingsRepository.exist({
|
||||||
where: {
|
where: {
|
||||||
followeeId: packedNote.userId,
|
followeeId: packedNote.userId,
|
||||||
@@ -450,19 +450,4 @@ export class NoteEntityService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[];
|
return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public async countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise<number> {
|
|
||||||
// 指定したユーザーの指定したノートのリノートがいくつあるか数える
|
|
||||||
const query = this.notesRepository.createQueryBuilder('note')
|
|
||||||
.where('note.userId = :userId', { userId })
|
|
||||||
.andWhere('note.renoteId = :renoteId', { renoteId });
|
|
||||||
|
|
||||||
// 指定した投稿を除く
|
|
||||||
if (excludeNoteId) {
|
|
||||||
query.andWhere('note.id != :excludeNoteId', { excludeNoteId });
|
|
||||||
}
|
|
||||||
|
|
||||||
return await query.getCount();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -146,64 +146,76 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getRelation(me: MiUser['id'], target: MiUser['id']) {
|
public async getRelation(me: MiUser['id'], target: MiUser['id']) {
|
||||||
const following = await this.followingsRepository.findOneBy({
|
const [
|
||||||
followerId: me,
|
|
||||||
followeeId: target,
|
|
||||||
});
|
|
||||||
return awaitAll({
|
|
||||||
id: target,
|
|
||||||
following,
|
following,
|
||||||
isFollowing: following != null,
|
isFollowed,
|
||||||
isFollowed: this.followingsRepository.count({
|
hasPendingFollowRequestFromYou,
|
||||||
|
hasPendingFollowRequestToYou,
|
||||||
|
isBlocking,
|
||||||
|
isBlocked,
|
||||||
|
isMuted,
|
||||||
|
isRenoteMuted,
|
||||||
|
] = await Promise.all([
|
||||||
|
this.followingsRepository.findOneBy({
|
||||||
|
followerId: me,
|
||||||
|
followeeId: target,
|
||||||
|
}),
|
||||||
|
this.followingsRepository.exist({
|
||||||
where: {
|
where: {
|
||||||
followerId: target,
|
followerId: target,
|
||||||
followeeId: me,
|
followeeId: me,
|
||||||
},
|
},
|
||||||
take: 1,
|
}),
|
||||||
}).then(n => n > 0),
|
this.followRequestsRepository.exist({
|
||||||
hasPendingFollowRequestFromYou: this.followRequestsRepository.count({
|
|
||||||
where: {
|
where: {
|
||||||
followerId: me,
|
followerId: me,
|
||||||
followeeId: target,
|
followeeId: target,
|
||||||
},
|
},
|
||||||
take: 1,
|
}),
|
||||||
}).then(n => n > 0),
|
this.followRequestsRepository.exist({
|
||||||
hasPendingFollowRequestToYou: this.followRequestsRepository.count({
|
|
||||||
where: {
|
where: {
|
||||||
followerId: target,
|
followerId: target,
|
||||||
followeeId: me,
|
followeeId: me,
|
||||||
},
|
},
|
||||||
take: 1,
|
}),
|
||||||
}).then(n => n > 0),
|
this.blockingsRepository.exist({
|
||||||
isBlocking: this.blockingsRepository.count({
|
|
||||||
where: {
|
where: {
|
||||||
blockerId: me,
|
blockerId: me,
|
||||||
blockeeId: target,
|
blockeeId: target,
|
||||||
},
|
},
|
||||||
take: 1,
|
}),
|
||||||
}).then(n => n > 0),
|
this.blockingsRepository.exist({
|
||||||
isBlocked: this.blockingsRepository.count({
|
|
||||||
where: {
|
where: {
|
||||||
blockerId: target,
|
blockerId: target,
|
||||||
blockeeId: me,
|
blockeeId: me,
|
||||||
},
|
},
|
||||||
take: 1,
|
}),
|
||||||
}).then(n => n > 0),
|
this.mutingsRepository.exist({
|
||||||
isMuted: this.mutingsRepository.count({
|
|
||||||
where: {
|
where: {
|
||||||
muterId: me,
|
muterId: me,
|
||||||
muteeId: target,
|
muteeId: target,
|
||||||
},
|
},
|
||||||
take: 1,
|
}),
|
||||||
}).then(n => n > 0),
|
this.renoteMutingsRepository.exist({
|
||||||
isRenoteMuted: this.renoteMutingsRepository.count({
|
|
||||||
where: {
|
where: {
|
||||||
muterId: me,
|
muterId: me,
|
||||||
muteeId: target,
|
muteeId: target,
|
||||||
},
|
},
|
||||||
take: 1,
|
}),
|
||||||
}).then(n => n > 0),
|
]);
|
||||||
});
|
|
||||||
|
return {
|
||||||
|
id: target,
|
||||||
|
following,
|
||||||
|
isFollowing: following != null,
|
||||||
|
isFollowed,
|
||||||
|
hasPendingFollowRequestFromYou,
|
||||||
|
hasPendingFollowRequestToYou,
|
||||||
|
isBlocking,
|
||||||
|
isBlocked,
|
||||||
|
isMuted,
|
||||||
|
isRenoteMuted,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -290,24 +302,6 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
|
|
||||||
const user = typeof src === 'object' ? src : await this.usersRepository.findOneByOrFail({ id: src });
|
const user = typeof src === 'object' ? src : await this.usersRepository.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
// migration
|
|
||||||
if (user.avatarId != null && user.avatarUrl === null) {
|
|
||||||
const avatar = await this.driveFilesRepository.findOneByOrFail({ id: user.avatarId });
|
|
||||||
user.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar');
|
|
||||||
this.usersRepository.update(user.id, {
|
|
||||||
avatarUrl: user.avatarUrl,
|
|
||||||
avatarBlurhash: avatar.blurhash,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (user.bannerId != null && user.bannerUrl === null) {
|
|
||||||
const banner = await this.driveFilesRepository.findOneByOrFail({ id: user.bannerId });
|
|
||||||
user.bannerUrl = this.driveFileEntityService.getPublicUrl(banner);
|
|
||||||
this.usersRepository.update(user.id, {
|
|
||||||
bannerUrl: user.bannerUrl,
|
|
||||||
bannerBlurhash: banner.blurhash,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const meId = me ? me.id : null;
|
const meId = me ? me.id : null;
|
||||||
const isMe = meId === user.id;
|
const isMe = meId === user.id;
|
||||||
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
|
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
|
||||||
@@ -452,7 +446,8 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
||||||
mutedWords: profile!.mutedWords,
|
mutedWords: profile!.mutedWords,
|
||||||
mutedInstances: profile!.mutedInstances,
|
mutedInstances: profile!.mutedInstances,
|
||||||
mutingNotificationTypes: profile!.mutingNotificationTypes,
|
mutingNotificationTypes: [], // 後方互換性のため
|
||||||
|
notificationRecieveConfig: profile!.notificationRecieveConfig,
|
||||||
emailNotificationTypes: profile!.emailNotificationTypes,
|
emailNotificationTypes: profile!.emailNotificationTypes,
|
||||||
achievements: profile!.achievements,
|
achievements: profile!.achievements,
|
||||||
loggedInDays: profile!.loggedInDates.length,
|
loggedInDays: profile!.loggedInDates.length,
|
||||||
@@ -486,6 +481,7 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
isMuted: relation.isMuted,
|
isMuted: relation.isMuted,
|
||||||
isRenoteMuted: relation.isRenoteMuted,
|
isRenoteMuted: relation.isRenoteMuted,
|
||||||
notify: relation.following?.notify ?? 'none',
|
notify: relation.following?.notify ?? 'none',
|
||||||
|
withReplies: relation.following?.withReplies ?? false,
|
||||||
} : {}),
|
} : {}),
|
||||||
} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;
|
} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;
|
||||||
|
|
||||||
|
@@ -5,11 +5,12 @@
|
|||||||
|
|
||||||
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 { UserListJoiningsRepository, UserListsRepository } from '@/models/_.js';
|
import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import type { } from '@/models/Blocking.js';
|
import type { } from '@/models/Blocking.js';
|
||||||
import type { MiUserList } from '@/models/UserList.js';
|
import type { MiUserList } from '@/models/UserList.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { UserEntityService } from './UserEntityService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserListEntityService {
|
export class UserListEntityService {
|
||||||
@@ -17,8 +18,10 @@ export class UserListEntityService {
|
|||||||
@Inject(DI.userListsRepository)
|
@Inject(DI.userListsRepository)
|
||||||
private userListsRepository: UserListsRepository,
|
private userListsRepository: UserListsRepository,
|
||||||
|
|
||||||
@Inject(DI.userListJoiningsRepository)
|
@Inject(DI.userListMembershipsRepository)
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||||
|
|
||||||
|
private userEntityService: UserEntityService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +31,7 @@ export class UserListEntityService {
|
|||||||
): Promise<Packed<'UserList'>> {
|
): Promise<Packed<'UserList'>> {
|
||||||
const userList = typeof src === 'object' ? src : await this.userListsRepository.findOneByOrFail({ id: src });
|
const userList = typeof src === 'object' ? src : await this.userListsRepository.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
const users = await this.userListJoiningsRepository.findBy({
|
const users = await this.userListMembershipsRepository.findBy({
|
||||||
userListId: userList.id,
|
userListId: userList.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,5 +43,18 @@ export class UserListEntityService {
|
|||||||
isPublic: userList.isPublic,
|
isPublic: userList.isPublic,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async packMembershipsMany(
|
||||||
|
memberships: MiUserListMembership[],
|
||||||
|
) {
|
||||||
|
return Promise.all(memberships.map(async x => ({
|
||||||
|
id: x.id,
|
||||||
|
createdAt: x.createdAt.toISOString(),
|
||||||
|
userId: x.userId,
|
||||||
|
user: await this.userEntityService.pack(x.userId),
|
||||||
|
withReplies: x.withReplies,
|
||||||
|
})));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -10,6 +10,7 @@ export const DI = {
|
|||||||
redis: Symbol('redis'),
|
redis: Symbol('redis'),
|
||||||
redisForPub: Symbol('redisForPub'),
|
redisForPub: Symbol('redisForPub'),
|
||||||
redisForSub: Symbol('redisForSub'),
|
redisForSub: Symbol('redisForSub'),
|
||||||
|
redisForTimelines: Symbol('redisForTimelines'),
|
||||||
|
|
||||||
//#region Repositories
|
//#region Repositories
|
||||||
usersRepository: Symbol('usersRepository'),
|
usersRepository: Symbol('usersRepository'),
|
||||||
@@ -30,7 +31,7 @@ export const DI = {
|
|||||||
userPublickeysRepository: Symbol('userPublickeysRepository'),
|
userPublickeysRepository: Symbol('userPublickeysRepository'),
|
||||||
userListsRepository: Symbol('userListsRepository'),
|
userListsRepository: Symbol('userListsRepository'),
|
||||||
userListFavoritesRepository: Symbol('userListFavoritesRepository'),
|
userListFavoritesRepository: Symbol('userListFavoritesRepository'),
|
||||||
userListJoiningsRepository: Symbol('userListJoiningsRepository'),
|
userListMembershipsRepository: Symbol('userListMembershipsRepository'),
|
||||||
userNotePiningsRepository: Symbol('userNotePiningsRepository'),
|
userNotePiningsRepository: Symbol('userNotePiningsRepository'),
|
||||||
userIpsRepository: Symbol('userIpsRepository'),
|
userIpsRepository: Symbol('userIpsRepository'),
|
||||||
usedUsernamesRepository: Symbol('usedUsernamesRepository'),
|
usedUsernamesRepository: Symbol('usedUsernamesRepository'),
|
||||||
@@ -63,7 +64,6 @@ export const DI = {
|
|||||||
promoNotesRepository: Symbol('promoNotesRepository'),
|
promoNotesRepository: Symbol('promoNotesRepository'),
|
||||||
promoReadsRepository: Symbol('promoReadsRepository'),
|
promoReadsRepository: Symbol('promoReadsRepository'),
|
||||||
relaysRepository: Symbol('relaysRepository'),
|
relaysRepository: Symbol('relaysRepository'),
|
||||||
mutedNotesRepository: Symbol('mutedNotesRepository'),
|
|
||||||
channelsRepository: Symbol('channelsRepository'),
|
channelsRepository: Symbol('channelsRepository'),
|
||||||
channelFollowingsRepository: Symbol('channelFollowingsRepository'),
|
channelFollowingsRepository: Symbol('channelFollowingsRepository'),
|
||||||
channelFavoritesRepository: Symbol('channelFavoritesRepository'),
|
channelFavoritesRepository: Symbol('channelFavoritesRepository'),
|
||||||
|
@@ -3,16 +3,16 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function isUserRelated(note: any, userIds: Set<string>): boolean {
|
export function isUserRelated(note: any, userIds: Set<string>, ignoreAuthor = false): boolean {
|
||||||
if (userIds.has(note.userId)) {
|
if (userIds.has(note.userId) && !ignoreAuthor) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.reply != null && userIds.has(note.reply.userId)) {
|
if (note.reply != null && note.reply.userId !== note.userId && userIds.has(note.reply.userId)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.renote != null && userIds.has(note.renote.userId)) {
|
if (note.renote != null && note.renote.userId !== note.userId && userIds.has(note.renote.userId)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -9,6 +9,7 @@ import { MiUser } from './User.js';
|
|||||||
|
|
||||||
@Entity('following')
|
@Entity('following')
|
||||||
@Index(['followerId', 'followeeId'], { unique: true })
|
@Index(['followerId', 'followeeId'], { unique: true })
|
||||||
|
@Index(['followeeId', 'followerHost', 'isFollowerHibernated'])
|
||||||
export class MiFollowing {
|
export class MiFollowing {
|
||||||
@PrimaryColumn(id())
|
@PrimaryColumn(id())
|
||||||
public id: string;
|
public id: string;
|
||||||
@@ -45,6 +46,17 @@ export class MiFollowing {
|
|||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
public follower: MiUser | null;
|
public follower: MiUser | null;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public isFollowerHibernated: boolean;
|
||||||
|
|
||||||
|
// タイムラインにその人のリプライまで含めるかどうか
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public withReplies: boolean;
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 32,
|
length: 32,
|
||||||
|
@@ -335,6 +335,18 @@ export class MiMeta {
|
|||||||
})
|
})
|
||||||
public feedbackUrl: string | null;
|
public feedbackUrl: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1024,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public impressumUrl: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1024,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public privacyPolicyUrl: string | null;
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 8192,
|
length: 8192,
|
||||||
nullable: true,
|
nullable: true,
|
||||||
@@ -471,4 +483,24 @@ export class MiMeta {
|
|||||||
length: 1024, array: true, default: '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }',
|
length: 1024, array: true, default: '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }',
|
||||||
})
|
})
|
||||||
public preservedUsernames: string[];
|
public preservedUsernames: string[];
|
||||||
|
|
||||||
|
@Column('integer', {
|
||||||
|
default: 300,
|
||||||
|
})
|
||||||
|
public perLocalUserUserTimelineCacheMax: number;
|
||||||
|
|
||||||
|
@Column('integer', {
|
||||||
|
default: 100,
|
||||||
|
})
|
||||||
|
public perRemoteUserUserTimelineCacheMax: number;
|
||||||
|
|
||||||
|
@Column('integer', {
|
||||||
|
default: 300,
|
||||||
|
})
|
||||||
|
public perUserHomeTimelineCacheMax: number;
|
||||||
|
|
||||||
|
@Column('integer', {
|
||||||
|
default: 300,
|
||||||
|
})
|
||||||
|
public perUserListTimelineCacheMax: number;
|
||||||
}
|
}
|
||||||
|
@@ -1,53 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm';
|
|
||||||
import { mutedNoteReasons } from '@/types.js';
|
|
||||||
import { id } from './util/id.js';
|
|
||||||
import { MiNote } from './Note.js';
|
|
||||||
import { MiUser } from './User.js';
|
|
||||||
|
|
||||||
@Entity('muted_note')
|
|
||||||
@Index(['noteId', 'userId'], { unique: true })
|
|
||||||
export class MiMutedNote {
|
|
||||||
@PrimaryColumn(id())
|
|
||||||
public id: string;
|
|
||||||
|
|
||||||
@Index()
|
|
||||||
@Column({
|
|
||||||
...id(),
|
|
||||||
comment: 'The note ID.',
|
|
||||||
})
|
|
||||||
public noteId: MiNote['id'];
|
|
||||||
|
|
||||||
@ManyToOne(type => MiNote, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn()
|
|
||||||
public note: MiNote | null;
|
|
||||||
|
|
||||||
@Index()
|
|
||||||
@Column({
|
|
||||||
...id(),
|
|
||||||
comment: 'The user ID.',
|
|
||||||
})
|
|
||||||
public userId: MiUser['id'];
|
|
||||||
|
|
||||||
@ManyToOne(type => MiUser, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn()
|
|
||||||
public user: MiUser | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ミュートされた理由。
|
|
||||||
*/
|
|
||||||
@Index()
|
|
||||||
@Column('enum', {
|
|
||||||
enum: mutedNoteReasons,
|
|
||||||
comment: 'The reason of the MutedNote.',
|
|
||||||
})
|
|
||||||
public reason: typeof mutedNoteReasons[number];
|
|
||||||
}
|
|
@@ -18,7 +18,6 @@ export class MiNote {
|
|||||||
@PrimaryColumn(id())
|
@PrimaryColumn(id())
|
||||||
public id: string;
|
public id: string;
|
||||||
|
|
||||||
@Index()
|
|
||||||
@Column('timestamp with time zone', {
|
@Column('timestamp with time zone', {
|
||||||
comment: 'The created date of the Note.',
|
comment: 'The created date of the Note.',
|
||||||
})
|
})
|
||||||
@@ -139,11 +138,6 @@ export class MiNote {
|
|||||||
})
|
})
|
||||||
public url: string | null;
|
public url: string | null;
|
||||||
|
|
||||||
@Column('integer', {
|
|
||||||
default: 0, select: false,
|
|
||||||
})
|
|
||||||
public score: number;
|
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column({
|
@Column({
|
||||||
...id(),
|
...id(),
|
||||||
@@ -151,7 +145,6 @@ export class MiNote {
|
|||||||
})
|
})
|
||||||
public fileIds: MiDriveFile['id'][];
|
public fileIds: MiDriveFile['id'][];
|
||||||
|
|
||||||
@Index()
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 256, array: true, default: '{}',
|
length: 256, array: true, default: '{}',
|
||||||
})
|
})
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user