Merge pull request #10506 from misskey-dev/develop

13.11.0
This commit is contained in:
syuilo 2023-04-08 21:27:21 +09:00 committed by GitHub
commit a096f621cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
419 changed files with 22473 additions and 7365 deletions

View File

@ -51,6 +51,23 @@ db:
#extra: #extra:
# ssl: true # ssl: true
dbReplications: false
# You can configure any number of replicas here
#dbSlaves:
# -
# host:
# port:
# db:
# user:
# pass:
# -
# host:
# port:
# db:
# user:
# pass:
# ┌─────────────────────┐ # ┌─────────────────────┐
#───┘ Redis configuration └───────────────────────────────────── #───┘ Redis configuration └─────────────────────────────────────
@ -62,6 +79,22 @@ redis:
#prefix: example-prefix #prefix: example-prefix
#db: 1 #db: 1
#redisForPubsub:
# host: redis
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
#redisForJobQueue:
# host: redis
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# ┌─────────────────────────────┐ # ┌─────────────────────────────┐
#───┘ Elasticsearch configuration └───────────────────────────── #───┘ Elasticsearch configuration └─────────────────────────────

View File

@ -51,6 +51,23 @@ db:
#extra: #extra:
# ssl: true # ssl: true
dbReplications: false
# You can configure any number of replicas here
#dbSlaves:
# -
# host:
# port:
# db:
# user:
# pass:
# -
# host:
# port:
# db:
# user:
# pass:
# ┌─────────────────────┐ # ┌─────────────────────┐
#───┘ Redis configuration └───────────────────────────────────── #───┘ Redis configuration └─────────────────────────────────────
@ -62,6 +79,22 @@ redis:
#prefix: example-prefix #prefix: example-prefix
#db: 1 #db: 1
#redisForPubsub:
# host: localhost
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
#redisForJobQueue:
# host: localhost
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# ┌─────────────────────────────┐ # ┌─────────────────────────────┐
#───┘ Elasticsearch configuration └───────────────────────────── #───┘ Elasticsearch configuration └─────────────────────────────

View File

@ -51,6 +51,23 @@ db:
#extra: #extra:
# ssl: true # ssl: true
dbReplications: false
# You can configure any number of replicas here
#dbSlaves:
# -
# host:
# port:
# db:
# user:
# pass:
# -
# host:
# port:
# db:
# user:
# pass:
# ┌─────────────────────┐ # ┌─────────────────────┐
#───┘ Redis configuration └───────────────────────────────────── #───┘ Redis configuration └─────────────────────────────────────
@ -62,6 +79,22 @@ redis:
#prefix: example-prefix #prefix: example-prefix
#db: 1 #db: 1
#redisForPubsub:
# host: redis
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
#redisForJobQueue:
# host: redis
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# ┌─────────────────────────────┐ # ┌─────────────────────────────┐
#───┘ Elasticsearch configuration └───────────────────────────── #───┘ Elasticsearch configuration └─────────────────────────────

36
.github/workflows/api-misskey-js.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: API report (misskey.js)
on: [push, pull_request]
jobs:
report:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3.3.0
- run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v3.6.0
with:
node-version: 18.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm i --frozen-lockfile
- name: Build
run: pnpm --filter misskey-js build
- name: Check files
run: ls packages/misskey-js/built
- name: API report
run: pnpm --filter misskey-js api-prod
- name: Show report
if: always()
run: cat packages/misskey-js/temp/misskey-js.api.md

View File

@ -36,6 +36,7 @@ jobs:
- backend - backend
- frontend - frontend
- sw - sw
- misskey-js
steps: steps:
- uses: actions/checkout@v3.3.0 - uses: actions/checkout@v3.3.0
with: with:
@ -61,6 +62,7 @@ jobs:
matrix: matrix:
workspace: workspace:
- backend - backend
- misskey-js
steps: steps:
- uses: actions/checkout@v3.3.0 - uses: actions/checkout@v3.3.0
with: with:

74
.github/workflows/storybook.yml vendored Normal file
View File

@ -0,0 +1,74 @@
name: Storybook
on:
push:
branches-ignore:
- l10n_develop
jobs:
build:
runs-on: ubuntu-latest
env:
NODE_OPTIONS: "--max_old_space_size=7168"
steps:
- uses: actions/checkout@v3.3.0
with:
fetch-depth: 0
submodules: true
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 7
run_install: false
- name: Use Node.js 18.x
uses: actions/setup-node@v3.6.0
with:
node-version: 18.x
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
run: git diff --exit-code pnpm-lock.yaml
- name: Build misskey-js
run: pnpm --filter misskey-js build
- name: Build storybook
run: pnpm --filter frontend build-storybook
- name: Publish to Chromatic
if: github.ref == 'refs/heads/master'
run: pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- name: Publish to Chromatic
if: github.ref != 'refs/heads/master'
id: chromatic
run: |
DIFF="${{ github.event.before }} HEAD"
if [ "$DIFF" = "0000000000000000000000000000000000000000 HEAD" ]; then
DIFF="HEAD"
fi
CHROMATIC_PARAMETER="$(node packages/frontend/.storybook/changes.js $(git diff-tree --no-commit-id --name-only -r $(echo "$DIFF") | xargs))"
if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
fi
pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static $(echo "$CHROMATIC_PARAMETER")
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- name: Notify that Chromatic will skip testing
uses: actions/github-script@v6.4.0
if: github.ref != 'refs/heads/master' && github.ref != 'refs/heads/develop' && steps.chromatic.outputs.skip == 'true'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.repos.createCommitComment({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: context.sha,
body: 'Chromatic will skip testing but you may still have to [review the changes on Chromatic](https://www.chromatic.com/pullrequests?appId=6428f7d7b962f0b79f97d6e4).'
})
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: storybook
path: packages/frontend/storybook-static

View File

@ -24,7 +24,7 @@ jobs:
POSTGRES_DB: test-misskey POSTGRES_DB: test-misskey
POSTGRES_HOST_AUTH_METHOD: trust POSTGRES_HOST_AUTH_METHOD: trust
redis: redis:
image: redis:6 image: redis:7
ports: ports:
- 56312:6379 - 56312:6379

View File

@ -63,7 +63,7 @@ jobs:
POSTGRES_DB: test-misskey POSTGRES_DB: test-misskey
POSTGRES_HOST_AUTH_METHOD: trust POSTGRES_HOST_AUTH_METHOD: trust
redis: redis:
image: redis:6 image: redis:7
ports: ports:
- 56312:6379 - 56312:6379

52
.github/workflows/test-misskey-js.yml vendored Normal file
View File

@ -0,0 +1,52 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Test (misskey.js)
on:
push:
branches: [ develop ]
pull_request:
branches: [ develop ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- name: Checkout
uses: actions/checkout@v3.3.0
- run: corepack enable
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3.6.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
run: git diff --exit-code pnpm-lock.yaml
- name: Build
run: pnpm --filter misskey-js build
- name: Test
run: pnpm --filter misskey-js test
env:
CI: true
- name: Upload Coverage
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/misskey-js/coverage/coverage-final.json

2
.gitignore vendored
View File

@ -55,6 +55,8 @@ api-docs.json
.DS_Store .DS_Store
/files /files
ormconfig.json ormconfig.json
temp
/packages/frontend/src/**/*.stories.ts
# blender backups # blender backups
*.blend1 *.blend1

View File

@ -12,7 +12,48 @@
--> -->
## 13.10.3 (unreleased) ## 13.11.0
### NOTE
- このバージョンからRedis 7.xが必要です。
- アップデートを行うと全ての通知およびアンテナのノートはリセットされます。
### General
- チャンネルをお気に入りに登録できるように
- チャンネルにノートをピン留めできるように
### Client
- 投稿フォームのデザインを改善
- 検索ページでURLを入力した際に照会したときと同等の挙動をするように
- ノートのリアクションを大きく表示するオプションを追加
- ギャラリー一覧にメディア表示と同じように NSFW 設定を反映するように(ホバーで表示)
- オブジェクトストレージの設定画面を分かりやすく
- 広告・お知らせが新規登録時に増殖しないように
- 「にゃああああああああああああああ!!!!!!!!!!!!」 (`isCat`) 有効時にアバターに表示される猫耳について挙動を変更
- 「UIにぼかし効果を使用」 (`useBlurEffect`) で次の挙動が有効になります
- 猫耳のアバター内部部分をぼかしでマスク表示してより猫耳っぽく見えるように
- 「UIのアニメーションを減らす」 (`reduceAnimation`) で猫耳を撫でられなくなります
- Add Minimizing ("folding") of windows
### Server
- PostgreSQLのレプリケーション対応
- 設定ファイルの `dbReplications` および `dbSlaves` にて設定できます
- イベント用Redisを別サーバーに分離できるように
- ジョブキュー用Redisを別サーバーに分離できるように
- サーバーの全体的なパフォーマンスを向上
- ノート作成時のパフォーマンスを向上
- アンテナのタイムライン取得時のパフォーマンスを向上
- チャンネルのタイムライン取得時のパフォーマンスを向上
- 通知に関する全体的なパフォーマンスを向上
- webhookがcontent-type text/plain;charset=UTF-8 で飛んでくる問題を修正
## 13.10.3
### Changes
- オブジェクトストレージのリージョン指定が必須になりました
- リージョンの指定の無いサービスは us-east-1 を設定してください
- 値が空の場合は設定ファイルまたは環境変数の使用を試みます
- e.g. ~/aws/config, AWS_REGION
### General ### General
- コンディショナルロールの条件に「投稿数が~以下」「投稿数が~以上」を追加 - コンディショナルロールの条件に「投稿数が~以下」「投稿数が~以上」を追加

View File

@ -203,6 +203,116 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
vue-routerとの最大の違いは、niraxは複数のルーターが存在することを許可している点です。 vue-routerとの最大の違いは、niraxは複数のルーターが存在することを許可している点です。
これにより、アプリ内ウィンドウでブラウザとは個別にルーティングすることなどが可能になります。 これにより、アプリ内ウィンドウでブラウザとは個別にルーティングすることなどが可能になります。
## Storybook
Misskey uses [Storybook](https://storybook.js.org/) for UI development.
### Setup & Run
#### Universal
##### Setup
```bash
pnpm --filter misskey-js build
pnpm --filter frontend tsc -p .storybook && (node packages/frontend/.storybook/preload-locale.js & node packages/frontend/.storybook/preload-theme.js)
```
##### Run
```bash
node packages/frontend/.storybook/generate.js && pnpm --filter frontend storybook dev
```
#### macOS & Linux
##### Setup
```bash
pnpm --filter misskey-js build
```
##### Run
```bash
pnpm --filter frontend storybook-dev
```
### Usage
When you create a new component (in this example, `MyComponent.vue`), the story file (`MyComponent.stories.ts`) will be automatically generated by the `.storybook/generate.js` script.
You can override the default story by creating a impl story file (`MyComponent.stories.impl.ts`).
```ts
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable import/no-duplicates */
import { StoryObj } from '@storybook/vue3';
import MyComponent from './MyComponent.vue';
export const Default = {
render(args) {
return {
components: {
MyComponent,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
},
template: '<MyComponent v-bind="props" />',
};
},
args: {
foo: 'bar',
},
parameters: {
layout: 'centered',
},
} satisfies StoryObj<typeof MkAvatar>;
```
If you want to opt-out from the automatic generation, create a `MyComponent.stories.impl.ts` file and add the following line to the file.
```ts
import MyComponent from './MyComponent.vue';
void MyComponent;
```
You can override the component meta by creating a meta story file (`MyComponent.stories.meta.ts`).
```ts
export const argTypes = {
scale: {
control: {
type: 'range',
min: 1,
max: 4,
},
};
```
Also, you can use msw to mock API requests in the storybook. Creating a `MyComponent.stories.msw.ts` file to define the mock handlers.
```ts
import { rest } from 'msw';
export const handlers = [
rest.post('/api/notes/timeline', (req, res, ctx) => {
return res(
ctx.json([]),
);
}),
];
```
Don't forget to re-run the `.storybook/generate.js` script after adding, editing, or removing the above files.
## Notes ## Notes
### How to resolve conflictions occurred at pnpm-lock.yaml? ### How to resolve conflictions occurred at pnpm-lock.yaml?

View File

@ -23,6 +23,7 @@ COPY --link ["scripts", "./scripts"]
COPY --link ["packages/backend/package.json", "./packages/backend/"] COPY --link ["packages/backend/package.json", "./packages/backend/"]
COPY --link ["packages/frontend/package.json", "./packages/frontend/"] COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
COPY --link ["packages/sw/package.json", "./packages/sw/"] COPY --link ["packages/sw/package.json", "./packages/sw/"]
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
pnpm i --frozen-lockfile --aggregate-output pnpm i --frozen-lockfile --aggregate-output

View File

@ -54,6 +54,25 @@ With Misskey's built in drive, you get cloud storage right in your social media,
Misskey Documentation can be found at [Misskey Hub](https://misskey-hub.net/), some of the links and graphics above also lead to specific portions of it. Misskey Documentation can be found at [Misskey Hub](https://misskey-hub.net/), some of the links and graphics above also lead to specific portions of it.
## Sponsors ## Sponsors
<div align="center"> <div align="center">
<a class="rss3" title="RSS3" href="https://rss3.io/" target="_blank"><img src="https://rss3.mypinata.cloud/ipfs/QmUG6H3Z7D5P511shn7sB4CPmpjH5uZWu4m5mWX7U3Gqbu" alt="RSS3" height="60"></a> <a class="rss3" title="RSS3" href="https://rss3.io/" target="_blank"><img src="https://rss3.mypinata.cloud/ipfs/QmUG6H3Z7D5P511shn7sB4CPmpjH5uZWu4m5mWX7U3Gqbu" alt="RSS3" height="60"></a>
</div> </div>
## Thanks
<a href="https://www.chromatic.com/"><img src="https://user-images.githubusercontent.com/321738/84662277-e3db4f80-af1b-11ea-88f5-91d67a5e59f6.png" height="30" alt="Chromatic" /></a>
Thanks to [Chromatic](https://www.chromatic.com/) for providing the visual testing platform that helps us review UI changes and catch visual regressions.
<a href="https://about.codecov.io/for/open-source/"><img src="https://about.codecov.io/wp-content/themes/codecov/assets/brand/sentry-cobranding/logos/codecov-by-sentry-logo.svg" height="30" alt="Codecov" /></a>
Thanks to [Codecov](https://about.codecov.io/for/open-source/) for providing the code coverage platform that helps us improve our test coverage.
<a href="https://crowdin.com/"><img src="https://user-images.githubusercontent.com/20679825/230709597-1299a011-171a-4294-a91e-355a9b37c672.svg" height="30" alt="Crowdin" /></a>
Thanks to [Crowdin](https://crowdin.com/) for providing the localization platform that helps us translate Misskey into many languages.
<a href="https://hub.docker.com/"><img src="https://user-images.githubusercontent.com/20679825/230148221-f8e73a32-a49b-47c3-9029-9a15c3824f92.png" height="30" alt="Docker" /></a>
Thanks to [Docker](https://hub.docker.com/) for providing the container platform that helps us run Misskey in production.

View File

@ -72,16 +72,50 @@ db:
#extra: #extra:
# ssl: true # ssl: true
dbReplications: false
# You can configure any number of replicas here
#dbSlaves:
# -
# host:
# port:
# db:
# user:
# pass:
# -
# host:
# port:
# db:
# user:
# pass:
# ┌─────────────────────┐ # ┌─────────────────────┐
#───┘ Redis configuration └───────────────────────────────────── #───┘ Redis configuration └─────────────────────────────────────
redis: redis:
host: localhost host: localhost
port: 6379 port: 6379
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
#pass: example-pass #pass: example-pass
#prefix: example-prefix #prefix: example-prefix
#db: 1 #db: 1
#redisForPubsub:
# host: localhost
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
#redisForJobQueue:
# host: localhost
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# ┌─────────────────────────────┐ # ┌─────────────────────────────┐
#───┘ Elasticsearch configuration └───────────────────────────── #───┘ Elasticsearch configuration └─────────────────────────────

View File

@ -1277,4 +1277,3 @@ _deck:
_webhookSettings: _webhookSettings:
name: "الإسم" name: "الإسم"
active: "مفعّل" active: "مفعّل"

View File

@ -1356,4 +1356,3 @@ _deck:
_webhookSettings: _webhookSettings:
name: "নাম" name: "নাম"
active: "চালু" active: "চালু"

View File

@ -460,4 +460,3 @@ _deck:
list: "Llistes" list: "Llistes"
mentions: "Mencions" mentions: "Mencions"
direct: "Publicacions directes" direct: "Publicacions directes"

View File

@ -779,4 +779,3 @@ _deck:
_webhookSettings: _webhookSettings:
name: "Jméno" name: "Jméno"
active: "Zapnuto" active: "Zapnuto"

View File

@ -1,3 +1,2 @@
--- ---
_lang_: "Dansk" _lang_: "Dansk"

View File

@ -67,7 +67,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? Notizen mit dieser Datei werden ebenso verschwinden." driveFileDeleteConfirm: "Möchtest du die Datei „{name}“ wirklich löschen? Sie wird in allen Inhalten, die sie verwenden, 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."
@ -460,7 +460,7 @@ aboutX: "Über {x}"
emojiStyle: "Emoji-Stil" emojiStyle: "Emoji-Stil"
native: "Nativ" native: "Nativ"
disableDrawer: "Keine ausfahrbaren Menüs verwenden" disableDrawer: "Keine ausfahrbaren Menüs verwenden"
showNoteActionsOnlyHover: "Aktionen für Notizen nur bei Mouseover anzeigen" showNoteActionsOnlyHover: "Notizmenü nur bei Mouseover anzeigen"
noHistory: "Kein Verlauf gefunden" noHistory: "Kein Verlauf gefunden"
signinHistory: "Anmeldungsverlauf" signinHistory: "Anmeldungsverlauf"
enableAdvancedMfm: "Erweitertes MFM aktivieren" enableAdvancedMfm: "Erweitertes MFM aktivieren"
@ -500,12 +500,13 @@ objectStoragePrefixDesc: "Dateien werden in Ordnern unter diesem Prefix gespeich
objectStorageEndpoint: "Endpoint" objectStorageEndpoint: "Endpoint"
objectStorageEndpointDesc: "Im Falle von S3 leerlassen, für andere Anbieter den relevanten Endpoint im Format „<host>“ oder „<host>:<port>“ angeben." objectStorageEndpointDesc: "Im Falle von S3 leerlassen, für andere Anbieter den relevanten Endpoint im Format „<host>“ oder „<host>:<port>“ angeben."
objectStorageRegion: "Region" objectStorageRegion: "Region"
objectStorageRegionDesc: "Gib eine Region wie z.B. „xx-east-1“ an. Falls dein Anbieter nicht zwischen Regionen unterscheidet, lass dieses Feld leer oder gib „us-east-1“ an." objectStorageRegionDesc: "Gib eine Region wie z.B. „xx-east-1“ an. Falls dein Anbieter nicht zwischen Regionen unterscheidet, gib „us-east-1“ an. Lasse es leer bei Verwendung von AWS Konfigurationsdateien oder Umgebungsvariablen."
objectStorageUseSSL: "SSL verwenden" objectStorageUseSSL: "SSL verwenden"
objectStorageUseSSLDesc: "Deaktiviere dies, falls du für API-Verbindungen kein HTTPS verwenden wirst" objectStorageUseSSLDesc: "Deaktiviere dies, falls du für API-Verbindungen kein HTTPS verwenden wirst"
objectStorageUseProxy: "Über Proxy verbinden" objectStorageUseProxy: "Über Proxy verbinden"
objectStorageUseProxyDesc: "Deaktiviere dies, falls du für Verbindungen zur API keinen Proxy verwenden wirst" objectStorageUseProxyDesc: "Deaktiviere dies, falls du für Verbindungen zur API keinen Proxy verwenden wirst"
objectStorageSetPublicRead: "Bei Upload auf \"public-read\" stellen" objectStorageSetPublicRead: "Bei Upload auf \"public-read\" stellen"
s3ForcePathStyleDesc: "Ist s3ForcePathStyle aktiviert, so muss der Bucketname nicht im Hostnamen der URL, sondern im Pfad der URL angeben werden. Diese Option muss eventuell aktiviert werden, wenn Dienste wie z.B. eine selbstbetriebene Minio-Instanz verwendet werden."
serverLogs: "Serverprotokolle" serverLogs: "Serverprotokolle"
deleteAll: "Alle löschen" deleteAll: "Alle löschen"
showFixedPostForm: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik anzeigen" showFixedPostForm: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik anzeigen"
@ -919,6 +920,7 @@ pushNotificationNotSupported: "Entweder dein Browser oder deine Instanz unterst
sendPushNotificationReadMessage: "Push-Benachrichtigungen löschen, sobald die relevanten Benachrichtigungen oder Nachrichten gelesen wurden" sendPushNotificationReadMessage: "Push-Benachrichtigungen löschen, sobald die relevanten Benachrichtigungen oder Nachrichten gelesen wurden"
sendPushNotificationReadMessageCaption: "Eine Push-Benachrichtigungen mit dem Inhalt \"{emptyPushNotificationMessage}\" wird kurz eingeblendet. Dies kann gegebenenfalls den Batterieverbrauch deines Gerätes erhöhen." sendPushNotificationReadMessageCaption: "Eine Push-Benachrichtigungen mit dem Inhalt \"{emptyPushNotificationMessage}\" wird kurz eingeblendet. Dies kann gegebenenfalls den Batterieverbrauch deines Gerätes erhöhen."
windowMaximize: "Maximieren" windowMaximize: "Maximieren"
windowMinimize: "Minimieren"
windowRestore: "Wiederherstellen" windowRestore: "Wiederherstellen"
caption: "Beschreibung" caption: "Beschreibung"
loggedInAsBot: "Momentan als Bot angemeldet" loggedInAsBot: "Momentan als Bot angemeldet"
@ -960,7 +962,9 @@ copyErrorInfo: "Fehlerdetails kopieren"
joinThisServer: "Bei dieser Instanz registrieren" joinThisServer: "Bei dieser Instanz registrieren"
exploreOtherServers: "Eine andere Instanz finden" exploreOtherServers: "Eine andere Instanz finden"
letsLookAtTimeline: "Die Chronik durchstöbern" letsLookAtTimeline: "Die Chronik durchstöbern"
disableFederationWarn: "Dies deaktiviert Föderation, aber alle Notizen bleiben, sofern nicht umgestellt, öffentlich. In den meisten Fällen wird diese Option nicht benötigt." disableFederationConfirm: "Föderation wirklich deaktivieren?"
disableFederationConfirmWarn: "Auch mit deaktivierter Föderation bleiben Notizen, sofern nicht umgestellt, öffentlich. In den meisten Fällen wird dies nicht benötigt."
disableFederationOk: "Deaktivieren"
invitationRequiredToRegister: "Diese Instanz ist einladungsbasiert. Du musst einen validen Einladungscode eingeben, um dich zu registrieren." invitationRequiredToRegister: "Diese Instanz ist einladungsbasiert. Du musst einen validen Einladungscode eingeben, um dich zu registrieren."
emailNotSupported: "Diese Instanz unterstützt das Versenden von Emails nicht" emailNotSupported: "Diese Instanz unterstützt das Versenden von Emails nicht"
postToTheChannel: "In Kanal senden" postToTheChannel: "In Kanal senden"
@ -980,6 +984,21 @@ drivecleaner: "Drive-Reiniger"
retryAllQueuesNow: "Sofort Warteschlangen erneut ausführen" retryAllQueuesNow: "Sofort Warteschlangen erneut ausführen"
retryAllQueuesConfirmTitle: "Wirklich erneut versuchen?" retryAllQueuesConfirmTitle: "Wirklich erneut versuchen?"
retryAllQueuesConfirmText: "Dies wird zu einer temporären Erhöhung der Serverlast führen." retryAllQueuesConfirmText: "Dies wird zu einer temporären Erhöhung der Serverlast führen."
enableChartsForRemoteUser: "Diagramme für Nutzer fremder Instanzen erstellen"
enableChartsForFederatedInstances: "Diagramme für fremde Instanzen erstellen"
showClipButtonInNoteFooter: "\"Clip\" zum Notizmenu hinzufügen"
largeNoteReactions: "Reaktionen vergrößert anzeigen"
noteIdOrUrl: "Notiz-ID oder URL"
accountMigration: "Konto-Umzug"
accountMoved: "Dieser Benutzer ist zu einem neuen Konto umgezogen:"
_accountMigration:
moveTo: "Dieses Konto zu einem neuen umziehen"
moveToLabel: "Umzugsziel:"
moveAccountDescription: "Ein Umzug kann nicht rückgängig gemacht werden. Stelle zuerst sicher, dass du auf dem Umzugsziel einen Alias erstellt hast. Gib dann das Umzugsziel in folgendem Format ein: @person@instance.com"
moveFrom: "Von einem anderen Konto zu diesem umziehen"
moveFromLabel: "Umzugsursprung:"
moveFromDescription: "Stelle sicher, dass du auf dem Umzugsursprungskonto einen Alias zu diesem Konto erstellt hast, falls du die Follower des Ursprungskontos übertragen möchtest. Dies muss vor dem Umzug geschehen! Gib dann das Ursprungskonto in folgendem Format an: @person@instance.com"
migrationConfirm: "Dieses Konto wirklich zu {account} umziehen? Sobald der Umzug beginnt, kann er nicht rückgängig gemacht werden, und dieses Konto nicht wieder im ursprünglichen Zustand verwendet werden.\n\nÜberprüfe zusätzlich, dass du auf dem Umzugsziel einen Alias eingerichtet hast."
_achievements: _achievements:
earnedAt: "Freigeschaltet am" earnedAt: "Freigeschaltet am"
_types: _types:
@ -1252,7 +1271,7 @@ _role:
gtlAvailable: "Kann auf die globale Chronik zugreifen" gtlAvailable: "Kann auf die globale Chronik zugreifen"
ltlAvailable: "Kann auf die lokale Chronik zugreifen" ltlAvailable: "Kann auf die lokale Chronik zugreifen"
canPublicNote: "Kann öffentliche Notizen erstellen" canPublicNote: "Kann öffentliche Notizen erstellen"
canInvite: "Kann Einladungscodes für diese Instanz erstellen" canInvite: "Erstellung von Einladungscodes für diese Instanz"
canManageCustomEmojis: "Benutzerdefinierte Emojis verwalten" canManageCustomEmojis: "Benutzerdefinierte Emojis verwalten"
driveCapacity: "Drive-Kapazität" driveCapacity: "Drive-Kapazität"
pinMax: "Maximale Anzahl an angehefteten Notizen" pinMax: "Maximale Anzahl an angehefteten Notizen"
@ -1405,7 +1424,7 @@ _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"
title: "Blendet Notizen von stummgeschalteten Instanzen aus." title: "Blendet Notizen von stummgeschalteten Instanzen aus."
heading: "Liste der stummzuschaltenden Instanzen" heading: "Stummzuschaltende Instanzen"
_theme: _theme:
explore: "Farbschemata erforschen" explore: "Farbschemata erforschen"
install: "Farbschemata installieren" install: "Farbschemata installieren"
@ -1877,6 +1896,16 @@ _drivecleaner:
orderBySizeDesc: "Absteigende Dateigrößen" orderBySizeDesc: "Absteigende Dateigrößen"
orderByCreatedAtAsc: "Aufsteigendes Erstelldatum" orderByCreatedAtAsc: "Aufsteigendes Erstelldatum"
_webhookSettings: _webhookSettings:
createWebhook: "Webhook erstellen"
name: "Name" name: "Name"
secret: "Secret"
events: "Webhook-Ereignisse"
active: "Aktiviert" active: "Aktiviert"
_events:
follow: "Wenn du jemandem folgst"
followed: "Wenn dir jemand folgt"
note: "Wenn du eine Notiz schickst"
reply: "Wenn du eine Antwort erhältst"
renote: "Wenn du ein Renote erhältst"
reaction: "Wenn du eine Reaktion erhältst"
mention: "Wenn du erwähnt wirst"

View File

@ -394,4 +394,3 @@ _deck:
mentions: "Επισημάνσεις" mentions: "Επισημάνσεις"
_webhookSettings: _webhookSettings:
name: "Όνομα" name: "Όνομα"

View File

@ -506,6 +506,7 @@ objectStorageUseSSLDesc: "Turn this off if you are not going to use HTTPS for AP
objectStorageUseProxy: "Connect over Proxy" objectStorageUseProxy: "Connect over Proxy"
objectStorageUseProxyDesc: "Turn this off if you are not going to use a Proxy for API connections" objectStorageUseProxyDesc: "Turn this off if you are not going to use a Proxy for API connections"
objectStorageSetPublicRead: "Set \"public-read\" on upload" objectStorageSetPublicRead: "Set \"public-read\" on upload"
s3ForcePathStyleDesc: "If s3ForcePathStyle is enabled, the bucket name has to included in the path of the URL as opposed to the hostname of the URL. You may need to enable this setting when using services such as a self-hosted Minio instance."
serverLogs: "Server logs" serverLogs: "Server logs"
deleteAll: "Delete all" deleteAll: "Delete all"
showFixedPostForm: "Display the posting form at the top of the timeline" showFixedPostForm: "Display the posting form at the top of the timeline"
@ -919,6 +920,7 @@ pushNotificationNotSupported: "Your browser or instance does not support push no
sendPushNotificationReadMessage: "Delete push notifications once the relevant notifications or messages have been read" sendPushNotificationReadMessage: "Delete push notifications once the relevant notifications or messages have been read"
sendPushNotificationReadMessageCaption: "A notification containing the text \"{emptyPushNotificationMessage}\" will be displayed for a short time. This may increase the battery usage of your device, if applicable." sendPushNotificationReadMessageCaption: "A notification containing the text \"{emptyPushNotificationMessage}\" will be displayed for a short time. This may increase the battery usage of your device, if applicable."
windowMaximize: "Maximize" windowMaximize: "Maximize"
windowMinimize: "Minimize"
windowRestore: "Restore" windowRestore: "Restore"
caption: "Caption" caption: "Caption"
loggedInAsBot: "Currently logged in as bot" loggedInAsBot: "Currently logged in as bot"
@ -960,7 +962,9 @@ copyErrorInfo: "Copy error details"
joinThisServer: "Sign up at this instance" joinThisServer: "Sign up at this instance"
exploreOtherServers: "Look for another instance" exploreOtherServers: "Look for another instance"
letsLookAtTimeline: "Have a look at the timeline" letsLookAtTimeline: "Have a look at the timeline"
disableFederationWarn: "This will disable federation, but posts will continue to be public unless set otherwise. You usually do not need to use this setting." disableFederationConfirm: "Really disable federation?"
disableFederationConfirmWarn: "Even if defederated, posts will continue to be public unless set otherwise. You usually do not need to do this."
disableFederationOk: "Disable"
invitationRequiredToRegister: "This instance is invite-only. You must enter a valid invite code sign up." invitationRequiredToRegister: "This instance is invite-only. You must enter a valid invite code sign up."
emailNotSupported: "This instance does not support sending emails" emailNotSupported: "This instance does not support sending emails"
postToTheChannel: "Post to channel" postToTheChannel: "Post to channel"
@ -983,6 +987,19 @@ retryAllQueuesConfirmText: "This will temporarily increase the server load."
enableChartsForRemoteUser: "Generate remote user data charts" enableChartsForRemoteUser: "Generate remote user data charts"
enableChartsForFederatedInstances: "Generate remote instance data charts" enableChartsForFederatedInstances: "Generate remote instance data charts"
showClipButtonInNoteFooter: "Add \"Clip\" to note action menu" showClipButtonInNoteFooter: "Add \"Clip\" to note action menu"
largeNoteReactions: "Enlargen displayed reactions"
noteIdOrUrl: "Note ID or URL"
accountMigration: "Account Migration"
accountMoved: "This user has moved to a new account:"
_accountMigration:
moveTo: "Migrate this account to a different one"
moveToLabel: "Account to move to:"
moveAccountDescription: "This action is irreversible. First, create an alias for this account on the account you wish to move to. Then, enter the account to move to in the following format: @person@instance.com"
moveFrom: "Migrate another account to this one"
moveFromLabel: "Account to move from:"
moveFromDescription: "Create an alias for the account to move from on this account if you wish to transfer its followers. This has to be done before the transfer! Then, enter the account to move to in the following format: @person@instance.com"
migrationConfirm: "Really migrate this account to {account}? Once started, this process cannot be stopped or taken back, and you will not be able to use this account in its original state anymore.\n\nAlso, confirm you've created an alias at the account to migrate to."
_achievements: _achievements:
earnedAt: "Unlocked at" earnedAt: "Unlocked at"
_types: _types:
@ -1893,4 +1910,3 @@ _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"

View File

@ -506,6 +506,7 @@ objectStorageUseSSLDesc: "Desactive esto si no va a usar HTTPS para la conexión
objectStorageUseProxy: "Conectarse a través de Proxy" objectStorageUseProxy: "Conectarse a través de Proxy"
objectStorageUseProxyDesc: "Desactive esto si no va a usar Proxy para la conexión de Almacenamiento de objetos" objectStorageUseProxyDesc: "Desactive esto si no va a usar Proxy para la conexión de Almacenamiento de objetos"
objectStorageSetPublicRead: "Seleccionar \"public-read\" al subir " objectStorageSetPublicRead: "Seleccionar \"public-read\" al subir "
s3ForcePathStyleDesc: "Si s3ForcePathStyle esta habilitado el nombre del bucket debe ser especificado como parte de la URL en lugar del nombre de host en la URL. Puede ser necesario activar esta opción cuando se utilice, por ejemplo, Minio en un servidor propio."
serverLogs: "Registros del servidor" serverLogs: "Registros del servidor"
deleteAll: "Eliminar todos" deleteAll: "Eliminar todos"
showFixedPostForm: "Mostrar el formulario de las entradas encima de la línea de tiempo" showFixedPostForm: "Mostrar el formulario de las entradas encima de la línea de tiempo"
@ -919,6 +920,7 @@ pushNotificationNotSupported: "El navegador o la instancia no admiten notificaci
sendPushNotificationReadMessage: "Eliminar las notificaciones push después de leer las notificaciones y los mensajes" sendPushNotificationReadMessage: "Eliminar las notificaciones push después de leer las notificaciones y los mensajes"
sendPushNotificationReadMessageCaption: "La notificación \"{emptyPushNotificationMessage}\" aparecerá momentáneamente. Esto puede aumentar el consumo de batería del dispositivo." sendPushNotificationReadMessageCaption: "La notificación \"{emptyPushNotificationMessage}\" aparecerá momentáneamente. Esto puede aumentar el consumo de batería del dispositivo."
windowMaximize: "Maximizar" windowMaximize: "Maximizar"
windowMinimize: "Minimizar"
windowRestore: "Regresar" windowRestore: "Regresar"
caption: "Pie de foto" caption: "Pie de foto"
loggedInAsBot: "Inicio sesión como cuenta bot." loggedInAsBot: "Inicio sesión como cuenta bot."
@ -960,7 +962,9 @@ copyErrorInfo: "Copiar detalles del error"
joinThisServer: "Registrarse en esta instancia" joinThisServer: "Registrarse en esta instancia"
exploreOtherServers: "Buscar otra instancia" exploreOtherServers: "Buscar otra instancia"
letsLookAtTimeline: "Mirar la línea de tiempo local" letsLookAtTimeline: "Mirar la línea de tiempo local"
disableFederationWarn: "Esto desactivará la federación, pero las publicaciones segurán siendo públicas al menos que se configure diferente. Usualmente no necesitas usar esta configuración." disableFederationConfirm: "¿Estas seguro que quieres desactivar la federación?"
disableFederationConfirmWarn: "Aunque no exista federación los posts no serán marcados como privados. En la mayoría de los casos, no es necesario hacer los posts no federar."
disableFederationOk: "Desactivar."
invitationRequiredToRegister: "Esta instancia está configurada sólo por invitación, tienes que ingresar un código de invitación válido." invitationRequiredToRegister: "Esta instancia está configurada sólo por invitación, tienes que ingresar un código de invitación válido."
emailNotSupported: "Esta instancia no soporta el envío de correo electrónico" emailNotSupported: "Esta instancia no soporta el envío de correo electrónico"
postToTheChannel: "Publicar en el canal" postToTheChannel: "Publicar en el canal"
@ -980,6 +984,21 @@ drivecleaner: "Limpiador del Drive"
retryAllQueuesNow: "Reintentar inmediatamente todas las colas" retryAllQueuesNow: "Reintentar inmediatamente todas las colas"
retryAllQueuesConfirmTitle: "Desea ¿reintentar inmediatamente todas las colas?" retryAllQueuesConfirmTitle: "Desea ¿reintentar inmediatamente todas las colas?"
retryAllQueuesConfirmText: "La carga del servidor está incrementándose temporalmente " retryAllQueuesConfirmText: "La carga del servidor está incrementándose temporalmente "
enableChartsForRemoteUser: "Generar gráficas de usuarios remotos."
enableChartsForFederatedInstances: "Generar gráficos de servidores remotos"
showClipButtonInNoteFooter: "Añadir \"Clip\" al menú de notas"
largeNoteReactions: "Agrandar las reacciones de las notas"
noteIdOrUrl: "ID o URL de la nota"
accountMigration: "Migración de cuenta"
accountMoved: "Este usuario se ha mudado a una nueva cuenta:"
_accountMigration:
moveTo: "Mover esta cuenta a una nueva"
moveToLabel: "Cuenta destino:"
moveAccountDescription: "Esta operación no puede deshacerse. En primer lugar, asegúrese de haber creado un alias para esta cuenta en la cuenta a la que se va a trasladar. Después de crear el alias, introduzca la cuenta a la que se está trasladando de la siguiente manera: @person@instance.com"
moveFrom: "Trasladar de otra cuenta a ésta"
moveFromLabel: "Cuenta desde la que se realiza el traslado:"
moveFromDescription: "Si quieres transferir seguidores de otra cuenta a esta cuenta y trasladarlos, tendrás que crear un alias aquí. Asegúrate de crearlo antes de realizar el traslado. Introduce la cuenta desde la que estás moviendo los seguidores así: @person@instance.com"
migrationConfirm: "¿Estás seguro de que quieres mover esta cuenta a {account}? Una vez trasladada, no podrás deshacer el traslado y no podrás volver a utilizar la cuenta original.\n\nAdemás, compruebe que ha configurado un alias en el destino del traslado."
_achievements: _achievements:
earnedAt: "Desbloqueado el" earnedAt: "Desbloqueado el"
_types: _types:
@ -1276,6 +1295,8 @@ _role:
followersMoreThanOrEq: "Tiene X o más seguidores" followersMoreThanOrEq: "Tiene X o más seguidores"
followingLessThanOrEq: "Sigue X o menos cuentas" followingLessThanOrEq: "Sigue X o menos cuentas"
followingMoreThanOrEq: "Sigue X o más cuentas" followingMoreThanOrEq: "Sigue X o más cuentas"
notesLessThanOrEq: "El número de notas es inferior o igual a"
notesMoreThanOrEq: "El número de notas es superior o igual a"
and: "Condicional AND" and: "Condicional AND"
or: "Condicional OR" or: "Condicional OR"
not: "Condicional NOT" not: "Condicional NOT"
@ -1875,6 +1896,16 @@ _drivecleaner:
orderBySizeDesc: "Más grandes" orderBySizeDesc: "Más grandes"
orderByCreatedAtAsc: "Más antiguos" orderByCreatedAtAsc: "Más antiguos"
_webhookSettings: _webhookSettings:
createWebhook: "Crear Webhook"
name: "Nombre" name: "Nombre"
secret: "Secreto"
events: "Eventos de webhook"
active: "Activado" active: "Activado"
_events:
follow: "Cuando se sigue a alguien"
followed: "Cuando se es seguido"
note: "Cuando se publica una nota"
reply: "Cuando se recibe una respuesta"
renote: "Cuando reciba un \"re-note\""
reaction: "Cuando se recibe una reacción"
mention: "Cuando hay una mención"

View File

@ -1470,4 +1470,3 @@ _deck:
_webhookSettings: _webhookSettings:
name: "Nom" name: "Nom"
active: "Activé" active: "Activé"

View File

@ -1,2 +1 @@
--- ---

View File

@ -1,2 +1 @@
--- ---

View File

@ -1806,4 +1806,3 @@ _deck:
_webhookSettings: _webhookSettings:
name: "Nama" name: "Nama"
active: "Aktif" active: "Aktif"

View File

@ -170,7 +170,7 @@ proxyAccountDescription: "Un profilo proxy funziona come follower per i profili
host: "Server remoto" host: "Server remoto"
selectUser: "Seleziona profilo" selectUser: "Seleziona profilo"
recipient: "Destinatario" recipient: "Destinatario"
annotation: "Descrizione" annotation: "Annotazione"
federation: "Federazione" federation: "Federazione"
instances: "Istanza" instances: "Istanza"
registeredAt: "Registrato presso" registeredAt: "Registrato presso"
@ -212,7 +212,7 @@ intro: "L'installazione di Misskey è terminata! Si prega di creare il profilo a
done: "Fine" done: "Fine"
processing: "In elaborazione" processing: "In elaborazione"
preview: "Anteprima" preview: "Anteprima"
default: "Medio" default: "Predefinito"
defaultValueIs: "Predefinito: {value}" defaultValueIs: "Predefinito: {value}"
noCustomEmojis: "Nessun emoji" noCustomEmojis: "Nessun emoji"
noJobs: "Nessun lavoro" noJobs: "Nessun lavoro"
@ -237,14 +237,14 @@ more: "Di più!"
featured: "Tendenze" featured: "Tendenze"
usernameOrUserId: "Nome utente o ID utente" usernameOrUserId: "Nome utente o ID utente"
noSuchUser: "Nessun utente trovato" noSuchUser: "Nessun utente trovato"
lookup: "Cerca" lookup: "Ricerca remota"
announcements: "Annunci" announcements: "Annunci"
imageUrl: "URL dell'immagine" imageUrl: "URL dell'immagine"
remove: "Elimina" remove: "Elimina"
removed: "Eliminato con successo" removed: "Eliminato con successo"
removeAreYouSure: "Vuoi davvero eliminare \"{x}\"?" removeAreYouSure: "Vuoi davvero eliminare \"{x}\"?"
deleteAreYouSure: "Eliminare \"{x}\"?" deleteAreYouSure: "Eliminare \"{x}\"?"
resetAreYouSure: "Reimposta" resetAreYouSure: "Ripristinare?"
saved: "Salvato" saved: "Salvato"
messaging: "Messaggi" messaging: "Messaggi"
upload: "Carica" upload: "Carica"
@ -409,7 +409,7 @@ lastUsedAt: "Uso più recente: {t}"
unregister: "Annulla l'iscrizione" unregister: "Annulla l'iscrizione"
passwordLessLogin: "Accedi senza password" passwordLessLogin: "Accedi senza password"
passwordLessLoginDescription: "Accedi senza password, usando la chiave di sicurezza" passwordLessLoginDescription: "Accedi senza password, usando la chiave di sicurezza"
resetPassword: "Reimposta password" resetPassword: "Ripristina la password"
newPasswordIs: "La tua nuova password è「{password}」" newPasswordIs: "La tua nuova password è「{password}」"
reduceUiAnimation: "Ridurre le animazioni dell'interfaccia" reduceUiAnimation: "Ridurre le animazioni dell'interfaccia"
share: "Condividi" share: "Condividi"
@ -596,7 +596,7 @@ notificationType: "Tipo di notifiche"
edit: "Modifica" edit: "Modifica"
emailServer: "Server email" emailServer: "Server email"
enableEmail: "Abilita consegna email" enableEmail: "Abilita consegna email"
emailConfigInfo: "Utilizzato per verificare il tuo indirizzo di posta elettronica e per reimpostare la tua password" emailConfigInfo: "Utilizzato per verificare il tuo indirizzo di posta elettronica e per ripristinare la password"
email: "Email" email: "Email"
emailAddress: "Indirizzo di posta elettronica" emailAddress: "Indirizzo di posta elettronica"
smtpConfig: "Impostazioni del server SMTP" smtpConfig: "Impostazioni del server SMTP"
@ -960,16 +960,15 @@ copyErrorInfo: "Copia le informazioni sull'errore"
joinThisServer: "Registrati su questa istanza" joinThisServer: "Registrati su questa istanza"
exploreOtherServers: "Trova altre istanze" exploreOtherServers: "Trova altre istanze"
letsLookAtTimeline: "Sbircia la timeline" letsLookAtTimeline: "Sbircia la timeline"
disableFederationWarn: "Disabilita la federazione. Questo cambiamento non rende le pubblicazioni private. Di solito non è necessario abilitare questa opzione." invitationRequiredToRegister: "L'accesso a questa istanza è solo ad invito. Può registrarsi solo chi ha un codice fornito dall'amministrazione."
invitationRequiredToRegister: "L'accesso a questo nodo è solo ad invito. Devi inserire un codice d'invito valido. Puoi richiedere un codice all'amministratore."
emailNotSupported: "L'istanza non supporta l'invio di email" emailNotSupported: "L'istanza non supporta l'invio di email"
postToTheChannel: "Pubblica sul canale" postToTheChannel: "Pubblica nel canale"
cannotBeChangedLater: "Non sarà più modificabile" cannotBeChangedLater: "Non sarà più modificabile"
reactionAcceptance: "Accettazione reazioni" reactionAcceptance: "Accettazione reazioni"
likeOnly: "Solo i Like" likeOnly: "Solo i Like"
likeOnlyForRemote: "Solo Like remoti" likeOnlyForRemote: "Solo Like remoti"
rolesAssignedToMe: "I miei ruoli" rolesAssignedToMe: "I miei ruoli"
resetPasswordConfirm: "Vuoi reimpostare la password?" resetPasswordConfirm: "Vuoi davvero ripristinare la password?"
sensitiveWords: "Parole sensibili" sensitiveWords: "Parole sensibili"
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."
notesSearchNotAvailable: "Non è possibile cercare tra le Note." notesSearchNotAvailable: "Non è possibile cercare tra le Note."
@ -980,6 +979,11 @@ drivecleaner: "Drive cleaner"
retryAllQueuesNow: "Ritenta di consumare tutte le code" retryAllQueuesNow: "Ritenta di consumare tutte le code"
retryAllQueuesConfirmTitle: "Vuoi ritentare adesso?" retryAllQueuesConfirmTitle: "Vuoi ritentare adesso?"
retryAllQueuesConfirmText: "Potrebbe sovraccaricare il server temporaneamente." retryAllQueuesConfirmText: "Potrebbe sovraccaricare il server temporaneamente."
enableChartsForRemoteUser: "Abilita i grafici per i profili remoti"
enableChartsForFederatedInstances: "Abilita i grafici per le istanze federate"
showClipButtonInNoteFooter: "Aggiungi il bottone Clip tra le azioni delle Note"
largeNoteReactions: "Ingrandisci le reazioni"
noteIdOrUrl: "ID della Nota o URL"
_achievements: _achievements:
earnedAt: "Data di conseguimento" earnedAt: "Data di conseguimento"
_types: _types:
@ -1276,6 +1280,8 @@ _role:
followersMoreThanOrEq: "Ha più di N follower" followersMoreThanOrEq: "Ha più di N follower"
followingLessThanOrEq: "Segue N profili o meno" followingLessThanOrEq: "Segue N profili o meno"
followingMoreThanOrEq: "Segue N profili o più" followingMoreThanOrEq: "Segue N profili o più"
notesLessThanOrEq: "Conteggio Note inferiore o uguale a"
notesMoreThanOrEq: "Conteggio Note maggiore o uguale a"
and: "E" and: "E"
or: "O" or: "O"
not: "NON" not: "NON"
@ -1314,8 +1320,8 @@ _ad:
hide: "Nascondi" hide: "Nascondi"
_forgotPassword: _forgotPassword:
enterEmail: "Inserisci l'indirizzo di posta elettronica che hai registrato nel tuo profilo. Il collegamento necessario per ripristinare la password verrà inviato a questo indirizzo." enterEmail: "Inserisci l'indirizzo di posta elettronica che hai registrato nel tuo profilo. Il collegamento necessario per ripristinare la password verrà inviato a questo indirizzo."
ifNoEmail: "Se nessun indirizzo e-mail è stato registrato, si prega di contattare l'amministratore·trice dell'istanza." ifNoEmail: "Se il tuo indirizzo email non risulta registrato, contatta l'amministrazione dell'istanza."
contactAdmin: "Poiché questa istanza non permette l'utilizzo di una mail, si prega di contattare l'amministratore·trice dell'istanza per poter ripristinare la password." contactAdmin: "Poiché questa istanza non permette di impostare l'indirizzo mail, contatta l'amministrazione per ripristinare la password.\n"
_gallery: _gallery:
my: "Le mie pubblicazioni" my: "Le mie pubblicazioni"
liked: "Pubblicazioni che mi piacciono" liked: "Pubblicazioni che mi piacciono"
@ -1870,11 +1876,21 @@ _dialog:
charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({corrente})" charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({corrente})"
_disabledTimeline: _disabledTimeline:
title: "Timeline disabilitata" title: "Timeline disabilitata"
description: "Il tuo ruolo non ha i permessi per accedere a questa timeline" description: "Il ruolo in cui sei non ti permette di leggere questa timeline"
_drivecleaner: _drivecleaner:
orderBySizeDesc: "Dal più grande al più piccolo" orderBySizeDesc: "Dal più grande al più piccolo"
orderByCreatedAtAsc: "Dal più vecchio al più recente" orderByCreatedAtAsc: "Dal più vecchio al più recente"
_webhookSettings: _webhookSettings:
createWebhook: "Creazione Webhook"
name: "Nome" name: "Nome"
secret: "Segreto"
events: "Quando eseguire il Webhook"
active: "Attivo" active: "Attivo"
_events:
follow: "Quando segui un profilo"
followed: "Quando ti segue un profilo"
note: "Quando pubblichi una Nota"
reply: "Quando rispondono ad una Nota"
renote: "Quando la Nota è Rinotata"
reaction: "Quando ricevo una reazione"
mention: "Quando mi menzionano"

View File

@ -500,12 +500,13 @@ objectStoragePrefixDesc: "このprefixのディレクトリ下に格納されま
objectStorageEndpoint: "Endpoint" objectStorageEndpoint: "Endpoint"
objectStorageEndpointDesc: "S3の場合は空、それ以外の場合は各サービスのendpointを指定してください。'<host>'または'<host>:<port>'のように指定します。" objectStorageEndpointDesc: "S3の場合は空、それ以外の場合は各サービスのendpointを指定してください。'<host>'または'<host>:<port>'のように指定します。"
objectStorageRegion: "Region" objectStorageRegion: "Region"
objectStorageRegionDesc: "'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は、空または'us-east-1'にしてください。" objectStorageRegionDesc: "'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は'us-east-1'にしてください。AWS設定ファイルまたは環境変数を参照する場合は空にしてください。"
objectStorageUseSSL: "SSLを使用する" objectStorageUseSSL: "SSLを使用する"
objectStorageUseSSLDesc: "API接続にhttpsを使用しない場合はオフにしてください" objectStorageUseSSLDesc: "API接続にhttpsを使用しない場合はオフにしてください"
objectStorageUseProxy: "Proxyを利用する" objectStorageUseProxy: "Proxyを利用する"
objectStorageUseProxyDesc: "API接続にproxyを利用しない場合はオフにしてください" objectStorageUseProxyDesc: "API接続にproxyを利用しない場合はオフにしてください"
objectStorageSetPublicRead: "アップロード時に'public-read'を設定する" objectStorageSetPublicRead: "アップロード時に'public-read'を設定する"
s3ForcePathStyleDesc: "s3ForcePathStyleを有効にすると、バケット名をURLのホスト名ではなくパスの一部として指定することを強制します。セルフホストされたMinioなどの使用時に有効にする必要がある場合があります。"
serverLogs: "サーバーログ" serverLogs: "サーバーログ"
deleteAll: "全て削除" deleteAll: "全て削除"
showFixedPostForm: "タイムライン上部に投稿フォームを表示する" showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
@ -919,6 +920,7 @@ pushNotificationNotSupported: "ブラウザかサーバーがプッシュ通知
sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を削除する" sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を削除する"
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」という通知が一瞬表示されるようになります。端末の電池消費量が増加する可能性があります。" sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」という通知が一瞬表示されるようになります。端末の電池消費量が増加する可能性があります。"
windowMaximize: "最大化" windowMaximize: "最大化"
windowMinimize: "最小化"
windowRestore: "元に戻す" windowRestore: "元に戻す"
caption: "キャプション" caption: "キャプション"
loggedInAsBot: "Botアカウントでログイン中" loggedInAsBot: "Botアカウントでログイン中"
@ -960,7 +962,9 @@ copyErrorInfo: "エラー情報をコピー"
joinThisServer: "このサーバーに登録する" joinThisServer: "このサーバーに登録する"
exploreOtherServers: "他のサーバーを探す" exploreOtherServers: "他のサーバーを探す"
letsLookAtTimeline: "タイムラインを見てみる" letsLookAtTimeline: "タイムラインを見てみる"
disableFederationWarn: "連合が無効になっています。無効にしても投稿が非公開にはなりません。ほとんどの場合、このオプションを有効にする必要はありません。" disableFederationConfirm: "連合なしにしますか?"
disableFederationConfirmWarn: "連合なしにしても投稿は非公開になりません。ほとんどの場合、連合なしにする必要はありません。"
disableFederationOk: "連合なしにする"
invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。" invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。"
emailNotSupported: "このサーバーではメール配信はサポートされていません" emailNotSupported: "このサーバーではメール配信はサポートされていません"
postToTheChannel: "チャンネルに投稿" postToTheChannel: "チャンネルに投稿"
@ -983,6 +987,19 @@ retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大するこ
enableChartsForRemoteUser: "リモートユーザーのチャートを生成" enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
enableChartsForFederatedInstances: "リモートサーバーのチャートを生成" enableChartsForFederatedInstances: "リモートサーバーのチャートを生成"
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加" showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
largeNoteReactions: "ノートのリアクションを大きく表示"
noteIdOrUrl: "ートIDまたはURL"
accountMigration: "アカウントの引っ越し"
accountMoved: "このユーザーは新しいアカウントに引っ越しました:"
_accountMigration:
moveTo: "このアカウントを新しいアカウントに引っ越す"
moveToLabel: "引っ越し先のアカウント:"
moveAccountDescription: "この操作は取り消せません。まずは引っ越し先のアカウントでこのアカウントに対しエイリアスを作成したことを確認してください。エイリアス作成後、引っ越し先のアカウントをこのように入力してください:@person@instance.com"
moveFrom: "別のアカウントからこのアカウントに引っ越す"
moveFromLabel: "引っ越し元のアカウント:"
moveFromDescription: "別のアカウントからこのアカウントにフォロワーを引き継いで引っ越したい場合、ここでエイリアスを作成しておく必要があります。必ず引っ越しを実行する前に作成してください!引っ越し元のアカウントをこのように入力してください:@person@instance.com"
migrationConfirm: "本当にこのアカウントを {account} に引っ越しますか?一度引っ越しを行うと取り消せず、二度とこのアカウントを元の状態で使用できなくなります。\nまた、引っ越し先のアカウントでエイリアスを作成したことを確認してください。"
_achievements: _achievements:
earnedAt: "獲得日時" earnedAt: "獲得日時"
@ -1949,4 +1966,3 @@ _webhookSettings:
renote: "Renoteされたとき" renote: "Renoteされたとき"
reaction: "リアクションがあったとき" reaction: "リアクションがあったとき"
mention: "メンションされたとき" mention: "メンションされたとき"

View File

@ -16,8 +16,8 @@ cancel: "やめとく"
noThankYou: "やめとく" noThankYou: "やめとく"
enterUsername: "ユーザー名を入れてや" enterUsername: "ユーザー名を入れてや"
renotedBy: "{user}がRenoteしたで" renotedBy: "{user}がRenoteしたで"
noNotes: "ノートなんてあらへんで" noNotes: "ノートはあらへん"
noNotifications: "通知なんてあらへんで" noNotifications: "通知はあらへん"
instance: "サーバー" instance: "サーバー"
settings: "設定" settings: "設定"
basicSettings: "基本設定" basicSettings: "基本設定"
@ -25,13 +25,13 @@ otherSettings: "ほかの設定"
openInWindow: "ウィンドウで開くで" openInWindow: "ウィンドウで開くで"
profile: "プロフィール" profile: "プロフィール"
timeline: "タイムライン" timeline: "タイムライン"
noAccountDescription: "自己紹介食ってもた" noAccountDescription: "自己紹介はあらへん"
login: "ログイン" login: "ログイン"
loggingIn: "ログインしよるで" loggingIn: "ログインしよるで"
logout: "ログアウト" logout: "ログアウト"
signup: "新規登録" signup: "新規登録"
uploading: "アップロードしとるで" uploading: "アップロードしとるで"
save: "保存" save: "とっとく"
users: "ユーザー" users: "ユーザー"
addUser: "ユーザーを追加や" addUser: "ユーザーを追加や"
favorite: "お気に入り" favorite: "お気に入り"
@ -81,9 +81,9 @@ followsYou: "フォローされとるで"
createList: "リスト作る" createList: "リスト作る"
manageLists: "リストの管理" manageLists: "リストの管理"
error: "エラー" error: "エラー"
somethingHappened: "なんかアカンことが起こったで" somethingHappened: "なんかあかんわ"
retry: "もっぺんやる?" retry: "もっぺんやる?"
pageLoadError: "ページの読み込みに失敗してもうたわ…" pageLoadError: "ページが読み込めんかったわ。"
pageLoadErrorDescription: "これは普通ならネットワークかブラウザキャッシュが悪さしてるんよ。キャッシュをほかすか、もうちょっとだけ待ってくれへん?" pageLoadErrorDescription: "これは普通ならネットワークかブラウザキャッシュが悪さしてるんよ。キャッシュをほかすか、もうちょっとだけ待ってくれへん?"
serverIsDead: "サーバーからの応答がないで。もうちょい待ってから試してみてな。" serverIsDead: "サーバーからの応答がないで。もうちょい待ってから試してみてな。"
youShouldUpgradeClient: "このページを表示するには、リロードして新しいバージョンのクライアントを使ってなー。" youShouldUpgradeClient: "このページを表示するには、リロードして新しいバージョンのクライアントを使ってなー。"
@ -108,8 +108,8 @@ inChannelQuote: "チャンネル内引用"
pinnedNote: "ピン留めされとるノート" pinnedNote: "ピン留めされとるノート"
pinned: "ピン留めしとく" pinned: "ピン留めしとく"
you: "あんた" you: "あんた"
clickToShow: "押したら見えるで" clickToShow: "押したら出ら"
sensitive: "ちょっとアカンやつやで" sensitive: "気いつけて見いや"
add: "増やす" add: "増やす"
reaction: "リアクション" reaction: "リアクション"
reactions: "リアクション" reactions: "リアクション"
@ -122,8 +122,8 @@ unmarkAsSensitive: "そこまでアカンことないやろ"
enterFileName: "ファイル名を入れてや" enterFileName: "ファイル名を入れてや"
mute: "ミュート" mute: "ミュート"
unmute: "ミュートやめたる" unmute: "ミュートやめたる"
renoteMute: "リノートは見いひん" renoteMute: "Renoteは見いひん"
renoteUnmute: "リノートもやっぱ見るわ" renoteUnmute: "Renoteもやっぱ見るわ"
block: "ブロック" block: "ブロック"
unblock: "ブロックやめたる" unblock: "ブロックやめたる"
suspend: "凍結" suspend: "凍結"
@ -141,14 +141,14 @@ editWidgetsExit: "編集終ったで"
customEmojis: "カスタム絵文字" customEmojis: "カスタム絵文字"
emoji: "絵文字" emoji: "絵文字"
emojis: "絵文字" emojis: "絵文字"
emojiName: "絵文字名" emojiName: "絵文字はんの"
emojiUrl: "絵文字画像URL" emojiUrl: "絵文字画像URL"
addEmoji: "絵文字を追加" addEmoji: "絵文字を追加"
settingGuide: "ええ感じの設定" settingGuide: "ええ感じの設定"
cacheRemoteFiles: "リモートのファイルをキャッシュする" cacheRemoteFiles: "リモートのファイルをキャッシュする"
cacheRemoteFilesDescription: "この設定を切っとくと、リモートファイルをキャッシュせず直リンクするようになるで。サーバーの容量は節約できるけど、サムネイルが作られんくなるから通信量が増えるで。" cacheRemoteFilesDescription: "この設定を切っとったら、リモートファイルをキャッシュせんと直リンクするようになるで。サーバーの容量は節約できるけど、サムネイルを作らんなるから通信量が増えるで。"
flagAsBot: "Botにするで" flagAsBot: "Botにするで"
flagAsBotDescription: "もしこのアカウントをプログラム使うて運用するんやったら、このフラグをオンにしてや。オンにすれば、反応がバーッて連鎖するのを避けるために開発者が使うたり、Misskeyのシステム上での扱いがBotに合ったもんになるからな。" flagAsBotDescription: "もしこのアカウントをプログラム使うて運用するんやったら、このフラグをオンにしてや。オンにすれば、反応がバーッて連鎖せんように開発者が使うたり、Misskeyのシステム上での扱いがBotに合ったもんになるからな。"
flagAsCat: "Catやで" flagAsCat: "Catやで"
flagAsCatDescription: "ワレ、猫ちゃんならこのフラグをつけてみ?" flagAsCatDescription: "ワレ、猫ちゃんならこのフラグをつけてみ?"
flagShowTimelineReplies: "タイムラインにノートへの返信を表示するで" flagShowTimelineReplies: "タイムラインにノートへの返信を表示するで"
@ -194,10 +194,10 @@ network: "ネットワーク"
disk: "ディスク" disk: "ディスク"
instanceInfo: "サーバー情報" instanceInfo: "サーバー情報"
statistics: "統計" statistics: "統計"
clearQueue: "キューにさいなら" clearQueue: "キューをほかす"
clearQueueConfirmTitle: "キューをクリアしまっか?" clearQueueConfirmTitle: "キューをほかしとこか?"
clearQueueConfirmText: "未配達の投稿は配送されなくなるで。ふつうこの操作を行う必要は無いんやけどな。" clearQueueConfirmText: "未配達の投稿は配送されなるで。ふつうこの操作を行う必要は無いんやけどな。"
clearCachedFiles: "キャッシュにさいなら" clearCachedFiles: "キャッシュをほかす"
clearCachedFilesConfirm: "キャッシュされとるリモートファイルをみんなほかしてええか?" clearCachedFilesConfirm: "キャッシュされとるリモートファイルをみんなほかしてええか?"
blockedInstances: "ブロックしたサーバー" blockedInstances: "ブロックしたサーバー"
blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。ついでにそのサブドメインもブロックするで。" blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。ついでにそのサブドメインもブロックするで。"
@ -206,7 +206,7 @@ mutedUsers: "ミュートしたユーザー"
blockedUsers: "ブロックしたユーザー" blockedUsers: "ブロックしたユーザー"
noUsers: "ユーザーはおらん" noUsers: "ユーザーはおらん"
editProfile: "プロフィールをいじる" editProfile: "プロフィールをいじる"
noteDeleteConfirm: "このノートを削除しまっか?" noteDeleteConfirm: "このノートをほかしてええか?"
pinLimitExceeded: "これ以上ピン留めできひん" pinLimitExceeded: "これ以上ピン留めできひん"
intro: "Misskeyのインストールが完了したで管理者アカウントを作ってや。" intro: "Misskeyのインストールが完了したで管理者アカウントを作ってや。"
done: "でけた" done: "でけた"
@ -226,9 +226,9 @@ notResponding: "応答してへんで"
instanceFollowing: "サーバーのフォロー" instanceFollowing: "サーバーのフォロー"
instanceFollowers: "サーバーのフォロワー\n" instanceFollowers: "サーバーのフォロワー\n"
instanceUsers: "サーバーのユーザー" instanceUsers: "サーバーのユーザー"
changePassword: "パスワード変える" changePassword: "パスワードをいじる"
security: "セキュリティ" security: "セキュリティ"
retypedNotMatch: "入れたやつ同じになってないで。" retypedNotMatch: "入れたやつ合うてへんわ。"
currentPassword: "今のパスワード" currentPassword: "今のパスワード"
newPassword: "次のパスワード" newPassword: "次のパスワード"
newPasswordRetype: "今度のパスワード(もっぺん入れて)" newPasswordRetype: "今度のパスワード(もっぺん入れて)"
@ -258,7 +258,7 @@ uploadFromUrlRequested: "アップロードしたい言うといたで"
uploadFromUrlMayTakeTime: "アップロード終わるんにちょい時間かかるかもしれへんわ。" uploadFromUrlMayTakeTime: "アップロード終わるんにちょい時間かかるかもしれへんわ。"
explore: "みつける" explore: "みつける"
messageRead: "もう読んだ" messageRead: "もう読んだ"
noMoreHistory: "これより過去の履歴はあらへんで" noMoreHistory: "これより昔のんはあらへんで"
startMessaging: "チャットやるで" startMessaging: "チャットやるで"
nUsersRead: "{n}人が読んでもうた" nUsersRead: "{n}人が読んでもうた"
agreeTo: "{0}に同意したで" agreeTo: "{0}に同意したで"
@ -294,14 +294,14 @@ createFolder: "フォルダー作る"
renameFolder: "フォルダー名を変える" renameFolder: "フォルダー名を変える"
deleteFolder: "フォルダーをほかす" deleteFolder: "フォルダーをほかす"
addFile: "ファイルを追加" addFile: "ファイルを追加"
emptyDrive: "ドライブにはなんも残っとらん" emptyDrive: "ドライブは空っぽや"
emptyFolder: "このフォルダーは空や" emptyFolder: "このフォルダーは空や"
unableToDelete: "消そうおもってんけどな、あかんかったわ" unableToDelete: "消んかったわ"
inputNewFileName: "今度のファイル名は何にするん?" inputNewFileName: "今度のファイル名は何にするん?"
inputNewDescription: "新しいキャプションを入れてや" inputNewDescription: "新しいキャプションを入れてや"
inputNewFolderName: "今度のフォルダ名は何にするん?" inputNewFolderName: "今度のフォルダ名は何にするん?"
circularReferenceFolder: "移動先のフォルダーは、移動するフォルダーのサブフォルダーや。" circularReferenceFolder: "移動先のフォルダーは、移動するフォルダーのサブフォルダーや。"
hasChildFilesOrFolders: "このフォルダ、まだなんか入っとるから消されへん" hasChildFilesOrFolders: "このフォルダは空っぽちゃうから消されへん"
copyUrl: "URLをコピー" copyUrl: "URLをコピー"
rename: "名前を変えるで" rename: "名前を変えるで"
avatar: "アイコン" avatar: "アイコン"
@ -506,6 +506,7 @@ objectStorageUseSSLDesc: "API接続にhttpsを使わん場合はオフにする
objectStorageUseProxy: "Proxyを使う" objectStorageUseProxy: "Proxyを使う"
objectStorageUseProxyDesc: "API接続にproxy使わんのやったら切ってくれへん" objectStorageUseProxyDesc: "API接続にproxy使わんのやったら切ってくれへん"
objectStorageSetPublicRead: "アップロードした時に'public-read'を設定してや" objectStorageSetPublicRead: "アップロードした時に'public-read'を設定してや"
s3ForcePathStyleDesc: "s3ForcePathStyleを使たらバケット名をURLのホスト名やなくてパスの一部として必ず指定させるようになるで。セルフホストされたMinioとかを使うてるんやったら有効にせなあかん場合があるで。"
serverLogs: "サーバーログ" serverLogs: "サーバーログ"
deleteAll: "全部ほかす" deleteAll: "全部ほかす"
showFixedPostForm: "タイムラインの上の方で投稿できるようにやってくれへん?" showFixedPostForm: "タイムラインの上の方で投稿できるようにやってくれへん?"
@ -643,7 +644,7 @@ reporter: "通報者"
reporteeOrigin: "通報先" reporteeOrigin: "通報先"
reporterOrigin: "通報元" reporterOrigin: "通報元"
forwardReport: "リモートサーバーに通報を転送するで" forwardReport: "リモートサーバーに通報を転送するで"
forwardReportIsAnonymous: "リモートインスタンスからはあんたの情報は見れへんくって、匿名のシステムアカウントとして表示されるで。" forwardReportIsAnonymous: "リモートサーバーからはあんたの情報は見えんなって、匿名のシステムアカウントとして表示されるで。"
send: "送信" send: "送信"
abuseMarkAsResolved: "対応したで" abuseMarkAsResolved: "対応したで"
openInNewTab: "新しいタブで開く" openInNewTab: "新しいタブで開く"
@ -739,7 +740,7 @@ capacity: "容量"
inUse: "使用中" inUse: "使用中"
editCode: "コードを編集" editCode: "コードを編集"
apply: "適用" apply: "適用"
receiveAnnouncementFromInstance: "インスタンスからのお知らせを受け取る" receiveAnnouncementFromInstance: "サーバーからのお知らせを受け取る"
emailNotification: "メール通知" emailNotification: "メール通知"
publish: "公開" publish: "公開"
inChannelSearch: "チャンネル内検索" inChannelSearch: "チャンネル内検索"
@ -767,7 +768,7 @@ active: "アクティブ"
offline: "オフライン" offline: "オフライン"
notRecommended: "あんま推奨しやんで" notRecommended: "あんま推奨しやんで"
botProtection: "Botプロテクション" botProtection: "Botプロテクション"
instanceBlocking: "インスタンスブロック" instanceBlocking: "サーバーブロック"
selectAccount: "アカウントを選んでなー" selectAccount: "アカウントを選んでなー"
switchAccount: "アカウントを変えるで" switchAccount: "アカウントを変えるで"
enabled: "有効" enabled: "有効"
@ -851,8 +852,8 @@ themeColor: "テーマカラー"
size: "大きさ" size: "大きさ"
numberOfColumn: "列の数" numberOfColumn: "列の数"
searchByGoogle: "探す" searchByGoogle: "探す"
instanceDefaultLightTheme: "インスタンスの最初の明るいテーマ" instanceDefaultLightTheme: "サーバーおすすめの明るいテーマ"
instanceDefaultDarkTheme: "インスタンスの最初の暗いテーマ" instanceDefaultDarkTheme: "サーバーおすすめのの暗いテーマ"
instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入するで。" instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入するで。"
mutePeriod: "ミュートする期間" mutePeriod: "ミュートする期間"
period: "期限" period: "期限"
@ -866,7 +867,7 @@ reflectMayTakeTime: "反映されるまで時間がかかることがあるで"
failedToFetchAccountInformation: "アカウントの取得に失敗したみたいや…" failedToFetchAccountInformation: "アカウントの取得に失敗したみたいや…"
rateLimitExceeded: "レート制限が超えたみたいやで" rateLimitExceeded: "レート制限が超えたみたいやで"
cropImage: "画像のクロップ" cropImage: "画像のクロップ"
cropImageAsk: "画像をクロップしたってええか?" cropImageAsk: "画像をクロップしええか?"
cropYes: "切り抜いたる" cropYes: "切り抜いたる"
cropNo: "切り抜かへん" cropNo: "切り抜かへん"
file: "ファイル" file: "ファイル"
@ -901,11 +902,11 @@ sensitiveMediaDetection: "センシティブなメディアの検出"
localOnly: "ローカルのみ" localOnly: "ローカルのみ"
remoteOnly: "リモートのみ" remoteOnly: "リモートのみ"
failedToUpload: "アップロードに失敗してもうたわ…" failedToUpload: "アップロードに失敗してもうたわ…"
cannotUploadBecauseInappropriate: "不適切な内容を含むかもしれへんって判定されたでアップロードできまへん。" cannotUploadBecauseInappropriate: "不適切な内容を含むかもしれへんって判定されたからアップロードできへんわ。"
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いでアップロードできまへん。" cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いからアップロードできへんわ。"
beta: "ベータ" beta: "ベータ"
enableAutoSensitive: "自動NSFW判定" enableAutoSensitive: "自動NSFW判定"
enableAutoSensitiveDescription: "使える時は、機械学習を使って自動でメディアにNSFWフラグを設定するで。この機能をオフにしても、インスタンスによっては自動で設定されることがあるで。" enableAutoSensitiveDescription: "使える時は、機械学習を使って自動でメディアにNSFWフラグを設定するで。この機能をオフにしても、サーバーによっては自動で設定されることがあるで。"
activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかとかを判定して積極的に行うで。オフにすると単に文字列として正しいかどうかだけチェックするで。" activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかとかを判定して積極的に行うで。オフにすると単に文字列として正しいかどうかだけチェックするで。"
navbar: "ナビゲーションバー" navbar: "ナビゲーションバー"
shuffle: "シャッフルするで" shuffle: "シャッフルするで"
@ -915,10 +916,11 @@ pushNotification: "プッシュ通知"
subscribePushNotification: "プッシュ通知をオンにするで" subscribePushNotification: "プッシュ通知をオンにするで"
unsubscribePushNotification: "プッシュ通知を止めるで" unsubscribePushNotification: "プッシュ通知を止めるで"
pushNotificationAlreadySubscribed: "プッシュ通知はオンになってるで" pushNotificationAlreadySubscribed: "プッシュ通知はオンになってるで"
pushNotificationNotSupported: "ブラウザかインスタンスがプッシュ通知に対応してないみたいやで。" pushNotificationNotSupported: "ブラウザかサーバーがプッシュ通知に対応してないみたいやで。"
sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を消すで" sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を消すで"
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」っていう表示が一瞬表示されるようになるで。端末の電池使用量が増える可能性があるで。" sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」っていう表示が一瞬表示されるようになるで。端末の電池使用量が増える可能性があるで。"
windowMaximize: "最大化" windowMaximize: "最大化"
windowMinimize: "最小化"
windowRestore: "元に戻す" windowRestore: "元に戻す"
caption: "キャプション" caption: "キャプション"
loggedInAsBot: "Botアカウントでログイン中やで" loggedInAsBot: "Botアカウントでログイン中やで"
@ -931,7 +933,7 @@ numberOfLikes: "いいね数"
show: "表示" show: "表示"
neverShow: "今後表示しない" neverShow: "今後表示しない"
remindMeLater: "また後で" remindMeLater: "また後で"
didYouLikeMisskey: "Misskeyを気に入っとっただけましたん" didYouLikeMisskey: "Misskey気に入ってくれた"
pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアやで。これからも開発を続けれるように、寄付したってな~。" pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアやで。これからも開発を続けれるように、寄付したってな~。"
roles: "ロール" roles: "ロール"
role: "ロール" role: "ロール"
@ -941,7 +943,7 @@ assign: "アサイン"
unassign: "アサインを解除" unassign: "アサインを解除"
color: "色" color: "色"
manageCustomEmojis: "カスタム絵文字の管理" manageCustomEmojis: "カスタム絵文字の管理"
youCannotCreateAnymore: "これ以上作れなさそうや" youCannotCreateAnymore: "これ以上作れなさそうや"
cannotPerformTemporary: "一時的に利用できへんで" cannotPerformTemporary: "一時的に利用できへんで"
cannotPerformTemporaryDescription: "操作回数が制限を超えたから一時的に利用できへんくなったで。ちょっと時間置いてからもう一回やってやー。" cannotPerformTemporaryDescription: "操作回数が制限を超えたから一時的に利用できへんくなったで。ちょっと時間置いてからもう一回やってやー。"
preset: "プリセット" preset: "プリセット"
@ -960,7 +962,9 @@ copyErrorInfo: "エラー情報をコピー"
joinThisServer: "このサーバーに登録するわ" joinThisServer: "このサーバーに登録するわ"
exploreOtherServers: "他のサーバー見てみる" exploreOtherServers: "他のサーバー見てみる"
letsLookAtTimeline: "タイムライン見てみーや" letsLookAtTimeline: "タイムライン見てみーや"
disableFederationWarn: "連合が無効になっとるで。無効にしても投稿は非公開ってわけちゃうねん。大体の場合はこのオプションを有効にする必要は別にないで。" disableFederationConfirm: "連合なしにしとくか?"
disableFederationConfirmWarn: "連合なしにしても投稿は非公開にはならへんで。大体の場合は連合なしにする必要はないで。"
disableFederationOk: "連合なしにしとく"
invitationRequiredToRegister: "今このサーバー招待制になってもうてんねん。招待コードを持っとるんやったら登録できるで。" invitationRequiredToRegister: "今このサーバー招待制になってもうてんねん。招待コードを持っとるんやったら登録できるで。"
emailNotSupported: "このサーバーはメール配信がサポートされてへんみたいやわ" emailNotSupported: "このサーバーはメール配信がサポートされてへんみたいやわ"
postToTheChannel: "チャンネルに投稿" postToTheChannel: "チャンネルに投稿"
@ -983,6 +987,9 @@ retryAllQueuesConfirmText: "一時的にサーバー重なるかもしれへん
enableChartsForRemoteUser: "リモートユーザーのチャートを作る" enableChartsForRemoteUser: "リモートユーザーのチャートを作る"
enableChartsForFederatedInstances: "リモートサーバーのチャートを作る" enableChartsForFederatedInstances: "リモートサーバーのチャートを作る"
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加" showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
largeNoteReactions: "ノートのリアクションを大きする"
noteIdOrUrl: "ートIDかURL"
accountMoved: "このユーザーはさらのアカウントに引っ越したで:"
_achievements: _achievements:
earnedAt: "貰った日ぃ" earnedAt: "貰った日ぃ"
_types: _types:
@ -1043,7 +1050,7 @@ _achievements:
_login7: _login7:
title: "ビギナーⅡ" title: "ビギナーⅡ"
description: "通算7日ログインした" description: "通算7日ログインした"
flavor: "慣れてきたんちゃう?" flavor: "慣れてきたんちゃう?"
_login15: _login15:
title: "ビギナーⅢ" title: "ビギナーⅢ"
description: "通算15日ログインした" description: "通算15日ログインした"
@ -1147,7 +1154,7 @@ _achievements:
_iLoveMisskey: _iLoveMisskey:
title: "Misskey好きやねん" title: "Misskey好きやねん"
description: "\"I ❤ #Misskey\"を投稿した" description: "\"I ❤ #Misskey\"を投稿した"
flavor: "Misskeyを使ってくれてありがとう by 開発チーム" flavor: "Misskeyを使ってくれておおきに by 開発チーム"
_foundTreasure: _foundTreasure:
title: "なんでも鑑定団" title: "なんでも鑑定団"
description: "隠されたお宝を発見した" description: "隠されたお宝を発見した"
@ -1173,7 +1180,7 @@ _achievements:
description: "ホームタイムラインの流速が20npmを超す" description: "ホームタイムラインの流速が20npmを超す"
_viewInstanceChart: _viewInstanceChart:
title: "アナリスト" title: "アナリスト"
description: "インスタンスのチャートを表示した" description: "サーバーのチャートを表示した"
_outputHelloWorldOnScratchpad: _outputHelloWorldOnScratchpad:
title: "Hello, world!" title: "Hello, world!"
description: "スクラッチパッドで hello worldを出力した" description: "スクラッチパッドで hello worldを出力した"
@ -1210,7 +1217,7 @@ _achievements:
_loggedInOnNewYearsDay: _loggedInOnNewYearsDay:
title: "あけましておめでとうございます!" title: "あけましておめでとうございます!"
description: "元旦にログインした" description: "元旦にログインした"
flavor: "今年も弊インスタンスをよろしくお願いします" flavor: "今年も弊サーバーをよろしゅう頼みますわ"
_cookieClicked: _cookieClicked:
title: "クッキー叩くやつ" title: "クッキー叩くやつ"
description: "クッキー叩いてもうた" description: "クッキー叩いてもうた"
@ -1225,8 +1232,8 @@ _role:
name: "ロール名" name: "ロール名"
description: "ロールの説明" description: "ロールの説明"
permission: "ロールの権限" permission: "ロールの権限"
descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関わる操作を行えるで。\n<b>管理者</b>はインスタンスの全ての設定を変更できるで。" descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関わる操作を行えるで。\n<b>管理者</b>はサーバーの全ての設定を変更できるで。"
assignTarget: "アサインターゲット" assignTarget: "アサイン"
descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれてるかを手動で管理するで。\n<b>コンディショナル</b>は条件を設定して、それに合うユーザーが自動で含まれるようになるで。" descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれてるかを手動で管理するで。\n<b>コンディショナル</b>は条件を設定して、それに合うユーザーが自動で含まれるようになるで。"
manual: "マニュアル" manual: "マニュアル"
conditional: "コンディショナル" conditional: "コンディショナル"
@ -1255,7 +1262,7 @@ _role:
gtlAvailable: "グローバルタイムラインの閲覧" gtlAvailable: "グローバルタイムラインの閲覧"
ltlAvailable: "ローカルタイムラインの閲覧" ltlAvailable: "ローカルタイムラインの閲覧"
canPublicNote: "パブリック投稿の許可" canPublicNote: "パブリック投稿の許可"
canInvite: "インスタンス招待コードの発行" canInvite: "サーバー招待コードの発行"
canManageCustomEmojis: "カスタム絵文字の管理" canManageCustomEmojis: "カスタム絵文字の管理"
driveCapacity: "ドライブ容量" driveCapacity: "ドライブ容量"
pinMax: "ノートのピン留めの最大数" pinMax: "ノートのピン留めの最大数"
@ -1267,7 +1274,7 @@ _role:
userListMax: "ユーザーリストの作成可能数" userListMax: "ユーザーリストの作成可能数"
userEachUserListsMax: "ユーザーリスト内のユーザーの最大数" userEachUserListsMax: "ユーザーリスト内のユーザーの最大数"
rateLimitFactor: "レートリミット" rateLimitFactor: "レートリミット"
descriptionOfRateLimitFactor: "ちっちゃいほど制限が緩なって、大きいほど制限されるで。" descriptionOfRateLimitFactor: "ちっちゃいほど制限が緩なって、大きいほど制限されるで。"
canHideAds: "広告を表示させへん" canHideAds: "広告を表示させへん"
canSearchNotes: "ノート検索を使わすかどうか" canSearchNotes: "ノート検索を使わすかどうか"
_condition: _condition:
@ -1320,7 +1327,7 @@ _ad:
_forgotPassword: _forgotPassword:
enterEmail: "アカウントに登録したメールアドレスをここに入力してや。そのアドレス宛に、パスワードリセット用のリンクが送られるから待っててな~。" enterEmail: "アカウントに登録したメールアドレスをここに入力してや。そのアドレス宛に、パスワードリセット用のリンクが送られるから待っててな~。"
ifNoEmail: "メールアドレスを登録してへんのやったら、管理者まで教えてな~。" ifNoEmail: "メールアドレスを登録してへんのやったら、管理者まで教えてな~。"
contactAdmin: "このインスタンスはメールに対応してへんから、パスワードリセットをしたいときは管理者まで教えてな~。" contactAdmin: "このサーバーはメールに対応してへんから、パスワードリセットをしたいときは管理者まで教えてな~。"
_gallery: _gallery:
my: "あんたの投稿" my: "あんたの投稿"
liked: "いいねした投稿" liked: "いいねした投稿"
@ -1405,10 +1412,10 @@ _wordMute:
hard: "ハード" hard: "ハード"
mutedNotes: "ミュートされたノート" mutedNotes: "ミュートされたノート"
_instanceMute: _instanceMute:
instanceMuteDescription: "ミュートしたインスタンスのユーザーへの返信を含めて、設定したインスタンスの全てのートとRenoteをミュートにするで。" instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したインスタンスの全てのートとRenoteをミュートにするで。"
instanceMuteDescription2: "改行で区切って設定するんやで" instanceMuteDescription2: "改行で区切って設定するんやで"
title: "設定したインスタンスのノートを隠すで。" title: "設定したサーバーのノートを隠すで。"
heading: "ミュートするインスタンス" heading: "ミュートするサーバー"
_theme: _theme:
explore: "テーマを探す" explore: "テーマを探す"
install: "テーマのインストール" install: "テーマのインストール"
@ -1630,7 +1637,7 @@ _widgets:
digitalClock: "デジタル時計" digitalClock: "デジタル時計"
unixClock: "UNIX時計" unixClock: "UNIX時計"
federation: "連合" federation: "連合"
instanceCloud: "インスタンスクラウド" instanceCloud: "サーバークラウド"
postForm: "投稿フォーム" postForm: "投稿フォーム"
slideshow: "スライドショー" slideshow: "スライドショー"
button: "ボタン" button: "ボタン"
@ -1681,7 +1688,7 @@ _visibility:
specified: "ダイレクト" specified: "ダイレクト"
specifiedDescription: "選んだユーザーのみに公開するで" specifiedDescription: "選んだユーザーのみに公開するで"
disableFederation: "連合なし" disableFederation: "連合なし"
disableFederationDescription: "他インスタンスへは送らんとくわ" disableFederationDescription: "他サーバーへは送らんとくわ"
_postForm: _postForm:
replyPlaceholder: "このノートに返信..." replyPlaceholder: "このノートに返信..."
quotePlaceholder: "このノートを引用..." quotePlaceholder: "このノートを引用..."
@ -1893,4 +1900,3 @@ _webhookSettings:
renote: "Renoteされるとき" renote: "Renoteされるとき"
reaction: "リアクションがあるとき~!" reaction: "リアクションがあるとき~!"
mention: "メンションがあるとき~!" mention: "メンションがあるとき~!"

View File

@ -1,2 +1 @@
--- ---

View File

@ -103,4 +103,3 @@ _deck:
_columns: _columns:
notifications: "Ilɣuyen" notifications: "Ilɣuyen"
list: "Tibdarin" list: "Tibdarin"

View File

@ -83,4 +83,3 @@ _deck:
notifications: "ಅಧಿಸೂಚನೆಗಳು" notifications: "ಅಧಿಸೂಚನೆಗಳು"
tl: "ಸಮಯಸಾಲು" tl: "ಸಮಯಸಾಲು"
mentions: "ಹೆಸರಿಸಿದ" mentions: "ಹೆಸರಿಸಿದ"

View File

@ -958,7 +958,6 @@ copyErrorInfo: "오류 정보 복사"
joinThisServer: "이 서버에 가입" joinThisServer: "이 서버에 가입"
exploreOtherServers: "다른 서버 둘러보기" exploreOtherServers: "다른 서버 둘러보기"
letsLookAtTimeline: "타임라인 구경하기" letsLookAtTimeline: "타임라인 구경하기"
disableFederationWarn: "연합이 비활성화됩니다. 비활성화해도 게시물이 비공개가 되지는 않습니다. 대부분의 경우 이 옵션을 활성화할 필요가 없습니다."
invitationRequiredToRegister: "현재 이 서버는 비공개입니다. 회원가입을 하시려면 초대 코드가 필요합니다." invitationRequiredToRegister: "현재 이 서버는 비공개입니다. 회원가입을 하시려면 초대 코드가 필요합니다."
emailNotSupported: "이 서버에서는 메일 전송을 지원하지 않습니다" emailNotSupported: "이 서버에서는 메일 전송을 지원하지 않습니다"
postToTheChannel: "채널에 게시하기" postToTheChannel: "채널에 게시하기"
@ -1851,4 +1850,3 @@ _dialog:
_webhookSettings: _webhookSettings:
name: "이름" name: "이름"
active: "활성화" active: "활성화"

View File

@ -368,4 +368,3 @@ _deck:
list: "ລາຍການ" list: "ລາຍການ"
channel: "ຊ່ອງ" channel: "ຊ່ອງ"
mentions: "ກ່າວເຖິງ" mentions: "ກ່າວເຖິງ"

View File

@ -485,4 +485,3 @@ _deck:
mentions: "Vermeldingen" mentions: "Vermeldingen"
_webhookSettings: _webhookSettings:
name: "Naam" name: "Naam"

View File

@ -1,3 +1,2 @@
--- ---
_lang_: "Norsk Bokmål" _lang_: "Norsk Bokmål"

View File

@ -129,6 +129,7 @@ unblockConfirm: "Czy na pewno chcesz odblokować to konto?"
suspendConfirm: "Czy na pewno chcesz zawiesić to konto?" suspendConfirm: "Czy na pewno chcesz zawiesić to konto?"
unsuspendConfirm: "Czy na pewno chcesz cofnąć zawieszenie tego konta?" unsuspendConfirm: "Czy na pewno chcesz cofnąć zawieszenie tego konta?"
selectList: "Wybierz listę" selectList: "Wybierz listę"
selectChannel: "Wybierz kanał"
selectAntenna: "Wybierz Antennę" selectAntenna: "Wybierz Antennę"
selectWidget: "Wybierz widżet" selectWidget: "Wybierz widżet"
editWidgets: "Edytuj widżety" editWidgets: "Edytuj widżety"
@ -149,6 +150,7 @@ flagAsCatDescription: "Przełącz tę opcję, aby konto było oznaczone jako kot
flagShowTimelineReplies: "Pokazuj odpowiedzi na osi czasu" flagShowTimelineReplies: "Pokazuj odpowiedzi na osi czasu"
autoAcceptFollowed: "Automatycznie przyjmuj prośby o możliwość obserwacji od użytkowników, których obserwujesz" autoAcceptFollowed: "Automatycznie przyjmuj prośby o możliwość obserwacji od użytkowników, których obserwujesz"
addAccount: "Dodaj konto" addAccount: "Dodaj konto"
reloadAccountsList: "Odśwież listę kont"
loginFailed: "Nie udało się zalogować" loginFailed: "Nie udało się zalogować"
showOnRemote: "Zobacz na zdalnej instancji" showOnRemote: "Zobacz na zdalnej instancji"
general: "Ogólne" general: "Ogólne"
@ -159,6 +161,7 @@ searchWith: "Szukaj: {q}"
youHaveNoLists: "Nie masz żadnej listy" youHaveNoLists: "Nie masz żadnej listy"
followConfirm: "Czy na pewno chcesz zaobserwować {name}?" followConfirm: "Czy na pewno chcesz zaobserwować {name}?"
proxyAccount: "Konto proxy" proxyAccount: "Konto proxy"
proxyAccountDescription: "Opis konta pełnomocniczego"
host: "Host" host: "Host"
selectUser: "Wybierz użytkownika" selectUser: "Wybierz użytkownika"
recipient: "Odbiorca" recipient: "Odbiorca"
@ -253,6 +256,7 @@ noMoreHistory: "Nie ma dalszej historii"
startMessaging: "Rozpocznij czat" startMessaging: "Rozpocznij czat"
nUsersRead: "przeczytano przez {n}" nUsersRead: "przeczytano przez {n}"
agreeTo: "Wyrażam zgodę na {0}" agreeTo: "Wyrażam zgodę na {0}"
agreeBelow: "Zaakceptuj poniżej"
tos: "Regulamin" tos: "Regulamin"
start: "Rozpocznij" start: "Rozpocznij"
home: "Strona główna" home: "Strona główna"
@ -385,13 +389,19 @@ about: "Informacje"
aboutMisskey: "O Misskey" aboutMisskey: "O Misskey"
administrator: "Admin" administrator: "Admin"
token: "Token" token: "Token"
2fa: "Klucz 2FA "
totp: "Klucz aplikacji uwierzytelniającej (totp)"
totpDescription: "Opis klucza czasowego"
moderator: "Moderator" moderator: "Moderator"
moderation: "Moderacja" moderation: "Moderacja"
nUsersMentioned: "{n} wspomnianych użytkowników" nUsersMentioned: "{n} wspomnianych użytkowników"
securityKeyAndPasskey: "Klucz bezpieczeństwa i klucze Passkey"
securityKey: "Klucz bezpieczeństwa" securityKey: "Klucz bezpieczeństwa"
lastUsed: "Ostatnio używane" lastUsed: "Ostatnio używane"
lastUsedAt: "Ostatnio używane w"
unregister: "Cofnij rejestrację" unregister: "Cofnij rejestrację"
passwordLessLogin: "Skonfiguruj logowanie bez użycia hasła" passwordLessLogin: "Skonfiguruj logowanie bez użycia hasła"
passwordLessLoginDescription: "Opis logowania bez użycia hasła"
resetPassword: "Zresetuj hasło" resetPassword: "Zresetuj hasło"
newPasswordIs: "Nowe hasło to „{password}”" newPasswordIs: "Nowe hasło to „{password}”"
reduceUiAnimation: "Ogranicz animacje w UI" reduceUiAnimation: "Ogranicz animacje w UI"
@ -518,11 +528,16 @@ disablePagesScript: "Wyłącz AiScript na Stronach"
updateRemoteUser: "Aktualizuj zdalne dane o użytkowniku" updateRemoteUser: "Aktualizuj zdalne dane o użytkowniku"
deleteAllFiles: "Usuń wszystkie pliki" deleteAllFiles: "Usuń wszystkie pliki"
deleteAllFilesConfirm: "Czy na pewno chcesz usunąć wszystkie pliki?" deleteAllFilesConfirm: "Czy na pewno chcesz usunąć wszystkie pliki?"
removeAllFollowing: "Przestań obserwować"
removeAllFollowingDescription: "Przestań obserwować wszystkie konta z {host}. Wykonaj to, jeżeli instancja już nie istnieje." removeAllFollowingDescription: "Przestań obserwować wszystkie konta z {host}. Wykonaj to, jeżeli instancja już nie istnieje."
userSuspended: "To konto zostało zawieszone." userSuspended: "To konto zostało zawieszone."
userSilenced: "Ten użytkownik został wyciszony." userSilenced: "Ten użytkownik został wyciszony."
yourAccountSuspendedTitle: "To konto jest zawieszone" yourAccountSuspendedTitle: "To konto jest zawieszone"
yourAccountSuspendedDescription: "To konto zostało zawieszone z powodu złamania regulaminu serwera lub innych podobnych. Skontaktuj się z administratorem, jeśli chciałbyś poznać bardziej szczegółowy powód. Proszę nie zakładać nowego konta." yourAccountSuspendedDescription: "To konto zostało zawieszone z powodu złamania regulaminu serwera lub innych podobnych. Skontaktuj się z administratorem, jeśli chciałbyś poznać bardziej szczegółowy powód. Proszę nie zakładać nowego konta."
tokenRevoked: "Token odrzucony"
tokenRevokedDescription: "Opis odrzuconego tokena"
accountDeleted: "Konto usunięte"
accountDeletedDescription: "Opis konta usuniętego"
menu: "Menu" menu: "Menu"
divider: "Rozdzielacz" divider: "Rozdzielacz"
addItem: "Dodaj element" addItem: "Dodaj element"
@ -548,7 +563,9 @@ author: "Autor"
leaveConfirm: "Są niezapisane zmiany. Czy chcesz je odrzucić?" leaveConfirm: "Są niezapisane zmiany. Czy chcesz je odrzucić?"
manage: "Zarządzanie" manage: "Zarządzanie"
plugins: "Wtyczki" plugins: "Wtyczki"
preferencesBackups: "Kopia zapasowa ustawień"
deck: "Tablica" deck: "Tablica"
undeck: "oddkouj"
useBlurEffectForModal: "Używaj efektu rozmycia w modalach" useBlurEffectForModal: "Używaj efektu rozmycia w modalach"
useFullReactionPicker: "Używaj pełnowymiarowego wybornika reakcji" useFullReactionPicker: "Używaj pełnowymiarowego wybornika reakcji"
width: "Szerokość" width: "Szerokość"
@ -815,6 +832,8 @@ tenMinutes: "10 minut"
oneHour: "1 godzina" oneHour: "1 godzina"
oneDay: "1 dzień" oneDay: "1 dzień"
oneWeek: "1 tydzień" oneWeek: "1 tydzień"
oneMonth: "jeden miesiąc"
failedToFetchAccountInformation: "Nie udało się uzyskać informacji o koncie"
file: "Pliki" file: "Pliki"
recommended: "Zalecane" recommended: "Zalecane"
check: "Zweryfikuj" check: "Zweryfikuj"
@ -1360,4 +1379,3 @@ _deck:
_webhookSettings: _webhookSettings:
name: "Nazwa" name: "Nazwa"
active: "Właczono" active: "Właczono"

View File

@ -557,4 +557,3 @@ _deck:
direct: "Notas diretas" direct: "Notas diretas"
_webhookSettings: _webhookSettings:
name: "Nome" name: "Nome"

View File

@ -703,4 +703,3 @@ _deck:
mentions: "Mențiuni" mentions: "Mențiuni"
_webhookSettings: _webhookSettings:
name: "Nume" name: "Nume"

View File

@ -950,7 +950,6 @@ copyErrorInfo: "Скопировать код ошибки"
joinThisServer: "Присоединяйтесь к этому серверу" joinThisServer: "Присоединяйтесь к этому серверу"
exploreOtherServers: "Искать другие сервера" exploreOtherServers: "Искать другие сервера"
letsLookAtTimeline: "Давайте посмотрим на ленту" letsLookAtTimeline: "Давайте посмотрим на ленту"
disableFederationWarn: "Объединение отключено. Если вы отключите это, сообщение не будет приватным. В большинстве случаев вам не нужно включать эту опцию."
_achievements: _achievements:
earnedAt: "Разблокировано в" earnedAt: "Разблокировано в"
_types: _types:
@ -1839,4 +1838,3 @@ _dialog:
_webhookSettings: _webhookSettings:
name: "Название" name: "Название"
active: "Вкл." active: "Вкл."

View File

@ -1,2 +1 @@
--- ---

View File

@ -1477,4 +1477,3 @@ _deck:
_webhookSettings: _webhookSettings:
name: "Názov" name: "Názov"
active: "Zapnuté" active: "Zapnuté"

View File

@ -343,9 +343,15 @@ recentlyRegisteredUsers: "Nyligen registrerade användare"
userList: "Listor" userList: "Listor"
aboutMisskey: "Om Misskey" aboutMisskey: "Om Misskey"
administrator: "Administratör" administrator: "Administratör"
passwordLessLogin: "Lösenordsfri inloggning"
passwordLessLoginDescription: "Tillåter lösenordsfri inloggning med endast en säkerhetsnyckel eller en passkey."
newPasswordIs: "Det nya lösenordet är \"{password}\"" newPasswordIs: "Det nya lösenordet är \"{password}\""
share: "Dela" share: "Dela"
enable: "Aktivera" enable: "Aktivera"
weakPassword: "Svagt Lösenord"
normalPassword: "Medel Lösenord"
strongPassword: "Starkt Lösenord"
signinFailed: "Kan inte logga in. Det angivna användarnamnet eller lösenordet är felaktigt."
serviceworkerInfo: "Måste vara aktiverad för pushnotiser." serviceworkerInfo: "Måste vara aktiverad för pushnotiser."
enableInfiniteScroll: "Ladda mer automatiskt" enableInfiniteScroll: "Ladda mer automatiskt"
enablePlayer: "Öppna videospelare" enablePlayer: "Öppna videospelare"
@ -354,11 +360,13 @@ enableEmail: "Aktivera epost-utskick"
smtpHost: "Värd" smtpHost: "Värd"
smtpUser: "Användarnamn" smtpUser: "Användarnamn"
smtpPass: "Lösenord" smtpPass: "Lösenord"
emptyToDisableSmtpAuth: "Lämna användarnamn och lösenord tomt för att avaktivera SMTP verifiering"
clearCache: "Rensa cache" clearCache: "Rensa cache"
enabled: "Aktiverad" enabled: "Aktiverad"
user: "Användare" user: "Användare"
global: "Global" global: "Global"
squareAvatars: "Visa fyrkantiga profilbilder" squareAvatars: "Visa fyrkantiga profilbilder"
incorrectPassword: "Fel lösenord."
searchByGoogle: "Sök" searchByGoogle: "Sök"
file: "Filer" file: "Filer"
enableAutoSensitive: "Automatisk NSFW markering" enableAutoSensitive: "Automatisk NSFW markering"
@ -368,6 +376,15 @@ subscribePushNotification: "Aktivera pushnotiser"
unsubscribePushNotification: "Avaktivera pushnotiser" unsubscribePushNotification: "Avaktivera pushnotiser"
pushNotificationAlreadySubscribed: "Pushnotiser är redan aktiverade" pushNotificationAlreadySubscribed: "Pushnotiser är redan aktiverade"
pushNotificationNotSupported: "Din webbläsare eller instans har inte stöd för pushnotiser" pushNotificationNotSupported: "Din webbläsare eller instans har inte stöd för pushnotiser"
windowMaximize: "Maximera"
windowMinimize: "Minimera"
windowRestore: "Återställ"
resetPasswordConfirm: "Återställ verkligen ditt lösenord?"
_achievements:
_types:
_open3windows:
title: "Flera Fönster"
description: "Ha minst 3 fönster öppna samtidigt"
_email: _email:
_follow: _follow:
title: "följde dig" title: "följde dig"
@ -384,6 +401,7 @@ _sfx:
chat: "Chatt" chat: "Chatt"
antenna: "Antenner" antenna: "Antenner"
_2fa: _2fa:
passwordToTOTP: "Skriv in ditt lösenord"
renewTOTPCancel: "Nej tack" renewTOTPCancel: "Nej tack"
_antennaSources: _antennaSources:
all: "Alla noter" all: "Alla noter"
@ -444,4 +462,3 @@ _deck:
mentions: "Omnämningar" mentions: "Omnämningar"
_webhookSettings: _webhookSettings:
active: "Aktiverad" active: "Aktiverad"

View File

@ -955,7 +955,6 @@ copyErrorInfo: "คัดลอกรายละเอียดข้อผิ
joinThisServer: "ลงชื่อสมัครใช้ในอินสแตนซ์นี้" joinThisServer: "ลงชื่อสมัครใช้ในอินสแตนซ์นี้"
exploreOtherServers: "มองหาอินสแตนซ์อื่น" exploreOtherServers: "มองหาอินสแตนซ์อื่น"
letsLookAtTimeline: "ลองดูที่ไทม์ไลน์" letsLookAtTimeline: "ลองดูที่ไทม์ไลน์"
disableFederationWarn: "การดำเนินการนี้ถ้าหากจะปิดใช้งานการรวมศูนย์ แต่โพสต์ดังกล่าวนั้นจะยังคงเป็นสาธารณะต่อไป ยกเว้นแต่ว่าจะตั้งค่าเป็นอย่างอื่น โดยปกติคุณไม่จำเป็นต้องใช้การตั้งค่านี้นะ"
invitationRequiredToRegister: "อินสแตนซ์นี้เป็นแบบรับเชิญเท่านั้น คุณต้องป้อนรหัสเชิญที่ถูกต้องถึงจะลงทะเบียนได้นะค่ะ" invitationRequiredToRegister: "อินสแตนซ์นี้เป็นแบบรับเชิญเท่านั้น คุณต้องป้อนรหัสเชิญที่ถูกต้องถึงจะลงทะเบียนได้นะค่ะ"
emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมลนะค่ะ" emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมลนะค่ะ"
postToTheChannel: "โพสต์ลงช่อง" postToTheChannel: "โพสต์ลงช่อง"
@ -1869,4 +1868,3 @@ _drivecleaner:
_webhookSettings: _webhookSettings:
name: "ชื่อ" name: "ชื่อ"
active: "เปิดใช้งาน" active: "เปิดใช้งาน"

View File

@ -60,4 +60,3 @@ _deck:
_columns: _columns:
notifications: "Bildirim" notifications: "Bildirim"
tl: "Zaman çizelgesi" tl: "Zaman çizelgesi"

View File

@ -2,4 +2,3 @@
_lang_: "ياپونچە" _lang_: "ياپونچە"
search: "ئىزدەش" search: "ئىزدەش"
searchByGoogle: "ئىزدەش" searchByGoogle: "ئىزدەش"

View File

@ -1641,4 +1641,3 @@ _deck:
_webhookSettings: _webhookSettings:
name: "Ім'я" name: "Ім'я"
active: "Увімкнено" active: "Увімкнено"

View File

@ -1707,4 +1707,3 @@ _dialog:
_webhookSettings: _webhookSettings:
name: "Tên" name: "Tên"
active: "Đã bật" active: "Đã bật"

View File

@ -16,7 +16,7 @@ cancel: "取消"
noThankYou: "不用,谢谢" noThankYou: "不用,谢谢"
enterUsername: "输入用户名" enterUsername: "输入用户名"
renotedBy: "由 {user} 转贴" renotedBy: "由 {user} 转贴"
noNotes: "没有帖" noNotes: "没有帖"
noNotifications: "无通知" noNotifications: "无通知"
instance: "服务器" instance: "服务器"
settings: "设置" settings: "设置"
@ -25,7 +25,7 @@ otherSettings: "其他设置"
openInWindow: "在新窗口中打开" openInWindow: "在新窗口中打开"
profile: "个人资料" profile: "个人资料"
timeline: "时间线" timeline: "时间线"
noAccountDescription: "这个人很懒,没有写自我介绍" noAccountDescription: "此用户尚无自我介绍"
login: "登录" login: "登录"
loggingIn: "正在登录..." loggingIn: "正在登录..."
logout: "登出" logout: "登出"
@ -60,7 +60,7 @@ youGotNewFollower: "你有新的关注者"
receiveFollowRequest: "您收到了关注请求" receiveFollowRequest: "您收到了关注请求"
followRequestAccepted: "您的关注请求被通过了" followRequestAccepted: "您的关注请求被通过了"
mention: "提及" mention: "提及"
mentions: "提" mentions: "提到我的"
directNotes: "私信" directNotes: "私信"
importAndExport: "导入和导出" importAndExport: "导入和导出"
import: "导入" import: "导入"
@ -85,7 +85,7 @@ somethingHappened: "出现了一些问题!"
retry: "重试" retry: "重试"
pageLoadError: "页面加载失败。" pageLoadError: "页面加载失败。"
pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。" pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。"
serverIsDead: "服务器没有响应。 请稍等片刻,然后重试。" serverIsDead: "没有服务器响应。 请稍后再试。"
youShouldUpgradeClient: "请重新加载并使用新版本的客户端查看此页面。" youShouldUpgradeClient: "请重新加载并使用新版本的客户端查看此页面。"
enterListName: "输入列表名称" enterListName: "输入列表名称"
privacy: "隐私" privacy: "隐私"
@ -95,7 +95,7 @@ follow: "关注"
followRequest: "关注申请" followRequest: "关注申请"
followRequests: "关注申请" followRequests: "关注申请"
unfollow: "取消关注" unfollow: "取消关注"
followRequestPending: "发送关注请求" followRequestPending: "关注请求批准中"
enterEmoji: "输入表情符号" enterEmoji: "输入表情符号"
renote: "转发" renote: "转发"
unrenote: "取消转发" unrenote: "取消转发"
@ -119,7 +119,7 @@ rememberNoteVisibility: "保存上次设置的可见性"
attachCancel: "删除附件" attachCancel: "删除附件"
markAsSensitive: "标记为敏感内容" markAsSensitive: "标记为敏感内容"
unmarkAsSensitive: "取消标记为敏感内容" unmarkAsSensitive: "取消标记为敏感内容"
enterFileName: "输入文件名" enterFileName: "输入文件名"
mute: "屏蔽" mute: "屏蔽"
unmute: "解除屏蔽" unmute: "解除屏蔽"
renoteMute: "屏蔽转帖" renoteMute: "屏蔽转帖"
@ -142,10 +142,10 @@ customEmojis: "自定义表情符号"
emoji: "表情符号" emoji: "表情符号"
emojis: "表情符号" emojis: "表情符号"
emojiName: "表情符号名称" emojiName: "表情符号名称"
emojiUrl: "表情符号地址" emojiUrl: "emoji 地址"
addEmoji: "添加表情符号" addEmoji: "添加表情符号"
settingGuide: "推荐配置" settingGuide: "推荐配置"
cacheRemoteFiles: "远程文件缓存" cacheRemoteFiles: "缓存远程文件"
cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程服务器载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。" cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程服务器载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。"
flagAsBot: "这是一个机器人账号" flagAsBot: "这是一个机器人账号"
flagAsBotDescription: "如果此帐户由程序控制请启用此项。启用后此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为并让Misskey的内部系统将此帐户识别为机器人。" flagAsBotDescription: "如果此帐户由程序控制请启用此项。启用后此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为并让Misskey的内部系统将此帐户识别为机器人。"
@ -153,7 +153,7 @@ flagAsCat: "将这个账户设定为一只猫"
flagAsCatDescription: "如果您想表明此帐户是一只猫,请打开此标志。\n开启后会在您的头像上出现猫耳朵并将你的帖子中的「na」替换为「nya」日文同理。" flagAsCatDescription: "如果您想表明此帐户是一只猫,请打开此标志。\n开启后会在您的头像上出现猫耳朵并将你的帖子中的「na」替换为「nya」日文同理。"
flagShowTimelineReplies: "在时间线上显示帖子的回复" flagShowTimelineReplies: "在时间线上显示帖子的回复"
flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。" flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。"
autoAcceptFollowed: "自动允许关注者的关注" autoAcceptFollowed: "自动允许来自我关注的用户对我的关注请求"
addAccount: "添加账户" addAccount: "添加账户"
reloadAccountsList: "更新账户列表" reloadAccountsList: "更新账户列表"
loginFailed: "登录失败" loginFailed: "登录失败"
@ -203,7 +203,7 @@ blockedInstances: "被阻拦的服务器"
blockedInstancesDescription: "设定要阻拦的服务器,以换行来进行分割。被阻拦的服务器将无法与本服务器进行交换通讯。" blockedInstancesDescription: "设定要阻拦的服务器,以换行来进行分割。被阻拦的服务器将无法与本服务器进行交换通讯。"
muteAndBlock: "屏蔽/拉黑" muteAndBlock: "屏蔽/拉黑"
mutedUsers: "已屏蔽用户" mutedUsers: "已屏蔽用户"
blockedUsers: "拉黑的用户" blockedUsers: "拉黑的用户"
noUsers: "无用户" noUsers: "无用户"
editProfile: "编辑资料" editProfile: "编辑资料"
noteDeleteConfirm: "要删除该帖子吗?" noteDeleteConfirm: "要删除该帖子吗?"
@ -336,7 +336,7 @@ enableLocalTimeline: "启用本地时间线功能"
enableGlobalTimeline: "启用全局时间线" enableGlobalTimeline: "启用全局时间线"
disablingTimelinesInfo: "即使时间线功能被禁用,出于方便,管理员和数据图表也可以继续使用。" disablingTimelinesInfo: "即使时间线功能被禁用,出于方便,管理员和数据图表也可以继续使用。"
registration: "注册" registration: "注册"
enableRegistration: "允许新用户注册" enableRegistration: "允许任何人注册"
invite: "邀请" invite: "邀请"
driveCapacityPerLocalAccount: "每个用户的网盘空间" driveCapacityPerLocalAccount: "每个用户的网盘空间"
driveCapacityPerRemoteAccount: "每个远程用户的网盘容量" driveCapacityPerRemoteAccount: "每个远程用户的网盘容量"
@ -354,7 +354,7 @@ pinnedNotes: "已置顶的帖子"
hcaptcha: "hCaptcha" hcaptcha: "hCaptcha"
enableHcaptcha: "启用 hCaptcha" enableHcaptcha: "启用 hCaptcha"
hcaptchaSiteKey: "网站密钥" hcaptchaSiteKey: "网站密钥"
hcaptchaSecretKey: "hCaptcha 密钥(SecretKey)" hcaptchaSecretKey: "密钥"
recaptcha: "reCAPTCHA" recaptcha: "reCAPTCHA"
enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)" enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)"
recaptchaSiteKey: "网站密钥" recaptchaSiteKey: "网站密钥"
@ -506,6 +506,7 @@ objectStorageUseSSLDesc: "如果不使用https进行API连接请关闭。"
objectStorageUseProxy: "使用代理" objectStorageUseProxy: "使用代理"
objectStorageUseProxyDesc: "如果您不使用代理进行API连接请将其关闭。" objectStorageUseProxyDesc: "如果您不使用代理进行API连接请将其关闭。"
objectStorageSetPublicRead: "上传时设置为public-read" objectStorageSetPublicRead: "上传时设置为public-read"
s3ForcePathStyleDesc: "启用 s3ForcePathStyle 会强制将存储桶名称指定为 URL 中路径的一部分,而不是主机名。使用自托管 Minio 等时可能需要启用。"
serverLogs: "服务器日志" serverLogs: "服务器日志"
deleteAll: "全部删除" deleteAll: "全部删除"
showFixedPostForm: "在时间线顶部显示发帖框" showFixedPostForm: "在时间线顶部显示发帖框"
@ -919,6 +920,7 @@ pushNotificationNotSupported: "浏览器或服务器不支持推送通知消息"
sendPushNotificationReadMessage: "删除已读推送通知消息" sendPushNotificationReadMessage: "删除已读推送通知消息"
sendPushNotificationReadMessageCaption: "“{emptyPushNotificationMessage}”的通知消息将会显示。您终端设备的电池消耗可能会增加。" sendPushNotificationReadMessageCaption: "“{emptyPushNotificationMessage}”的通知消息将会显示。您终端设备的电池消耗可能会增加。"
windowMaximize: "最大化" windowMaximize: "最大化"
windowMinimize: "最小化"
windowRestore: "还原" windowRestore: "还原"
caption: "标题" caption: "标题"
loggedInAsBot: "以Bot账户登录" loggedInAsBot: "以Bot账户登录"
@ -960,7 +962,9 @@ copyErrorInfo: "复制错误信息"
joinThisServer: "在本服务器上注册" joinThisServer: "在本服务器上注册"
exploreOtherServers: "探索其他服务器" exploreOtherServers: "探索其他服务器"
letsLookAtTimeline: "时间线" letsLookAtTimeline: "时间线"
disableFederationWarn: "联合被禁用。 禁用它并不能使帖子变成私人的。 在大多数情况下,这个选项不需要被启用。" disableFederationConfirm: "确定要禁用联合?"
disableFederationConfirmWarn: "禁用联合不会将帖子设为私有。在大多数情况下,不需要禁用联合。"
disableFederationOk: "联合禁用"
invitationRequiredToRegister: "此服务器目前只允许拥有邀请码的人注册。" invitationRequiredToRegister: "此服务器目前只允许拥有邀请码的人注册。"
emailNotSupported: "此服务器不支持发送邮件" emailNotSupported: "此服务器不支持发送邮件"
postToTheChannel: "发布到频道" postToTheChannel: "发布到频道"
@ -980,6 +984,11 @@ drivecleaner: "网盘整理"
retryAllQueuesNow: "立刻重试所有队列" retryAllQueuesNow: "立刻重试所有队列"
retryAllQueuesConfirmTitle: "要再尝试一次吗?" retryAllQueuesConfirmTitle: "要再尝试一次吗?"
retryAllQueuesConfirmText: "可能会使服务器负荷在一定时间内增加" retryAllQueuesConfirmText: "可能会使服务器负荷在一定时间内增加"
enableChartsForRemoteUser: "生成远程用户的图表"
enableChartsForFederatedInstances: "生成远程服务器的图表"
showClipButtonInNoteFooter: "在贴文下方显示便签按钮"
largeNoteReactions: "使用大图标来显示回应"
noteIdOrUrl: "帖子ID或URL"
_achievements: _achievements:
earnedAt: "达成时间" earnedAt: "达成时间"
_types: _types:
@ -1252,7 +1261,7 @@ _role:
gtlAvailable: "查看全局时间线" gtlAvailable: "查看全局时间线"
ltlAvailable: "查看本地时间线" ltlAvailable: "查看本地时间线"
canPublicNote: "允许公开发帖" canPublicNote: "允许公开发帖"
canInvite: "发放实例邀请码" canInvite: "发放服务器邀请码"
canManageCustomEmojis: "管理自定义表情符号" canManageCustomEmojis: "管理自定义表情符号"
driveCapacity: "网盘容量" driveCapacity: "网盘容量"
pinMax: "帖子置顶数量限制" pinMax: "帖子置顶数量限制"
@ -1276,6 +1285,8 @@ _role:
followersMoreThanOrEq: "关注者不少于" followersMoreThanOrEq: "关注者不少于"
followingLessThanOrEq: "关注中不多于" followingLessThanOrEq: "关注中不多于"
followingMoreThanOrEq: "关注中不少于" followingMoreThanOrEq: "关注中不少于"
notesLessThanOrEq: "帖子数在~以下"
notesMoreThanOrEq: "帖子数在~以上"
and: "符合以下全部条件" and: "符合以下全部条件"
or: "符合以下任一条件" or: "符合以下任一条件"
not: "不符合以下任何条件" not: "不符合以下任何条件"
@ -1624,7 +1635,7 @@ _widgets:
photos: "照片" photos: "照片"
digitalClock: "数字时钟" digitalClock: "数字时钟"
unixClock: "UNIX时钟" unixClock: "UNIX时钟"
federation: "联邦宇宙" federation: "联"
instanceCloud: "服务器云" instanceCloud: "服务器云"
postForm: "投稿窗口" postForm: "投稿窗口"
slideshow: "幻灯片展示" slideshow: "幻灯片展示"
@ -1875,6 +1886,16 @@ _drivecleaner:
orderBySizeDesc: "按大小降序排列" orderBySizeDesc: "按大小降序排列"
orderByCreatedAtAsc: "按添加日期降序排列" orderByCreatedAtAsc: "按添加日期降序排列"
_webhookSettings: _webhookSettings:
createWebhook: "创建 Webhook"
name: "名称" name: "名称"
secret: "密钥"
events: "何时运行Webhook"
active: "已启用" active: "已启用"
_events:
follow: "关注时"
followed: "被关注时"
note: "发布贴文时"
reply: "收到回复时"
renote: "被转发时"
reaction: "被回应时"
mention: "被提及时"

View File

@ -15,7 +15,7 @@ gotIt: "知道了"
cancel: "取消" cancel: "取消"
noThankYou: "現在不要" noThankYou: "現在不要"
enterUsername: "輸入使用者名稱" enterUsername: "輸入使用者名稱"
renotedBy: "{user} 轉了" renotedBy: "{user} 轉了"
noNotes: "無貼文。" noNotes: "無貼文。"
noNotifications: "沒有通知" noNotifications: "沒有通知"
instance: "實例" instance: "實例"
@ -99,9 +99,9 @@ followRequestPending: "追隨許可批准中"
enterEmoji: "輸入表情符號" enterEmoji: "輸入表情符號"
renote: "轉發" renote: "轉發"
unrenote: "取消轉發" unrenote: "取消轉發"
renoted: "轉成功" renoted: "轉成功"
cantRenote: "無法轉發此貼文。" cantRenote: "無法轉發此貼文。"
cantReRenote: "無法轉傳之前已經轉傳過的內容。" cantReRenote: "無法轉發之前已經轉發過的內容。"
quote: "引用" quote: "引用"
inChannelRenote: "在頻道內轉發" inChannelRenote: "在頻道內轉發"
inChannelQuote: "在頻道內引用" inChannelQuote: "在頻道內引用"
@ -960,7 +960,6 @@ copyErrorInfo: "複製錯誤資訊"
joinThisServer: "在此伺服器上註冊" joinThisServer: "在此伺服器上註冊"
exploreOtherServers: "探索其他伺服器" exploreOtherServers: "探索其他伺服器"
letsLookAtTimeline: "看看時間軸" letsLookAtTimeline: "看看時間軸"
disableFederationWarn: "聯邦被停用了。即使停用也不會讓您的貼文不公開,在大多數情況下,不需要啟用這個選項。"
invitationRequiredToRegister: "目前這個伺服器為邀請制,必須擁有邀請碼才能註冊。" invitationRequiredToRegister: "目前這個伺服器為邀請制,必須擁有邀請碼才能註冊。"
emailNotSupported: "這個伺服器不支援寄送郵件" emailNotSupported: "這個伺服器不支援寄送郵件"
postToTheChannel: "發布到頻道" postToTheChannel: "發布到頻道"
@ -980,6 +979,11 @@ drivecleaner: "雲端硬碟清掃器"
retryAllQueuesNow: "立刻重試所有佇列" retryAllQueuesNow: "立刻重試所有佇列"
retryAllQueuesConfirmTitle: "要現在重試嗎?" retryAllQueuesConfirmTitle: "要現在重試嗎?"
retryAllQueuesConfirmText: "伺服器的負荷可能會暫時增加。" retryAllQueuesConfirmText: "伺服器的負荷可能會暫時增加。"
enableChartsForRemoteUser: "生成遠端用戶的圖表"
enableChartsForFederatedInstances: "生成遠端伺服器的圖表"
showClipButtonInNoteFooter: "將摘錄添加至貼文"
largeNoteReactions: "將貼文的反應放大顯示"
noteIdOrUrl: "貼文ID或URL"
_achievements: _achievements:
earnedAt: "獲得日期" earnedAt: "獲得日期"
_types: _types:
@ -1096,7 +1100,7 @@ _achievements:
title: "有備而來" title: "有備而來"
description: "設定了個人檔案" description: "設定了個人檔案"
_markedAsCat: _markedAsCat:
title: "我是貓" title: "吾輩乃貓是也"
description: "已將帳戶設定為貓" description: "已將帳戶設定為貓"
flavor: "還沒有名字。" flavor: "還沒有名字。"
_following1: _following1:
@ -1276,6 +1280,8 @@ _role:
followersMoreThanOrEq: "追隨者人數在~以上" followersMoreThanOrEq: "追隨者人數在~以上"
followingLessThanOrEq: "追隨人數在~以下" followingLessThanOrEq: "追隨人數在~以下"
followingMoreThanOrEq: "追隨人數在~以上" followingMoreThanOrEq: "追隨人數在~以上"
notesLessThanOrEq: "發布數在~以下"
notesMoreThanOrEq: "發布數在~以上"
and: "~和~" and: "~和~"
or: "~或~" or: "~或~"
not: "~否" not: "~否"
@ -1875,6 +1881,16 @@ _drivecleaner:
orderBySizeDesc: "檔案由大到小" orderBySizeDesc: "檔案由大到小"
orderByCreatedAtAsc: "依照加入的日期順序" orderByCreatedAtAsc: "依照加入的日期順序"
_webhookSettings: _webhookSettings:
createWebhook: "建立 Webhook"
name: "名稱" name: "名稱"
secret: "秘密"
events: "什麼時候運行Webhook"
active: "已啟用" active: "已啟用"
_events:
follow: "當你追隨時"
followed: "當被追隨時"
note: "當發布貼文時"
reply: "當收到回覆時"
renote: "當被轉發時"
reaction: "當獲得反應時"
mention: "當被提到時"

View File

@ -1,12 +1,12 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.10.3", "version": "13.11.0",
"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@7.29.3", "packageManager": "pnpm@8.1.1",
"workspaces": [ "workspaces": [
"packages/frontend", "packages/frontend",
"packages/backend", "packages/backend",
@ -16,6 +16,7 @@
"scripts": { "scripts": {
"build-pre": "node ./scripts/build-pre.js", "build-pre": "node ./scripts/build-pre.js",
"build": "pnpm build-pre && pnpm -r build && pnpm gulp", "build": "pnpm build-pre && pnpm -r build && pnpm gulp",
"build-storybook": "pnpm --filter frontend build-storybook",
"start": "pnpm check:connect && cd packages/backend && node ./built/boot/index.js", "start": "pnpm check:connect && cd packages/backend && node ./built/boot/index.js",
"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/index.js", "start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/index.js",
"init": "pnpm migrate", "init": "pnpm migrate",
@ -50,16 +51,16 @@
"gulp-replace": "1.1.4", "gulp-replace": "1.1.4",
"gulp-terser": "2.1.0", "gulp-terser": "2.1.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"typescript": "4.9.5" "typescript": "5.0.3"
}, },
"devDependencies": { "devDependencies": {
"@types/gulp": "4.0.10", "@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1", "@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.54.1", "@typescript-eslint/eslint-plugin": "5.57.1",
"@typescript-eslint/parser": "5.54.1", "@typescript-eslint/parser": "5.57.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "12.7.0", "cypress": "12.9.0",
"eslint": "8.35.0", "eslint": "8.37.0",
"start-server-and-test": "2.0.0" "start-server-and-test": "2.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {

View File

@ -1,8 +1,15 @@
import Redis from 'ioredis';
import { loadConfig } from './built/config.js'; import { loadConfig } from './built/config.js';
import { createRedisConnection } from './built/redis.js';
const config = loadConfig(); const config = loadConfig();
const redis = createRedisConnection(config); const redis = new Redis({
port: config.redis.port,
host: config.redis.host,
family: config.redis.family == null ? 0 : config.redis.family,
password: config.redis.pass,
keyPrefix: `${config.redis.prefix}:`,
db: config.redis.db ?? 0,
});
redis.on('connect', () => redis.disconnect()); redis.on('connect', () => redis.disconnect());
redis.on('error', (e) => { redis.on('error', (e) => {

View File

@ -0,0 +1,21 @@
export class channelFavorite1680228513388 {
name = 'channelFavorite1680228513388'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "channel_favorite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "channelId" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "PK_59bddfd54d48689a298d41af00c" PRIMARY KEY ("id")); COMMENT ON COLUMN "channel_favorite"."createdAt" IS 'The created date of the ChannelFavorite.'`);
await queryRunner.query(`CREATE INDEX "IDX_735a5544f9249d412255f47f95" ON "channel_favorite" ("createdAt") `);
await queryRunner.query(`CREATE INDEX "IDX_d3ca0db011b75ac2a940a2337d" ON "channel_favorite" ("channelId") `);
await queryRunner.query(`CREATE INDEX "IDX_8302bd27226605ece14842fb25" ON "channel_favorite" ("userId") `);
await queryRunner.query(`ALTER TABLE "channel_favorite" ADD CONSTRAINT "FK_d3ca0db011b75ac2a940a2337d2" FOREIGN KEY ("channelId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "channel_favorite" ADD CONSTRAINT "FK_8302bd27226605ece14842fb25a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "channel_favorite" DROP CONSTRAINT "FK_8302bd27226605ece14842fb25a"`);
await queryRunner.query(`ALTER TABLE "channel_favorite" DROP CONSTRAINT "FK_d3ca0db011b75ac2a940a2337d2"`);
await queryRunner.query(`DROP INDEX "public"."IDX_8302bd27226605ece14842fb25"`);
await queryRunner.query(`DROP INDEX "public"."IDX_d3ca0db011b75ac2a940a2337d"`);
await queryRunner.query(`DROP INDEX "public"."IDX_735a5544f9249d412255f47f95"`);
await queryRunner.query(`DROP TABLE "channel_favorite"`);
}
}

View File

@ -0,0 +1,11 @@
export class channelNotePining1680238118084 {
name = 'channelNotePining1680238118084'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "channel" ADD "pinnedNoteIds" character varying(128) array NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "channel" DROP COLUMN "pinnedNoteIds"`);
}
}

View File

@ -0,0 +1,10 @@
export class cleanup1680491187535 {
name = 'cleanup1680491187535'
async up(queryRunner) {
await queryRunner.query(`DROP TABLE "antenna_note" `);
}
async down(queryRunner) {
}
}

View File

@ -0,0 +1,11 @@
export class cleanup1680582195041 {
name = 'cleanup1680582195041'
async up(queryRunner) {
await queryRunner.query(`DROP TABLE "notification" `);
}
async down(queryRunner) {
}
}

View File

@ -0,0 +1,17 @@
export class AvatarUrlAndBannerUrl1680775031481 {
name = 'AvatarUrlAndBannerUrl1680775031481'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ADD "avatarUrl" character varying(512)`);
await queryRunner.query(`ALTER TABLE "user" ADD "bannerUrl" character varying(512)`);
await queryRunner.query(`ALTER TABLE "user" ADD "avatarBlurhash" character varying(128)`);
await queryRunner.query(`ALTER TABLE "user" ADD "bannerBlurhash" character varying(128)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerBlurhash"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarBlurhash"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerUrl"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarUrl"`);
}
}

View File

@ -0,0 +1,17 @@
export class AccountMove1680931179228 {
name = 'AccountMove1680931179228'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ADD "movedToUri" character varying(512)`);
await queryRunner.query(`COMMENT ON COLUMN "user"."movedToUri" IS 'The URI of the new account of the User'`);
await queryRunner.query(`ALTER TABLE "user" ADD "alsoKnownAs" text`);
await queryRunner.query(`COMMENT ON COLUMN "user"."alsoKnownAs" IS 'URIs the user is known as too'`);
}
async down(queryRunner) {
await queryRunner.query(`COMMENT ON COLUMN "user"."alsoKnownAs" IS 'URIs the user is known as too'`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "alsoKnownAs"`);
await queryRunner.query(`COMMENT ON COLUMN "user"."movedToUri" IS 'The URI of the new account of the User'`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "movedToUri"`);
}
}

View File

@ -22,42 +22,42 @@
"test-and-coverage": "pnpm jest-and-coverage" "test-and-coverage": "pnpm jest-and-coverage"
}, },
"optionalDependencies": { "optionalDependencies": {
"@swc/core-android-arm64": "^1.3.11", "@swc/core-android-arm64": "1.3.11",
"@swc/core-darwin-arm64": "^1.3.38", "@swc/core-darwin-arm64": "1.3.46",
"@swc/core-darwin-x64": "^1.3.38", "@swc/core-darwin-x64": "1.3.46",
"@swc/core-linux-arm-gnueabihf": "^1.3.38", "@swc/core-linux-arm-gnueabihf": "1.3.46",
"@swc/core-linux-arm64-gnu": "^1.3.38", "@swc/core-linux-arm64-gnu": "1.3.46",
"@swc/core-linux-arm64-musl": "^1.3.38", "@swc/core-linux-arm64-musl": "1.3.46",
"@swc/core-linux-x64-gnu": "^1.3.38", "@swc/core-linux-x64-gnu": "1.3.46",
"@swc/core-linux-x64-musl": "^1.3.38", "@swc/core-linux-x64-musl": "1.3.46",
"@swc/core-win32-arm64-msvc": "^1.3.38", "@swc/core-win32-arm64-msvc": "1.3.46",
"@swc/core-win32-ia32-msvc": "^1.3.38", "@swc/core-win32-ia32-msvc": "1.3.46",
"@swc/core-win32-x64-msvc": "^1.3.38", "@swc/core-win32-x64-msvc": "1.3.46",
"@tensorflow/tfjs": "4.2.0", "@tensorflow/tfjs": "4.2.0",
"@tensorflow/tfjs-node": "4.2.0" "@tensorflow/tfjs-node": "4.2.0"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.294.0", "@aws-sdk/client-s3": "3.306.0",
"@aws-sdk/lib-storage": "^3.294.0", "@aws-sdk/lib-storage": "3.306.0",
"@aws-sdk/node-http-handler": "^3.292.0", "@aws-sdk/node-http-handler": "3.306.0",
"@bull-board/api": "5.0.0", "@bull-board/api": "5.0.0",
"@bull-board/fastify": "5.0.0", "@bull-board/fastify": "5.0.0",
"@bull-board/ui": "5.0.0", "@bull-board/ui": "5.0.0",
"@discordapp/twemoji": "14.0.2", "@discordapp/twemoji": "14.1.2",
"@fastify/accepts": "4.1.0", "@fastify/accepts": "4.1.0",
"@fastify/cookie": "8.3.0", "@fastify/cookie": "8.3.0",
"@fastify/cors": "8.2.0", "@fastify/cors": "8.2.1",
"@fastify/http-proxy": "8.4.0", "@fastify/http-proxy": "9.0.0",
"@fastify/multipart": "7.4.2", "@fastify/multipart": "7.5.0",
"@fastify/static": "6.9.0", "@fastify/static": "6.10.0",
"@fastify/view": "7.4.1", "@fastify/view": "7.4.1",
"@nestjs/common": "9.3.9", "@nestjs/common": "9.4.0",
"@nestjs/core": "9.3.9", "@nestjs/core": "9.4.0",
"@nestjs/testing": "9.3.9", "@nestjs/testing": "9.4.0",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.0.2", "@sinonjs/fake-timers": "10.0.2",
"@swc/cli": "0.1.62", "@swc/cli": "0.1.62",
"@swc/core": "1.3.38", "@swc/core": "1.3.46",
"accepts": "1.3.8", "accepts": "1.3.8",
"ajv": "8.12.0", "ajv": "8.12.0",
"archiver": "5.3.1", "archiver": "5.3.1",
@ -76,7 +76,7 @@
"date-fns": "2.29.3", "date-fns": "2.29.3",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"fastify": "4.14.1", "fastify": "4.15.0",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "18.2.1", "file-type": "18.2.1",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
@ -88,21 +88,21 @@
"ip-cidr": "3.1.0", "ip-cidr": "3.1.0",
"is-svg": "4.3.2", "is-svg": "4.3.2",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "21.1.0", "jsdom": "21.1.1",
"json5": "2.2.3", "json5": "2.2.3",
"jsonld": "8.1.1", "jsonld": "8.1.1",
"jsrsasign": "10.6.1", "jsrsasign": "10.7.0",
"mfm-js": "0.23.3", "mfm-js": "0.23.3",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"misskey-js": "0.0.15", "misskey-js": "workspace:*",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.3.0", "node-fetch": "3.3.1",
"nodemailer": "6.9.1", "nodemailer": "6.9.1",
"nsfwjs": "2.4.2", "nsfwjs": "2.4.2",
"oauth": "0.10.0", "oauth": "0.10.0",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"otpauth": "^9.0.2", "otpauth": "9.1.1",
"parse5": "7.1.2", "parse5": "7.1.2",
"pg": "8.10.0", "pg": "8.10.0",
"private-ip": "3.0.0", "private-ip": "3.0.0",
@ -125,7 +125,7 @@
"sanitize-html": "2.10.0", "sanitize-html": "2.10.0",
"seedrandom": "3.0.5", "seedrandom": "3.0.5",
"semver": "7.3.8", "semver": "7.3.8",
"sharp": "0.31.3", "sharp": "0.32.0",
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp", "sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
@ -133,25 +133,25 @@
"systeminformation": "5.17.12", "systeminformation": "5.17.12",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.1", "tmp": "0.2.1",
"tsc-alias": "1.8.3", "tsc-alias": "1.8.5",
"tsconfig-paths": "4.1.2", "tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typeorm": "0.3.11", "typeorm": "0.3.13",
"typescript": "4.9.5", "typescript": "5.0.3",
"ulid": "2.3.0", "ulid": "2.3.0",
"unzipper": "0.10.11", "unzipper": "0.10.11",
"uuid": "9.0.0", "uuid": "9.0.0",
"vary": "1.1.2", "vary": "1.1.2",
"web-push": "3.5.0", "web-push": "3.5.0",
"websocket": "1.0.34", "websocket": "1.0.34",
"ws": "8.12.1", "ws": "8.13.0",
"xev": "3.0.2" "xev": "3.0.2"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "29.5.0", "@jest/globals": "29.5.0",
"@swc/jest": "0.2.24", "@swc/jest": "0.2.24",
"@types/accepts": "1.3.5", "@types/accepts": "1.3.5",
"@types/archiver": "5.3.1", "@types/archiver": "5.3.2",
"@types/bcryptjs": "2.4.2", "@types/bcryptjs": "2.4.2",
"@types/bull": "4.10.0", "@types/bull": "4.10.0",
"@types/cbor": "6.0.0", "@types/cbor": "6.0.0",
@ -160,13 +160,13 @@
"@types/escape-regexp": "0.0.1", "@types/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.21", "@types/fluent-ffmpeg": "2.1.21",
"@types/ioredis": "4.28.10", "@types/ioredis": "4.28.10",
"@types/jest": "29.4.0", "@types/jest": "29.5.0",
"@types/js-yaml": "4.0.5", "@types/js-yaml": "4.0.5",
"@types/jsdom": "21.1.0", "@types/jsdom": "21.1.1",
"@types/jsonld": "1.5.8", "@types/jsonld": "1.5.8",
"@types/jsrsasign": "10.5.5", "@types/jsrsasign": "10.5.8",
"@types/mime-types": "2.1.1", "@types/mime-types": "2.1.1",
"@types/node": "18.15.0", "@types/node": "18.15.11",
"@types/node-fetch": "3.0.3", "@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.7", "@types/nodemailer": "6.4.7",
"@types/oauth": "0.9.1", "@types/oauth": "0.9.1",
@ -178,7 +178,7 @@
"@types/ratelimiter": "3.4.4", "@types/ratelimiter": "3.4.4",
"@types/redis": "4.0.11", "@types/redis": "4.0.11",
"@types/rename": "1.0.4", "@types/rename": "1.0.4",
"@types/sanitize-html": "2.8.1", "@types/sanitize-html": "2.9.0",
"@types/semver": "7.3.13", "@types/semver": "7.3.13",
"@types/sharp": "0.31.1", "@types/sharp": "0.31.1",
"@types/sinonjs__fake-timers": "8.1.2", "@types/sinonjs__fake-timers": "8.1.2",
@ -190,11 +190,11 @@
"@types/web-push": "3.3.2", "@types/web-push": "3.3.2",
"@types/websocket": "1.0.5", "@types/websocket": "1.0.5",
"@types/ws": "8.5.4", "@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.54.1", "@typescript-eslint/eslint-plugin": "5.57.1",
"@typescript-eslint/parser": "5.54.1", "@typescript-eslint/parser": "5.57.1",
"aws-sdk-client-mock": "^2.1.1", "aws-sdk-client-mock": "^2.1.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.35.0", "eslint": "8.37.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"execa": "6.1.0", "execa": "6.1.0",
"jest": "29.5.0", "jest": "29.5.0",

View File

@ -2,18 +2,15 @@ import { setTimeout } from 'node:timers/promises';
import { Global, Inject, Module } from '@nestjs/common'; import { Global, Inject, Module } from '@nestjs/common';
import Redis from 'ioredis'; import Redis from 'ioredis';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import { createRedisConnection } from '@/redis.js';
import { DI } from './di-symbols.js'; import { DI } from './di-symbols.js';
import { loadConfig } from './config.js'; import { loadConfig } from './config.js';
import { createPostgresDataSource } from './postgres.js'; import { createPostgresDataSource } from './postgres.js';
import { RepositoryModule } from './models/RepositoryModule.js'; import { RepositoryModule } from './models/RepositoryModule.js';
import type { Provider, OnApplicationShutdown } from '@nestjs/common'; import type { Provider, OnApplicationShutdown } from '@nestjs/common';
const config = loadConfig();
const $config: Provider = { const $config: Provider = {
provide: DI.config, provide: DI.config,
useValue: config, useValue: loadConfig(),
}; };
const $db: Provider = { const $db: Provider = {
@ -28,18 +25,31 @@ const $db: Provider = {
const $redis: Provider = { const $redis: Provider = {
provide: DI.redis, provide: DI.redis,
useFactory: (config) => { useFactory: (config) => {
const redisClient = createRedisConnection(config); return new Redis({
return redisClient; port: config.redis.port,
host: config.redis.host,
family: config.redis.family == null ? 0 : config.redis.family,
password: config.redis.pass,
keyPrefix: `${config.redis.prefix}:`,
db: config.redis.db ?? 0,
});
}, },
inject: [DI.config], inject: [DI.config],
}; };
const $redisSubscriber: Provider = { const $redisForPubsub: Provider = {
provide: DI.redisSubscriber, provide: DI.redisForPubsub,
useFactory: (config) => { useFactory: (config) => {
const redisSubscriber = createRedisConnection(config); const redis = new Redis({
redisSubscriber.subscribe(config.host); port: config.redisForPubsub.port,
return redisSubscriber; host: config.redisForPubsub.host,
family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family,
password: config.redisForPubsub.pass,
keyPrefix: `${config.redisForPubsub.prefix}:`,
db: config.redisForPubsub.db ?? 0,
});
redis.subscribe(config.host);
return redis;
}, },
inject: [DI.config], inject: [DI.config],
}; };
@ -47,14 +57,14 @@ const $redisSubscriber: Provider = {
@Global() @Global()
@Module({ @Module({
imports: [RepositoryModule], imports: [RepositoryModule],
providers: [$config, $db, $redis, $redisSubscriber], providers: [$config, $db, $redis, $redisForPubsub],
exports: [$config, $db, $redis, $redisSubscriber, RepositoryModule], exports: [$config, $db, $redis, $redisForPubsub, RepositoryModule],
}) })
export class GlobalModule implements OnApplicationShutdown { export class GlobalModule implements OnApplicationShutdown {
constructor( constructor(
@Inject(DI.db) private db: DataSource, @Inject(DI.db) private db: DataSource,
@Inject(DI.redis) private redisClient: Redis.Redis, @Inject(DI.redis) private redisClient: Redis.Redis,
@Inject(DI.redisSubscriber) private redisSubscriber: Redis.Redis, @Inject(DI.redisForPubsub) private redisForPubsub: Redis.Redis,
) {} ) {}
async onApplicationShutdown(signal: string): Promise<void> { async onApplicationShutdown(signal: string): Promise<void> {
@ -69,7 +79,7 @@ export class GlobalModule implements OnApplicationShutdown {
await Promise.all([ await Promise.all([
this.db.destroy(), this.db.destroy(),
this.redisClient.disconnect(), this.redisClient.disconnect(),
this.redisSubscriber.disconnect(), this.redisForPubsub.disconnect(),
]); ]);
} }
} }

View File

@ -25,6 +25,14 @@ export type Source = {
disableCache?: boolean; disableCache?: boolean;
extra?: { [x: string]: string }; extra?: { [x: string]: string };
}; };
dbReplications?: boolean;
dbSlaves?: {
host: string;
port: number;
db: string;
user: string;
pass: string;
}[];
redis: { redis: {
host: string; host: string;
port: number; port: number;
@ -33,6 +41,22 @@ export type Source = {
db?: number; db?: number;
prefix?: string; prefix?: string;
}; };
redisForPubsub?: {
host: string;
port: number;
family?: number;
pass: string;
db?: number;
prefix?: string;
};
redisForJobQueue?: {
host: string;
port: number;
family?: number;
pass: string;
db?: number;
prefix?: string;
};
elasticsearch: { elasticsearch: {
host: string; host: string;
port: number; port: number;
@ -91,6 +115,8 @@ export type Mixin = {
mediaProxy: string; mediaProxy: string;
externalMediaProxyEnabled: boolean; externalMediaProxyEnabled: boolean;
videoThumbnailGenerator: string | null; videoThumbnailGenerator: string | null;
redisForPubsub: NonNullable<Source['redisForPubsub']>;
redisForJobQueue: NonNullable<Source['redisForJobQueue']>;
}; };
export type Config = Source & Mixin; export type Config = Source & Mixin;
@ -151,6 +177,8 @@ export function loadConfig() {
: null; : null;
if (!config.redis.prefix) config.redis.prefix = mixin.host; if (!config.redis.prefix) config.redis.prefix = mixin.host;
if (config.redisForPubsub == null) config.redisForPubsub = config.redis;
if (config.redisForJobQueue == null) config.redisForJobQueue = config.redis;
return Object.assign(config, mixin); return Object.assign(config, mixin);
} }

View File

@ -0,0 +1,114 @@
import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import { bindThis } from '@/decorators.js';
import { DI } from '@/di-symbols.js';
import type { LocalUser } from '@/models/entities/User.js';
import { User } from '@/models/entities/User.js';
import type { FollowingsRepository, UsersRepository } from '@/models/index.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { AccountUpdateService } from '@/core/AccountUpdateService.js';
import { RelayService } from '@/core/RelayService.js';
@Injectable()
export class AccountMoveService {
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private userEntityService: UserEntityService,
private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService,
private globalEventService: GlobalEventService,
private userFollowingService: UserFollowingService,
private accountUpdateService: AccountUpdateService,
private relayService: RelayService,
) {
}
/**
* Move a local account to a remote account.
*
* After delivering Move activity, its local followers unfollow the old account and then follow the new one.
*/
@bindThis
public async moveToRemote(src: LocalUser, dst: User): Promise<unknown> {
// Make sure that the destination is a remote account.
if (this.userEntityService.isLocalUser(dst)) throw new Error('move destiantion is not remote');
if (!dst.uri) throw new Error('destination uri is empty');
// add movedToUri to indicate that the user has moved
const update = {} as Partial<User>;
update.alsoKnownAs = src.alsoKnownAs?.concat([dst.uri]) ?? [dst.uri];
update.movedToUri = dst.uri;
await this.usersRepository.update(src.id, update);
const srcPerson = await this.apRendererService.renderPerson(src);
const updateAct = this.apRendererService.addContext(this.apRendererService.renderUpdate(srcPerson, src));
await this.apDeliverManagerService.deliverToFollowers(src, updateAct);
this.relayService.deliverToRelays(src, updateAct);
// Deliver Move activity to the followers of the old account
const moveAct = this.apRendererService.addContext(this.apRendererService.renderMove(src, dst));
await this.apDeliverManagerService.deliverToFollowers(src, moveAct);
// Publish meUpdated event
const iObj = await this.userEntityService.pack<true, true>(src.id, src, { detail: true, includeSecrets: true });
this.globalEventService.publishMainStream(src.id, 'meUpdated', iObj);
// follow the new account and unfollow the old one
const followings = await this.followingsRepository.find({
relations: {
follower: true,
},
where: {
followeeId: src.id,
followerHost: IsNull(), // follower is local
},
});
for (const following of followings) {
if (!following.follower) continue;
try {
await this.userFollowingService.follow(following.follower, dst);
await this.userFollowingService.unfollow(following.follower, src);
} catch {
/* empty */
}
}
return iObj;
}
/**
* Create an alias of an old remote account.
*
* The user's new profile will be published to the followers.
*/
@bindThis
public async createAlias(me: LocalUser, updates: Partial<User>): Promise<unknown> {
await this.usersRepository.update(me.id, updates);
// Publish meUpdated event
const iObj = await this.userEntityService.pack<true, true>(me.id, me, {
detail: true,
includeSecrets: true,
});
this.globalEventService.publishMainStream(me.id, 'meUpdated', iObj);
if (me.isLocked === false) {
await this.userFollowingService.acceptAllFollowRequests(me);
}
this.accountUpdateService.publishToFollowers(me.id);
return iObj;
}
}

View File

@ -12,7 +12,7 @@ import { PushNotificationService } from '@/core/PushNotificationService.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 { MutingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.js'; import type { MutingsRepository, NotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.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 { StreamMessages } from '@/server/api/stream/types.js';
@ -24,8 +24,11 @@ export class AntennaService implements OnApplicationShutdown {
private antennas: Antenna[]; private antennas: Antenna[];
constructor( constructor(
@Inject(DI.redisSubscriber) @Inject(DI.redis)
private redisSubscriber: Redis.Redis, private redisClient: Redis.Redis,
@Inject(DI.redisForPubsub)
private redisForPubsub: Redis.Redis,
@Inject(DI.mutingsRepository) @Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository, private mutingsRepository: MutingsRepository,
@ -33,9 +36,6 @@ export class AntennaService implements OnApplicationShutdown {
@Inject(DI.notesRepository) @Inject(DI.notesRepository)
private notesRepository: NotesRepository, private notesRepository: NotesRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
@Inject(DI.antennasRepository) @Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository, private antennasRepository: AntennasRepository,
@ -52,12 +52,12 @@ export class AntennaService implements OnApplicationShutdown {
this.antennasFetched = false; this.antennasFetched = false;
this.antennas = []; this.antennas = [];
this.redisSubscriber.on('message', this.onRedisMessage); this.redisForPubsub.on('message', this.onRedisMessage);
} }
@bindThis @bindThis
public onApplicationShutdown(signal?: string | undefined) { public onApplicationShutdown(signal?: string | undefined) {
this.redisSubscriber.off('message', this.onRedisMessage); this.redisForPubsub.off('message', this.onRedisMessage);
} }
@bindThis @bindThis
@ -92,54 +92,13 @@ export class AntennaService implements OnApplicationShutdown {
@bindThis @bindThis
public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise<void> { public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise<void> {
// 通知しない設定になっているか、自分自身の投稿なら既読にする this.redisClient.xadd(
const read = !antenna.notify || (antenna.userId === noteUser.id); `antennaTimeline:${antenna.id}`,
'MAXLEN', '~', '200',
this.antennaNotesRepository.insert({ `${this.idService.parse(note.id).date.getTime()}-*`,
id: this.idService.genId(), 'note', note.id);
antennaId: antenna.id,
noteId: note.id,
read: read,
});
this.globalEventService.publishAntennaStream(antenna.id, 'note', note); this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
if (!read) {
const mutings = await this.mutingsRepository.find({
where: {
muterId: antenna.userId,
},
select: ['muteeId'],
});
// Copy
const _note: Note = {
...note,
};
if (note.replyId != null) {
_note.reply = await this.notesRepository.findOneByOrFail({ id: note.replyId });
}
if (note.renoteId != null) {
_note.renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
}
if (isUserRelated(_note, new Set<string>(mutings.map(x => x.muteeId)))) {
return;
}
// 2秒経っても既読にならなかったら通知
setTimeout(async () => {
const unread = await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false });
if (unread) {
this.globalEventService.publishMainStream(antenna.userId, 'unreadAntenna', antenna);
this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', {
antenna: { id: antenna.id, name: antenna.name },
note: await this.noteEntityService.pack(note),
});
}
}, 2000);
}
} }
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている // NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている

View File

@ -0,0 +1,172 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, UserProfile, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
import type { LocalUser, User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
export class CacheService implements OnApplicationShutdown {
public userByIdCache: MemoryKVCache<User>;
public localUserByNativeTokenCache: MemoryKVCache<LocalUser | null>;
public localUserByIdCache: MemoryKVCache<LocalUser>;
public uriPersonCache: MemoryKVCache<User | null>;
public userProfileCache: RedisKVCache<UserProfile>;
public userMutingsCache: RedisKVCache<Set<string>>;
public userBlockingCache: RedisKVCache<Set<string>>;
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
public renoteMutingsCache: RedisKVCache<Set<string>>;
public userFollowingsCache: RedisKVCache<Set<string>>;
public userFollowingChannelsCache: RedisKVCache<Set<string>>;
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.redisForPubsub)
private redisForPubsub: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.renoteMutingsRepository)
private renoteMutingsRepository: RenoteMutingsRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
private userEntityService: UserEntityService,
) {
//this.onMessage = this.onMessage.bind(this);
this.userByIdCache = new MemoryKVCache<User>(Infinity);
this.localUserByNativeTokenCache = new MemoryKVCache<LocalUser | null>(Infinity);
this.localUserByIdCache = new MemoryKVCache<LocalUser>(Infinity);
this.uriPersonCache = new MemoryKVCache<User | null>(Infinity);
this.userProfileCache = new RedisKVCache<UserProfile>(this.redisClient, 'userProfile', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.userProfilesRepository.findOneByOrFail({ userId: key }),
toRedisConverter: (value) => JSON.stringify(value),
fromRedisConverter: (value) => JSON.parse(value), // TODO: date型の考慮
});
this.userMutingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userMutings', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.mutingsRepository.find({ where: { muterId: key }, select: ['muteeId'] }).then(xs => new Set(xs.map(x => x.muteeId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.userBlockingCache = new RedisKVCache<Set<string>>(this.redisClient, 'userBlocking', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.blockingsRepository.find({ where: { blockerId: key }, select: ['blockeeId'] }).then(xs => new Set(xs.map(x => x.blockeeId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.userBlockedCache = new RedisKVCache<Set<string>>(this.redisClient, 'userBlocked', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.blockingsRepository.find({ where: { blockeeId: key }, select: ['blockerId'] }).then(xs => new Set(xs.map(x => x.blockerId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.renoteMutingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'renoteMutings', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.renoteMutingsRepository.find({ where: { muterId: key }, select: ['muteeId'] }).then(xs => new Set(xs.map(x => x.muteeId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.userFollowingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowings', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.userFollowingChannelsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowingChannels', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.channelFollowingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.redisForPubsub.on('message', this.onMessage);
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as StreamMessages['internal']['payload'];
switch (type) {
case 'userChangeSuspendedState':
case 'remoteUserUpdated': {
const user = await this.usersRepository.findOneByOrFail({ id: body.id });
this.userByIdCache.set(user.id, user);
for (const [k, v] of this.uriPersonCache.cache.entries()) {
if (v.value?.id === user.id) {
this.uriPersonCache.set(k, user);
}
}
if (this.userEntityService.isLocalUser(user)) {
this.localUserByNativeTokenCache.set(user.token!, user);
this.localUserByIdCache.set(user.id, user);
}
break;
}
case 'userTokenRegenerated': {
const user = await this.usersRepository.findOneByOrFail({ id: body.id }) as LocalUser;
this.localUserByNativeTokenCache.delete(body.oldToken);
this.localUserByNativeTokenCache.set(body.newToken, user);
break;
}
case 'follow': {
const follower = this.userByIdCache.get(body.followerId);
if (follower) follower.followingCount++;
const followee = this.userByIdCache.get(body.followeeId);
if (followee) followee.followersCount++;
break;
}
default:
break;
}
}
}
@bindThis
public findUserById(userId: User['id']) {
return this.userByIdCache.fetch(userId, () => this.usersRepository.findOneByOrFail({ id: userId }));
}
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
this.redisForPubsub.off('message', this.onMessage);
}
}

View File

@ -1,4 +1,5 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js'; import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js'; import { AiService } from './AiService.js';
import { AntennaService } from './AntennaService.js'; import { AntennaService } from './AntennaService.js';
@ -38,9 +39,9 @@ import { S3Service } from './S3Service.js';
import { SignupService } from './SignupService.js'; import { SignupService } from './SignupService.js';
import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService.js'; import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService.js';
import { UserBlockingService } from './UserBlockingService.js'; import { UserBlockingService } from './UserBlockingService.js';
import { UserCacheService } from './UserCacheService.js'; import { CacheService } from './CacheService.js';
import { UserFollowingService } from './UserFollowingService.js'; import { UserFollowingService } from './UserFollowingService.js';
import { UserKeypairStoreService } from './UserKeypairStoreService.js'; import { UserKeypairService } from './UserKeypairService.js';
import { UserListService } from './UserListService.js'; import { UserListService } from './UserListService.js';
import { UserMutingService } from './UserMutingService.js'; import { UserMutingService } from './UserMutingService.js';
import { UserSuspendService } from './UserSuspendService.js'; import { UserSuspendService } from './UserSuspendService.js';
@ -119,6 +120,7 @@ import type { Provider } from '@nestjs/common';
//#region 文字列ベースでのinjection用(循環参照対応のため) //#region 文字列ベースでのinjection用(循環参照対応のため)
const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService }; const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService };
const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService }; const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService };
const $AiService: Provider = { provide: 'AiService', useExisting: AiService }; const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService }; const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService };
@ -159,9 +161,9 @@ const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service };
const $SignupService: Provider = { provide: 'SignupService', useExisting: SignupService }; const $SignupService: Provider = { provide: 'SignupService', useExisting: SignupService };
const $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthenticationService', useExisting: TwoFactorAuthenticationService }; const $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthenticationService', useExisting: TwoFactorAuthenticationService };
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService }; const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
const $UserCacheService: Provider = { provide: 'UserCacheService', useExisting: UserCacheService }; const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService };
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService }; const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
const $UserKeypairStoreService: Provider = { provide: 'UserKeypairStoreService', useExisting: UserKeypairStoreService }; const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService }; const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService }; const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService }; const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
@ -242,6 +244,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
], ],
providers: [ providers: [
LoggerService, LoggerService,
AccountMoveService,
AccountUpdateService, AccountUpdateService,
AiService, AiService,
AntennaService, AntennaService,
@ -282,9 +285,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
SignupService, SignupService,
TwoFactorAuthenticationService, TwoFactorAuthenticationService,
UserBlockingService, UserBlockingService,
UserCacheService, CacheService,
UserFollowingService, UserFollowingService,
UserKeypairStoreService, UserKeypairService,
UserListService, UserListService,
UserMutingService, UserMutingService,
UserSuspendService, UserSuspendService,
@ -359,6 +362,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
//#region 文字列ベースでのinjection用(循環参照対応のため) //#region 文字列ベースでのinjection用(循環参照対応のため)
$LoggerService, $LoggerService,
$AccountMoveService,
$AccountUpdateService, $AccountUpdateService,
$AiService, $AiService,
$AntennaService, $AntennaService,
@ -399,9 +403,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$SignupService, $SignupService,
$TwoFactorAuthenticationService, $TwoFactorAuthenticationService,
$UserBlockingService, $UserBlockingService,
$UserCacheService, $CacheService,
$UserFollowingService, $UserFollowingService,
$UserKeypairStoreService, $UserKeypairService,
$UserListService, $UserListService,
$UserMutingService, $UserMutingService,
$UserSuspendService, $UserSuspendService,
@ -477,6 +481,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
exports: [ exports: [
QueueModule, QueueModule,
LoggerService, LoggerService,
AccountMoveService,
AccountUpdateService, AccountUpdateService,
AiService, AiService,
AntennaService, AntennaService,
@ -517,9 +522,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
SignupService, SignupService,
TwoFactorAuthenticationService, TwoFactorAuthenticationService,
UserBlockingService, UserBlockingService,
UserCacheService, CacheService,
UserFollowingService, UserFollowingService,
UserKeypairStoreService, UserKeypairService,
UserListService, UserListService,
UserMutingService, UserMutingService,
UserSuspendService, UserSuspendService,
@ -593,6 +598,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
//#region 文字列ベースでのinjection用(循環参照対応のため) //#region 文字列ベースでのinjection用(循環参照対応のため)
$LoggerService, $LoggerService,
$AccountMoveService,
$AccountUpdateService, $AccountUpdateService,
$AiService, $AiService,
$AntennaService, $AntennaService,
@ -633,9 +639,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$SignupService, $SignupService,
$TwoFactorAuthenticationService, $TwoFactorAuthenticationService,
$UserBlockingService, $UserBlockingService,
$UserCacheService, $CacheService,
$UserFollowingService, $UserFollowingService,
$UserKeypairStoreService, $UserKeypairService,
$UserListService, $UserListService,
$UserMutingService, $UserMutingService,
$UserSuspendService, $UserSuspendService,

View File

@ -1,24 +1,28 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In, IsNull } from 'typeorm'; import { DataSource, In, IsNull } from 'typeorm';
import Redis from 'ioredis';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { Emoji } from '@/models/entities/Emoji.js'; import type { Emoji } from '@/models/entities/Emoji.js';
import type { EmojisRepository, Note } from '@/models/index.js'; import type { EmojisRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { KVCache } from '@/misc/cache.js'; import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { ReactionService } from '@/core/ReactionService.js';
import { query } from '@/misc/prelude/url.js'; import { query } from '@/misc/prelude/url.js';
@Injectable() @Injectable()
export class CustomEmojiService { export class CustomEmojiService {
private cache: KVCache<Emoji | null>; private cache: MemoryKVCache<Emoji | null>;
public localEmojisCache: RedisSingleCache<Map<string, Emoji>>;
constructor( constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@ -32,9 +36,16 @@ export class CustomEmojiService {
private idService: IdService, private idService: IdService,
private emojiEntityService: EmojiEntityService, private emojiEntityService: EmojiEntityService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private reactionService: ReactionService,
) { ) {
this.cache = new KVCache<Emoji | null>(1000 * 60 * 60 * 12); this.cache = new MemoryKVCache<Emoji | null>(1000 * 60 * 60 * 12);
this.localEmojisCache = new RedisSingleCache<Map<string, Emoji>>(this.redisClient, 'localEmojis', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60 * 3, // 3m
fetcher: () => this.emojisRepository.find({ where: { host: IsNull() } }).then(emojis => new Map(emojis.map(emoji => [emoji.name, emoji]))),
toRedisConverter: (value) => JSON.stringify(value.values()),
fromRedisConverter: (value) => new Map(JSON.parse(value).map((x: Emoji) => [x.name, x])), // TODO: Date型の変換
});
} }
@bindThis @bindThis
@ -60,7 +71,7 @@ export class CustomEmojiService {
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
if (data.host == null) { if (data.host == null) {
await this.db.queryResultCache?.remove(['meta_emojis']); this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiAdded', { this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: await this.emojiEntityService.packDetailed(emoji.id), emoji: await this.emojiEntityService.packDetailed(emoji.id),
@ -70,6 +81,146 @@ export class CustomEmojiService {
return emoji; return emoji;
} }
@bindThis
public async update(id: Emoji['id'], data: {
name?: string;
category?: string | null;
aliases?: string[];
license?: string | null;
}): Promise<void> {
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
name: data.name,
category: data.category,
aliases: data.aliases,
license: data.license,
});
this.localEmojisCache.refresh();
const updated = await this.emojiEntityService.packDetailed(emoji.id);
if (emoji.name === data.name) {
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: [updated],
});
} else {
this.globalEventService.publishBroadcastStream('emojiDeleted', {
emojis: [await this.emojiEntityService.packDetailed(emoji)],
});
this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: updated,
});
}
}
@bindThis
public async addAliasesBulk(ids: Emoji['id'][], aliases: string[]) {
const emojis = await this.emojisRepository.findBy({
id: In(ids),
});
for (const emoji of emojis) {
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
aliases: [...new Set(emoji.aliases.concat(aliases))],
});
}
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ids),
});
}
@bindThis
public async setAliasesBulk(ids: Emoji['id'][], aliases: string[]) {
await this.emojisRepository.update({
id: In(ids),
}, {
updatedAt: new Date(),
aliases: aliases,
});
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ids),
});
}
@bindThis
public async removeAliasesBulk(ids: Emoji['id'][], aliases: string[]) {
const emojis = await this.emojisRepository.findBy({
id: In(ids),
});
for (const emoji of emojis) {
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
aliases: emoji.aliases.filter(x => !aliases.includes(x)),
});
}
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ids),
});
}
@bindThis
public async setCategoryBulk(ids: Emoji['id'][], category: string | null) {
await this.emojisRepository.update({
id: In(ids),
}, {
updatedAt: new Date(),
category: category,
});
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ids),
});
}
@bindThis
public async delete(id: Emoji['id']) {
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
await this.emojisRepository.delete(emoji.id);
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiDeleted', {
emojis: [await this.emojiEntityService.packDetailed(emoji)],
});
}
@bindThis
public async deleteBulk(ids: Emoji['id'][]) {
const emojis = await this.emojisRepository.findBy({
id: In(ids),
});
for (const emoji of emojis) {
await this.emojisRepository.delete(emoji.id);
}
this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiDeleted', {
emojis: await this.emojiEntityService.packDetailedMany(emojis),
});
}
@bindThis @bindThis
private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null { private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null {
// クエリに使うホスト // クエリに使うホスト
@ -84,7 +235,7 @@ export class CustomEmojiService {
} }
@bindThis @bindThis
private parseEmojiStr(emojiName: string, noteUserHost: string | null) { public parseEmojiStr(emojiName: string, noteUserHost: string | null) {
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/); const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
if (!match) return { name: null, host: null }; if (!match) return { name: null, host: null };
@ -143,30 +294,6 @@ export class CustomEmojiService {
return res; return res;
} }
@bindThis
public aggregateNoteEmojis(notes: Note[]) {
let emojis: { name: string | null; host: string | null; }[] = [];
for (const note of notes) {
emojis = emojis.concat(note.emojis
.map(e => this.parseEmojiStr(e, note.userHost)));
if (note.renote) {
emojis = emojis.concat(note.renote.emojis
.map(e => this.parseEmojiStr(e, note.renote!.userHost)));
if (note.renote.user) {
emojis = emojis.concat(note.renote.user.emojis
.map(e => this.parseEmojiStr(e, note.renote!.userHost)));
}
}
const customReactions = Object.keys(note.reactions).map(x => this.reactionService.decodeReaction(x)).filter(x => x.name != null) as typeof emojis;
emojis = emojis.concat(customReactions);
if (note.user) {
emojis = emojis.concat(note.user.emojis
.map(e => this.parseEmojiStr(e, note.userHost)));
}
}
return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[];
}
/** /**
* *
*/ */

View File

@ -36,8 +36,5 @@ export class DeleteAccountService {
await this.usersRepository.update(user.id, { await this.usersRepository.update(user.id, {
isDeleted: true, isDeleted: true,
}); });
// Terminate streaming
this.globalEventService.publishUserEvent(user.id, 'terminate', {});
} }
} }

View File

@ -1,7 +1,8 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import type { InstancesRepository } from '@/models/index.js'; import type { InstancesRepository } from '@/models/index.js';
import type { Instance } from '@/models/entities/Instance.js'; import type { Instance } from '@/models/entities/Instance.js';
import { KVCache } from '@/misc/cache.js'; import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
@ -9,23 +10,40 @@ import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class FederatedInstanceService { export class FederatedInstanceService {
private cache: KVCache<Instance>; public federatedInstanceCache: RedisKVCache<Instance | null>;
constructor( constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.instancesRepository) @Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository, private instancesRepository: InstancesRepository,
private utilityService: UtilityService, private utilityService: UtilityService,
private idService: IdService, private idService: IdService,
) { ) {
this.cache = new KVCache<Instance>(1000 * 60 * 60); this.federatedInstanceCache = new RedisKVCache<Instance | null>(this.redisClient, 'federatedInstance', {
lifetime: 1000 * 60 * 60 * 24, // 24h
memoryCacheLifetime: 1000 * 60 * 30, // 30m
fetcher: (key) => this.instancesRepository.findOneBy({ host: key }),
toRedisConverter: (value) => JSON.stringify(value),
fromRedisConverter: (value) => {
const parsed = JSON.parse(value);
return {
...parsed,
firstRetrievedAt: new Date(parsed.firstRetrievedAt),
latestRequestReceivedAt: parsed.latestRequestReceivedAt ? new Date(parsed.latestRequestReceivedAt) : null,
infoUpdatedAt: parsed.infoUpdatedAt ? new Date(parsed.infoUpdatedAt) : null,
};
},
});
} }
@bindThis @bindThis
public async fetch(host: string): Promise<Instance> { public async fetch(host: string): Promise<Instance> {
host = this.utilityService.toPuny(host); host = this.utilityService.toPuny(host);
const cached = this.cache.get(host); const cached = await this.federatedInstanceCache.get(host);
if (cached) return cached; if (cached) return cached;
const index = await this.instancesRepository.findOneBy({ host }); const index = await this.instancesRepository.findOneBy({ host });
@ -37,10 +55,10 @@ export class FederatedInstanceService {
firstRetrievedAt: new Date(), firstRetrievedAt: new Date(),
}).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
this.cache.set(host, i); this.federatedInstanceCache.set(host, i);
return i; return i;
} else { } else {
this.cache.set(host, index); this.federatedInstanceCache.set(host, index);
return index; return index;
} }
} }
@ -49,10 +67,10 @@ export class FederatedInstanceService {
public async updateCachePartial(host: string, data: Partial<Instance>): Promise<void> { public async updateCachePartial(host: string, data: Partial<Instance>): Promise<void> {
host = this.utilityService.toPuny(host); host = this.utilityService.toPuny(host);
const cached = this.cache.get(host); const cached = await this.federatedInstanceCache.get(host);
if (cached == null) return; if (cached == null) return;
this.cache.set(host, { this.federatedInstanceCache.set(host, {
...cached, ...cached,
...data, ...data,
}); });

View File

@ -14,7 +14,6 @@ import type {
MainStreamTypes, MainStreamTypes,
NoteStreamTypes, NoteStreamTypes,
UserListStreamTypes, UserListStreamTypes,
UserStreamTypes,
} from '@/server/api/stream/types.js'; } 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';
@ -49,11 +48,6 @@ export class GlobalEventService {
this.publish('internal', type, typeof value === 'undefined' ? null : value); this.publish('internal', type, typeof value === 'undefined' ? null : value);
} }
@bindThis
public publishUserEvent<K extends keyof UserStreamTypes>(userId: User['id'], type: K, value?: UserStreamTypes[K]): void {
this.publish(`user:${userId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis @bindThis
public publishBroadcastStream<K extends keyof BroadcastTypes>(type: K, value?: BroadcastTypes[K]): void { public publishBroadcastStream<K extends keyof BroadcastTypes>(type: K, value?: BroadcastTypes[K]): void {
this.publish('broadcast', type, typeof value === 'undefined' ? null : value); this.publish('broadcast', type, typeof value === 'undefined' ? null : value);

View File

@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { ulid } from 'ulid'; import { ulid } from 'ulid';
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 { genAid } from '@/misc/id/aid.js'; import { genAid, parseAid } from '@/misc/id/aid.js';
import { genMeid } from '@/misc/id/meid.js'; import { genMeid } from '@/misc/id/meid.js';
import { genMeidg } from '@/misc/id/meidg.js'; import { genMeidg } from '@/misc/id/meidg.js';
import { genObjectId } from '@/misc/id/object-id.js'; import { genObjectId } from '@/misc/id/object-id.js';
@ -32,4 +32,17 @@ export class IdService {
default: throw new Error('unrecognized id generation method'); default: throw new Error('unrecognized id generation method');
} }
} }
@bindThis
public parse(id: string): { date: Date; } {
switch (this.method) {
case 'aid': return parseAid(id);
// TODO
//case 'meid':
//case 'meidg':
//case 'ulid':
//case 'objectid':
default: throw new Error('unrecognized id generation method');
}
}
} }

View File

@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import type { LocalUser } from '@/models/entities/User.js'; import type { LocalUser } from '@/models/entities/User.js';
import type { UsersRepository } from '@/models/index.js'; import type { UsersRepository } from '@/models/index.js';
import { KVCache } from '@/misc/cache.js'; import { MemorySingleCache } from '@/misc/cache.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -11,7 +11,7 @@ const ACTOR_USERNAME = 'instance.actor' as const;
@Injectable() @Injectable()
export class InstanceActorService { export class InstanceActorService {
private cache: KVCache<LocalUser>; private cache: MemorySingleCache<LocalUser>;
constructor( constructor(
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
@ -19,12 +19,12 @@ export class InstanceActorService {
private createSystemUserService: CreateSystemUserService, private createSystemUserService: CreateSystemUserService,
) { ) {
this.cache = new KVCache<LocalUser>(Infinity); this.cache = new MemorySingleCache<LocalUser>(Infinity);
} }
@bindThis @bindThis
public async getInstanceActor(): Promise<LocalUser> { public async getInstanceActor(): Promise<LocalUser> {
const cached = this.cache.get(null); const cached = this.cache.get();
if (cached) return cached; if (cached) return cached;
const user = await this.usersRepository.findOneBy({ const user = await this.usersRepository.findOneBy({
@ -33,11 +33,11 @@ export class InstanceActorService {
}) as LocalUser | undefined; }) as LocalUser | undefined;
if (user) { if (user) {
this.cache.set(null, user); this.cache.set(user);
return user; return user;
} else { } else {
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as LocalUser; const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as LocalUser;
this.cache.set(null, created); this.cache.set(created);
return created; return created;
} }
} }

View File

@ -14,8 +14,8 @@ export class MetaService implements OnApplicationShutdown {
private intervalId: NodeJS.Timer; private intervalId: NodeJS.Timer;
constructor( constructor(
@Inject(DI.redisSubscriber) @Inject(DI.redisForPubsub)
private redisSubscriber: Redis.Redis, private redisForPubsub: Redis.Redis,
@Inject(DI.db) @Inject(DI.db)
private db: DataSource, private db: DataSource,
@ -33,7 +33,7 @@ export class MetaService implements OnApplicationShutdown {
}, 1000 * 60 * 5); }, 1000 * 60 * 5);
} }
this.redisSubscriber.on('message', this.onMessage); this.redisForPubsub.on('message', this.onMessage);
} }
@bindThis @bindThis
@ -122,6 +122,6 @@ export class MetaService implements OnApplicationShutdown {
@bindThis @bindThis
public onApplicationShutdown(signal?: string | undefined) { public onApplicationShutdown(signal?: string | undefined) {
clearInterval(this.intervalId); clearInterval(this.intervalId);
this.redisSubscriber.off('message', this.onMessage); this.redisForPubsub.off('message', this.onMessage);
} }
} }

View File

@ -1,6 +1,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 } from 'typeorm';
import Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { extractMentions } from '@/misc/extract-mentions.js'; import { extractMentions } from '@/misc/extract-mentions.js';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
@ -19,7 +20,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
import { checkWordMute } from '@/misc/check-word-mute.js'; import { checkWordMute } from '@/misc/check-word-mute.js';
import type { Channel } from '@/models/entities/Channel.js'; import type { Channel } from '@/models/entities/Channel.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { KVCache } from '@/misc/cache.js'; import { MemorySingleCache } from '@/misc/cache.js';
import type { UserProfile } from '@/models/entities/UserProfile.js'; import type { UserProfile } from '@/models/entities/UserProfile.js';
import { RelayService } from '@/core/RelayService.js'; import { RelayService } from '@/core/RelayService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
@ -46,7 +47,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';
const mutedWordsCache = new KVCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); const mutedWordsCache = new MemorySingleCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -150,6 +151,9 @@ export class NoteCreateService implements OnApplicationShutdown {
@Inject(DI.db) @Inject(DI.db)
private db: DataSource, private db: DataSource,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -321,6 +325,14 @@ 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) {
this.redisClient.xadd(
`channelTimeline:${data.channel.id}`,
'MAXLEN', '~', '1000',
`${this.idService.parse(note.id).date.getTime()}-*`,
'note', note.id);
}
setImmediate('post created', { signal: this.#shutdownController.signal }).then( setImmediate('post created', { signal: this.#shutdownController.signal }).then(
() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!), () => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
() => { /* aborted, ignore this */ }, () => { /* aborted, ignore this */ },
@ -461,7 +473,7 @@ export class NoteCreateService implements OnApplicationShutdown {
this.incNotesCountOfUser(user); this.incNotesCountOfUser(user);
// Word mute // Word mute
mutedWordsCache.fetch(null, () => this.userProfilesRepository.find({ mutedWordsCache.fetch(() => this.userProfilesRepository.find({
where: { where: {
enableWordMute: true, enableWordMute: true,
}, },
@ -490,18 +502,6 @@ export class NoteCreateService implements OnApplicationShutdown {
}); });
} }
// Channel
if (note.channelId) {
this.channelFollowingsRepository.findBy({ followeeId: note.channelId }).then(followings => {
for (const following of followings) {
this.noteReadService.insertNoteUnread(following.followerId, note, {
isSpecified: false,
isMentioned: false,
});
}
});
}
if (data.reply) { if (data.reply) {
this.saveReply(data.reply, note); this.saveReply(data.reply, note);
} }

View File

@ -1,28 +1,20 @@
import { setTimeout } from 'node:timers/promises'; import { setTimeout } from 'node:timers/promises';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { In, IsNull, Not } from 'typeorm'; import { In } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import type { Channel } from '@/models/entities/Channel.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import type { Note } from '@/models/entities/Note.js'; import type { Note } from '@/models/entities/Note.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 type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository, AntennaNotesRepository } from '@/models/index.js'; import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/index.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { NotificationService } from './NotificationService.js';
import { AntennaService } from './AntennaService.js';
import { PushNotificationService } from './PushNotificationService.js';
@Injectable() @Injectable()
export class NoteReadService implements OnApplicationShutdown { export class NoteReadService implements OnApplicationShutdown {
#shutdownController = new AbortController(); #shutdownController = new AbortController();
constructor( constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.noteUnreadsRepository) @Inject(DI.noteUnreadsRepository)
private noteUnreadsRepository: NoteUnreadsRepository, private noteUnreadsRepository: NoteUnreadsRepository,
@ -32,21 +24,8 @@ export class NoteReadService implements OnApplicationShutdown {
@Inject(DI.noteThreadMutingsRepository) @Inject(DI.noteThreadMutingsRepository)
private noteThreadMutingsRepository: NoteThreadMutingsRepository, private noteThreadMutingsRepository: NoteThreadMutingsRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private notificationService: NotificationService,
private antennaService: AntennaService,
private pushNotificationService: PushNotificationService,
) { ) {
} }
@ -57,7 +36,6 @@ export class NoteReadService implements OnApplicationShutdown {
isMentioned: boolean; isMentioned: boolean;
}): Promise<void> { }): Promise<void> {
//#region ミュートしているなら無視 //#region ミュートしているなら無視
// TODO: 現在の仕様ではChannelにミュートは適用されないのでよしなにケアする
const mute = await this.mutingsRepository.findBy({ const mute = await this.mutingsRepository.findBy({
muterId: userId, muterId: userId,
}); });
@ -77,7 +55,6 @@ export class NoteReadService implements OnApplicationShutdown {
userId: userId, userId: userId,
isSpecified: params.isSpecified, isSpecified: params.isSpecified,
isMentioned: params.isMentioned, isMentioned: params.isMentioned,
noteChannelId: note.channelId,
noteUserId: note.userId, noteUserId: note.userId,
}; };
@ -95,9 +72,6 @@ export class NoteReadService implements OnApplicationShutdown {
if (params.isSpecified) { if (params.isSpecified) {
this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id); this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
} }
if (note.channelId) {
this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id);
}
}, () => { /* aborted, ignore it */ }); }, () => { /* aborted, ignore it */ });
} }
@ -105,23 +79,9 @@ export class NoteReadService implements OnApplicationShutdown {
public async read( public async read(
userId: User['id'], userId: User['id'],
notes: (Note | Packed<'Note'>)[], notes: (Note | Packed<'Note'>)[],
info?: {
following: Set<User['id']>;
followingChannels: Set<Channel['id']>;
},
): Promise<void> { ): Promise<void> {
const followingChannels = info?.followingChannels ? info.followingChannels : new Set<string>((await this.channelFollowingsRepository.find({
where: {
followerId: userId,
},
select: ['followeeId'],
})).map(x => x.followeeId));
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
const readMentions: (Note | Packed<'Note'>)[] = []; const readMentions: (Note | Packed<'Note'>)[] = [];
const readSpecifiedNotes: (Note | Packed<'Note'>)[] = []; const readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
const readChannelNotes: (Note | Packed<'Note'>)[] = [];
const readAntennaNotes: (Note | Packed<'Note'>)[] = [];
for (const note of notes) { for (const note of notes) {
if (note.mentions && note.mentions.includes(userId)) { if (note.mentions && note.mentions.includes(userId)) {
@ -129,25 +89,13 @@ export class NoteReadService implements OnApplicationShutdown {
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) { } else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
readSpecifiedNotes.push(note); readSpecifiedNotes.push(note);
} }
if (note.channelId && followingChannels.has(note.channelId)) {
readChannelNotes.push(note);
}
if (note.user != null) { // たぶんnullになることは無いはずだけど一応
for (const antenna of myAntennas) {
if (await this.antennaService.checkHitAntenna(antenna, note, note.user)) {
readAntennaNotes.push(note);
}
}
}
} }
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) { if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0)) {
// Remove the record // Remove the record
await this.noteUnreadsRepository.delete({ await this.noteUnreadsRepository.delete({
userId: userId, userId: userId,
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id), ...readChannelNotes.map(n => n.id)]), noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
}); });
// TODO: ↓まとめてクエリしたい // TODO: ↓まとめてクエリしたい
@ -171,49 +119,6 @@ export class NoteReadService implements OnApplicationShutdown {
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
} }
}); });
this.noteUnreadsRepository.countBy({
userId: userId,
noteChannelId: Not(IsNull()),
}).then(channelNoteCount => {
if (channelNoteCount === 0) {
// 全て既読になったイベントを発行
this.globalEventService.publishMainStream(userId, 'readAllChannels');
}
});
this.notificationService.readNotificationByQuery(userId, {
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
});
}
if (readAntennaNotes.length > 0) {
await this.antennaNotesRepository.update({
antennaId: In(myAntennas.map(a => a.id)),
noteId: In(readAntennaNotes.map(n => n.id)),
}, {
read: true,
});
// TODO: まとめてクエリしたい
for (const antenna of myAntennas) {
const count = await this.antennaNotesRepository.countBy({
antennaId: antenna.id,
read: false,
});
if (count === 0) {
this.globalEventService.publishMainStream(userId, 'readAntenna', antenna);
this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id });
}
}
this.userEntityService.getHasUnreadAntenna(userId).then(unread => {
if (!unread) {
this.globalEventService.publishMainStream(userId, 'readAllAntennas');
this.pushNotificationService.pushNotification(userId, 'readAllAntennas', undefined);
}
});
} }
} }

View File

@ -1,8 +1,9 @@
import { setTimeout } from 'node:timers/promises'; import { setTimeout } from 'node:timers/promises';
import Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { MutingsRepository, NotificationsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import type { MutingsRepository, UserProfile, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import type { Notification } from '@/models/entities/Notification.js'; import type { Notification } from '@/models/entities/Notification.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
@ -11,21 +12,22 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { PushNotificationService } from '@/core/PushNotificationService.js'; import { PushNotificationService } from '@/core/PushNotificationService.js';
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { CacheService } from '@/core/CacheService.js';
@Injectable() @Injectable()
export class NotificationService implements OnApplicationShutdown { export class NotificationService implements OnApplicationShutdown {
#shutdownController = new AbortController(); #shutdownController = new AbortController();
constructor( constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
@Inject(DI.notificationsRepository)
private notificationsRepository: NotificationsRepository,
@Inject(DI.mutingsRepository) @Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository, private mutingsRepository: MutingsRepository,
@ -34,54 +36,36 @@ export class NotificationService implements OnApplicationShutdown {
private idService: IdService, private idService: IdService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private pushNotificationService: PushNotificationService, private pushNotificationService: PushNotificationService,
private cacheService: CacheService,
) { ) {
} }
@bindThis @bindThis
public async readNotification( public async readAllNotification(
userId: User['id'], userId: User['id'],
notificationIds: Notification['id'][], force = false,
) { ) {
if (notificationIds.length === 0) return; const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
// Update documents const latestNotificationIdsRes = await this.redisClient.xrevrange(
const result = await this.notificationsRepository.update({ `notificationTimeline:${userId}`,
notifieeId: userId, '+',
id: In(notificationIds), '-',
isRead: false, 'COUNT', 1);
}, { const latestNotificationId = latestNotificationIdsRes[0]?.[0];
isRead: true,
});
if (result.affected === 0) return; if (latestNotificationId == null) return;
if (!await this.userEntityService.getHasUnreadNotification(userId)) return this.postReadAllNotifications(userId); this.redisClient.set(`latestReadNotification:${userId}`, latestNotificationId);
else return this.postReadNotifications(userId, notificationIds);
}
@bindThis if (force || latestReadNotificationId == null || (latestReadNotificationId < latestNotificationId)) {
public async readNotificationByQuery( return this.postReadAllNotifications(userId);
userId: User['id'], }
query: Record<string, any>,
) {
const notificationIds = await this.notificationsRepository.findBy({
...query,
notifieeId: userId,
isRead: false,
}).then(notifications => notifications.map(notification => notification.id));
return this.readNotification(userId, notificationIds);
} }
@bindThis @bindThis
private postReadAllNotifications(userId: User['id']) { private postReadAllNotifications(userId: User['id']) {
this.globalEventService.publishMainStream(userId, 'readAllNotifications'); this.globalEventService.publishMainStream(userId, 'readAllNotifications');
return this.pushNotificationService.pushNotification(userId, 'readAllNotifications', undefined);
}
@bindThis
private postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) {
return this.pushNotificationService.pushNotification(userId, 'readNotifications', { notificationIds });
} }
@bindThis @bindThis
@ -90,45 +74,43 @@ export class NotificationService implements OnApplicationShutdown {
type: Notification['type'], type: Notification['type'],
data: Partial<Notification>, data: Partial<Notification>,
): Promise<Notification | null> { ): Promise<Notification | null> {
if (data.notifierId && (notifieeId === data.notifierId)) { const profile = await this.cacheService.userProfileCache.fetch(notifieeId);
return null; const isMuted = profile.mutingNotificationTypes.includes(type);
if (isMuted) return null;
if (data.notifierId) {
if (notifieeId === data.notifierId) {
return null;
}
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId);
if (mutings.has(data.notifierId)) {
return null;
}
} }
const profile = await this.userProfilesRepository.findOneBy({ userId: notifieeId }); const notification = {
const isMuted = profile?.mutingNotificationTypes.includes(type);
// Create notification
const notification = await this.notificationsRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
createdAt: new Date(), createdAt: new Date(),
notifieeId: notifieeId,
type: type, type: type,
// 相手がこの通知をミュートしているようなら、既読を予めつけておく
isRead: isMuted,
...data, ...data,
} as Partial<Notification>) } as Notification;
.then(x => this.notificationsRepository.findOneByOrFail(x.identifiers[0]));
const packed = await this.notificationEntityService.pack(notification, {}); const redisIdPromise = this.redisClient.xadd(
`notificationTimeline:${notifieeId}`,
'MAXLEN', '~', '300',
`${this.idService.parse(notification.id).date.getTime()}-*`,
'data', JSON.stringify(notification));
const packed = await this.notificationEntityService.pack(notification, notifieeId, {});
// Publish notification event // Publish notification event
this.globalEventService.publishMainStream(notifieeId, 'notification', packed); this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する // 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => { setTimeout(2000, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => {
const fresh = await this.notificationsRepository.findOneBy({ id: notification.id }); const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${notifieeId}`);
if (fresh == null) return; // 既に削除されているかもしれない if (latestReadNotificationId && (latestReadNotificationId >= await redisIdPromise)) return;
if (fresh.isRead) return;
//#region ただしミュートしているユーザーからの通知なら無視
const mutings = await this.mutingsRepository.findBy({
muterId: notifieeId,
});
if (data.notifierId && mutings.map(m => m.muteeId).includes(data.notifierId)) {
return;
}
//#endregion
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed); this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed); this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);

View File

@ -15,10 +15,6 @@ type PushNotificationsTypes = {
antenna: { id: string, name: string }; antenna: { id: string, name: string };
note: Packed<'Note'>; note: Packed<'Note'>;
}; };
'readNotifications': { notificationIds: string[] };
'readAllNotifications': undefined;
'readAntenna': { antennaId: string };
'readAllAntennas': undefined;
}; };
// Reduce length because push message servers have character limits // Reduce length because push message servers have character limits
@ -72,14 +68,6 @@ export class PushNotificationService {
}); });
for (const subscription of subscriptions) { for (const subscription of subscriptions) {
// Continue if sendReadMessage is false
if ([
'readNotifications',
'readAllNotifications',
'readAntenna',
'readAllAntennas',
].includes(type) && !subscription.sendReadMessage) continue;
const pushSubscription = { const pushSubscription = {
endpoint: subscription.endpoint, endpoint: subscription.endpoint,
keys: { keys: {

View File

@ -8,13 +8,13 @@ import type { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, End
function q<T>(config: Config, name: string, limitPerSec = -1) { function q<T>(config: Config, name: string, limitPerSec = -1) {
return new Bull<T>(name, { return new Bull<T>(name, {
redis: { redis: {
port: config.redis.port, port: config.redisForJobQueue.port,
host: config.redis.host, host: config.redisForJobQueue.host,
family: config.redis.family == null ? 0 : config.redis.family, family: config.redisForJobQueue.family == null ? 0 : config.redisForJobQueue.family,
password: config.redis.pass, password: config.redisForJobQueue.pass,
db: config.redis.db ?? 0, db: config.redisForJobQueue.db ?? 0,
}, },
prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : 'queue', prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue` : 'queue',
limiter: limitPerSec > 0 ? { limiter: limitPerSec > 0 ? {
max: limitPerSec, max: limitPerSec,
duration: 1000, duration: 1000,

View File

@ -1,7 +1,6 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { EmojisRepository, BlockingsRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js'; import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import type { RemoteUser, User } from '@/models/entities/User.js'; import type { RemoteUser, User } from '@/models/entities/User.js';
import type { Note } from '@/models/entities/Note.js'; import type { Note } from '@/models/entities/Note.js';
@ -20,6 +19,7 @@ import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
const FALLBACK = '❤'; const FALLBACK = '❤';
@ -60,9 +60,6 @@ export class ReactionService {
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.notesRepository) @Inject(DI.notesRepository)
private notesRepository: NotesRepository, private notesRepository: NotesRepository,
@ -74,6 +71,7 @@ export class ReactionService {
private utilityService: UtilityService, private utilityService: UtilityService,
private metaService: MetaService, private metaService: MetaService,
private customEmojiService: CustomEmojiService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private userBlockingService: UserBlockingService, private userBlockingService: UserBlockingService,
@ -104,7 +102,6 @@ export class ReactionService {
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) { if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) {
reaction = '❤️'; reaction = '❤️';
} else { } else {
// TODO: cache
reaction = await this.toDbReaction(reaction, user.host); reaction = await this.toDbReaction(reaction, user.host);
} }
@ -158,20 +155,22 @@ export class ReactionService {
// カスタム絵文字リアクションだったら絵文字情報も送る // カスタム絵文字リアクションだったら絵文字情報も送る
const decodedReaction = this.decodeReaction(reaction); const decodedReaction = this.decodeReaction(reaction);
const emoji = await this.emojisRepository.findOne({ const customEmoji = decodedReaction.name == null ? null : decodedReaction.host == null
where: { ? (await this.customEmojiService.localEmojisCache.fetch()).get(decodedReaction.name)
name: decodedReaction.name, : await this.emojisRepository.findOne(
host: decodedReaction.host ?? IsNull(), {
}, where: {
select: ['name', 'host', 'originalUrl', 'publicUrl'], name: decodedReaction.name,
}); host: decodedReaction.host,
},
});
this.globalEventService.publishNoteStream(note.id, 'reacted', { this.globalEventService.publishNoteStream(note.id, 'reacted', {
reaction: decodedReaction.reaction, reaction: decodedReaction.reaction,
emoji: emoji != null ? { emoji: customEmoji != null ? {
name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`, name: customEmoji.host ? `${customEmoji.name}@${customEmoji.host}` : `${customEmoji.name}@.`,
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ) // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: emoji.publicUrl || emoji.originalUrl, url: customEmoji.publicUrl || customEmoji.originalUrl,
} : null, } : null,
userId: user.id, userId: user.id,
}); });
@ -310,10 +309,12 @@ export class ReactionService {
const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/); const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
if (custom) { if (custom) {
const name = custom[1]; const name = custom[1];
const emoji = await this.emojisRepository.findOneBy({ const emoji = reacterHost == null
host: reacterHost ?? IsNull(), ? (await this.customEmojiService.localEmojisCache.fetch()).get(name)
name, : await this.emojisRepository.findOneBy({
}); host: reacterHost,
name,
});
if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`; if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
} }

View File

@ -3,7 +3,7 @@ import { IsNull } from 'typeorm';
import type { LocalUser, User } from '@/models/entities/User.js'; import type { LocalUser, User } from '@/models/entities/User.js';
import type { RelaysRepository, UsersRepository } from '@/models/index.js'; import type { RelaysRepository, UsersRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { KVCache } from '@/misc/cache.js'; import { MemorySingleCache } from '@/misc/cache.js';
import type { Relay } from '@/models/entities/Relay.js'; import type { Relay } from '@/models/entities/Relay.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
@ -16,7 +16,7 @@ const ACTOR_USERNAME = 'relay.actor' as const;
@Injectable() @Injectable()
export class RelayService { export class RelayService {
private relaysCache: KVCache<Relay[]>; private relaysCache: MemorySingleCache<Relay[]>;
constructor( constructor(
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
@ -30,7 +30,7 @@ export class RelayService {
private createSystemUserService: CreateSystemUserService, private createSystemUserService: CreateSystemUserService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
) { ) {
this.relaysCache = new KVCache<Relay[]>(1000 * 60 * 10); this.relaysCache = new MemorySingleCache<Relay[]>(1000 * 60 * 10);
} }
@bindThis @bindThis
@ -109,7 +109,7 @@ export class RelayService {
public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise<void> { public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise<void> {
if (activity == null) return; if (activity == null) return;
const relays = await this.relaysCache.fetch(null, () => this.relaysRepository.findBy({ const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({
status: 'accepted', status: 'accepted',
})); }));
if (relays.length === 0) return; if (relays.length === 0) return;

View File

@ -2,12 +2,12 @@ import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis'; import Redis from 'ioredis';
import { In } from 'typeorm'; import { In } from 'typeorm';
import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js'; import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
import { KVCache } from '@/misc/cache.js'; import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { UserCacheService } from '@/core/UserCacheService.js'; import { CacheService } from '@/core/CacheService.js';
import type { RoleCondFormulaValue } from '@/models/entities/Role.js'; import type { RoleCondFormulaValue } from '@/models/entities/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 { StreamMessages } from '@/server/api/stream/types.js';
@ -57,15 +57,15 @@ export const DEFAULT_POLICIES: RolePolicies = {
@Injectable() @Injectable()
export class RoleService implements OnApplicationShutdown { export class RoleService implements OnApplicationShutdown {
private rolesCache: KVCache<Role[]>; private rolesCache: MemorySingleCache<Role[]>;
private roleAssignmentByUserIdCache: KVCache<RoleAssignment[]>; private roleAssignmentByUserIdCache: MemoryKVCache<RoleAssignment[]>;
public static AlreadyAssignedError = class extends Error {}; public static AlreadyAssignedError = class extends Error {};
public static NotAssignedError = class extends Error {}; public static NotAssignedError = class extends Error {};
constructor( constructor(
@Inject(DI.redisSubscriber) @Inject(DI.redisForPubsub)
private redisSubscriber: Redis.Redis, private redisForPubsub: Redis.Redis,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -77,17 +77,17 @@ export class RoleService implements OnApplicationShutdown {
private roleAssignmentsRepository: RoleAssignmentsRepository, private roleAssignmentsRepository: RoleAssignmentsRepository,
private metaService: MetaService, private metaService: MetaService,
private userCacheService: UserCacheService, private cacheService: CacheService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private idService: IdService, private idService: IdService,
) { ) {
//this.onMessage = this.onMessage.bind(this); //this.onMessage = this.onMessage.bind(this);
this.rolesCache = new KVCache<Role[]>(Infinity); this.rolesCache = new MemorySingleCache<Role[]>(1000 * 60 * 60 * 1);
this.roleAssignmentByUserIdCache = new KVCache<RoleAssignment[]>(Infinity); this.roleAssignmentByUserIdCache = new MemoryKVCache<RoleAssignment[]>(1000 * 60 * 60 * 1);
this.redisSubscriber.on('message', this.onMessage); this.redisForPubsub.on('message', this.onMessage);
} }
@bindThis @bindThis
@ -98,7 +98,7 @@ export class RoleService implements OnApplicationShutdown {
const { type, body } = obj.message as StreamMessages['internal']['payload']; const { type, body } = obj.message as StreamMessages['internal']['payload'];
switch (type) { switch (type) {
case 'roleCreated': { case 'roleCreated': {
const cached = this.rolesCache.get(null); const cached = this.rolesCache.get();
if (cached) { if (cached) {
cached.push({ cached.push({
...body, ...body,
@ -110,7 +110,7 @@ export class RoleService implements OnApplicationShutdown {
break; break;
} }
case 'roleUpdated': { case 'roleUpdated': {
const cached = this.rolesCache.get(null); const cached = this.rolesCache.get();
if (cached) { if (cached) {
const i = cached.findIndex(x => x.id === body.id); const i = cached.findIndex(x => x.id === body.id);
if (i > -1) { if (i > -1) {
@ -125,9 +125,9 @@ export class RoleService implements OnApplicationShutdown {
break; break;
} }
case 'roleDeleted': { case 'roleDeleted': {
const cached = this.rolesCache.get(null); const cached = this.rolesCache.get();
if (cached) { if (cached) {
this.rolesCache.set(null, cached.filter(x => x.id !== body.id)); this.rolesCache.set(cached.filter(x => x.id !== body.id));
} }
break; break;
} }
@ -214,9 +214,9 @@ export class RoleService implements OnApplicationShutdown {
// 期限切れのロールを除外 // 期限切れのロールを除外
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now)); assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
const assignedRoleIds = assigns.map(x => x.roleId); const assignedRoleIds = assigns.map(x => x.roleId);
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id)); const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id));
const user = roles.some(r => r.target === 'conditional') ? await this.userCacheService.findById(userId) : null; const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula)); const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula));
return [...assignedRoles, ...matchedCondRoles]; return [...assignedRoles, ...matchedCondRoles];
} }
@ -231,11 +231,11 @@ export class RoleService implements OnApplicationShutdown {
// 期限切れのロールを除外 // 期限切れのロールを除外
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now)); assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
const assignedRoleIds = assigns.map(x => x.roleId); const assignedRoleIds = assigns.map(x => x.roleId);
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id)); const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id));
const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional')); const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional'));
if (badgeCondRoles.length > 0) { if (badgeCondRoles.length > 0) {
const user = roles.some(r => r.target === 'conditional') ? await this.userCacheService.findById(userId) : null; const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, r.condFormula)); const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, r.condFormula));
return [...assignedBadgeRoles, ...matchedBadgeCondRoles]; return [...assignedBadgeRoles, ...matchedBadgeCondRoles];
} else { } else {
@ -301,7 +301,7 @@ export class RoleService implements OnApplicationShutdown {
@bindThis @bindThis
public async getModeratorIds(includeAdmins = true): Promise<User['id'][]> { public async getModeratorIds(includeAdmins = true): Promise<User['id'][]> {
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator); const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator);
const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({ const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
roleId: In(moderatorRoles.map(r => r.id)), roleId: In(moderatorRoles.map(r => r.id)),
@ -321,7 +321,7 @@ export class RoleService implements OnApplicationShutdown {
@bindThis @bindThis
public async getAdministratorIds(): Promise<User['id'][]> { public async getAdministratorIds(): Promise<User['id'][]> {
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({})); const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const administratorRoles = roles.filter(r => r.isAdministrator); const administratorRoles = roles.filter(r => r.isAdministrator);
const assigns = administratorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({ const assigns = administratorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
roleId: In(administratorRoles.map(r => r.id)), roleId: In(administratorRoles.map(r => r.id)),
@ -400,6 +400,6 @@ export class RoleService implements OnApplicationShutdown {
@bindThis @bindThis
public onApplicationShutdown(signal?: string | undefined) { public onApplicationShutdown(signal?: string | undefined) {
this.redisSubscriber.off('message', this.onMessage); this.redisForPubsub.off('message', this.onMessage);
} }
} }

View File

@ -42,7 +42,7 @@ export class S3Service {
accessKeyId: meta.objectStorageAccessKey, accessKeyId: meta.objectStorageAccessKey,
secretAccessKey: meta.objectStorageSecretKey, secretAccessKey: meta.objectStorageSecretKey,
} : undefined, } : undefined,
region: meta.objectStorageRegion ?? undefined, region: meta.objectStorageRegion ? meta.objectStorageRegion : undefined, // 空文字列もundefinedにするため ?? は使わない
tls: meta.objectStorageUseSSL, tls: meta.objectStorageUseSSL,
forcePathStyle: meta.objectStorageEndpoint ? meta.objectStorageS3ForcePathStyle : false, // AWS with endPoint omitted forcePathStyle: meta.objectStorageEndpoint ? meta.objectStorageS3ForcePathStyle : false, // AWS with endPoint omitted
requestHandler: new NodeHttpHandler(handlerOption), requestHandler: new NodeHttpHandler(handlerOption),

View File

@ -1,39 +1,29 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import Redis from 'ioredis'; import { ModuleRef } from '@nestjs/core';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import type { Blocking } from '@/models/entities/Blocking.js'; import type { Blocking } from '@/models/entities/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 PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { UsersRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js'; import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.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';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { WebhookService } from '@/core/WebhookService.js'; import { WebhookService } from '@/core/WebhookService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { KVCache } from '@/misc/cache.js'; import { CacheService } from '@/core/CacheService.js';
import { StreamMessages } from '@/server/api/stream/types.js'; import { UserFollowingService } from '@/core/UserFollowingService.js';
@Injectable() @Injectable()
export class UserBlockingService implements OnApplicationShutdown { export class UserBlockingService implements OnModuleInit {
private logger: Logger; private logger: Logger;
private userFollowingService: UserFollowingService;
// キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ
private blockingsByUserIdCache: KVCache<User['id'][]>;
constructor( constructor(
@Inject(DI.redisSubscriber) private moduleRef: ModuleRef,
private redisSubscriber: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.followRequestsRepository) @Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository, private followRequestsRepository: FollowRequestsRepository,
@ -47,47 +37,20 @@ export class UserBlockingService implements OnApplicationShutdown {
@Inject(DI.userListJoiningsRepository) @Inject(DI.userListJoiningsRepository)
private userListJoiningsRepository: UserListJoiningsRepository, private userListJoiningsRepository: UserListJoiningsRepository,
private cacheService: CacheService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
private queueService: QueueService, private queueService: QueueService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private webhookService: WebhookService, private webhookService: WebhookService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private perUserFollowingChart: PerUserFollowingChart,
private loggerService: LoggerService, private loggerService: LoggerService,
) { ) {
this.logger = this.loggerService.getLogger('user-block'); this.logger = this.loggerService.getLogger('user-block');
this.blockingsByUserIdCache = new KVCache<User['id'][]>(Infinity);
this.redisSubscriber.on('message', this.onMessage);
} }
@bindThis onModuleInit() {
private async onMessage(_: string, data: string): Promise<void> { this.userFollowingService = this.moduleRef.get('UserFollowingService');
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as StreamMessages['internal']['payload'];
switch (type) {
case 'blockingCreated': {
const cached = this.blockingsByUserIdCache.get(body.blockerId);
if (cached) {
this.blockingsByUserIdCache.set(body.blockerId, [...cached, ...[body.blockeeId]]);
}
break;
}
case 'blockingDeleted': {
const cached = this.blockingsByUserIdCache.get(body.blockerId);
if (cached) {
this.blockingsByUserIdCache.set(body.blockerId, cached.filter(x => x !== body.blockeeId));
}
break;
}
default:
break;
}
}
} }
@bindThis @bindThis
@ -95,8 +58,8 @@ export class UserBlockingService implements OnApplicationShutdown {
await Promise.all([ await Promise.all([
this.cancelRequest(blocker, blockee), this.cancelRequest(blocker, blockee),
this.cancelRequest(blockee, blocker), this.cancelRequest(blockee, blocker),
this.unFollow(blocker, blockee), this.userFollowingService.unfollow(blocker, blockee),
this.unFollow(blockee, blocker), this.userFollowingService.unfollow(blockee, blocker),
this.removeFromList(blockee, blocker), this.removeFromList(blockee, blocker),
]); ]);
@ -111,6 +74,9 @@ export class UserBlockingService implements OnApplicationShutdown {
await this.blockingsRepository.insert(blocking); await this.blockingsRepository.insert(blocking);
this.cacheService.userBlockingCache.refresh(blocker.id);
this.cacheService.userBlockedCache.refresh(blockee.id);
this.globalEventService.publishInternalEvent('blockingCreated', { this.globalEventService.publishInternalEvent('blockingCreated', {
blockerId: blocker.id, blockerId: blocker.id,
blockeeId: blockee.id, blockeeId: blockee.id,
@ -148,7 +114,6 @@ export class UserBlockingService implements OnApplicationShutdown {
this.userEntityService.pack(followee, follower, { this.userEntityService.pack(followee, follower, {
detail: true, detail: true,
}).then(async packed => { }).then(async packed => {
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed); this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
@ -173,54 +138,6 @@ export class UserBlockingService implements OnApplicationShutdown {
} }
} }
@bindThis
private async unFollow(follower: User, followee: User) {
const following = await this.followingsRepository.findOneBy({
followerId: follower.id,
followeeId: followee.id,
});
if (following == null) {
return;
}
await Promise.all([
this.followingsRepository.delete(following.id),
this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),
this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1),
this.perUserFollowingChart.update(follower, followee, false),
]);
// Publish unfollow event
if (this.userEntityService.isLocalUser(follower)) {
this.userEntityService.pack(followee, follower, {
detail: true,
}).then(async packed => {
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
this.queueService.webhookDeliver(webhook, 'unfollow', {
user: packed,
});
}
});
}
// リモートにフォローをしていたらUndoFollow送信
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
this.queueService.deliver(follower, content, followee.inbox, false);
}
// リモートからフォローをされていたらRejectFollow送信
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee));
this.queueService.deliver(followee, content, follower.inbox, false);
}
}
@bindThis @bindThis
private async removeFromList(listOwner: User, user: User) { private async removeFromList(listOwner: User, user: User) {
const userLists = await this.userListsRepository.findBy({ const userLists = await this.userListsRepository.findBy({
@ -254,6 +171,9 @@ export class UserBlockingService implements OnApplicationShutdown {
await this.blockingsRepository.delete(blocking.id); await this.blockingsRepository.delete(blocking.id);
this.cacheService.userBlockingCache.refresh(blocker.id);
this.cacheService.userBlockedCache.refresh(blockee.id);
this.globalEventService.publishInternalEvent('blockingDeleted', { this.globalEventService.publishInternalEvent('blockingDeleted', {
blockerId: blocker.id, blockerId: blocker.id,
blockeeId: blockee.id, blockeeId: blockee.id,
@ -268,17 +188,6 @@ export class UserBlockingService implements OnApplicationShutdown {
@bindThis @bindThis
public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise<boolean> { public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise<boolean> {
const blockedUserIds = await this.blockingsByUserIdCache.fetch(blockerId, () => this.blockingsRepository.find({ return (await this.cacheService.userBlockingCache.fetch(blockerId)).has(blockeeId);
where: {
blockerId,
},
select: ['blockeeId'],
}).then(records => records.map(record => record.blockeeId)));
return blockedUserIds.includes(blockeeId);
}
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
this.redisSubscriber.off('message', this.onMessage);
} }
} }

View File

@ -1,88 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import type { UsersRepository } from '@/models/index.js';
import { KVCache } from '@/misc/cache.js';
import type { LocalUser, User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
export class UserCacheService implements OnApplicationShutdown {
public userByIdCache: KVCache<User>;
public localUserByNativeTokenCache: KVCache<LocalUser | null>;
public localUserByIdCache: KVCache<LocalUser>;
public uriPersonCache: KVCache<User | null>;
constructor(
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private userEntityService: UserEntityService,
) {
//this.onMessage = this.onMessage.bind(this);
this.userByIdCache = new KVCache<User>(Infinity);
this.localUserByNativeTokenCache = new KVCache<LocalUser | null>(Infinity);
this.localUserByIdCache = new KVCache<LocalUser>(Infinity);
this.uriPersonCache = new KVCache<User | null>(Infinity);
this.redisSubscriber.on('message', this.onMessage);
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as StreamMessages['internal']['payload'];
switch (type) {
case 'userChangeSuspendedState':
case 'remoteUserUpdated': {
const user = await this.usersRepository.findOneByOrFail({ id: body.id });
this.userByIdCache.set(user.id, user);
for (const [k, v] of this.uriPersonCache.cache.entries()) {
if (v.value?.id === user.id) {
this.uriPersonCache.set(k, user);
}
}
if (this.userEntityService.isLocalUser(user)) {
this.localUserByNativeTokenCache.set(user.token, user);
this.localUserByIdCache.set(user.id, user);
}
break;
}
case 'userTokenRegenerated': {
const user = await this.usersRepository.findOneByOrFail({ id: body.id }) as LocalUser;
this.localUserByNativeTokenCache.delete(body.oldToken);
this.localUserByNativeTokenCache.set(body.newToken, user);
break;
}
case 'follow': {
const follower = this.userByIdCache.get(body.followerId);
if (follower) follower.followingCount++;
const followee = this.userByIdCache.get(body.followeeId);
if (followee) followee.followersCount++;
break;
}
default:
break;
}
}
}
@bindThis
public findById(userId: User['id']) {
return this.userByIdCache.fetch(userId, () => this.usersRepository.findOneByOrFail({ id: userId }));
}
@bindThis
public onApplicationShutdown(signal?: string | undefined) {
this.redisSubscriber.off('message', this.onMessage);
}
}

View File

@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable, OnModuleInit, forwardRef } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
@ -18,6 +19,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UserBlockingService } from '@/core/UserBlockingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { CacheService } from '@/core/CacheService.js';
import Logger from '../logger.js'; import Logger from '../logger.js';
const logger = new Logger('following/create'); const logger = new Logger('following/create');
@ -36,8 +38,12 @@ type Remote = RemoteUser | {
type Both = Local | Remote; type Both = Local | Remote;
@Injectable() @Injectable()
export class UserFollowingService { export class UserFollowingService implements OnModuleInit {
private userBlockingService: UserBlockingService;
constructor( constructor(
private moduleRef: ModuleRef,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -53,8 +59,8 @@ export class UserFollowingService {
@Inject(DI.instancesRepository) @Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository, private instancesRepository: InstancesRepository,
private cacheService: CacheService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private userBlockingService: UserBlockingService,
private idService: IdService, private idService: IdService,
private queueService: QueueService, private queueService: QueueService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
@ -68,6 +74,10 @@ export class UserFollowingService {
) { ) {
} }
onModuleInit() {
this.userBlockingService = this.moduleRef.get('UserBlockingService');
}
@bindThis @bindThis
public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string): Promise<void> { public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string): Promise<void> {
const [follower, followee] = await Promise.all([ const [follower, followee] = await Promise.all([
@ -172,6 +182,8 @@ export class UserFollowingService {
} }
}); });
this.cacheService.userFollowingsCache.refresh(follower.id);
const req = await this.followRequestsRepository.findOneBy({ const req = await this.followRequestsRepository.findOneBy({
followeeId: followee.id, followeeId: followee.id,
followerId: follower.id, followerId: follower.id,
@ -225,7 +237,6 @@ export class UserFollowingService {
this.userEntityService.pack(followee.id, follower, { this.userEntityService.pack(followee.id, follower, {
detail: true, detail: true,
}).then(async packed => { }).then(async packed => {
this.globalEventService.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
this.globalEventService.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); this.globalEventService.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
@ -279,6 +290,8 @@ export class UserFollowingService {
await this.followingsRepository.delete(following.id); await this.followingsRepository.delete(following.id);
this.cacheService.userFollowingsCache.refresh(follower.id);
this.decrementFollowing(follower, followee); this.decrementFollowing(follower, followee);
// Publish unfollow event // Publish unfollow event
@ -286,7 +299,6 @@ export class UserFollowingService {
this.userEntityService.pack(followee.id, follower, { this.userEntityService.pack(followee.id, follower, {
detail: true, detail: true,
}).then(async packed => { }).then(async packed => {
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed); this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
@ -579,7 +591,6 @@ export class UserFollowingService {
detail: true, detail: true,
}); });
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packedFollowee);
this.globalEventService.publishMainStream(follower.id, 'unfollow', packedFollowee); this.globalEventService.publishMainStream(follower.id, 'unfollow', packedFollowee);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));

View File

@ -0,0 +1,34 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import type { User } from '@/models/entities/User.js';
import type { UserKeypairsRepository } from '@/models/index.js';
import { RedisKVCache } from '@/misc/cache.js';
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class UserKeypairService {
private cache: RedisKVCache<UserKeypair>;
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.userKeypairsRepository)
private userKeypairsRepository: UserKeypairsRepository,
) {
this.cache = new RedisKVCache<UserKeypair>(this.redisClient, 'userKeypair', {
lifetime: 1000 * 60 * 60 * 24, // 24h
memoryCacheLifetime: Infinity,
fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }),
toRedisConverter: (value) => JSON.stringify(value),
fromRedisConverter: (value) => JSON.parse(value),
});
}
@bindThis
public async getUserKeypair(userId: User['id']): Promise<UserKeypair> {
return await this.cache.fetch(userId);
}
}

View File

@ -1,24 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import type { User } from '@/models/entities/User.js';
import type { UserKeypairsRepository } from '@/models/index.js';
import { KVCache } from '@/misc/cache.js';
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class UserKeypairStoreService {
private cache: KVCache<UserKeypair>;
constructor(
@Inject(DI.userKeypairsRepository)
private userKeypairsRepository: UserKeypairsRepository,
) {
this.cache = new KVCache<UserKeypair>(Infinity);
}
@bindThis
public async getUserKeypair(userId: User['id']): Promise<UserKeypair> {
return await this.cache.fetch(userId, () => this.userKeypairsRepository.findOneByOrFail({ userId: userId }));
}
}

View File

@ -1,34 +1,47 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository, MutingsRepository } from '@/models/index.js'; import { In } from 'typeorm';
import type { MutingsRepository, Muting } from '@/models/index.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { QueueService } from '@/core/QueueService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
@Injectable() @Injectable()
export class UserMutingService { export class UserMutingService {
constructor( constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.mutingsRepository) @Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository, private mutingsRepository: MutingsRepository,
private idService: IdService, private idService: IdService,
private queueService: QueueService, private cacheService: CacheService,
private globalEventService: GlobalEventService,
) { ) {
} }
@bindThis @bindThis
public async mute(user: User, target: User): Promise<void> { public async mute(user: User, target: User, expiresAt: Date | null = null): Promise<void> {
await this.mutingsRepository.insert({ await this.mutingsRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
createdAt: new Date(), createdAt: new Date(),
expiresAt: expiresAt ?? null,
muterId: user.id, muterId: user.id,
muteeId: target.id, muteeId: target.id,
}); });
this.cacheService.userMutingsCache.refresh(user.id);
}
@bindThis
public async unmute(mutings: Muting[]): Promise<void> {
if (mutings.length === 0) return;
await this.mutingsRepository.delete({
id: In(mutings.map(m => m.id)),
});
const muterIds = [...new Set(mutings.map(m => m.muterId))];
for (const muterId of muterIds) {
this.cacheService.userMutingsCache.refresh(muterId);
}
} }
} }

View File

@ -37,7 +37,7 @@ export class VideoProcessingService {
}); });
}); });
return await this.imageProcessingService.convertToWebp(`${dir}/out.png`, 498, 280); return await this.imageProcessingService.convertToWebp(`${dir}/out.png`, 498, 422);
} finally { } finally {
cleanup(); cleanup();
} }

View File

@ -13,14 +13,14 @@ export class WebhookService implements OnApplicationShutdown {
private webhooks: Webhook[] = []; private webhooks: Webhook[] = [];
constructor( constructor(
@Inject(DI.redisSubscriber) @Inject(DI.redisForPubsub)
private redisSubscriber: Redis.Redis, private redisForPubsub: Redis.Redis,
@Inject(DI.webhooksRepository) @Inject(DI.webhooksRepository)
private webhooksRepository: WebhooksRepository, private webhooksRepository: WebhooksRepository,
) { ) {
//this.onMessage = this.onMessage.bind(this); //this.onMessage = this.onMessage.bind(this);
this.redisSubscriber.on('message', this.onMessage); this.redisForPubsub.on('message', this.onMessage);
} }
@bindThis @bindThis
@ -82,6 +82,6 @@ export class WebhookService implements OnApplicationShutdown {
@bindThis @bindThis
public onApplicationShutdown(signal?: string | undefined) { public onApplicationShutdown(signal?: string | undefined) {
this.redisSubscriber.off('message', this.onMessage); this.redisForPubsub.off('message', this.onMessage);
} }
} }

View File

@ -3,9 +3,9 @@ import escapeRegexp from 'escape-regexp';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { KVCache } from '@/misc/cache.js'; import { MemoryKVCache } from '@/misc/cache.js';
import type { UserPublickey } from '@/models/entities/UserPublickey.js'; import type { UserPublickey } from '@/models/entities/UserPublickey.js';
import { UserCacheService } from '@/core/UserCacheService.js'; import { CacheService } from '@/core/CacheService.js';
import type { Note } from '@/models/entities/Note.js'; import type { Note } from '@/models/entities/Note.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RemoteUser, User } from '@/models/entities/User.js'; import { RemoteUser, User } from '@/models/entities/User.js';
@ -31,8 +31,8 @@ export type UriParseResult = {
@Injectable() @Injectable()
export class ApDbResolverService { export class ApDbResolverService {
private publicKeyCache: KVCache<UserPublickey | null>; private publicKeyCache: MemoryKVCache<UserPublickey | null>;
private publicKeyByUserIdCache: KVCache<UserPublickey | null>; private publicKeyByUserIdCache: MemoryKVCache<UserPublickey | null>;
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
@ -47,11 +47,11 @@ export class ApDbResolverService {
@Inject(DI.userPublickeysRepository) @Inject(DI.userPublickeysRepository)
private userPublickeysRepository: UserPublickeysRepository, private userPublickeysRepository: UserPublickeysRepository,
private userCacheService: UserCacheService, private cacheService: CacheService,
private apPersonService: ApPersonService, private apPersonService: ApPersonService,
) { ) {
this.publicKeyCache = new KVCache<UserPublickey | null>(Infinity); this.publicKeyCache = new MemoryKVCache<UserPublickey | null>(Infinity);
this.publicKeyByUserIdCache = new KVCache<UserPublickey | null>(Infinity); this.publicKeyByUserIdCache = new MemoryKVCache<UserPublickey | null>(Infinity);
} }
@bindThis @bindThis
@ -107,11 +107,11 @@ export class ApDbResolverService {
if (parsed.local) { if (parsed.local) {
if (parsed.type !== 'users') return null; if (parsed.type !== 'users') return null;
return await this.userCacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({ return await this.cacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({
id: parsed.id, id: parsed.id,
}).then(x => x ?? undefined)) ?? null; }).then(x => x ?? undefined)) ?? null;
} else { } else {
return await this.userCacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({ return await this.cacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({
uri: parsed.uri, uri: parsed.uri,
})); }));
} }
@ -138,7 +138,7 @@ export class ApDbResolverService {
if (key == null) return null; if (key == null) return null;
return { return {
user: await this.userCacheService.findById(key.userId) as RemoteUser, user: await this.cacheService.findUserById(key.userId) as RemoteUser,
key, key,
}; };
} }

View File

@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm'; import { In, IsNull } from 'typeorm';
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 { UserFollowingService } from '@/core/UserFollowingService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js';
@ -22,7 +22,7 @@ import { QueueService } from '@/core/QueueService.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js'; import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { RemoteUser } from '@/models/entities/User.js'; import type { RemoteUser } from '@/models/entities/User.js';
import { getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { getApHrefNullable, getApId, getApIds, getApType, getOneApHrefNullable, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
import { ApNoteService } from './models/ApNoteService.js'; import { ApNoteService } from './models/ApNoteService.js';
import { ApLoggerService } from './ApLoggerService.js'; import { ApLoggerService } from './ApLoggerService.js';
import { ApDbResolverService } from './ApDbResolverService.js'; import { ApDbResolverService } from './ApDbResolverService.js';
@ -31,7 +31,7 @@ import { ApAudienceService } from './ApAudienceService.js';
import { ApPersonService } from './models/ApPersonService.js'; import { ApPersonService } from './models/ApPersonService.js';
import { ApQuestionService } from './models/ApQuestionService.js'; import { ApQuestionService } from './models/ApQuestionService.js';
import type { Resolver } from './ApResolverService.js'; import type { Resolver } from './ApResolverService.js';
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate } from './type.js'; import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js';
@Injectable() @Injectable()
export class ApInboxService { export class ApInboxService {
@ -139,6 +139,8 @@ export class ApInboxService {
await this.block(actor, activity); await this.block(actor, activity);
} else if (isFlag(activity)) { } else if (isFlag(activity)) {
await this.flag(actor, activity); await this.flag(actor, activity);
} else if (isMove(activity)) {
//await this.move(actor, activity);
} else { } else {
this.logger.warn(`unrecognized activity type: ${activity.type}`); this.logger.warn(`unrecognized activity type: ${activity.type}`);
} }
@ -728,4 +730,59 @@ export class ApInboxService {
return `skip: Unknown type: ${getApType(object)}`; return `skip: Unknown type: ${getApType(object)}`;
} }
} }
@bindThis
private async move(actor: RemoteUser, activity: IMove): Promise<string> {
// fetch the new and old accounts
const targetUri = getApHrefNullable(activity.target);
if (!targetUri) return 'skip: invalid activity target';
let new_acc = await this.apPersonService.resolvePerson(targetUri);
let old_acc = await this.apPersonService.resolvePerson(actor.uri);
// update them if they're remote
if (new_acc.uri) await this.apPersonService.updatePerson(new_acc.uri);
if (old_acc.uri) await this.apPersonService.updatePerson(old_acc.uri);
// retrieve updated users
new_acc = await this.apPersonService.resolvePerson(targetUri);
old_acc = await this.apPersonService.resolvePerson(actor.uri);
// check if alsoKnownAs of the new account is valid
let isValidMove = true;
if (old_acc.uri) {
if (!new_acc.alsoKnownAs?.includes(old_acc.uri)) {
isValidMove = false;
}
} else if (!new_acc.alsoKnownAs?.includes(old_acc.id)) {
isValidMove = false;
}
if (!isValidMove) {
return 'skip: accounts invalid';
}
// add target uri to movedToUri in order to indicate that the user has moved
await this.usersRepository.update(old_acc.id, { movedToUri: targetUri });
// follow the new account and unfollow the old one
const followings = await this.followingsRepository.find({
relations: {
follower: true,
},
where: {
followeeId: old_acc.id,
followerHost: IsNull(), // follower is local
},
});
for (const following of followings) {
if (!following.follower) continue;
try {
await this.userFollowingService.follow(following.follower, new_acc);
await this.userFollowingService.unfollow(following.follower, old_acc);
} catch {
/* empty */
}
}
return 'ok';
}
} }

View File

@ -14,16 +14,18 @@ import type { NoteReaction } from '@/models/entities/NoteReaction.js';
import type { Emoji } from '@/models/entities/Emoji.js'; import type { Emoji } from '@/models/entities/Emoji.js';
import type { Poll } from '@/models/entities/Poll.js'; import type { Poll } from '@/models/entities/Poll.js';
import type { PollVote } from '@/models/entities/PollVote.js'; import type { PollVote } from '@/models/entities/PollVote.js';
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js'; import { UserKeypairService } from '@/core/UserKeypairService.js';
import { MfmService } from '@/core/MfmService.js'; import { MfmService } from '@/core/MfmService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import type { UserKeypair } from '@/models/entities/UserKeypair.js'; import type { UserKeypair } from '@/models/entities/UserKeypair.js';
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, EmojisRepository, PollsRepository } from '@/models/index.js'; import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, EmojisRepository, PollsRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { LdSignatureService } from './LdSignatureService.js'; import { LdSignatureService } from './LdSignatureService.js';
import { ApMfmService } from './ApMfmService.js'; import { ApMfmService } from './ApMfmService.js';
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
import type { IIdentifier } from './models/identifier.js'; import type { IIdentifier } from './models/identifier.js';
@Injectable() @Injectable()
@ -50,10 +52,11 @@ export class ApRendererService {
@Inject(DI.pollsRepository) @Inject(DI.pollsRepository)
private pollsRepository: PollsRepository, private pollsRepository: PollsRepository,
private customEmojiService: CustomEmojiService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private driveFileEntityService: DriveFileEntityService, private driveFileEntityService: DriveFileEntityService,
private ldSignatureService: LdSignatureService, private ldSignatureService: LdSignatureService,
private userKeypairStoreService: UserKeypairStoreService, private userKeypairService: UserKeypairService,
private apMfmService: ApMfmService, private apMfmService: ApMfmService,
private mfmService: MfmService, private mfmService: MfmService,
) { ) {
@ -272,11 +275,7 @@ export class ApRendererService {
if (reaction.startsWith(':')) { if (reaction.startsWith(':')) {
const name = reaction.replaceAll(':', ''); const name = reaction.replaceAll(':', '');
// TODO: cache const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name);
const emoji = await this.emojisRepository.findOneBy({
name,
host: IsNull(),
});
if (emoji) object.tag = [this.renderEmoji(emoji)]; if (emoji) object.tag = [this.renderEmoji(emoji)];
} }
@ -293,6 +292,22 @@ export class ApRendererService {
}; };
} }
@bindThis
public renderMove(
src: { id: User['id']; host: User['host']; uri: User['host'] },
dst: { id: User['id']; host: User['host']; uri: User['host'] },
): IMove {
const actor = this.userEntityService.isLocalUser(src) ? `${this.config.url}/users/${src.id}` : src.uri!;
const target = this.userEntityService.isLocalUser(dst) ? `${this.config.url}/users/${dst.id}` : dst.uri!;
return {
id: `${this.config.url}/moves/${src.id}/${dst.id}`,
actor,
type: 'Move',
object: actor,
target,
};
}
@bindThis @bindThis
public async renderNote(note: Note, dive = true): Promise<IPost> { public async renderNote(note: Note, dive = true): Promise<IPost> {
const getPromisedFiles = async (ids: string[]) => { const getPromisedFiles = async (ids: string[]) => {
@ -473,7 +488,7 @@ export class ApRendererService {
...hashtagTags, ...hashtagTags,
]; ];
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id); const keypair = await this.userKeypairService.getUserKeypair(user.id);
const person = { const person = {
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person', type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
@ -499,6 +514,14 @@ export class ApRendererService {
attachment: attachment.length ? attachment : undefined, attachment: attachment.length ? attachment : undefined,
} as any; } as any;
if (user.movedToUri) {
person.movedTo = user.movedToUri;
}
if (user.alsoKnownAs) {
person.alsoKnownAs = user.alsoKnownAs;
}
if (profile.birthday) { if (profile.birthday) {
person['vcard:bday'] = profile.birthday; person['vcard:bday'] = profile.birthday;
} }
@ -640,7 +663,7 @@ export class ApRendererService {
@bindThis @bindThis
public async attachLdSignature(activity: any, user: { id: User['id']; host: null; }): Promise<IActivity> { public async attachLdSignature(activity: any, user: { id: User['id']; host: null; }): Promise<IActivity> {
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id); const keypair = await this.userKeypairService.getUserKeypair(user.id);
const ldSignature = this.ldSignatureService.use(); const ldSignature = this.ldSignatureService.use();
ldSignature.debug = false; ldSignature.debug = false;
@ -701,13 +724,9 @@ export class ApRendererService {
private async getEmojis(names: string[]): Promise<Emoji[]> { private async getEmojis(names: string[]): Promise<Emoji[]> {
if (names == null || names.length === 0) return []; if (names == null || names.length === 0) return [];
const emojis = await Promise.all( const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
names.map(name => this.emojisRepository.findOneBy({ const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);
name,
host: IsNull(),
})),
);
return emojis.filter(emoji => emoji != null) as Emoji[]; return emojis;
} }
} }

View File

@ -4,7 +4,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js'; import { UserKeypairService } from '@/core/UserKeypairService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -131,7 +131,7 @@ export class ApRequestService {
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
private userKeypairStoreService: UserKeypairStoreService, private userKeypairService: UserKeypairService,
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
private loggerService: LoggerService, private loggerService: LoggerService,
) { ) {
@ -143,7 +143,7 @@ export class ApRequestService {
public async signedPost(user: { id: User['id'] }, url: string, object: any) { public async signedPost(user: { id: User['id'] }, url: string, object: any) {
const body = JSON.stringify(object); const body = JSON.stringify(object);
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id); const keypair = await this.userKeypairService.getUserKeypair(user.id);
const req = ApRequestCreator.createSignedPost({ const req = ApRequestCreator.createSignedPost({
key: { key: {
@ -170,7 +170,7 @@ export class ApRequestService {
*/ */
@bindThis @bindThis
public async signedGet(url: string, user: { id: User['id'] }) { public async signedGet(url: string, user: { id: User['id'] }) {
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id); const keypair = await this.userKeypairService.getUserKeypair(user.id);
const req = ApRequestCreator.createSignedGet({ const req = ApRequestCreator.createSignedGet({
key: { key: {

View File

@ -1,5 +1,6 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { forwardRef, Inject, Injectable } from '@nestjs/common';
import promiseLimit from 'promise-limit'; import promiseLimit from 'promise-limit';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { PollsRepository, EmojisRepository } from '@/models/index.js'; import type { PollsRepository, EmojisRepository } from '@/models/index.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
@ -342,14 +343,16 @@ export class ApNoteService {
const eomjiTags = toArray(tags).filter(isEmoji); const eomjiTags = toArray(tags).filter(isEmoji);
const existingEmojis = await this.emojisRepository.findBy({
host,
name: In(eomjiTags.map(tag => tag.name!.replaceAll(':', ''))),
});
return await Promise.all(eomjiTags.map(async tag => { return await Promise.all(eomjiTags.map(async tag => {
const name = tag.name!.replace(/^:/, '').replace(/:$/, ''); const name = tag.name!.replaceAll(':', '');
tag.icon = toSingle(tag.icon); tag.icon = toSingle(tag.icon);
const exists = await this.emojisRepository.findOneBy({ const exists = existingEmojis.find(x => x.name === name);
host,
name,
});
if (exists) { if (exists) {
if ((tag.updated != null && exists.updatedAt == null) if ((tag.updated != null && exists.updatedAt == null)

View File

@ -8,7 +8,7 @@ import type { Config } from '@/config.js';
import type { RemoteUser } from '@/models/entities/User.js'; import type { RemoteUser } from '@/models/entities/User.js';
import { User } from '@/models/entities/User.js'; import { User } from '@/models/entities/User.js';
import { truncate } from '@/misc/truncate.js'; import { truncate } from '@/misc/truncate.js';
import type { UserCacheService } from '@/core/UserCacheService.js'; import type { CacheService } from '@/core/CacheService.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
@ -31,6 +31,7 @@ import type { UtilityService } from '@/core/UtilityService.js';
import type { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js'; import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
import { extractApHashtags } from './tag.js'; import { extractApHashtags } from './tag.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
@ -49,12 +50,13 @@ const summaryLength = 2048;
export class ApPersonService implements OnModuleInit { export class ApPersonService implements OnModuleInit {
private utilityService: UtilityService; private utilityService: UtilityService;
private userEntityService: UserEntityService; private userEntityService: UserEntityService;
private driveFileEntityService: DriveFileEntityService;
private idService: IdService; private idService: IdService;
private globalEventService: GlobalEventService; private globalEventService: GlobalEventService;
private metaService: MetaService; private metaService: MetaService;
private federatedInstanceService: FederatedInstanceService; private federatedInstanceService: FederatedInstanceService;
private fetchInstanceMetadataService: FetchInstanceMetadataService; private fetchInstanceMetadataService: FetchInstanceMetadataService;
private userCacheService: UserCacheService; private cacheService: CacheService;
private apResolverService: ApResolverService; private apResolverService: ApResolverService;
private apNoteService: ApNoteService; private apNoteService: ApNoteService;
private apImageService: ApImageService; private apImageService: ApImageService;
@ -97,7 +99,7 @@ export class ApPersonService implements OnModuleInit {
//private metaService: MetaService, //private metaService: MetaService,
//private federatedInstanceService: FederatedInstanceService, //private federatedInstanceService: FederatedInstanceService,
//private fetchInstanceMetadataService: FetchInstanceMetadataService, //private fetchInstanceMetadataService: FetchInstanceMetadataService,
//private userCacheService: UserCacheService, //private cacheService: CacheService,
//private apResolverService: ApResolverService, //private apResolverService: ApResolverService,
//private apNoteService: ApNoteService, //private apNoteService: ApNoteService,
//private apImageService: ApImageService, //private apImageService: ApImageService,
@ -113,12 +115,13 @@ export class ApPersonService implements OnModuleInit {
onModuleInit() { onModuleInit() {
this.utilityService = this.moduleRef.get('UtilityService'); this.utilityService = this.moduleRef.get('UtilityService');
this.userEntityService = this.moduleRef.get('UserEntityService'); this.userEntityService = this.moduleRef.get('UserEntityService');
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
this.idService = this.moduleRef.get('IdService'); this.idService = this.moduleRef.get('IdService');
this.globalEventService = this.moduleRef.get('GlobalEventService'); this.globalEventService = this.moduleRef.get('GlobalEventService');
this.metaService = this.moduleRef.get('MetaService'); this.metaService = this.moduleRef.get('MetaService');
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService'); this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService'); this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService');
this.userCacheService = this.moduleRef.get('UserCacheService'); this.cacheService = this.moduleRef.get('CacheService');
this.apResolverService = this.moduleRef.get('ApResolverService'); this.apResolverService = this.moduleRef.get('ApResolverService');
this.apNoteService = this.moduleRef.get('ApNoteService'); this.apNoteService = this.moduleRef.get('ApNoteService');
this.apImageService = this.moduleRef.get('ApImageService'); this.apImageService = this.moduleRef.get('ApImageService');
@ -207,14 +210,14 @@ export class ApPersonService implements OnModuleInit {
public async fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> { public async fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> {
if (typeof uri !== 'string') throw new Error('uri is not string'); if (typeof uri !== 'string') throw new Error('uri is not string');
const cached = this.userCacheService.uriPersonCache.get(uri); const cached = this.cacheService.uriPersonCache.get(uri);
if (cached) return cached; if (cached) return cached;
// URIがこのサーバーを指しているならデータベースからフェッチ // URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(this.config.url + '/')) { if (uri.startsWith(this.config.url + '/')) {
const id = uri.split('/').pop(); const id = uri.split('/').pop();
const u = await this.usersRepository.findOneBy({ id }); const u = await this.usersRepository.findOneBy({ id });
if (u) this.userCacheService.uriPersonCache.set(uri, u); if (u) this.cacheService.uriPersonCache.set(uri, u);
return u; return u;
} }
@ -222,7 +225,7 @@ export class ApPersonService implements OnModuleInit {
const exist = await this.usersRepository.findOneBy({ uri }); const exist = await this.usersRepository.findOneBy({ uri });
if (exist) { if (exist) {
this.userCacheService.uriPersonCache.set(uri, exist); this.cacheService.uriPersonCache.set(uri, exist);
return exist; return exist;
} }
//#endregion //#endregion
@ -278,6 +281,8 @@ export class ApPersonService implements OnModuleInit {
lastFetchedAt: new Date(), lastFetchedAt: new Date(),
name: truncate(person.name, nameLength), name: truncate(person.name, nameLength),
isLocked: !!person.manuallyApprovesFollowers, isLocked: !!person.manuallyApprovesFollowers,
movedToUri: person.movedTo,
alsoKnownAs: person.alsoKnownAs,
isExplorable: !!person.discoverable, isExplorable: !!person.discoverable,
username: person.preferredUsername, username: person.preferredUsername,
usernameLower: person.preferredUsername!.toLowerCase(), usernameLower: person.preferredUsername!.toLowerCase(),
@ -356,32 +361,44 @@ export class ApPersonService implements OnModuleInit {
const avatarId = avatar ? avatar.id : null; const avatarId = avatar ? avatar.id : null;
const bannerId = banner ? banner.id : null; const bannerId = banner ? banner.id : null;
const avatarUrl = avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null;
const bannerUrl = banner ? this.driveFileEntityService.getPublicUrl(banner) : null;
const avatarBlurhash = avatar ? avatar.blurhash : null;
const bannerBlurhash = banner ? banner.blurhash : null;
await this.usersRepository.update(user!.id, { await this.usersRepository.update(user!.id, {
avatarId, avatarId,
bannerId, bannerId,
avatarUrl,
bannerUrl,
avatarBlurhash,
bannerBlurhash,
}); });
user!.avatarId = avatarId; user!.avatarId = avatarId;
user!.bannerId = bannerId; user!.bannerId = bannerId;
//#endregion user!.avatarUrl = avatarUrl;
user!.bannerUrl = bannerUrl;
user!.avatarBlurhash = avatarBlurhash;
user!.bannerBlurhash = bannerBlurhash;
//#endregion
//#region カスタム絵文字取得 //#region カスタム絵文字取得
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => { const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => {
this.logger.info(`extractEmojis: ${err}`); this.logger.info(`extractEmojis: ${err}`);
return [] as Emoji[]; return [] as Emoji[];
}); });
const emojiNames = emojis.map(emoji => emoji.name); const emojiNames = emojis.map(emoji => emoji.name);
await this.usersRepository.update(user!.id, { await this.usersRepository.update(user!.id, {
emojis: emojiNames, emojis: emojiNames,
}); });
//#endregion //#endregion
await this.updateFeatured(user!.id, resolver).catch(err => this.logger.error(err)); await this.updateFeatured(user!.id, resolver).catch(err => this.logger.error(err));
return user!; return user!;
} }
/** /**
@ -458,15 +475,21 @@ export class ApPersonService implements OnModuleInit {
isBot: getApType(object) === 'Service', isBot: getApType(object) === 'Service',
isCat: (person as any).isCat === true, isCat: (person as any).isCat === true,
isLocked: !!person.manuallyApprovesFollowers, isLocked: !!person.manuallyApprovesFollowers,
movedToUri: person.movedTo ?? null,
alsoKnownAs: person.alsoKnownAs ?? null,
isExplorable: !!person.discoverable, isExplorable: !!person.discoverable,
} as Partial<User>; } as Partial<User>;
if (avatar) { if (avatar) {
updates.avatarId = avatar.id; updates.avatarId = avatar.id;
updates.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar');
updates.avatarBlurhash = avatar.blurhash;
} }
if (banner) { if (banner) {
updates.bannerId = banner.id; updates.bannerId = banner.id;
updates.bannerUrl = this.driveFileEntityService.getPublicUrl(banner);
updates.bannerBlurhash = banner.blurhash;
} }
// Update user // Update user

View File

@ -157,6 +157,8 @@ export interface IActor extends IObject {
name?: string; name?: string;
preferredUsername?: string; preferredUsername?: string;
manuallyApprovesFollowers?: boolean; manuallyApprovesFollowers?: boolean;
movedTo?: string;
alsoKnownAs?: string[];
discoverable?: boolean; discoverable?: boolean;
inbox: string; inbox: string;
sharedInbox?: string; // 後方互換性のため sharedInbox?: string; // 後方互換性のため
@ -300,6 +302,11 @@ export interface IFlag extends IActivity {
type: 'Flag'; type: 'Flag';
} }
export interface IMove extends IActivity {
type: 'Move';
target: IObject | string;
}
export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create'; export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create';
export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete'; export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete';
export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update'; export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update';
@ -314,3 +321,4 @@ export const isLike = (object: IObject): object is ILike => getApType(object) ==
export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce'; export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block'; export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag'; export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move';

View File

@ -1,6 +1,6 @@
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 { AntennaNotesRepository, AntennasRepository } from '@/models/index.js'; import type { AntennasRepository } from '@/models/index.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import type { Antenna } from '@/models/entities/Antenna.js'; import type { Antenna } from '@/models/entities/Antenna.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -10,9 +10,6 @@ export class AntennaEntityService {
constructor( constructor(
@Inject(DI.antennasRepository) @Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository, private antennasRepository: AntennasRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
) { ) {
} }
@ -22,8 +19,6 @@ export class AntennaEntityService {
): Promise<Packed<'Antenna'>> { ): Promise<Packed<'Antenna'>> {
const antenna = typeof src === 'object' ? src : await this.antennasRepository.findOneByOrFail({ id: src }); const antenna = typeof src === 'object' ? src : await this.antennasRepository.findOneByOrFail({ id: src });
const hasUnreadNote = (await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false })) != null;
return { return {
id: antenna.id, id: antenna.id,
createdAt: antenna.createdAt.toISOString(), createdAt: antenna.createdAt.toISOString(),
@ -38,7 +33,7 @@ export class AntennaEntityService {
withReplies: antenna.withReplies, withReplies: antenna.withReplies,
withFile: antenna.withFile, withFile: antenna.withFile,
isActive: antenna.isActive, isActive: antenna.isActive,
hasUnreadNote, hasUnreadNote: false, // TODO
}; };
} }
} }

View File

@ -1,13 +1,14 @@
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 { ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NoteUnreadsRepository } from '@/models/index.js'; import type { ChannelFavoritesRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NoteUnreadsRepository, NotesRepository } from '@/models/index.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import type { } from '@/models/entities/Blocking.js'; import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import type { Channel } from '@/models/entities/Channel.js'; import type { Channel } from '@/models/entities/Channel.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js';
import { DriveFileEntityService } from './DriveFileEntityService.js'; import { DriveFileEntityService } from './DriveFileEntityService.js';
import { NoteEntityService } from './NoteEntityService.js';
import { In } from 'typeorm';
@Injectable() @Injectable()
export class ChannelEntityService { export class ChannelEntityService {
@ -18,13 +19,19 @@ export class ChannelEntityService {
@Inject(DI.channelFollowingsRepository) @Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository, private channelFollowingsRepository: ChannelFollowingsRepository,
@Inject(DI.channelFavoritesRepository)
private channelFavoritesRepository: ChannelFavoritesRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.noteUnreadsRepository) @Inject(DI.noteUnreadsRepository)
private noteUnreadsRepository: NoteUnreadsRepository, private noteUnreadsRepository: NoteUnreadsRepository,
@Inject(DI.driveFilesRepository) @Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository, private driveFilesRepository: DriveFilesRepository,
private userEntityService: UserEntityService, private noteEntityService: NoteEntityService,
private driveFileEntityService: DriveFileEntityService, private driveFileEntityService: DriveFileEntityService,
) { ) {
} }
@ -33,6 +40,7 @@ export class ChannelEntityService {
public async pack( public async pack(
src: Channel['id'] | Channel, src: Channel['id'] | Channel,
me?: { id: User['id'] } | null | undefined, me?: { id: User['id'] } | null | undefined,
detailed?: boolean,
): Promise<Packed<'Channel'>> { ): Promise<Packed<'Channel'>> {
const channel = typeof src === 'object' ? src : await this.channelsRepository.findOneByOrFail({ id: src }); const channel = typeof src === 'object' ? src : await this.channelsRepository.findOneByOrFail({ id: src });
const meId = me ? me.id : null; const meId = me ? me.id : null;
@ -46,6 +54,17 @@ export class ChannelEntityService {
followeeId: channel.id, followeeId: channel.id,
}) : null; }) : null;
const favorite = meId ? await this.channelFavoritesRepository.findOneBy({
userId: meId,
channelId: channel.id,
}) : null;
const pinnedNotes = channel.pinnedNoteIds.length > 0 ? await this.notesRepository.find({
where: {
id: In(channel.pinnedNoteIds),
},
}) : [];
return { return {
id: channel.id, id: channel.id,
createdAt: channel.createdAt.toISOString(), createdAt: channel.createdAt.toISOString(),
@ -54,13 +73,19 @@ export class ChannelEntityService {
description: channel.description, description: channel.description,
userId: channel.userId, userId: channel.userId,
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null, bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
pinnedNoteIds: channel.pinnedNoteIds,
usersCount: channel.usersCount, usersCount: channel.usersCount,
notesCount: channel.notesCount, notesCount: channel.notesCount,
...(me ? { ...(me ? {
isFollowing: following != null, isFollowing: following != null,
isFavorited: favorite != null,
hasUnreadNote, hasUnreadNote,
} : {}), } : {}),
...(detailed ? {
pinnedNotes: await this.noteEntityService.packMany(pinnedNotes, me),
} : {}),
}; };
} }
} }

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